diff --git a/WHATSNEW.md b/WHATSNEW.md
index 8e4aa862036de3cfb56c46d19dab9a11717edb22..13f764878f5feb5fe7a9377bafb469d565e917ce 100644
--- a/WHATSNEW.md
+++ b/WHATSNEW.md
@@ -185,6 +185,22 @@ database engines other than MySQL. The ORM was originally introduced in
 osTicket v1.8.0, but has seen the greatest boost in capability in this release.
 About 47% of the SQL queries are removed between v1.9.7 and v1.10
 
+osTicket v1.9.9
+===============
+### Enhancements
+  * Properly balance stripped and invalid HTML (#2145)
+  * Add MANIFEST file to deployment process and retire duplicate code for packaging (#2052)
+
+### Improvements
+  * Fix inability to configure LDAP and S3 plugins (*regression*) (59337b3)
+  * Fix incorrect whitespace in search indexed HTML content (#2111)
+  * Add support for invalid `multipart/relative` content type (aaf1b74)
+  * Force line breaks for very long HTML lines (56cc709)
+
+### Performance and Security
+  * Fix slow query for ticket counts for large datasets (c4ace2d)
+  * Fix slow thread load query (thanks @torohill) (7b7e855)
+
 osTicket v1.9.8.1
 =================
 ### Enhancements
diff --git a/include/class.cli.php b/include/class.cli.php
index 01f829dddb05dfaa1820b5e4b91a76da21f22e63..a4e3cfc16ab619d32fd854778b09dab81c4569fd 100644
--- a/include/class.cli.php
+++ b/include/class.cli.php
@@ -272,7 +272,7 @@ class Module {
                 continue;
             }
             // Allow multiple simple args like -Dvt
-            if ($arg[0] == '-' && strlen($arg) > 2) {
+            if ($arg[0] == '-' && $arg[1] != '-' && strlen($arg) > 2) {
                 foreach (str_split(substr($arg, 2)) as $O)
                     array_unshift($argv, "-{$O}");
                 $arg = substr($arg, 0, 2);
diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php
index 479e94736fd3c59e263e0ab017ad968837e94589..8af03362e532eb8a797ed3f52064ab5165d21d6f 100644
--- a/include/class.dynamic_forms.php
+++ b/include/class.dynamic_forms.php
@@ -1320,6 +1320,8 @@ class DynamicFormEntry extends VerySimpleModel {
     static function create($ht=false, $data=null) {
         $inst = parent::create($ht);
         $inst->set('created', new SqlFunction('NOW'));
+        if ($data)
+            $inst->setSource($data);
         foreach ($inst->getDynamicFields() as $field) {
             if (!($impl = $field->getImpl($field)))
                 continue;
diff --git a/include/class.filter_action.php b/include/class.filter_action.php
index 4cdac2e7a636ca4eecdb0f4b1a59186bd9bb423d..577a07d834dce6714b653ae83b69a889232d395d 100644
--- a/include/class.filter_action.php
+++ b/include/class.filter_action.php
@@ -414,7 +414,7 @@ class FA_AssignTopic extends TriggerAction {
     }
 
     function getConfigurationOptions() {
-        $choices = HelpTopic::getAllHelpTopics();
+        $choices = Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED);
         return array(
             'topic_id' => new ChoiceField(array(
                 'configuration' => array('prompt' => __('Unchanged')),
diff --git a/include/class.format.php b/include/class.format.php
index 85c54d234e02fb02f0aa2cb8bc91437a3127240b..4e5bb04b832d597104de8f287d495e14b953bf0f 100644
--- a/include/class.format.php
+++ b/include/class.format.php
@@ -165,7 +165,7 @@ class Format {
             $html = $doc->saveHTML();
             $html = preg_replace('`^<!DOCTYPE.+?>|<\?xml .+?>|</?html>|</?body>|</?head>|<meta .+?/?>`', '', $html); # <?php
         }
-        return preg_replace('`^<div>|</div>$`', '', $html);
+        return preg_replace('`^<div>|</div>$`', '', trim($html));
     }
 
     function html($html, $config=array()) {
diff --git a/include/class.search.php b/include/class.search.php
index 109d67a9c9f7b766e46b032cdd3d6f37d87738e3..efc9bc4a188ebe2253431359f8c919c7a17a34f9 100644
--- a/include/class.search.php
+++ b/include/class.search.php
@@ -389,7 +389,7 @@ class MysqlSearchBackend extends SearchBackend {
         $galera = db_result(db_query($sql));
 
         if ($galera && !$mysql56)
-            throw new Exception('Galera cannot be used with MyISAM tables');
+            throw new Exception('Galera cannot be used with MyISAM tables. Upgrade to MariaDB 10 / MySQL 5.6 is required');
         $engine = $galera ? 'InnodB' : ($mysql56 ? '' : 'MyISAM');
         if ($engine)
             $engine = 'ENGINE='.$engine;
@@ -402,12 +402,17 @@ class MysqlSearchBackend extends SearchBackend {
             primary key `object` (`object_type`, `object_id`),
             fulltext key `search` (`title`, `content`)
         ) $engine CHARSET=utf8";
-        return db_query($sql);
+        if (!db_query($sql))
+            return false;
+
+        // Start rebuilding the index
+        $this->getConfig()->set('reindex', 1);
+        return true;
     }
 
     /**
      * Cooperates with the cron system to automatically find content that is
-     * not index in the _search table and add it to the index.
+     * not indexed in the _search table and add it to the index.
      */
     function IndexOldStuff() {
         $class = get_class();
diff --git a/include/class.signal.php b/include/class.signal.php
index a98d4cf63a39979382bd668d857994db0afbeb12..f1c46e903f7aabd7cec620540d2699ad698a471c 100644
--- a/include/class.signal.php
+++ b/include/class.signal.php
@@ -95,7 +95,7 @@ class Signal {
                 continue;
             elseif ($check && !call_user_func_array($check, array($object, $data)))
                 continue;
-            call_user_func_array($callable, array($object, $data));
+            call_user_func_array($callable, array($object, &$data));
         }
     }
 }
diff --git a/include/class.ticket.php b/include/class.ticket.php
index f13fc2c5855bf2e0c51b812603733c4f234321c4..f0e7936558c91151ca37d00b015492efbe0af2f2 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -1361,31 +1361,36 @@ implements RestrictedAccess, Threadable {
         $this->lastupdate = SqlFunction::NOW();
         $this->save();
 
-        // Auto-assign to closing staff or last respondent
-        // If the ticket is closed and auto-claim is not enabled then put the
-        // ticket back to unassigned pool.
-        if ($this->isClosed() && !$cfg->autoClaimTickets()) {
-            $this->setStaffId(0);
-        } elseif(!($staff=$this->getStaff()) || !$staff->isAvailable()) {
-            // Ticket has no assigned staff -  if auto-claim is enabled then
-            // try assigning it to the last respondent (if available)
-            // otherwise leave the ticket unassigned.
-            if ($cfg->autoClaimTickets() //Auto claim is enabled.
-                    && ($lastrep=$this->getLastRespondent())
-                    && $lastrep->isAvailable()) {
-                $this->setStaffId($lastrep->getId()); //direct assignment;
-            } else {
-                $this->setStaffId(0); //unassign - last respondent is not available.
-            }
-        }
 
         // Reopen if closed AND reopenable
         // We're also checking autorespond flag because we don't want to
         // reopen closed tickets on auto-reply from end user. This is not to
         // confused with autorespond on new message setting
-        if ($autorespond && $this->isClosed() && $this->isReopenable())
+        if ($autorespond && $this->isClosed() && $this->isReopenable()) {
             $this->reopen();
 
+            // Auto-assign to closing staff or last respondent
+            // If the ticket is closed and auto-claim is not enabled then put the
+            // ticket back to unassigned pool.
+            if (!$cfg->autoClaimTickets()) {
+                $this->setStaffId(0);
+            }
+            elseif (!($staff = $this->getStaff()) || !$staff->isAvailable()) {
+                // Ticket has no assigned staff -  if auto-claim is enabled then
+                // try assigning it to the last respondent (if available)
+                // otherwise leave the ticket unassigned.
+                if (($lastrep = $this->getLastRespondent())
+                    && $lastrep->isAvailable()
+                ) {
+                    $this->setStaffId($lastrep->getId()); //direct assignment;
+                }
+                else {
+                    // unassign - last respondent is not available.
+                    $this->setStaffId(0);
+                }
+            }
+        }
+
         // Figure out the user
         if ($this->getOwnerId() == $message->getUserId())
             $user = new TicketOwner(
diff --git a/include/class.topic.php b/include/class.topic.php
index 776f99e896905946fb5d973b2fc8b930e1104f5a..77e85ec442243fec663224d944c02fc90a8a8c58 100644
--- a/include/class.topic.php
+++ b/include/class.topic.php
@@ -343,7 +343,7 @@ implements TemplateVariable {
             if (!$disabled && $info['disabled'])
                 continue;
             if ($disabled === self::DISPLAY_DISABLED && $info['disabled'])
-                $n .= " &mdash; ".__("(disabled)");
+                $n .= " - ".__("(disabled)");
             $requested_names[$id] = $n;
         }
 
diff --git a/include/class.variable.php b/include/class.variable.php
index 76acb1d0f2d2d789e312ee4554db8c40b0e4b630..da249610c2de7482cc09b6a4ee283334296a85ac 100644
--- a/include/class.variable.php
+++ b/include/class.variable.php
@@ -27,7 +27,7 @@ class VariableReplacer {
 
     var $errors;
 
-    function VariableReplacer($start_delim='%{', $end_delim='}') {
+    function VariableReplacer($start_delim='(?:%{|%%7B)', $end_delim='(?:}|%7D)') {
 
         $this->start_delim = $start_delim;
         $this->end_delim = $end_delim;
@@ -164,7 +164,8 @@ class VariableReplacer {
         $vars = array();
         foreach($result[0] as $k => $v) {
             if(isset($vars[$v])) continue;
-            $val=$this->_resolveVar($result[1][$k]);
+            // Format::html_balance() may urlencode() the contents here
+            $val=$this->_resolveVar(rawurldecode($result[1][$k]));
             if($val!==false)
                 $vars[$v] = $val;
         }
diff --git a/include/cli/modules/deploy.php b/include/cli/modules/deploy.php
index 7a795e6630a2e7a521c250fb4a71f0dd54b7db5e..74a11f02c3b103a106ac495eb225f86f7acad818 100644
--- a/include/cli/modules/deploy.php
+++ b/include/cli/modules/deploy.php
@@ -201,7 +201,7 @@ class Deployment extends Unpacker {
             if ($dryrun)
                 continue;
             if (!is_dir(dirname($dst)))
-                mkdir(dirname($dst), 0751, true);
+                mkdir(dirname($dst), 0755, true);
             $this->copyFile($src, $dst, $hash, octdec($mode));
         }
     }
diff --git a/include/cli/modules/unpack.php b/include/cli/modules/unpack.php
index add4abf5c9d68145aa7187c998ee77ebbb98b022..5635a03c98726ad7232ba066ac384b75d2ae80f6 100644
--- a/include/cli/modules/unpack.php
+++ b/include/cli/modules/unpack.php
@@ -171,7 +171,7 @@ class Unpacker extends Module {
                 if ($dryrun)
                     continue;
                 if (!is_dir($destination))
-                    mkdir($destination, 0751, true);
+                    mkdir($destination, 0755, true);
                 $this->copyFile($file, $target, $hash);
             }
         }
diff --git a/scp/tickets.php b/scp/tickets.php
index f3bbe8d7c00f6409974251bf5326aa7c9ec670ff..ae4f15349c25064c85de275a402908741cc74ab6 100644
--- a/scp/tickets.php
+++ b/scp/tickets.php
@@ -173,7 +173,7 @@ if($_POST && !$errors):
                      $errors['assignId']= sprintf('%s - %s',
                              __('Invalid assignee'),
                              __('get technical support'));
-                 elseif ($_POST['assignId'][0]!='s'
+                 elseif ($_POST['assignId'][0]=='s'
                          && $dept->assignMembersOnly()
                          && !$dept->isMember($id)) {
                      $errors['assignId'] = sprintf('%s. %s',