diff --git a/README.md b/README.md
index ccca57b02ea094d7eb01e04522f972eeae7fc58c..e231d24008f930eb0fead31fabb74006b27a0c24 100644
--- a/README.md
+++ b/README.md
@@ -37,12 +37,12 @@ install the software and track updates is to clone the public repository.
 Create a folder on you web server (using whatever method makes sense for
 you) and cd into it. Then clone the repository (the folder must be empty!):
 
-    git clone https://github.com/osTicket/osTicket-1.8 .
+    git clone https://github.com/osTicket/osTicket
 
 And deploy the code into somewhere in your server's www root folder, for
 instance
 
-    cd osTicket-1.8
+    cd osTicket
     php manage.php deploy --setup /var/www/htdocs/osticket/
 
 Then you can configure your server if necessary to serve that folder, and
@@ -60,8 +60,8 @@ osTicket supports upgrading from 1.6-rc1 and later versions. As with any
 upgrade, strongly consider a backup of your attachment files, database, and
 osTicket codebase before embarking on an upgrade.
 
-To trigger the update process, fetch the osTicket-1.8 tarball from either
-the osTicket [github](http://github.com/osTicket/osTicket-1.8/releases) page
+To trigger the update process, fetch the osTicket tarball from either
+the osTicket [github](http://github.com/osTicket/osTicket/releases) page
 or from the [osTicket website](http://osticket.com). Extract the tarball
 into the folder of your osTicket codebase. This can also be accomplished
 with the zip file, and a FTP client can of course be used to upload the new
diff --git a/WHATSNEW.md b/WHATSNEW.md
index 4812a8892ca4cfa988bb47fe87d3a42362463f16..b7162b2285383954e7fa8bae4159b4a83f0d3772 100644
--- a/WHATSNEW.md
+++ b/WHATSNEW.md
@@ -1,5 +1,166 @@
+osTicket v1.10-rc.3
+===================
+### Enhancements
+  * Compatibility with PHP7 (#2828)
+  * Share tickets among organization members (#2405)
+  * Add lock semantics compatible with v1.9 (lock on view) (f826189)
+  * Staff login backdrop is customizable (#2468)
+  * Add advanced search for closed date, thread last message, thread last
+    response (#2444)
+  * Disable auto-claim by department (#2591)
+  * Properly flag SYSTEM thread postings (#2702)
+  * Add option to use dept/agent name on replies (#2700)
+  * Add a preference option to set the sort order of the thread entries in DESC
+    or ASC order (#2700)
+  * Thread dates can be shown as relative or absolute timestamps (#2700)
+  * Make Avatars optional on thread view (#2701)
+  * Make Authentication Tokens Optional (auto-login links in emails) (#2714)
+  * Use icons for ticket and task actions (#2760)
+  * role: Add option to use primary role on assignment (#2832)
+
+### Improvements
+  * All improvements cited in v1.9.12 and v1.9.13
+  * Fix deleting of custom logos (#2433)
+  * Fix assignment setting on new tasks (#2452)
+  * Fix subject display of non-short-answer fields on ticket view and ticket
+    queue (#2463)
+  * Fix advanced search of ticket source (#2479)
+  * Forbid adding deleted forms via "Manage Forms" (#2483)
+  * Use horizontal tabs for translatable article content rather than the left
+    tabs in a table (#2484)
+  * Fix lock expiration time if PHP and database have different time zones
+    (#2533)
+  * Fix user class and ID matching from email headers (#2549)
+  * Fix emission of `Content-Language` header in client portal for multiple
+    system languages, thanks @t-oster (#2555)
+  * Fix deployment of fresh git repo or download on PHP 5.6 (#2571)
+  * Fix handling of abbreviated database timezones like `CDT` (#2570)
+  * Fix incorrect height display of avatars (#2580, #2609)
+  * Sort help topic names case insensitively, thanks @jdelhome3578 (#2530)
+  * Fix detection of looped emails (f2cac64)
+  * Fix crash in ticket preview (popout) if ticket has no thread (bd9e9c5)
+  * Fix javascript crash adding new ticket filter (d2af0eb)
+  * Fix crash if the `name` field of a user is a drop-down (ec0b2c5)
+  * Fix incorrect SQL query removing departments (cf6cd81)
+  * Properly fallback to database file storage if system is misconfigured (1580136)
+  * Fix crash handling fields with `__` in the name in the VisibilityConstraint
+    class (b3d09b6)
+  * Remove staff-dept records when removing an agent (ecf6931)
+  * Avoid crashing processing ORM records with NULL select_related models (#2589)
+  * Fix several full-text search related issues (#2588, #2603)
+  * Fix crash sending registration link for a guest user (#2552)
+  * Avoid showing lock icon for expired locks on ticket listing (#2617)
+  * Fix incorrect redirect from SSO authentication, thanks @kevinoconnor7
+    (#2641)
+  * Fix vertical overflow of uploaded image preview (#2616)
+  * Fix unnecessary dropping of CDATA table on MySQL 5.6 (#2638)
+  * Fix several issues on user directory ticket listing (#2626)
+  * Fix encoding of attachment filenames in emails (#2586)
+  * Fix warning rendering advanced search dialog, thanks @t-oster (#2594)
+  * Fix bounce message loop for message alert to a bad agent email address
+    (#2639)
+  * Make fulltext search optional on user lookup (#2657)
+  * Add the [claim] feature again (#2681)
+  * Fix agent's Signature & Timezone dropped on update (#2720)
+  * Fix crash in user CSV import (#2708)
+  * Fix crash in user ajax lookup (#2600)
+  * Send Reference and In-Reply-To headers only for thread items pertinent to
+    the receiving user (#2723)
+  * Properly clean HTML custom fields (#2736)
+  * Fix changing/saving properties on internal ticket statuses, with the
+    exception of the state (#2767)
+  * Fix CSV list import (#2738)
+  * Fix late redirect header for single ticket typeahead result (#2830)
+  * Add sortable column headers in the ticket and task queues (#2761)
+  * Fix several issues with the file CLI app (#2808)
+  * Fix config crash on install (#2827, #2844)
+  * Set due date based on user's timezone (#2812, #2981)
+  * Fix crash rendering some email addresses to string (#2844)
+  * Fix crash rendering thread with invalid timestamps (#2844)
+  * Log assignment note (comments), if any, when staff created ticket is
+    assigned (#2944)
+  * Change transient SLA, on transfer,  if target department has a valid SLA
+    (#2944)
+  * Fix typo on task transfer modal dialog (#2944)
+  * Fix ticket source on ticket edit (#2944)
+  * Convert user time to database time when querying stats (#2944)
+  * Fix date picker clearing input on invalid date format (#2944)
+  * Show topic-specific thank-you page (#2915)
+  * Department manager can be excluded from the new ticket alert (#2974)
+  * Do not scrub iframe `@src` attribute (#2940)
+
+### Performance and Security
+  * Use full-text search for quick-search typeahead boxes (#2479)
+  * Speed up a few slow and noisy queries (5c68eb3, 340fee7, 208fcc3)
+  * Lower memory requirements processing attachments (#2491, #2492)
+  * Ensure agent still has access when reopening a ticket (#2768)
+  * Always perform validation server-side for ajax uploads (#2844)
+  * Protect access to files shown in the FileUpload field (#2618)
+  * Decode entities prior to HTML scrubbing (#2940)
+
+### Known Issues
+  * Uploading multiple files simultaneous (via drag and drop) will cause some
+    files to be dropped
+
+osTicket v1.10-rc.2
+===================
+### Enhancements
+  * Lazy locking system for ticket locking (#2325, #2351, 37cdf25, de92ec5,
+    37a0676)
+  * Add settings for avatars and local "Oscar's A-Team" avatars (#2334)
+  * Several UI tweaks (7436195, #2426)
+  * Add transfer and assign mass actions to tickets (#2375)
+  * Import agents from the command line (#2323)
+  * User select dialog can be opened after closing in new ticket by staff
+    (605c313)
+  * Deadband new message alert and autoresponse to once per five minutes per
+    user per thread (598dedc)
+  * [Add Rule] button to add many new rules at one to a ticket filter (c03279d)
+
+### Improvements
+  * Fix several install and upgrade-related issues (fc10dcb, e1ca975, b709139,
+    abc8619, #2411, 832ea94, abb9a08, e3bb6c2, 8e373d4)
+  * Fix database timezone detection on Windows (#2297)
+  * Fix several tasks related issues (#2311, #2344, #2376, #2400, #2421, c3d48a9)
+  * Fix hiding of department-specific canned responses (#2315)
+  * Fix add and edit of ticket status list items (#2314)
+  * Fix incorrect definition of some ORM tables (#2324, 69839af)
+  * Fix crash rendering a closed ticket (#2328)
+  * Fix case-insensitive sorting of help topics (#2357)
+  * Fix several advanced search related issues (#2317, 3d4313f, ce3ceae,
+    b5e6d4e, 5a935ca)
+  * Fix incorrect SQL deleting a department (#2359)
+  * Fix incorrect array usage of department members for alerts (#2356)
+  * Add missing perm for view all agents' stats (#2358)
+  * Fix missing thread inline images from redactor image manager (be77da4)
+  * Fix updating configuration for file upload fields (2f4f9c1)
+  * Fix crash creating tickets with canned attachments (a156bba)
+  * Fix missing inline images in mailouts (84c9b54)
+  * Prefer submitted text over last-saved draft (46ab79b)
+  * Fix incorrect FAQ link in front-page sidebar (ea9dd5f)
+  * Fix missing assignee selection on new ticket by staff (7865eee)
+  * Fix issue details showing up on ticket edit (a183a98, 7fbd0f6)
+  * Fix inability to change SLA on some tickets (#2392)
+  * Fix auto-claim on new ticket by staff if a filter added a canned reply (c2ce2e9)
+  * Fix Dept::getMembersForAlerts() missing primary members (abc93efd)
+  * Fix inability to create tickets if missing the ASSIGN permission on all
+    depts (0c49e62)
+  * Fix inability as staff to reset a user's password (0006dd8)
+  * Render fields marked !visible and !editable, but required on the client
+    portal (7f55a0b)
+  * Fix sorting of help topics (a7cc49f, 08a32a4)
+  * Fix new message alert to a random staff member (d3685a9)
+  * Fix saving abbreviations on new list items (538087b)
+  * Fix parsing of some multi-part MIME messages (c57c22a)
+  * Fix numerous crashes
+
+### Performance and Security
+  * Improve performance loading the ticket view (6bba226, 4b12d54)
+  * Improve performance loading queue statistics (0a89510, 6b76402)
+  * Dramatically improve full-text search performance (167287d)
+
 osTicket 1.10
-=============
+==================
 ## Major New Features
 
 ### Internationalization, Phase III
diff --git a/include/ajax.forms.php b/include/ajax.forms.php
index f99870a9770d436cc36c028c068a5d25ba9061d9..520e8584a80b6107d4b63af9f7bd5b309694ac54 100644
--- a/include/ajax.forms.php
+++ b/include/ajax.forms.php
@@ -128,6 +128,20 @@ class DynamicFormsAjaxAPI extends AjaxController {
         include(STAFFINC_DIR . 'templates/list-items.tmpl.php');
     }
 
+    function previewListItem($list_id, $item_id) {
+
+        $list = DynamicList::lookup($list_id);
+        if (!$list)
+            Http::response(404, 'No such list item');
+
+        $list = CustomListHandler::forList($list);
+        if (!($item = $list->getItem( (int) $item_id)))
+            Http::response(404, 'No such list item');
+
+        $form = $list->getListItemBasicForm($item->ht, $item);
+        include(STAFFINC_DIR . 'templates/list-item-preview.tmpl.php');
+    }
+
     function saveListItem($list_id, $item_id) {
         global $thisstaff;
 
diff --git a/include/ajax.thread.php b/include/ajax.thread.php
index f53a55182d86b0de6348ea6d5b00d815d2465966..235a5540b09a649a73127976910a1d75ee409986 100644
--- a/include/ajax.thread.php
+++ b/include/ajax.thread.php
@@ -230,9 +230,13 @@ class ThreadAjaxAPI extends AjaxController {
 
         $errors = $info = array();
         if ($thread->updateCollaborators($_POST, $errors))
-            Http::response(201, sprintf('Recipients (%d of %d)',
-                        $thread->getNumActiveCollaborators(),
-                        $thread->getNumCollaborators()));
+            Http::response(201, $this->json_encode(array(
+                            'id' => $thread->getId(),
+                            'text' => sprintf('Recipients (%d of %d)',
+                                $thread->getNumActiveCollaborators(),
+                                $thread->getNumCollaborators())
+                            )
+                        ));
 
         if($errors && $errors['err'])
             $info +=array('error' => $errors['err']);
diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php
index 6131a294b747e7328cfe3a1dcfd88cb7ae8a7673..7bb040814714eeaa7b6f4400ef84d3e98391cccf 100644
--- a/include/class.dynamic_forms.php
+++ b/include/class.dynamic_forms.php
@@ -1347,11 +1347,13 @@ class DynamicFormEntryAnswer extends VerySimpleModel {
     }
 
     function getValue() {
-        if (!isset($this->_value) && isset($this->value)) {
+
+        if (!isset($this->_value)) {
             //XXX: We're settting the value here to avoid infinite loop
             $this->_value = false;
-            $this->_value = $this->getField()->to_php(
-                $this->get('value'), $this->get('value_id'));
+            if (isset($this->value))
+                $this->_value = $this->getField()->to_php(
+                        $this->get('value'), $this->get('value_id'));
         }
 
         return $this->_value;
@@ -1446,6 +1448,28 @@ class SelectionField extends FormField {
         return parent::getWidget($widgetClass);
     }
 
+    function display($value) {
+        global $thisstaff;
+
+        if (!is_array($value)
+                || !$thisstaff // Only agents can preview for now
+                || !($list=$this->getList()))
+            return parent::display($value);
+
+        $display = array();
+        foreach ($value as $k => $v) {
+            if (is_numeric($k)
+                    && ($i=$list->getItem((int) $k))
+                    && $i->hasProperties())
+                $display[] = $i->display();
+            else // Perhaps deleted  entry
+                $display[] = $v;
+        }
+
+        return implode(',', $display);
+
+    }
+
     function parse($value) {
 
         if (!($list=$this->getList()))
diff --git a/include/class.i18n.php b/include/class.i18n.php
index 3954ea0d0765a6777afc396c30e87f9fac251c05..a7e23a8ee63d3fabbcbb2e4568a25b60ab3eb3ef 100644
--- a/include/class.i18n.php
+++ b/include/class.i18n.php
@@ -490,6 +490,10 @@ class Internationalization {
         TextDomain::setDefaultDomain($domain);
         TextDomain::lookup()->setPath(I18N_DIR);
 
+        // Set the default locale to UTF-8. It will be changed by
+        // ::setLocaleForUser() later for web requests. See #2910
+        TextDomain::setLocale(LC_ALL, 'en_US.UTF-8');
+
         // User-specific translations
         function _N($msgid, $plural, $n) {
             return TextDomain::lookup()->getTranslation()
diff --git a/include/class.list.php b/include/class.list.php
index 84322f7006435453f857ee5ca82dd984b4555eff..69106502be674220c0f2c36c2293932554245471 100644
--- a/include/class.list.php
+++ b/include/class.list.php
@@ -68,8 +68,12 @@ interface CustomListItem {
     function getAbbrev();
     function getSortOrder();
 
+    function getList();
+    function getListId();
+
     function getConfiguration();
 
+    function hasProperties();
     function isEnabled();
     function isDeletable();
     function isEnableable();
@@ -664,10 +668,18 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem {
         $this->clearStatus(self::ENABLED);
     }
 
+    function hasProperties() {
+        return ($this->getForm() && $this->getForm()->getFields());
+    }
+
     function getId() {
         return $this->get('id');
     }
 
+    function getList() {
+        return $this->list;
+    }
+
     function getListId() {
         return $this->get('list_id');
     }
@@ -733,6 +745,10 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem {
         return $this->getConfigurationForm();
     }
 
+    function getFields() {
+        return $this->getForm()->getFields();
+    }
+
     function getVar($name) {
         $config = $this->getConfiguration();
         $name = mb_strtolower($name);
@@ -768,6 +784,15 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem {
         return $this->toString();
     }
 
+    function display() {
+        return sprintf('<a class="preview" href="#"
+                data-preview="#list/%d/items/%d/preview">%s</a>',
+                $this->getListId(),
+                $this->getId(),
+                $this->getValue()
+                );
+    }
+
     function update($vars, &$errors=array()) {
 
         if (!$vars['value']) {
@@ -1102,7 +1127,7 @@ implements CustomListItem, TemplateVariable {
         return $this->set($field, $this->get($field) | $flag);
     }
 
-    protected function hasProperties() {
+    function hasProperties() {
         return ($this->get('properties'));
     }
 
@@ -1254,6 +1279,11 @@ implements CustomListItem, TemplateVariable {
         return $this->_list;
     }
 
+    function getListId() {
+        if (($list = $this->getList()))
+            return $list->getId();
+    }
+
     function getConfigurationForm($source=null) {
         if (!$this->_form) {
             $config = $this->getConfiguration();
@@ -1283,6 +1313,10 @@ implements CustomListItem, TemplateVariable {
         return $this->_form;
     }
 
+    function getFields() {
+        return $this->getConfigurationForm()->getFields();
+    }
+
     function getConfiguration() {
 
         if (!$this->_settings) {
@@ -1367,6 +1401,15 @@ implements CustomListItem, TemplateVariable {
         return count($errors) === 0;
     }
 
+    function display() {
+        return sprintf('<a class="preview" href="#"
+                data-preview="#list/%d/items/%d/preview">%s</a>',
+                $this->getListId(),
+                $this->getId(),
+                $this->getLocalName()
+                );
+    }
+
     function update($vars, &$errors) {
         $fields = array('name', 'sort');
         foreach($fields as $k) {
diff --git a/include/class.plugin.php b/include/class.plugin.php
index 9d15bd3595a97adac8c6f0c67d45152ea8cb159b..d565b90594e34150278e698fdfc4b30595689f05 100644
--- a/include/class.plugin.php
+++ b/include/class.plugin.php
@@ -550,7 +550,10 @@ abstract class Plugin {
         $P = new Phar($phar);
         $sig = $P->getSignature();
         $info = array();
-        if ($r = dns_get_record($sig['hash'].'.'.self::$verify_domain, DNS_TXT)) {
+        $ignored = null;
+        if ($r = dns_get_record($sig['hash'].'.'.self::$verify_domain.'.',
+            DNS_TXT, $ignored, $ignored, true)
+        ) {
             foreach ($r as $rec) {
                 foreach (explode(';', $rec['txt']) as $kv) {
                     list($k, $v) = explode('=', trim($kv));
diff --git a/include/class.thread.php b/include/class.thread.php
index 31b2c5c3df702e43a2855bf736c7d93141a6b24d..b6fc9b5f15504d779dc7619d0e4a6fc384ab8bd2 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -197,15 +197,13 @@ class Thread extends VerySimpleModel {
             ));
         }
 
-        if ($cids) {
-            $this->collaborators->filter(array(
-                'thread_id' => $this->getId(),
-                Q::not(array('id__in' => $cids))
-            ))->update(array(
-                'updated' => SqlFunction::NOW(),
-                'isactive' => 0,
-            ));
-        }
+        $this->collaborators->filter(array(
+            'thread_id' => $this->getId(),
+            Q::not(array('id__in' => $cids ?: array(0)))
+        ))->update(array(
+            'updated' => SqlFunction::NOW(),
+            'isactive' => 0,
+        ));
 
         unset($this->ht['active_collaborators']);
         $this->_collaborators = null;
diff --git a/include/class.thread_actions.php b/include/class.thread_actions.php
index 428793399807d245f86cf0592041606784cff557..6650ee09810b2854ce7fe01f318813d6584d6706 100644
--- a/include/class.thread_actions.php
+++ b/include/class.thread_actions.php
@@ -164,6 +164,11 @@ JS
             $old = $original;
         }
 
+        // Move the attachments to the new entry
+        $old->attachments->update(array(
+            'object_id' => $entry->id
+        ));
+
         // Mark the new entry as edited (but not hidden nor guarded)
         $entry->flags = ($old->flags & ~(ThreadEntry::FLAG_HIDDEN | ThreadEntry::FLAG_GUARDED))
             | ThreadEntry::FLAG_EDITED;
diff --git a/include/cli/modules/deploy.php b/include/cli/modules/deploy.php
index 74a11f02c3b103a106ac495eb225f86f7acad818..eb76a165f0f9784d8b9ad1c7debf4edb735861d3 100644
--- a/include/cli/modules/deploy.php
+++ b/include/cli/modules/deploy.php
@@ -27,6 +27,9 @@ class Deployment extends Unpacker {
             'action'=>'store_true',
             'help'=>'Use `git ls-files -s` as files source. Eliminates
                 possibility of deploying untracked files');
+        $this->options['force'] = array('-f', '--force',
+            'action'=>'store_true',
+            'help'=>'Deploy all files, even if they have not changed');
         # super(*args);
         call_user_func_array(array('parent', '__construct'), func_get_args());
     }
@@ -45,13 +48,14 @@ class Deployment extends Unpacker {
      * Removes files from the deployment location that no longer exist in
      * the local repository
      */
-    function clean($local, $destination, $recurse=0, $exclude=false) {
+    function clean($local, $destination, $root, $recurse=0, $exclude=false) {
         $dryrun = $this->getOption('dry-run', false);
         $verbose = $dryrun || $this->getOption('verbose');
         $destination = rtrim($destination, '/') . '/';
         $contents = glob($destination.'{,.}*', GLOB_BRACE|GLOB_NOSORT);
         foreach ($contents as $i=>$file) {
-            if ($this->exclude($exclude, $file))
+            $relative = str_replace($root, "", $file);
+            if ($this->exclude($exclude, $relative))
                 continue;
             if (is_file($file)) {
                 $ltarget = $local . '/' . basename($file);
@@ -74,12 +78,13 @@ class Deployment extends Unpacker {
             foreach ($folders as $dir) {
                 if (in_array(basename($dir), array('.','..')))
                     continue;
-                elseif ($this->exclude($exclude, $dir))
+                $relative = str_replace($root, "", $dir);
+                if ($this->exclude($exclude, "$relative/"))
                     continue;
                 $this->clean(
                     $local.'/'.basename($dir),
                     $destination.basename($dir),
-                    $recurse - 1, $exclude);
+                    $root, $recurse - 1, $exclude);
             }
         }
         if (!$contents || !glob($destination.'{,.}*', GLOB_BRACE|GLOB_NOSORT)) {
@@ -123,6 +128,7 @@ class Deployment extends Unpacker {
             return false;
 
         $source = file_get_contents($src);
+        $original = crc32($source);
         $source = preg_replace(':<script(.*) src="([^"]+)\.js"></script>:',
             '<script$1 src="$2.js?'.$short.'"></script>',
             $source);
@@ -142,11 +148,20 @@ class Deployment extends Unpacker {
             "$1ini_set('$2', '0'); // Set by installer",
             $source);
 
-        return $source;
+        // return FALSE if the edited contents do not differ from the
+        // original contents
+        return $original != crc32($source) ? $source : false;
+    }
+
+    function isChanged($source, $hash=false) {
+        $local = str_replace($this->source.'/', '', $source);
+        $hash = $hash ?: $this->hashFile($source);
+        list($shash, $flag) = explode(':', $this->readManifest($local));
+        return ($flag === 'rewrite') ? $flag : $shash != $hash;
     }
 
-    function copyFile($source, $dest, $hash=false, $mode=0644) {
-        $contents = $this->getEditedContents($source);
+    function copyFile($source, $dest, $hash=false, $mode=0644, $contents=false) {
+        $contents = $contents ?: $this->getEditedContents($source);
         if ($contents === false)
             // Regular file
             return parent::copyFile($source, $dest, $hash, $mode);
@@ -154,7 +169,7 @@ class Deployment extends Unpacker {
         if (!file_put_contents($dest, $contents))
             $this->fail($dest.": Unable to apply rewrite rules");
 
-        $this->updateManifest($source, $hash);
+        $this->updateManifest($source, "$hash:rewrite");
         return chmod($dest, $mode);
     }
 
@@ -188,16 +203,21 @@ class Deployment extends Unpacker {
 
         $dryrun = $this->getOption('dry-run', false);
         $verbose = $this->getOption('verbose') || $dryrun;
+        $force = $this->getOption('force');
         while ($line = stream_get_line($pipes[1], 255, "\x00")) {
             list($mode, $hash, , $path) = preg_split('/\s+/', $line);
             $src = $source.$local.$path;
             if ($this->exclude($exclude, $src))
                 continue;
-            if (!$this->isChanged($src, $hash))
+            if (!$force && false === ($flag = $this->isChanged($src, $hash)))
                 continue;
             $dst = $destination.$path;
-            if ($verbose)
-                $this->stdout->write($dst."\n");
+            if ($verbose) {
+                $msg = $dst;
+                if (is_string($flag))
+                    $msg = "$msg ({$flag})";
+                $this->stdout->write("$msg\n");
+            }
             if ($dryrun)
                 continue;
             if (!is_dir(dirname($dst)))
@@ -241,7 +261,7 @@ class Deployment extends Unpacker {
             $exclusions);
         # Unpack the include folder
         $this->unpackage("$root/include/{,.}*", $include, -1,
-            array("*/include/ost-config.php"));
+            array("*/include/ost-config.php", "*.sw[a-z]"));
         if (!$options['dry-run']) {
             if ($include != "{$this->destination}/include/")
                 $this->change_include_dir($include);
@@ -249,14 +269,16 @@ class Deployment extends Unpacker {
 
         if ($options['clean']) {
             // Clean everything but include folder first
-            $this->clean($root, $this->destination, -1,
-                array($include, "setup/"));
-            $this->clean("$root/include", $include, -1,
+            $local_include = str_replace($this->destination, "", $include);
+            $this->clean($root, $this->destination, $this->destination, -1,
+                array($local_include, "setup/"));
+            $this->clean("$root/include", $include, $include, -1,
                 array("ost-config.php","settings.php","plugins/",
-                "*/.htaccess"));
+                "*/.htaccess", ".MANIFEST"));
         }
 
-        $this->writeManifest($this->destination);
+        if (!$options['dry-run'])
+            $this->writeManifest($this->destination);
     }
 }
 
diff --git a/include/cli/modules/i18n.php b/include/cli/modules/i18n.php
index f4c06caec1e1c34e7cb615ccb78957256b1a97b5..2adb14a9051d758dfd678631f3fe3fa37dcaa330 100644
--- a/include/cli/modules/i18n.php
+++ b/include/cli/modules/i18n.php
@@ -40,6 +40,12 @@ class i18n_Compiler extends Module {
             '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)'),
+        'zlib' => array('-z', '--zlib', 'default' => false,
+            'action' => 'store_true', 'help' => 'Compress PHAR with zlib'),
+        'bzip2' => array('-j', '--bzip2', 'default' => false,
+            'action' => 'store_true', 'help' => 'Compress PHAR with bzip2'),
+        'branch' => array('-b', '--branch', 'help' => 'Use a Crowdin branch
+            (other than the root)'),
     );
 
     var $epilog = "Note: If updating DNS, you will need to set
@@ -100,7 +106,7 @@ class i18n_Compiler extends Module {
                 $this->fail('API key is required');
             if (!$options['lang'])
                 $this->fail('Language code is required. See `list`');
-            $this->_build($options['lang']);
+            $this->_build($options['lang'], $options);
             break;
         case 'similar':
             $this->find_similar($options);
@@ -146,7 +152,7 @@ class i18n_Compiler extends Module {
         }
     }
 
-    function _build($lang) {
+    function _build($lang, $options) {
         list($code, $zip) = $this->_request("download/$lang.zip");
 
         if ($code !== 200)
@@ -164,19 +170,34 @@ class i18n_Compiler extends Module {
         @unlink(I18N_DIR."$lang.phar");
         $phar = new Phar(I18N_DIR."$lang.phar");
         $phar->startBuffering();
+        if ($options['zlib'])
+            $phar->compress(Phar::GZ, 'phar');
+        if ($options['bzip2'])
+            $phar->compress(Phar::BZ2, 'phar');
 
         $po_file = false;
 
+        $branch = false;
+        if ($options['branch'])
+            $branch = trim($options['branch'], '/') . '/';
         for ($i=0; $i<$zip->numFiles; $i++) {
             $info = $zip->statIndex($i);
+            if ($branch && strpos($info['name'], $branch) !== 0) {
+                // Skip files not part of the named branch
+                continue;
+            }
             $contents = $zip->getFromIndex($i);
             if (!$contents)
                 continue;
-            if (strpos($info['name'], '/messages.po') !== false) {
+            if (fnmatch('*/messages*.po', $info['name']) !== false) {
                 $po_file = $contents;
                 // Don't add the PO file as-is to the PHAR file
                 continue;
             }
+            elseif (!$branch && !file_exists(I18N_DIR . 'en_US/' . $info['name'])) {
+                // Skip files in (other) branches
+                continue;
+            }
             $phar->addFromString($info['name'], $contents);
         }
 
@@ -190,9 +211,9 @@ class i18n_Compiler extends Module {
         }
         foreach ($langs as $l) {
             list($code, $js) = $this->_http_get(
-                'http://imperavi.com/webdownload/redactor/lang/?lang='
-                .strtolower($l));
-            if ($code == 200 && ($js != 'File not found')) {
+                sprintf('https://imperavi.com/download/redactor/langs/%s/',
+                    strtolower($l)));
+            if ($code == 200 && strlen($js) > 100) {
                 $phar->addFromString('js/redactor.js', $js);
                 break;
             }
@@ -277,7 +298,9 @@ class i18n_Compiler extends Module {
         $po_header = Mail_Parse::splitHeaders($mo['']);
         $info = array(
             'Build-Date' => date(DATE_RFC822),
+            'Phrases-Version' => $po_header['X-Osticket-Major-Version'],
             'Build-Version' => trim(`git describe`),
+            'Build-Major-Version' => MAJOR_VERSION,
             'Language' => $po_header['Language'],
             #'Phrases' =>
             #'Translated' =>
@@ -308,6 +331,8 @@ class i18n_Compiler extends Module {
 
         if (!function_exists('openssl_get_privatekey'))
             $this->fail('OpenSSL extension required for signing');
+        if (!$options['pkey'] || !file_exists($options['pkey']))
+            $this->fail('Signing private key (-P) required');
         $private = openssl_get_privatekey(
                 file_get_contents($options['pkey']));
         if (!$private)
diff --git a/include/cli/modules/unpack.php b/include/cli/modules/unpack.php
index 75b73353deb0f212bed2a65cb420707c5b3eaedd..75fa1090f3790f7eefed5fac82581fd075de8a60 100644
--- a/include/cli/modules/unpack.php
+++ b/include/cli/modules/unpack.php
@@ -99,7 +99,7 @@ class Unpacker extends Module {
         if (!is_file($path))
             return null;
 
-        if (!preg_match_all('/^(\w+) (.+)$/mu', file_get_contents($path),
+        if (!preg_match_all('/^([\w:,]+) (.+)$/mu', file_get_contents($path),
             $lines, PREG_PATTERN_ORDER)
         ) {
             return null;
@@ -156,6 +156,7 @@ class Unpacker extends Module {
     function unpackage($folder, $destination, $recurse=0, $exclude=false) {
         $dryrun = $this->getOption('dry-run', false);
         $verbose = $this->getOption('verbose') || $dryrun;
+        $force = $this->getOption('force', false);
         if (substr($destination, -1) !== '/')
             $destination .= '/';
         foreach (glob($folder, GLOB_BRACE|GLOB_NOSORT) as $file) {
@@ -164,10 +165,15 @@ class Unpacker extends Module {
             if (is_file($file)) {
                 $target = $destination . basename($file);
                 $hash = $this->hashFile($file);
-                if (is_file($target) && !$this->isChanged($file, $hash))
+                if (!$force && is_file($target)
+                        && false === ($flag = $this->isChanged($file, $hash)))
                     continue;
-                if ($verbose)
-                    $this->stdout->write($target."\n");
+                if ($verbose) {
+                    $msg = $target;
+                    if (is_string($flag))
+                        $msg = "$msg ({$flag})";
+                    $this->stdout->write("$msg\n");
+                }
                 if ($dryrun)
                     continue;
                 if (!is_dir($destination))
diff --git a/include/staff/system.inc.php b/include/staff/system.inc.php
index ee70faefb3dc4fd878342e65edf65bf28865d34f..20198b8f88c040d7592a9d5c512bb25c3fc5029b 100644
--- a/include/staff/system.inc.php
+++ b/include/staff/system.inc.php
@@ -171,18 +171,24 @@ if (!$lv) { ?>
 <?php
     foreach (Internationalization::availableLanguages() as $info) {
         $p = $info['path'];
-        if ($info['phar']) $p = 'phar://' . $p;
-        if (file_exists($p . '/MANIFEST.php')) {
-            $manifest = (include $p . '/MANIFEST.php'); ?>
+        if ($info['phar'])
+            $p = 'phar://' . $p;
+?>
     <h3><strong><?php echo Internationalization::getLanguageDescription($info['code']); ?></strong>
         &mdash; <?php echo $manifest['Language']; ?>
-<?php       if ($info['phar'])
-                Plugin::showVerificationBadge($info['path']);
-            ?>
+<?php   if ($info['phar'])
+            Plugin::showVerificationBadge($info['path']); ?>
         </h3>
-        <div><?php echo __('Version'); ?>: <?php echo $manifest['Version']; ?>,
-            <?php echo __('Built'); ?>: <?php echo $manifest['Build-Date']; ?>
+        <div><?php echo sprintf('<code>%s</code> — %s', $info['code'],
+                str_replace(ROOT_DIR, '', $info['path'])); ?>
+<?php   if (file_exists($p . '/MANIFEST.php')) {
+            $manifest = (include $p . '/MANIFEST.php'); ?>
+            <br/> <?php echo __('Version'); ?>: <?php echo $manifest['Version'];
+                ?>, <?php echo sprintf(__('for version %s'),
+                    'v'.($manifest['Phrases-Version'] ?: '1.9')); ?>
+            <br/> <?php echo __('Built'); ?>: <?php echo $manifest['Build-Date']; ?>
+<?php   } ?>
         </div>
-<?php }
+<?php
     } ?>
 </div>
diff --git a/include/staff/templates/list-item-preview.tmpl.php b/include/staff/templates/list-item-preview.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..3ffcd1998faf69ef71af61c492f049c93d57ece3
--- /dev/null
+++ b/include/staff/templates/list-item-preview.tmpl.php
@@ -0,0 +1,27 @@
+<?php
+$name = $item->getValue();
+if ($abbrev=$item->getAbbrev())
+    $name = sprintf('%s (%s)', $name, $abbrev);
+
+?>
+<h2><?php echo Format::htmlchars($name); ?></h2>
+<hr/>
+
+<?php
+if ($item->hasProperties()) { ?>
+<div>
+    <table class="custom-info" width="100%">
+        <?php
+        foreach ($item->getFields() as $f) {
+            if (!$f->isVisible()) continue;
+        ?>
+            <tr><td style="width:30%;"><?php echo
+                Format::htmlchars($f->get('label')); ?>:</td>
+            <td><?php echo $f->display($f->value); ?></td>
+            </tr>
+        <?php }
+        ?>
+    </table>
+</div>
+<?php
+} ?>
diff --git a/include/staff/templates/list-item-row.tmpl.php b/include/staff/templates/list-item-row.tmpl.php
index 574f28e97d8cc2ca9b4e1c00c96e1b293f1aee41..b54fee23f2f4aec32524e5ed98c3e61b12eb1c3a 100644
--- a/include/staff/templates/list-item-row.tmpl.php
+++ b/include/staff/templates/list-item-row.tmpl.php
@@ -7,12 +7,17 @@
             <input type="checkbox" value="<?php echo $id; ?>" class="mass nowarn"/>
         </td>
         <td>
-            <a class="field-config"
+            <a class="field-config preview"
                style="overflow:inherit"
                href="#list/<?php
                 echo $list->getId(); ?>/item/<?php
                 echo $id ?>/update"
                id="item-<?php echo $id; ?>"
+               data-preview="<?php echo
+                sprintf('#/list/%d/items/%d/preview',
+                       $item->getListId(),
+                       $item->getId());
+                ?>"
             ><?php
                 echo sprintf('<i class="icon-edit" %s></i> ',
                         ($prop_fields && !$item->getConfiguration())
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index fc37d5912a39b5bd6f8254a64772a8d9840e1f16..43097ab357e17b35f6a75b561a6a1d348739d804 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -240,7 +240,7 @@ if($ticket->isOverdue())
             <table border="0" cellspacing="" cellpadding="4" width="100%">
                 <tr>
                     <th width="100"><?php echo __('Status');?>:</th>
-                    <td><?php echo ($S = $ticket->getStatus()) ? $S->getLocalName() : ''; ?></td>
+                    <td><?php echo ($S = $ticket->getStatus()) ? $S->display() : ''; ?></td>
                 </tr>
                 <tr>
                     <th><?php echo __('Priority');?>:</th>
@@ -584,7 +584,8 @@ if ($errors['err'] && isset($_POST['a'])) {
                     <label><strong><?php echo __('Collaborators'); ?>:</strong></label>
                 </td>
                 <td>
-                    <input type='checkbox' value='1' name="emailcollab" id="emailcollab"
+                    <input type='checkbox' value='1' name="emailcollab"
+                    id="t<?php echo $ticket->getThreadId(); ?>-emailcollab"
                         <?php echo ((!$info['emailcollab'] && !$errors) || isset($info['emailcollab']))?'checked="checked"':''; ?>
                         style="display:<?php echo $ticket->getThread()->getNumCollaborators() ? 'inline-block': 'none'; ?>;"
                         >
diff --git a/js/filedrop.field.js b/js/filedrop.field.js
index ce0bf154496ae63cdccdbe5c5537649bfde3f65d..b0bc57f15c0df300cd6a3bd1d9fce26d8d8440f5 100644
--- a/js/filedrop.field.js
+++ b/js/filedrop.field.js
@@ -247,7 +247,7 @@
     files: [],
     deletable: true,
     shim: !window.FileReader,
-    queuefiles: 4
+    queuefiles: 1
   };
 
   $.fn.filedropbox.messages = {
diff --git a/scp/ajax.php b/scp/ajax.php
index f01cf70b4bb00f0b4b4a6f17bab1c0d5d92e5a2c..de27157dc74093cf638db8a5a3e0550384e00857 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -71,6 +71,7 @@ $dispatcher = patterns('',
         url_get('^(?P<list>\w+)/items/search$', 'searchListItems'),
         url_get('^(?P<list>\w+)/item/(?P<id>\d+)/update$', 'getListItem'),
         url_post('^(?P<list>\w+)/item/(?P<id>\d+)/update$', 'saveListItem'),
+        url_get('^(?P<list>\w+)/items/(?P<id>\d+)/preview$', 'previewListItem'),
         url('^(?P<list>\w+)/item/add$', 'addListItem'),
         url('^(?P<list>\w+)/import$', 'importListItems'),
         url('^(?P<list>\w+)/manage$', 'massManageListItems'),
diff --git a/scp/css/scp.css b/scp/css/scp.css
index 2b12d668b35e890300911594ede78abbd0fc131c..86632ba2766ff353f499c2c13b1d4b8b60d68b16 100644
--- a/scp/css/scp.css
+++ b/scp/css/scp.css
@@ -2322,7 +2322,9 @@ tr.disabled th {
 }
 
 .label {
-    float: right;
+    display: inline-block;
+    position: relative;
+    bottom: 1px;
     margin-bottom: 4px;
     font-size: 11px;
     padding: 0px 7px;
diff --git a/scp/js/scp.js b/scp/js/scp.js
index 1e2dfa304ef14df52bd445a9bbb91e4797dc2b29..3c04b4a9cb6e17d33fd1ea22841433c1c5aaabbc 100644
--- a/scp/js/scp.js
+++ b/scp/js/scp.js
@@ -987,8 +987,9 @@ $(document).on('click', 'a.collaborator, a.collaborators', function(e) {
     e.preventDefault();
     var url = 'ajax.php/'+$(this).attr('href').substr(1);
     $.dialog(url, 201, function (xhr) {
-       $('input#emailcollab').show();
-       $('#recipients').text(xhr.responseText);
+       var resp = $.parseJSON(xhr.responseText);
+       $('input#t'+resp.id+'-emailcollab').show();
+       $('#t'+resp.id+'-recipients').text(resp.text);
        $('.tip_box').remove();
     }, {
         onshow: function() { $('#user-search').focus(); }
diff --git a/setup/inc/file-unclean.inc.php b/setup/inc/file-unclean.inc.php
index d84fdefd1586b13623a1169fe75c45840d5bf9dc..d9992f7f83cf0228ac37d684e29f9529083616f3 100644
--- a/setup/inc/file-unclean.inc.php
+++ b/setup/inc/file-unclean.inc.php
@@ -4,7 +4,7 @@ if(!defined('SETUPINC')) die('Kwaheri!');
     <div id="main">
             <h1 style="color:#FF7700;"><?php echo __('osTicket is already installed?');?></h1>
             <div id="intro">
-             <p><?php echo sprintf(__('Configuration file already changed - which could mean osTicket is already installed or the config file is currupted. If you are trying to upgrade osTicket, then go to %s Admin Panel %s.'), '<a href="../scp/admin.php" >', '</a>');?></p>
+             <p><?php echo sprintf(__('Configuration file already changed - which could mean osTicket is already installed or the config file is corrupted. If you are trying to upgrade osTicket, then go to %s Admin Panel %s.'), '<a href="../scp/admin.php" >', '</a>');?></p>
 
              <p><?php echo __('If you believe this is in error, please try replacing the config file with a unchanged template copy and try again or get technical help.');?></p>
              <p><?php echo sprintf(__('Refer to the %s Installation Guide %s on the wiki for more information.'), '<a target="_blank" href="http://osticket.com/wiki/Installation">', '</a>');?></p>