diff --git a/WHATSNEW.md b/WHATSNEW.md
index ce06e6a8acee174be1ff20dd5a6822deb904a771..c580634d187b26330c7fcecc42b29e55386ca6fc 100644
--- a/WHATSNEW.md
+++ b/WHATSNEW.md
@@ -1,3 +1,52 @@
+osTicket v1.9.5
+===============
+### Enhancements
+  * Add support for organization vars in templates
+    (`%{ticket.user.organization...}`) (#1561)
+  * Canned responses feature can now be disabled (#1562)
+  * Drop link redirection through l.php (#1640)
+  * Use unified file download script (#1641). Links can now be shared with
+    external users and accessed without authenticating.
+  * Ticket filters support matching and banning based on the Reply-To user
+    information (#1645)
+
+### Improvements
+  * Remove custom data when users are deleted (#1492)
+  * Fix matching of ticket number in subject (regression in v1.9.4) (#1486)
+  * Several minor translatable strings (#1441, #1489, #1560), thanks @Chefkeks
+  * Fix invalid UTF-8 chars PDF error for empty thread title (regression in
+    v1.9.4) (#1512)
+  * Consider auto response checkbox and department setting for new ticket by
+    staff (#1509)
+  * Fix PHP crash if `finfo` extension is missing (#1437)
+  * Fix export of choice field items (#1436)
+  * Properly handle alert and auto response flags from API (#1435), thanks
+    @stevepacker
+  * Fix current value of choice fields if set to boolean false (#1466)
+  * Do not reopen tickets for automated responses (#1529)
+  * Properly handle uppercase file extensions in file field configuration
+    (#1549)
+  * Fix release of ticket lock when navigating away from ticket view (#1552)
+  * Display FAQ article consistently on client portal (#1553)
+  * Avoid wrapping password reset URLs on text emails (#1558)
+  * Fix field requirement for clients when only required for agents (#1559)
+  * Fix language selection for new email template group (#1563)
+  * Fix incorrect status of new ticket if opened as `closed` and assigning to
+    an agent (#1565)
+  * Forbid disabling the only active administrator (#1569)
+  * Searching for tickets searches to midnight of the end date (#1572), thanks
+    @grintor
+  * Fix rejection of tickets by filter, even if a previous matching filter
+    would stop on match (#1644)
+  * Fix matching of `User / Email Address` in ticket filters (#1644)
+  * Properly HTML escape thread bodies when quoting (#1637)
+  * Use department email for agent alerts (#1555)
+  * Skip team assignment alert on new ticket if assigned to an agent (fddb3c7)
+  * Use custom form name as the page title when editing (#1646)
+
+### Performance and Security
+  * Fix possible XSS vulnerability in sortable table view pages (#1639)
+
 osTicket v1.9.4
 ===============
 ### Major New Features
diff --git a/api/api.inc.php b/api/api.inc.php
index fac03bccd1d848308809e78343838d5401325771..d1440c8b3a7e3c447dc6ab30cc0b6e31ade86c87 100644
--- a/api/api.inc.php
+++ b/api/api.inc.php
@@ -17,7 +17,8 @@ file_exists('../main.inc.php') or die('System Error');
 
 // Disable sessions for the API. API should be considered stateless and
 // shouldn't chew up database records to store sessions
-define('DISABLE_SESSION', true);
+if (!defined('DISABLE_SESSION'))
+    define('DISABLE_SESSION', true);
 
 require_once('../main.inc.php');
 require_once(INCLUDE_DIR.'class.http.php');
diff --git a/api/http.php b/api/http.php
index 2efd1a98c271d103987ffd2eda667e738e5812cd..3f8f721ec6f7b2af531b7d577858d18ce90a0890 100644
--- a/api/http.php
+++ b/api/http.php
@@ -13,6 +13,10 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
+// Use sessions — it's important for SSO authentication, which uses
+// /api/auth/ext
+define('DISABLE_SESSION', false);
+
 require 'api.inc.php';
 
 # Include the main api urls
diff --git a/attachment.php b/attachment.php
deleted file mode 100644
index 8e45d8e155c83cae6c31697f49f5b8bfd3910a9e..0000000000000000000000000000000000000000
--- a/attachment.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-/*********************************************************************
-    attachment.php
-
-    Attachments interface for clients.
-    Clients should never see the dir paths.
-
-    Peter Rotich <peter@osticket.com>
-    Copyright (c)  2006-2013 osTicket
-    http://www.osticket.com
-
-    Released under the GNU General Public License WITHOUT ANY WARRANTY.
-    See LICENSE.TXT for details.
-
-    vim: expandtab sw=4 ts=4 sts=4:
-**********************************************************************/
-require('secure.inc.php');
-require_once(INCLUDE_DIR.'class.attachment.php');
-// Basic checks
-if (!$thisclient
-        || !$_GET['id']
-        || !$_GET['h']
-        || !($attachment=Attachment::lookup($_GET['id']))
-        || !($file=$attachment->getFile())
-        || strcasecmp(trim($_GET['h']), $file->getDownloadHash())
-        || !($object=$attachment->getObject())
-        || !$object instanceof ThreadEntry
-        || !($ticket=$object->getThread()->getObject())
-        || !$ticket instanceof Ticket
-        )
-    Http::response(404, __('Unknown or invalid file'));
-
-if (!$ticket->checkUserAccess($thisclient))
-    die(__('Access Denied'));
-
-// Download the file..
-$file->download();
-?>
diff --git a/file.php b/file.php
new file mode 100644
index 0000000000000000000000000000000000000000..22cc8094be840bc25b322a6d571921f697beb8f8
--- /dev/null
+++ b/file.php
@@ -0,0 +1,39 @@
+<?php
+/*********************************************************************
+    file.php
+
+    File download facilitator for clients
+
+    Peter Rotich <peter@osticket.com>
+    Jared Hancock <jared@osticket.com>
+    Copyright (c)  2006-2014 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+    See LICENSE.TXT for details.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+require('client.inc.php');
+require_once(INCLUDE_DIR.'class.file.php');
+
+//Basic checks
+if (!$_GET['key']
+    || !$_GET['signature']
+    || !$_GET['expires']
+    || !($file = AttachmentFile::lookup($_GET['key']))
+) {
+    Http::response(404, __('Unknown or invalid file'));
+}
+
+// Validate session access hash - we want to make sure the link is FRESH!
+// and the user has access to the parent ticket!!
+if ($file->verifySignature($_GET['signature'], $_GET['expires'])) {
+    if (($s = @$_GET['s']) && strpos($file->getType(), 'image/') === 0)
+        return $file->display($s);
+
+    // Download the file..
+    $file->download(@$_GET['disposition'] ?: false, $_GET['expires']);
+}
+// else
+Http::response(404, __('Unknown or invalid file'));
diff --git a/image.php b/image.php
deleted file mode 100644
index 5b7f27283ba23175ff12772f4212a41859ea3e18..0000000000000000000000000000000000000000
--- a/image.php
+++ /dev/null
@@ -1,31 +0,0 @@
-<?php
-/*********************************************************************
-    image.php
-
-    Simply downloads the file...on hash validation as follows;
-
-    * Hash must be 64 chars long.
-    * First 32 chars is the perm. file hash
-    * Next 32 chars  is md5(file_id.session_id().file_hash)
-
-    Peter Rotich <peter@osticket.com>
-    Copyright (c)  2006-2013 osTicket
-    http://www.osticket.com
-
-    Released under the GNU General Public License WITHOUT ANY WARRANTY.
-    See LICENSE.TXT for details.
-
-    vim: expandtab sw=4 ts=4 sts=4:
-**********************************************************************/
-
-require('client.inc.php');
-require_once(INCLUDE_DIR.'class.file.php');
-$h=trim($_GET['h']);
-//basic checks
-if(!$h  || strlen($h)!=64  //32*2
-        || !($file=AttachmentFile::lookup(substr($h,0,32))) //first 32 is the file hash.
-        || strcasecmp($h, $file->getDownloadHash())) //next 32 is file id + session hash.
-    Http::response(404, __('Unknown or invalid file'));
-
-$file->display();
-?>
diff --git a/include/ajax.draft.php b/include/ajax.draft.php
index 2687ad94b53fb28e074d856394489aeae2c435db..fa9d5c9b638f7ea81af2a8f78a94d1331591197c 100644
--- a/include/ajax.draft.php
+++ b/include/ajax.draft.php
@@ -113,7 +113,7 @@ class DraftAjaxAPI extends AjaxController {
             'content_id' => 'cid:'.$f->getKey(),
             // Return draft_id to connect the auto draft creation
             'draft_id' => $draft->getId(),
-            'filelink' => sprintf('image.php?h=%s', $f->getDownloadHash())
+            'filelink' => $f->getDownloadUrl(false, 'inline'),
         ));
     }
 
@@ -335,7 +335,7 @@ class DraftAjaxAPI extends AjaxController {
         );
         while (list($id, $type) = db_fetch_row($res)) {
             $f = AttachmentFile::lookup($id);
-            $url = 'image.php?h='.$f->getDownloadHash();
+            $url = $f->getDownloadUrl();
             $files[] = array(
                 'thumb'=>$url.'&s=128',
                 'image'=>$url,
diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php
index eb7ff09bc8a547acbcbff29b615ad2975ca3dd73..8d7228dea69f4c74eb6eb464be1cf6bcdf5aa766 100644
--- a/include/ajax.tickets.php
+++ b/include/ajax.tickets.php
@@ -502,7 +502,7 @@ class TicketsAjaxAPI extends AjaxController {
                 Http::response(422, 'Unknown ticket variable');
 
             // Ticket thread variables are assumed to be quotes
-            $response = "<br/><blockquote>$response</blockquote><br/>";
+            $response = "<br/><blockquote>{$response->asVar()}</blockquote><br/>";
 
             //  Return text if html thread is not enabled
             if (!$cfg->isHtmlThreadEnabled())
diff --git a/include/api.tickets.php b/include/api.tickets.php
index 6371daa632ff5240b0bee90cb3bed69c0ac56384..d80b1582889f7bf3f944eea9808a3f675f6008f2 100644
--- a/include/api.tickets.php
+++ b/include/api.tickets.php
@@ -118,9 +118,11 @@ class TicketApiController extends ApiController {
     function createTicket($data) {
 
         # Pull off some meta-data
-        $alert = $data['alert'] ? $data['alert'] : true;
-        $autorespond = $data['autorespond'] ? $data['autorespond'] : true;
-        $data['source'] = $data['source'] ? $data['source'] : 'API';
+        $alert       = (bool) (isset($data['alert'])       ? $data['alert']       : true);
+        $autorespond = (bool) (isset($data['autorespond']) ? $data['autorespond'] : true);
+
+        # Assign default value to source if not defined, or defined as NULL
+        $data['source'] = isset($data['source']) ? $data['source'] : 'API';
 
         # Create the ticket with the data (attempt to anyway)
         $errors = array();
diff --git a/include/class.api.php b/include/class.api.php
index 9fe2142044062ef86033447ddf72a009a142bf6f..a1fed6764a7d7b9816243b22fa51c79ed2f4d6a7 100644
--- a/include/class.api.php
+++ b/include/class.api.php
@@ -330,9 +330,9 @@ class ApiXmlDataParser extends XmlDataParser {
             if ($key == "phone" && is_array($value)) {
                 $value = $value[":text"];
             } else if ($key == "alert") {
-                $value = (bool)$value;
+                $value = (bool) (strtolower($value) === 'false' ? false : $value);
             } else if ($key == "autorespond") {
-                $value = (bool)$value;
+                $value = (bool) (strtolower($value) === 'false' ? false : $value);
             } else if ($key == "message") {
                 if (!is_array($value)) {
                     $value = array(
diff --git a/include/class.attachment.php b/include/class.attachment.php
index 005b8215689291a679d629cf0adf73b3bed13c08..9126cff8c0c7f9f68b6071dae3c50dc09e38c7ca 100644
--- a/include/class.attachment.php
+++ b/include/class.attachment.php
@@ -183,7 +183,7 @@ class GenericAttachments {
     function _getList($separate=false, $inlines=false, $lang=false) {
         if(!isset($this->attachments)) {
             $this->attachments = array();
-            $sql='SELECT f.id, f.size, f.`key`, f.name '
+            $sql='SELECT f.id, f.size, f.`key`, f.signature, f.name '
                 .', a.inline, a.lang, a.id as attach_id '
                 .' FROM '.FILE_TABLE.' f '
                 .' INNER JOIN '.ATTACHMENT_TABLE.' a ON(f.id=a.file_id) '
@@ -191,8 +191,8 @@ class GenericAttachments {
                 .' AND a.object_id='.db_input($this->getId());
             if(($res=db_query($sql)) && db_num_rows($res)) {
                 while($rec=db_fetch_array($res)) {
-                    $rec['download'] = AttachmentFile::getDownloadForIdAndKey(
-                        $rec['id'], $rec['key']);
+                    $rec['download_url'] = AttachmentFile::generateDownloadUrl(
+                        $rec['id'], $rec['key'], $rec['signature']);
                     $this->attachments[] = $rec;
                 }
             }
diff --git a/include/class.client.php b/include/class.client.php
index 6ad2efc03b5e40ebeda8941ad56f7dd3a0aa4375..e353b977aaab06a3c3667b5d7f8e053987e120b1 100644
--- a/include/class.client.php
+++ b/include/class.client.php
@@ -40,9 +40,13 @@ abstract class TicketUser {
         $tag =  substr($name, 3);
         switch (strtolower($tag)) {
             case 'ticket_link':
-                return sprintf('%s/view.php?auth=%s',
+                return sprintf('%s/view.php?%s',
                         $cfg->getBaseUrl(),
-                        urlencode($this->getAuthToken()));
+                        Http::build_query(
+                            array('auth' => $this->getAuthToken()),
+                            false
+                            )
+                        );
                 break;
         }
 
diff --git a/include/class.config.php b/include/class.config.php
index e164fa8092afa723e5af770b2bb8dc42fa3e597a..fc488ee4619d4e5fdc8edf5761f8253546e07a18 100644
--- a/include/class.config.php
+++ b/include/class.config.php
@@ -206,6 +206,10 @@ class OsticketConfig extends Config {
         return ($this->get('enable_kb') && FAQ::countPublishedFAQs());
     }
 
+    function isCannedResponseEnabled() {
+        return $this->get('enable_premade');
+    }
+
     function getVersion() {
         return THIS_VERSION;
     }
diff --git a/include/class.dept.php b/include/class.dept.php
index 98d3fd80e218e1c7993b2f770e399dd95fdeacc1..e038c09f58da44bf682c699ff66474eff11d6fbc 100644
--- a/include/class.dept.php
+++ b/include/class.dept.php
@@ -102,6 +102,24 @@ class Dept extends VerySimpleModel {
         return $this->email_id;
     }
 
+    /**
+     * getAlertEmail
+     *
+     * Fetches either the department email (for replies) if configured.
+     * Otherwise, the system alert email address is used.
+     */
+    function getAlertEmail() {
+        global $cfg;
+
+        if (!$this->email && ($id = $this->getEmailId())) {
+            $this->email = Email::lookup($id);
+        }
+        if (!$this->email && $cfg) {
+            $this->email = $cfg->getAlertEmail();
+        }
+        return $this->email;
+    }
+
     function getEmail() {
         global $cfg;
 
diff --git a/include/class.draft.php b/include/class.draft.php
index 54d023a99831056b94e04f2a694f46bd919b50db..d777338d2296f39352dbe2488587a9b11f30ec1b 100644
--- a/include/class.draft.php
+++ b/include/class.draft.php
@@ -92,7 +92,7 @@ class Draft extends VerySimpleModel {
     }
 
     function setBody($body) {
-        // Change image.php urls back to content-id's
+        // Change file.php urls back to content-id's
         $body = Format::sanitize($body, false);
 
         $this->body = $body ?: ' ';
diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php
index be4f4af48565884d8d8c76775f0a57d8744ce0d4..181c1bdbd524bc8c3c2582c874b03ca127738ba9 100644
--- a/include/class.dynamic_forms.php
+++ b/include/class.dynamic_forms.php
@@ -459,7 +459,7 @@ class DynamicFormField extends VerySimpleModel {
     function getField($cache=true) {
         global $thisstaff;
 
-        // Create the `required` flag for the FormField instance
+        // Finagle the `required` flag for the FormField instance
         $ht = $this->ht;
         $ht['required'] = ($thisstaff) ? $this->isRequiredForStaff()
             : $this->isRequiredForUsers();
@@ -1199,16 +1199,18 @@ class SelectionField extends FormField {
             $value = JsonDataParser::parse($value) ?: $value;
 
         if (!is_array($value)) {
-            $config = $this->getConfiguration();
-            if (!$config['multiselect']) {
-                // CDATA may be built with comma-list
-                list($value,) = explode(',', $value, 2);
-            }
+            $values = array();
             $choices = $this->getChoices();
-            if (isset($choices[$value]))
-                $value = array($value => $choices[$value]);
-            elseif ($id && isset($choices[$id]))
-                $value = array($id => $choices[$id]);
+            foreach (explode(',', $value) as $V) {
+                if (isset($choices[$V]))
+                    $values[$V] = $choices[$V];
+            }
+            if ($id && isset($choices[$id]))
+                $values[$id] = $choices[$id];
+
+            if ($values)
+                return $values;
+            // else return $value unchanged
         }
         // Don't set the ID here as multiselect prevents using exactly one
         // ID value. Instead, stick with the JSON value only.
@@ -1328,13 +1330,6 @@ class SelectionField extends FormField {
         return $selection;
     }
 
-    function export($value) {
-        if ($value && is_numeric($value)
-                && ($item = DynamicListItem::lookup($value)))
-            return $item->toString();
-        return $value;
-    }
-
     function getFilterData() {
         $data = array(parent::getFilterData());
         if (($v = $this->getClean()) instanceof DynamicListItem) {
diff --git a/include/class.faq.php b/include/class.faq.php
index 34e56f93e09a71e79e7e15dc7b2598c0cf9b0431..9b95776031909bb948bacd74127e45182cca2135 100644
--- a/include/class.faq.php
+++ b/include/class.faq.php
@@ -81,7 +81,7 @@ class FAQ extends VerySimpleModel {
     function getQuestion() { return $this->question; }
     function getAnswer() { return $this->answer; }
     function getAnswerWithImages() {
-        return Format::viewableImages($this->answer, ROOT_PATH.'image.php');
+        return Format::viewableImages($this->answer);
     }
     function getTeaser() {
         return Format::truncate(Format::striptags($this->answer), 150);
@@ -326,12 +326,11 @@ class FAQ extends VerySimpleModel {
         if ($attachments = $this->getVisibleAttachments()) {
             foreach($attachments as $attachment ) {
             /* The h key must match validation in file.php */
-            $hash=$attachment['key'].md5($attachment['id'].session_id().strtolower($attachment['key']));
             if($attachment['size'])
                 $size=sprintf('&nbsp;<small>(<i>%s</i>)</small>',Format::file_size($attachment['size']));
 
-            $str.=sprintf('<a class="Icon file no-pjax" href="file.php?h=%s" target="%s">%s</a>%s&nbsp;%s',
-                    $hash, $target, Format::htmlchars($attachment['name']), $size, $separator);
+            $str.=sprintf('<a class="Icon file no-pjax" href="%s" target="%s">%s</a>%s&nbsp;%s',
+                    $attachment['download_url'], $target, Format::htmlchars($attachment['name']), $size, $separator);
 
             }
         }
diff --git a/include/class.file.php b/include/class.file.php
index 82b998a783e969b70918399f7559d0dd78d69f7c..2c2ab0fc69a37f19c3cda28e49b2cfa22d7bd925 100644
--- a/include/class.file.php
+++ b/include/class.file.php
@@ -110,19 +110,6 @@ class AttachmentFile {
         return $this->ht['created'];
     }
 
-    static function getDownloadForIdAndKey($id, $key) {
-        return strtolower($key . md5($id.session_id().strtolower($key)));
-    }
-
-
-    /**
-     * Retrieve a signature that can be sent to scp/file.php?h= in order to
-     * download this file
-     */
-    function getDownloadHash() {
-        return self::getDownloadForIdAndKey($this->getId(), $this->getKey());
-    }
-
     function open() {
         return FileStorageBackend::getInstance($this);
     }
@@ -203,11 +190,68 @@ class AttachmentFile {
         exit();
     }
 
-    function download() {
+    function getDownloadUrl($minage=false, $disposition=false, $handler=false) {
+        // XXX: Drop this when AttachmentFile goes to ORM
+        return static::generateDownloadUrl($this->getId(),
+            strtolower($this->getKey()), $this->getSignature(), $minage,
+            $disposition, $handler);
+    }
+
+    static function generateDownloadUrl($id, $key, $hash, $minage=false,
+        $disposition=false, $handler=false
+    ) {
+        // Expire at the nearest midnight, allowing at least 12 hours access
+        $minage = $minage ?: 43200;
+        $gmnow = Misc::gmtime() + $minage;
+        $expires = $gmnow + 86400 - ($gmnow % 86400);
+
+        // Generate a signature based on secret content
+        $signature = static::_genUrlSignature($id, $key, $hash, $expires);
+
+        $handler = $handler ?: ROOT_PATH . 'file.php';
+
+        // Return sanitized query string
+        $args = array(
+            'key' => $key,
+            'expires' => $expires,
+            'signature' => $signature,
+        );
+
+        if ($disposition)
+            $args['disposition'] = $disposition;
+
+        return $handler . '?' . http_build_query($args);
+    }
+
+    function verifySignature($signature, $expires) {
+        $gmnow = Misc::gmtime();
+        if ($expires < $gmnow)
+            return false;
+
+        $check = static::_genUrlSignature($this->getId(), $this->getKey(),
+            $this->getSignature(), $expires);
+        return $signature == $check;
+    }
+
+    static function _genUrlSignature($id, $key, $signature, $expires) {
+        $pieces = array(
+            'Host='.$_SERVER['HTTP_HOST'],
+            'Path='.ROOT_PATH,
+            'Id='.$id,
+            'Key='.strtolower($key),
+            'Hash='.$signature,
+            'Expires='.$expires,
+        );
+        return hash_hmac('sha1', implode("\n", $pieces), SECRET_SALT);
+    }
+
+    function download($disposition=false, $expires=false) {
+        $disposition = $disposition ?: 'inline';
         $bk = $this->open();
-        if ($bk->sendRedirectUrl('inline'))
+        if ($bk->sendRedirectUrl($disposition))
             return;
-        $this->makeCacheable();
+        $ttl = ($expires) ? $expires - Misc::gmtime() : false;
+        $this->makeCacheable($ttl);
         $type = $this->getType() ?: 'application/octet-stream';
         if (isset($_REQUEST['overridetype']))
             $type = $_REQUEST['overridetype'];
@@ -336,7 +380,7 @@ class AttachmentFile {
             return false;
         }
 
-        if (!$file['type']) {
+        if (!$file['type'] && extension_loaded('fileinfo')) {
             $finfo = new finfo(FILEINFO_MIME_TYPE);
             if ($file['data'])
                 $type = $finfo->buffer($file['data']);
@@ -345,9 +389,9 @@ class AttachmentFile {
 
             if ($type)
                 $file['type'] = $type;
-            else
-                $file['type'] = 'application/octet-stream';
         }
+        if (!$file['type'])
+            $file['type'] = 'application/octet-stream';
 
         $sql='INSERT INTO '.FILE_TABLE.' SET created=NOW() '
             .',type='.db_input(strtolower($file['type']))
diff --git a/include/class.filter.php b/include/class.filter.php
index d30a6c68b9186a9e5a7ca9b7cb56346ad286b7b2..acc3a3590a7921d1bee8599417c20fd1714e011a 100644
--- a/include/class.filter.php
+++ b/include/class.filter.php
@@ -137,7 +137,7 @@ class Filter {
     }
 
     function stopOnMatch() {
-        return ($this->ht['stop_on_match']);
+        return ($this->ht['stop_onmatch']);
     }
 
     function matchAllRules() {
@@ -316,9 +316,13 @@ class Filter {
         #       Override name with reply-to information from the TicketFilter
         #       match
         if ($this->useReplyToEmail() && $info['reply-to']) {
+            $changed = $info['reply-to'] != $ticket['email']
+                || ($info['reply-to-name'] && $ticket['name'] != $info['reply-to-name']);
             $ticket['email'] = $info['reply-to'];
             if ($info['reply-to-name'])
                 $ticket['name'] = $info['reply-to-name'];
+            if ($changed)
+                throw new FilterDataChanged();
         }
 
         # Use canned response.
@@ -738,7 +742,7 @@ class TicketFilter {
         $res = $this->getAllActive();
         if($res) {
             while (list($id) = db_fetch_row($res))
-                array_push($this->filters, new Filter($id));
+                $this->filters[] = new Filter($id);
         }
 
         return $this->filters;
@@ -764,36 +768,25 @@ class TicketFilter {
 
         return $this->short_list;
     }
-    /**
-     * Determine if the filters that match the received vars indicate that
-     * the email should be rejected
-     *
-     * Returns FALSE if the email should be acceptable. If the email should
-     * be rejected, the first filter that matches and has reject ticket set is
-     * returned.
-     */
-    function shouldReject() {
-        foreach ($this->getMatchingFilterList() as $filter) {
-            # Set reject if this filter indicates that the email should
-            # be blocked; however, don't unset $reject, because if it
-            # was set by another rule that did not set stopOnMatch(), we
-            # should still honor its configuration
-            if ($filter->rejectOnMatch()) return $filter;
-        }
-        return false;
-    }
     /**
      * Determine if any filters match the received email, and if so, apply
      * actions defined in those filters to the ticket-to-be-created.
+     *
+     * Throws:
+     * RejectedException if the email should not be acceptable. If the email
+     * should be rejected, the first filter that matches and has reject
+     * ticket set is returned.
      */
     function apply(&$ticket) {
         foreach ($this->getMatchingFilterList() as $filter) {
+            if ($filter->rejectOnMatch())
+                throw new RejectedException($filter, $ticket);
             $filter->apply($ticket, $this->vars);
             if ($filter->stopOnMatch()) break;
         }
     }
 
-    /* static */ function getAllActive() {
+    function getAllActive() {
 
         $sql='SELECT id FROM '.FILTER_TABLE
             .' WHERE isactive=1 '
@@ -949,6 +942,27 @@ class TicketFilter {
     }
 }
 
+class RejectedException extends Exception {
+    var $filter;
+    var $vars;
+
+    function __construct(Filter $filter, $vars) {
+        parent::__construct('Ticket rejected by a filter');
+        $this->filter = $filter;
+        $this->vars = $vars;
+    }
+
+    function getRejectingFilter() {
+        return $this->filter;
+    }
+
+    function get($what) {
+        return $this->vars[$what];
+    }
+}
+
+class FilterDataChanged extends Exception {}
+
 /**
  * Function: endsWith
  *
diff --git a/include/class.format.php b/include/class.format.php
index 28dc209917af48a4f449feaab1683b7fdf0b5a2d..9e6c8998538bdfbe908693ece1cf9428a151bee8 100644
--- a/include/class.format.php
+++ b/include/class.format.php
@@ -218,7 +218,7 @@ class Format {
             }
             unset($s);
             if ($styles)
-                $attributes['style'] = Format::htmlencode(implode(';', $styles));
+                $attributes['style'] = Format::htmlchars(implode(';', $styles));
             else
                 unset($attributes['style']);
         }
@@ -259,9 +259,10 @@ class Format {
     }
 
     function localizeInlineImages($text) {
-        // Change image.php urls back to content-id's
-        return preg_replace('/image\\.php\\?h=([\\w.-]{32})\\w{32}/',
-            'cid:$1', $text);
+        // Change file.php urls back to content-id's
+        return preg_replace(
+            '/src="(?:\/[^"]+?)?\/file\\.php\\?(?:\w+=[^&]+&(?:amp;)?)*?key=([^&]+)[^"]*/',
+            'src="cid:$1', $text);
     }
 
     function sanitize($text, $striptags=false) {
@@ -275,15 +276,14 @@ class Format {
         return $striptags?Format::striptags($text, false):$text;
     }
 
-    function htmlchars($var) {
-        return Format::htmlencode($var);
-    }
-
-    function htmlencode($var) {
+    function htmlchars($var, $sanitize = false) {
         static $phpversion = null;
 
         if (is_array($var))
-            return array_map(array('Format', 'htmlencode'), $var);
+            return array_map(array('Format', 'htmlchars'), $var);
+
+        if ($sanitize)
+            $var = Format::sanitize($var);
 
         if (!isset($phpversion))
             $phpversion = phpversion();
@@ -293,7 +293,7 @@ class Format {
             $flags |= ENT_HTML401;
 
         try {
-            return htmlentities( (string) $var, $flags, 'UTF-8', false);
+            return htmlspecialchars( (string) $var, $flags, 'UTF-8', false);
         } catch(Exception $e) {
             return $var;
         }
@@ -308,11 +308,11 @@ class Format {
         if (phpversion() >= '5.4.0')
             $flags |= ENT_HTML401;
 
-        return html_entity_decode($var, $flags, 'UTF-8');
+        return htmlspecialchars_decode($var, $flags);
     }
 
     function input($var) {
-        return Format::htmlencode($var);
+        return Format::htmlchars($var);
     }
 
     //Format text for display..
@@ -345,19 +345,17 @@ class Format {
     }
 
     //make urls clickable. Mainly for display
-    function clickableurls($text, $trampoline=true) {
+    function clickableurls($text, $target='_blank') {
         global $ost;
 
-        $token = $ost->getLinkToken();
-
         // Find all text between tags
         $text = preg_replace_callback(':^[^<]+|>[^<]+:',
-            function($match) use ($token, $trampoline) {
+            function($match) {
                 // Scan for things that look like URLs
                 return preg_replace_callback(
                     '`(?<!>)(((f|ht)tp(s?)://|(?<!//)www\.)([-+~%/.\w]+)(?:[-?#+=&;%@.\w]*)?)'
                    .'|(\b[_\.0-9a-z-]+@([0-9a-z][0-9a-z-]+\.)+[a-z]{2,4})`',
-                    function ($match) use ($token, $trampoline) {
+                    function ($match) {
                         if ($match[1]) {
                             while (in_array(substr($match[1], -1),
                                     array('.','?','-',':',';'))) {
@@ -367,13 +365,9 @@ class Format {
                             if (strpos($match[2], '//') === false) {
                                 $match[1] = 'http://' . $match[1];
                             }
-                            if ($trampoline)
-                                return '<a href="l.php?url='.urlencode($match[1])
-                                    .sprintf('&auth=%s" target="_blank">', $token)
-                                    .$match[1].'</a>'.$match[9];
-                            else
-                                return sprintf('<a href="%s">%s</a>%s',
-                                    $match[1], $match[1], $match[9]);
+
+                            return sprintf('<a href="%s">%s</a>%s',
+                                $match[1], $match[1], $match[9]);
                         } elseif ($match[6]) {
                             return sprintf('<a href="mailto:%1$s" target="_blank">%1$s</a>',
                                 $match[6]);
@@ -386,35 +380,20 @@ class Format {
         // Now change @href and @src attributes to come back through our
         // system as well
         $config = array(
-            'hook_tag' => function($e, $a=0) use ($token) {
+            'hook_tag' => function($e, $a=0) use ($target) {
                 static $eE = array('area'=>1, 'br'=>1, 'col'=>1, 'embed'=>1,
                     'hr'=>1, 'img'=>1, 'input'=>1, 'isindex'=>1, 'param'=>1);
                 if ($e == 'a' && $a) {
-                    if (isset($a['href'])
-                            && strpos($a['href'], 'mailto:') !== 0
-                            && strpos($a['href'], 'l.php?') === false)
-                        $a['href'] = 'l.php?url='.urlencode($a['href'])
-                            .'&amp;auth='.$token;
-                    // ALL link targets open in a new tab
-                    $a['target'] = '_blank';
+                    $a['target'] = $target;
                     $a['class'] = 'no-pjax';
                 }
-                // Images which are external are rewritten to <div
-                // data-src='url...'/>
-                elseif ($e == 'span' && $a && isset($a['data-src']))
-                    $a['data-src'] = 'l.php?url='.urlencode($a['data-src'])
-                        .'&amp;auth='.$token;
-                // URLs for videos need to route too
-                elseif ($e == 'iframe' && $a && isset($a['src']))
-                    $a['src'] = 'l.php?url='.urlencode($a['src'])
-                        .'&amp;auth='.$token;
+
                 $at = '';
                 if (is_array($a)) {
                     foreach ($a as $k=>$v)
                         $at .= " $k=\"$v\"";
                     return "<{$e}{$at}".(isset($eE[$e])?" /":"").">";
-                }
-                else {
+                } else {
                     return "</{$e}>";
                 }
             },
@@ -430,14 +409,14 @@ class Format {
     }
 
 
-    function viewableImages($html, $script='image.php') {
+    function viewableImages($html, $script=false) {
         return preg_replace_callback('/"cid:([\w._-]{32})"/',
         function($match) use ($script) {
             $hash = $match[1];
             if (!($file = AttachmentFile::lookup($hash)))
                 return $match[0];
-            return sprintf('"%s?h=%s" data-cid="%s"',
-                $script, $file->getDownloadHash(), $match[1]);
+            return sprintf('"%s" data-cid="%s"',
+                $file->getDownloadUrl(false, 'inline', $script), $match[1]);
         }, $html);
     }
 
diff --git a/include/class.forms.php b/include/class.forms.php
index 4188797bddf25a7496b4e6a382224c1651b8e296..f10b8b9343a02bb67afdf144a5369b99fca519e5 100644
--- a/include/class.forms.php
+++ b/include/class.forms.php
@@ -1140,27 +1140,33 @@ class ChoiceField extends FormField {
 
     function to_php($value) {
         if (is_string($value))
-            $array = JsonDataParser::parse($value) ?: $value;
-        else
-            $array = $value;
-        $config = $this->getConfiguration();
-        if (!$config['multiselect']) {
-            if (is_array($array) && count($array) < 2) {
-                reset($array);
-                return key($array);
-            }
-            if (is_string($array) && strpos($array, ',') !== false) {
-                list($array,) = explode(',', $array, 2);
+            $value = JsonDataParser::parse($value) ?: $value;
+
+        // CDATA table may be built with comma-separated key,value,key,value
+        if (is_string($value)) {
+            $values = array();
+            $choices = $this->getChoices();
+            foreach (explode(',', $value) as $V) {
+                if (isset($choices[$V]))
+                    $values[$V] = $choices[$V];
             }
+            if (array_filter($values))
+                $value = $values;
+        }
+        $config = $this->getConfiguration();
+        if (!$config['multiselect'] && is_array($value) && count($value) < 2) {
+            reset($value);
+            return key($value);
         }
-        return $array;
+        return $value;
     }
 
     function toString($value) {
-        $selection = $this->getChoice($value);
-        return is_array($selection)
-            ? (implode(', ', array_filter($selection)) ?: $value)
-            : (string) $selection;
+        if (!is_array($value))
+            $value = $this->getChoice($value);
+        if (is_array($value))
+            return implode(', ', $value);
+        return (string) $value;
     }
 
     function getChoice($value) {
@@ -2027,6 +2033,10 @@ class FileUploadField extends FormField {
                 else {
                     if ($ext[0] != '.')
                         $ext = '.' . $ext;
+
+                    // Ensure that the extension is lower-cased for comparison latr
+                    $ext = strtolower($ext);
+
                     // Add this to the MIME types list so it can be exported to
                     // the @accept attribute
                     if (!isset($extensions[$ext]))
@@ -2083,10 +2093,8 @@ class FileUploadField extends FormField {
     function display($value) {
         $links = array();
         foreach ($this->getFiles() as $f) {
-            $hash = strtolower($f['key']
-                . md5($f['id'].session_id().strtolower($f['key'])));
-            $links[] = sprintf('<a class="no-pjax" href="file.php?h=%s">%s</a>',
-                $hash, Format::htmlchars($f['name']));
+            $links[] = sprintf('<a class="no-pjax" href="%s">%s</a>',
+                Format::htmlchars($f['download_url']), Format::htmlchars($f['name']));
         }
         return implode('<br/>', $links);
     }
@@ -2403,7 +2411,7 @@ class ChoicesWidget extends Widget {
         }
 
         $values = $this->value;
-        if (!is_array($values) && $values) {
+        if (!is_array($values) && isset($values)) {
             $values = array($values => $this->field->getChoice($values));
         }
 
@@ -2666,6 +2674,7 @@ class FileUploadWidget extends Widget {
                     'name' => $file->getName(),
                     'type' => $file->getType(),
                     'size' => $file->getSize(),
+                    'download_url' => $file->getDownloadUrl(),
                 );
             }
         }
diff --git a/include/class.http.php b/include/class.http.php
index 18eeb2e1be6db38e00abd8ac3adc4c5632cd7d41..c358355a7cd438c621e25d9eabc548709c3ad128 100644
--- a/include/class.http.php
+++ b/include/class.http.php
@@ -63,11 +63,13 @@ class Http {
 
     function cacheable($etag, $modified, $ttl=3600) {
         // Thanks, http://stackoverflow.com/a/1583753/1025836
-        $last_modified = Misc::db2gmtime($modified);
-        header("Last-Modified: ".date('D, d M y H:i:s', $last_modified)." GMT", false);
+        // Timezone doesn't matter here — but the time needs to be
+        // consistent round trip to the browser and back.
+        $last_modified = strtotime($modified." GMT");
+        header("Last-Modified: ".date('D, d M Y H:i:s', $last_modified)." GMT", false);
         header('ETag: "'.$etag.'"');
         header("Cache-Control: private, max-age=$ttl");
-        header('Expires: ' . gmdate(DATE_RFC822, time() + $ttl)." GMT");
+        header('Expires: ' . gmdate('D, d M Y H:i:s', Misc::gmtime() + $ttl)." GMT");
         header('Pragma: private');
         if (@strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $last_modified ||
             @trim($_SERVER['HTTP_IF_NONE_MATCH'], '" ') == $etag) {
@@ -97,9 +99,8 @@ class Http {
 
     function download($filename, $type, $data=null, $disposition='attachment') {
         header('Pragma: private');
-        header('Expires: 0');
         header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
-        header('Cache-Control: private');
+        header('Cache-Control: private', false);
         header('Content-Type: '.$type);
         header(sprintf('Content-Disposition: %s; %s',
             $disposition,
@@ -111,5 +112,10 @@ class Http {
             exit;
         }
     }
+
+    static function build_query($vars, $encode=true, $separator='&amp;') {
+        return http_build_query(
+                ($encode ? Format::htmlchars($vars) : $vars), '', $separator);
+    }
 }
 ?>
diff --git a/include/class.nav.php b/include/class.nav.php
index e8359e00d44eddd282488283ab6f1c524fda6006..d3cbb943dfe2cd0e219060dea7802482f8d23585 100644
--- a/include/class.nav.php
+++ b/include/class.nav.php
@@ -125,6 +125,7 @@ class StaffNav {
     }
 
     function getSubMenus(){ //Private.
+        global $cfg;
 
         $staff = $this->staff;
         $submenus=array();
@@ -163,7 +164,7 @@ class StaffNav {
                     if($staff) {
                         if ($staff->getRole()->hasPerm(FAQ::PERM_MANAGE))
                             $subnav[]=array('desc'=>__('Categories'),'href'=>'categories.php','iconclass'=>'faq-categories');
-                        if ($staff->getRole()->hasPerm(CannedModel::PERM_MANAGE))
+                        if ($cfg->isCannedResponseEnabled() && $staff->getRole()->hasPerm(CannedModel::PERM_MANAGE))
                             $subnav[]=array('desc'=>__('Canned Responses'),'href'=>'canned.php','iconclass'=>'canned');
                     }
                    break;
diff --git a/include/class.organization.php b/include/class.organization.php
index fa0ffc9cf568bc85ee20eb8e6e2726bba8f1ac81..fbaa19df50dd8328b900e2c235421a3148862c15 100644
--- a/include/class.organization.php
+++ b/include/class.organization.php
@@ -261,6 +261,20 @@ class Organization extends OrganizationModel {
         return (string) $this->getName();
     }
 
+    function asVar() {
+        return (string) $this->getName();
+    }
+
+    function getVar($tag) {
+        if($tag && is_callable(array($this, 'get'.ucfirst($tag))))
+            return call_user_func(array($this, 'get'.ucfirst($tag)));
+
+        $tag = mb_strtolower($tag);
+        foreach ($this->getDynamicData() as $e)
+            if ($a = $e->getAnswer($tag))
+                return $a;
+    }
+
     function update($vars, &$errors) {
 
         $valid = true;
diff --git a/include/class.page.php b/include/class.page.php
index 18f92d64a280a17d4bfe288be9317a34fcdef2bd..7e7d8d7898bc347e7232ca1b1b73439fba3c4188 100644
--- a/include/class.page.php
+++ b/include/class.page.php
@@ -65,7 +65,7 @@ class Page extends VerySimpleModel {
         return $this->_getLocal('body', $lang);
     }
     function getBodyWithImages() {
-        return Format::viewableImages($this->getLocalBody(), ROOT_PATH.'image.php');
+        return Format::viewableImages($this->getLocalBody());
     }
 
     function _getLocal($what, $lang=false) {
diff --git a/include/class.pagenate.php b/include/class.pagenate.php
index acec4fb6cacbe6fb6b69d81c4267fcfa2369c507..2d6e8b8505233b5535432372d4848c145b6e4749 100644
--- a/include/class.pagenate.php
+++ b/include/class.pagenate.php
@@ -38,14 +38,19 @@ class PageNate {
         }
         $this->setURL($url);
     }
-    function setURL($url='',$vars=''){
-        if($url){
-            if(strpos($url,'?')===false)
-                $url=$url.'?';
-        }else{
-         $url=THISPAGE.'?';
+
+    function setURL($url='',$vars='') {
+        if ($url) {
+            if (strpos($url, '?')===false)
+                $url .= '?';
+        } else {
+         $url = THISPAGE.'?';
         }
-        $this->url=$url.$vars;
+
+        if ($vars && is_array($vars))
+            $vars = Http::build_query($vars);
+
+        $this->url = $url.$vars;
     }
 
     function getStart() {
diff --git a/include/class.staff.php b/include/class.staff.php
index 9ffd6bdeaf8a3ac738eb8fcb61ac5aa662aa942a..7e74fe9651562c43449014972eef08e71377c4ff 100644
--- a/include/class.staff.php
+++ b/include/class.staff.php
@@ -774,6 +774,20 @@ implements AuthenticatedUser {
         if(!$vars['group_id'])
             $errors['group_id']=__('Group is required');
 
+        // Ensure we will still have an administrator with access
+        if ($vars['isadmin'] !== '1' || $vars['isactive'] !== '1') {
+            $sql = 'select count(*), max(staff_id) from '.STAFF_TABLE
+                .' WHERE isadmin=1 and isactive=1';
+            if (($res = db_query($sql))
+                    && (list($count, $sid) = db_fetch_row($res))) {
+                if ($count == 1 && $sid = $id) {
+                    $errors['isadmin'] = __(
+                        'Cowardly refusing to remove or lock out the only active administrator'
+                    );
+                }
+            }
+        }
+
         if ($errors)
             return false;
 
diff --git a/include/class.thread.php b/include/class.thread.php
index 724471ed761666687721b266ab1243f9b4dfd2da..d3abcf411210dc6154f38d09a8a68a530838f71d 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -552,12 +552,11 @@ class ThreadEntry {
         return $this->attachments->getAll(false);
     }
 
-    function getAttachmentUrls($script='image.php') {
+    function getAttachmentUrls() {
         $json = array();
         foreach ($this->getAttachments() as $att) {
             $json[$att['key']] = array(
-                'download_url' => sprintf('attachment.php?id=%d&h=%s',
-                    $att['attach_id'], $att['download']),
+                'download_url' => $att['download_url'],
                 'filename' => $att['name'],
             );
         }
@@ -574,14 +573,8 @@ class ThreadEntry {
             if ($att['size'])
                 $size=sprintf('<em>(%s)</em>', Format::file_size($att['size']));
 
-            $str.=sprintf('<a class="Icon file no-pjax" href="%s?id=%d&h=%s" target="%s">%s</a>%s&nbsp;%s',
-                    $file,
-                    $att['attach_id'],
-                    $att['download'],
-                    $target,
-                    Format::htmlchars($att['name']),
-                    $size,
-                    $separator);
+            $str.=sprintf('<a class="Icon file no-pjax" href="%s" target="%s">%s</a>%s&nbsp;%s',
+                    $att['download_url'], $target, Format::htmlchars($att['name']), $size, $separator);
         }
 
         return $str;
@@ -867,7 +860,10 @@ class ThreadEntry {
         $match = array();
         if ($subject
                 && $mailinfo['email']
-                && preg_match("/\b#(\S+)/u", $subject, $match)
+                // Required `#` followed by one or more of
+                //      punctuation (-) then letters, numbers, and symbols
+                // (Try not to match closing punctuation (`]`) in [#12345])
+                && preg_match("/#((\p{P}*[^\p{C}\p{Z}\p{P}]+)+)/u", $subject, $match)
                 //Lookup by ticket number
                 && ($ticket = Ticket::lookupByNumber($match[1]))
                 //Lookup the user using the email address
@@ -1236,6 +1232,11 @@ class ThreadEntryBody /* extends SplString */ {
         return $this->display('html');
     }
 
+    function asVar() {
+        // Email template, assume HTML
+        return $this->display('email');
+    }
+
     function display($format=false) {
         throw new Exception('display: Abstract display() method not implemented');
     }
@@ -1271,21 +1272,15 @@ class TextThreadEntryBody extends ThreadEntryBody {
 
         switch ($output) {
         case 'html':
-            return '<div style="white-space:pre-wrap">'
-                .Format::clickableurls(Format::htmlchars($this->body)).'</div>';
         case 'email':
-            return '<div style="white-space:pre-wrap">'.$this->body.'</div>';
+            return '<div style="white-space:pre-wrap">'
+                .Format::htmlchars($this->body).'</div>';
         case 'pdf':
             return nl2br($this->body);
         default:
             return '<pre>'.$this->body.'</pre>';
         }
     }
-
-    function asVar() {
-        // Email template, assume HTML
-        return $this->display('email');
-    }
 }
 class HtmlThreadEntryBody extends ThreadEntryBody {
     function __construct($body, $options=array()) {
@@ -1326,7 +1321,7 @@ class HtmlThreadEntryBody extends ThreadEntryBody {
         case 'email':
             return $this->body;
         case 'pdf':
-            return Format::clickableurls($this->body, false);
+            return Format::clickableurls($this->body);
         default:
             return Format::display($this->body);
         }
diff --git a/include/class.ticket.php b/include/class.ticket.php
index b1b3ea30cddb714508d9cca00235dd7c4225eda1..1dd62e97cf49c0be0a0a727260f54062f8d0c8b3 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -1072,7 +1072,8 @@ class Ticket {
     }
 
     //Status helper.
-    function setStatus($status, $comments='', &$errors=array()) {
+
+    function setStatus($status, $comments='', &$errors=array(), $set_closing_agent=true) {
         global $thisstaff;
 
         if (!$thisstaff || !($role=$thisstaff->getRole($this->getDeptId())))
@@ -1116,7 +1117,7 @@ class Ticket {
                     return false;
                 }
                 $sql.=', closed=NOW(), lastupdate=NOW(), duedate=NULL ';
-                if ($thisstaff)
+                if ($thisstaff && $set_closing_agent)
                     $sql.=', staff_id='.db_input($thisstaff->getId());
 
                 $ecb = function($t) {
@@ -1148,21 +1149,24 @@ class Ticket {
         if (!db_query($sql) || !db_affected_rows())
             return false;
 
-        // Log status change b4 reload
-        $note = sprintf(__('Status changed from %s to %s by %s'),
-                $this->getStatus(),
-                $status,
-                $thisstaff ?: 'SYSTEM');
+        // Log status change b4 reload — if currently has a status. (On new
+        // ticket, the ticket is opened and thereafter the status is set to
+        // the requested status).
+        if ($current_status = $this->getStatus()) {
+            $note = sprintf(__('Status changed from %s to %s by %s'),
+                    $this->getStatus(),
+                    $status,
+                    $thisstaff ?: 'SYSTEM');
+
+            $alert = false;
+            if ($comments) {
+                $note .= sprintf('<hr>%s', $comments);
+                // Send out alerts if comments are included
+                $alert = true;
+            }
 
-        $alert = false;
-        if ($comments) {
-            $note .= sprintf('<hr>%s', $comments);
-            // Send out alerts if comments are included
-            $alert = true;
+            $this->logNote(__('Status Changed'), $note, $thisstaff, $alert);
         }
-
-        $this->logNote(__('Status Changed'), $note, $thisstaff, $alert);
-
         // Log events via callback
         if ($ecb) $ecb($this);
 
@@ -1264,7 +1268,7 @@ class Ticket {
         //Send alert to out sleepy & idle staff.
         if ($alertstaff
                 && $cfg->alertONNewTicket()
-                && ($email=$cfg->getAlertEmail())
+                && ($email=$dept->getAlertEmail())
                 && ($msg=$tpl->getNewTicketAlertMsgTemplate())) {
 
             $msg = $this->replaceVars($msg->asArray(), array('message' => $message));
@@ -1427,7 +1431,10 @@ class Ticket {
         }
 
         // Reopen if closed AND reopenable
-        if ($this->isClosed() && $this->isReopenable())
+        // 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())
             $this->reopen();
 
         // Figure out the user
@@ -1498,7 +1505,7 @@ class Ticket {
         $dept = $this->getDept();
         if(!$dept
                 || !($tpl = $dept->getTemplate())
-                || !($email = $cfg->getAlertEmail()))
+                || !($email = $dept->getAlertEmail()))
             return true;
 
         //recipients
@@ -1555,7 +1562,7 @@ class Ticket {
         //Get the message template
         if(($tpl = $dept->getTemplate())
                 && ($msg=$tpl->getOverdueAlertMsgTemplate())
-                && ($email=$cfg->getAlertEmail())) {
+                && ($email = $dept->getAlertEmail())) {
 
             $msg = $this->replaceVars($msg->asArray(),
                 array('comments' => $comments));
@@ -1740,7 +1747,7 @@ class Ticket {
         if(!$alert || !$cfg->alertONTransfer() || !($dept=$this->getDept()))
             return true; //no alerts!!
 
-         if(($email=$cfg->getAlertEmail())
+         if (($email = $dept->getAlertEmail())
                      && ($tpl = $dept->getTemplate())
                      && ($msg=$tpl->getTransferAlertMsgTemplate())) {
 
@@ -1969,7 +1976,7 @@ class Ticket {
                 'thread'=>$message);
         //If enabled...send alert to staff (New Message Alert)
         if($cfg->alertONNewMessage()
-                && ($email = $cfg->getAlertEmail())
+                && ($email = $dept->getAlertEmail())
                 && ($tpl = $dept->getTemplate())
                 && ($msg = $tpl->getNewMessageAlertMsgTemplate())) {
 
@@ -2227,7 +2234,7 @@ class Ticket {
         if(!$alert || !$cfg->alertONNewNote() || !($dept=$this->getDept()))
             return $note;
 
-        if(($email=$cfg->getAlertEmail())
+        if (($email = $dept->getAlertEmail())
                 && ($tpl = $dept->getTemplate())
                 && ($msg=$tpl->getNoteAlertMsgTemplate())) {
 
@@ -2596,12 +2603,70 @@ class Ticket {
         return db_fetch_array(db_query($sql));
     }
 
+    protected function filterTicketData($origin, $vars, $forms, $user=false) {
+        global $cfg;
+
+        // Unset all the filter data field data in case things change
+        // during recursive calls
+        foreach ($vars as $k=>$v)
+            if (strpos($k, 'field.') === 0)
+                unset($vars[$k]);
+
+        foreach ($forms as $F) {
+            if ($F) {
+                $vars += $F->getFilterData();
+            }
+        }
+
+        // Add in user and organization data for filtering
+        if ($user) {
+            $vars += $user->getFilterData();
+            $vars['email'] = $user->getEmail();
+            $vars['name'] = $user->getName();
+            if ($org = $user->getOrganization()) {
+                $vars += $org->getFilterData();
+            }
+        }
+        // Unpack the basic user information
+        else {
+            $interesting = array('name', 'email');
+            $user_form = UserForm::getUserForm()->getForm($vars);
+            // Add all the user-entered info for filtering
+            foreach ($user_form->getFields() as $f) {
+                $vars['field.'.$f->get('id')] = $f->toString($f->getClean());
+                if (in_array($f->get('name'), $interesting))
+                    $vars[$f->get('name')] = $vars['field.'.$f->get('id')];
+            }
+            // Add in organization data if one exists for this email domain
+            list($mailbox, $domain) = explode('@', $vars['email'], 2);
+            if ($org = Organization::forDomain($domain)) {
+                $vars += $org->getFilterData();
+            }
+        }
+
+        try {
+            // Make sure the email address is not banned
+            if (TicketFilter::isBanned($vars['email'])) {
+                throw new RejectedException(Banlist::getFilter(), $vars);
+            }
+
+            // Init ticket filters...
+            $ticket_filter = new TicketFilter($origin, $vars);
+            $ticket_filter->apply($vars);
+        }
+        catch (FilterDataChanged $ex) {
+            // Don't pass user recursively, assume the user has changed
+            return self::filterTicketData($origin, $vars, $forms);
+        }
+        return $vars;
+    }
+
     /*
      * The mother of all functions...You break it you fix it!
      *
      *  $autorespond and $alertstaff overrides config settings...
      */
-    static function create($vars, &$errors, $origin, $autorespond=true,
+    static function create(&$vars, &$errors, $origin, $autorespond=true,
             $alertstaff=true) {
         global $ost, $cfg, $thisclient, $_FILES;
 
@@ -2653,81 +2718,8 @@ class Ticket {
         if (!$form->isValid($field_filter('ticket')))
             $errors += $form->errors();
 
-        // Unpack dynamic variables into $vars for filter application
-        $vars += $form->getFilterData();
-
-        // Unpack the basic user information
-        if ($vars['uid'] && ($user = User::lookup($vars['uid']))) {
-            $vars['email'] = $user->getEmail();
-            $vars['name'] = $user->getName();
-            // Add in user and organization data for filtering
-            $vars += $user->getFilterData();
-            if ($org = $user->getOrganization()) {
-                $vars += $org->getFilterData();
-            }
-        }
-        else {
-            $interesting = array('name', 'email');
-            $user_form = UserForm::getUserForm()->getForm($vars);
-            // Add all the user-entered info for filtering
-            foreach ($user_form->getFields() as $f) {
-                $vars['field.'.$f->get('id')] = $f->toString($f->getClean());
-                if (in_array($f->get('name'), $interesting))
-                    $vars[$f->get('name')] = $vars['field.'.$f->get('id')];
-            }
-            // Add in organization data if one exists for this email domain
-            list($mailbox, $domain) = explode('@', $vars['email'], 2);
-            if ($org = Organization::forDomain($domain)) {
-                $vars += $org->getFilterData();
-            }
-        }
-
-
-        //Check for 403
-        if ($vars['email']
-                && Validator::is_email($vars['email'])) {
-
-            //Make sure the email address is not banned
-            if (TicketFilter::isBanned($vars['email'])) {
-                return $reject_ticket(sprintf(_S('Banned email - %s'), $vars['email']));
-            }
-
-            //Make sure the open ticket limit hasn't been reached. (LOOP CONTROL)
-            if ($cfg->getMaxOpenTickets() > 0
-                    && strcasecmp($origin, 'staff')
-                    && ($_user=TicketUser::lookupByEmail($vars['email']))
-                    && ($openTickets=$_user->getNumOpenTickets())
-                    && ($openTickets>=$cfg->getMaxOpenTickets()) ) {
-
-                $errors = array('err' => __("You've reached the maximum open tickets allowed."));
-                $ost->logWarning(sprintf(_S('Ticket denied - %s'), $vars['email']),
-                        sprintf(_S('Max open tickets (%1$d) reached for %2$s'),
-                            $cfg->getMaxOpenTickets(), $vars['email']),
-                        false);
-
-                return 0;
-            }
-        }
-
-        if ($vars['topicId']) {
-            if (($__topic=Topic::lookup($vars['topicId']))
-                && $__form = $__topic->getForm()
-            ) {
-                $__form = $__form->instanciate();
-                $__form->setSource($vars);
-                $vars += $__form->getFilterData();
-            }
-        }
-
-        //Init ticket filters...
-        $ticket_filter = new TicketFilter($origin, $vars);
-        // Make sure email contents should not be rejected
-        if ($ticket_filter
-                && ($filter=$ticket_filter->shouldReject())) {
-            return $reject_ticket(
-                sprintf(_S('Ticket rejected (%s) by filter "%s"'),
-                    $vars['email'], $filter->getName()));
-        }
+        if ($vars['uid'])
+            $user = User::lookup($vars['uid']);
 
         $id=0;
         $fields=array();
@@ -2767,7 +2759,42 @@ class Ticket {
         if (!$errors) {
 
             # Perform ticket filter actions on the new ticket arguments
-            if ($ticket_filter) $ticket_filter->apply($vars);
+            $__form = null;
+            if ($vars['topicId']) {
+                if (($__topic=Topic::lookup($vars['topicId']))
+                    && ($__form = $__topic->getForm())
+                ) {
+                    $__form = $__form->instanciate();
+                    $__form->setSource($vars);
+                }
+            }
+
+            try {
+                $vars = self::filterTicketData($origin, $vars,
+                    array($form, $__form), $user);
+            }
+            catch (RejectedException $ex) {
+                return $reject_ticket(
+                    sprintf(_S('Ticket rejected (%s) by filter "%s"'),
+                    $ex->vars['email'], $ex->getRejectingFilter()->getName())
+                );
+            }
+
+            //Make sure the open ticket limit hasn't been reached. (LOOP CONTROL)
+            if ($cfg->getMaxOpenTickets() > 0
+                    && strcasecmp($origin, 'staff')
+                    && ($_user=TicketUser::lookupByEmail($vars['email']))
+                    && ($openTickets=$_user->getNumOpenTickets())
+                    && ($openTickets>=$cfg->getMaxOpenTickets()) ) {
+
+                $errors = array('err' => __("You've reached the maximum open tickets allowed."));
+                $ost->logWarning(sprintf(_S('Ticket denied - %s'), $vars['email']),
+                        sprintf(_S('Max open tickets (%1$d) reached for %2$s'),
+                            $cfg->getMaxOpenTickets(), $vars['email']),
+                        false);
+
+                return 0;
+            }
 
             // Allow vars to be changed in ticket filter and applied to the user
             // account created or detected
@@ -2914,7 +2941,6 @@ class Ticket {
             .' ,`number`='.db_input($number)
             .' ,dept_id='.db_input($deptId)
             .' ,topic_id='.db_input($topicId)
-            .' ,status_id='.db_input($statusId)
             .' ,ip_address='.db_input($ipaddress)
             .' ,source='.db_input($source);
 
@@ -2995,12 +3021,20 @@ class Ticket {
             if ($vars['staffId'])
                  $ticket->assignToStaff($vars['staffId'], _S('Auto Assignment'));
             if ($vars['teamId'])
-                $ticket->assignToTeam($vars['teamId'], _S('Auto Assignment'));
+                // No team alert if also assigned to an individual agent
+                $ticket->assignToTeam($vars['teamId'], _S('Auto Assignment'),
+                    !$vars['staffId']);
         }
 
         // Update the estimated due date in the database
         $ticket->updateEstDueDate();
 
+        // Apply requested status — this should be done AFTER assignment,
+        // because if it is requested to be closed, it should not cause the
+        // ticket to be reopened for assignment.
+        if ($statusId)
+            $ticket->setStatus($statusId, false, $errors, false);
+
         /**********   double check auto-response  ************/
         //Override auto responder if the FROM email is one of the internal emails...loop control.
         if($autorespond && (Email::getIdByEmail($ticket->getEmail())))
@@ -3096,12 +3130,7 @@ class Ticket {
             $vars['response'] = $ticket->replaceVars($vars['response']);
             // $vars['cannedatachments'] contains the attachments placed on
             // the response form.
-            if(($response=$ticket->postReply($vars, $errors, false))) {
-                //Only state supported is closed on response
-                if(isset($vars['ticket_state']) &&
-                        $role->hasPerm(TicketModel::PERM_CLOSE))
-                    $ticket->setState($vars['ticket_state']);
-            }
+            $response = $ticket->postReply($vars, $errors, false);
         }
 
         // Not assigned...save optional note if any
@@ -3115,10 +3144,16 @@ class Ticket {
         }
 
         $ticket->reload();
+        $dept = $ticket->getDept();
+
+        // See if we need to skip auto-response.
+        $autorespond = isset($create_vars['autorespond'])
+            ? $create_vars['autorespond'] : true;
 
-        if(!$cfg->notifyONNewStaffTicket()
+        if (!$autorespond
                 || !isset($vars['alertuser'])
-                || !($dept=$ticket->getDept()))
+                || !$dept->autoRespONNewTicket()
+                || !$cfg->notifyONNewStaffTicket())
             return $ticket; //No alerts.
 
         //Send Notice to user --- if requested AND enabled!!
diff --git a/include/class.user.php b/include/class.user.php
index d12bd009e579b6343788d9a87dab3ce6a989be56..5db55087c1538f53bd2acb0311c3022165d2c7dc 100644
--- a/include/class.user.php
+++ b/include/class.user.php
@@ -308,7 +308,8 @@ class User extends UserModel {
             // Add in special `name` and `email` fields
             foreach (array('name', 'email') as $name) {
                 if ($f = $entry->getForm()->getField($name))
-                    $vars['field.'.$f->get('id')] = $this->getName();
+                    $vars['field.'.$f->get('id')] =
+                        $name == 'name' ? $this->getName() : $this->getEmail();
             }
         }
         return $vars;
@@ -568,6 +569,11 @@ class User extends UserModel {
         // Delete emails.
         $this->emails->expunge();
 
+        // Drop dynamic data
+        foreach ($this->getDynamicData() as $cd) {
+            $cd->delete();
+        }
+
         // Delete user
         return parent::delete();
     }
diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php
index e39e43671662b1eff01b62e89c79a9d843f1e6d8..d88ec62d51a78227497452396abf93111a0a9a10 100644
--- a/include/client/tickets.inc.php
+++ b/include/client/tickets.inc.php
@@ -3,10 +3,10 @@ if(!defined('OSTCLIENTINC') || !is_object($thisclient) || !$thisclient->isValid(
 
 $tickets = TicketModel::objects();
 
-$qstr='&'; //Query string collector
+$qs = array();
 $status=null;
 if(isset($_REQUEST['status'])) { //Query string status has nothing to do with the real status used below.
-    $qstr.='status='.urlencode($_REQUEST['status']);
+    $qs += array('status' => $_REQUEST['status']);
     //Status we are actually going to use on the query...making sure it is clean!
     $status=strtolower($_REQUEST['status']);
     switch(strtolower($_REQUEST['status'])) {
@@ -51,7 +51,7 @@ $tickets->filter(Q::any(array(
 // Perform basic search
 $search=($_REQUEST['a']=='search' && $_REQUEST['q']);
 if($search) {
-    $qstr.='&a='.urlencode($_REQUEST['a']).'&q='.urlencode($_REQUEST['q']);
+    $qs += array('a' => $_REQUEST['a'], 'q' => $_REQUEST['q']);
     if (is_numeric($_REQUEST['q'])) {
         $tickets->filter(array('number__startswith'=>$_REQUEST['q']));
     } else { //Deep search!
@@ -65,19 +65,11 @@ TicketForm::ensureDynamicDataView();
 $total=$tickets->count();
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 $pageNav=new Pagenate($total, $page, PAGE_LIMIT);
-$pageNav->setURL('tickets.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']));
+$qstr = '&amp;'. Http::build_query($qs);
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+$pageNav->setURL('tickets.php', $qs);
 $pageNav->paginate($tickets);
 
-//more stuff...
-$qselect.=' ,count(DISTINCT attach.id) as attachments ';
-$qfrom.=' LEFT JOIN '.THREAD_ENTRY_TABLE.' entry
-            ON (entry.thread_id=thread.id AND entry.`type` IN ("M", "R")) ';
-$qfrom.=' LEFT JOIN '.ATTACHMENT_TABLE.' attach
-            ON (attach.object_id=entry.id AND attach.`type` = "H") ';
-$qgroup=' GROUP BY ticket.ticket_id';
-
-$query="$qselect $qfrom $qwhere $qgroup ORDER BY $order_by $order LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
-//echo $query;
 $showing =$total ? $pageNav->showing() : "";
 if(!$results_type)
 {
diff --git a/include/client/view.inc.php b/include/client/view.inc.php
index 4c93a7b3bcb5b21eeac0657c1ccaf84ebe133b53..fe2f1b8aa481d93f4178743bfe4808ce82cda4e8 100644
--- a/include/client/view.inc.php
+++ b/include/client/view.inc.php
@@ -126,7 +126,7 @@ if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) {
                 <span><?php echo $poster; ?></span>
             </div>
             </th></tr>
-            <tr><td class="thread-body"><div><?php echo $entry['body']->toHtml(); ?></div></td></tr>
+            <tr><td class="thread-body"><div><?php echo Format::clickableurls($entry['body']->toHtml()); ?></div></td></tr>
             <?php
             if($entry['attachments']
                     && ($tentry=$ticket->getThreadEntry($entry['id']))
diff --git a/include/html2text.php b/include/html2text.php
index 0c1ec08330d8a8c77acd80d1f187d6b6c0d7d4a4..1d12c733c3d3582bc3daede37cd9580b08a88314 100644
--- a/include/html2text.php
+++ b/include/html2text.php
@@ -448,6 +448,11 @@ class HtmlAElement extends HtmlInlineElement {
             $href = substr($href, 7);
             $output = (($href != $output) ? "$href " : '') . "<$output>";
         } elseif (mb_strwidth($href) > $width / 2) {
+            if (mb_strwidth($output) > $width / 2) {
+                // Parse URL and use relative path part
+                if ($PU = parse_url($output))
+                    $output = $PU['host'] . $PU['path'];
+            }
             if ($href != $output)
                 $id = $this->getRoot()->addFootnote($output, $href);
             $output = "[$output][$id]";
diff --git a/include/mpdf/mpdf.php b/include/mpdf/mpdf.php
index af04fa3b483c53e838bd8f4835d2443dbe073764..9c7fe98ad44cd7a9848a0890d6583bfeefd52710 100755
--- a/include/mpdf/mpdf.php
+++ b/include/mpdf/mpdf.php
@@ -31136,6 +31136,7 @@ function purify_utf8($html,$lo=true) {
 function purify_utf8_text($txt) {
 	// For TEXT
 	// Make sure UTF-8 string of characters
+    if ($txt === null) $txt = '';
 	if (!$this->is_utf8($txt)) { $this->Error("Text contains invalid UTF-8 character(s)"); }
 
 	$txt = preg_replace("/\r/", "", $txt );
diff --git a/include/staff/apikey.inc.php b/include/staff/apikey.inc.php
index 1e6f845cf05beb9c4d65710b951346fc239a3fcc..fdf30da4bc844ed0cbdef37c3902a3916eca2716 100644
--- a/include/staff/apikey.inc.php
+++ b/include/staff/apikey.inc.php
@@ -1,24 +1,23 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
 
-$info=array();
-$qstr='';
+$info=$qs = array();
 if($api && $_REQUEST['a']!='add'){
     $title=__('Update API Key');
     $action='update';
     $submit_text=__('Save Changes');
     $info=$api->getHashtable();
-    $qstr.='&id='.$api->getId();
+    $qs += array('id' => $api->getId());
 }else {
     $title=__('Add New API Key');
     $action='add';
     $submit_text=__('Add Key');
     $info['isactive']=isset($info['isactive'])?$info['isactive']:1;
-    $qstr.='&a='.urlencode($_REQUEST['a']);
+    $qs += array('a' => $_REQUEST['a']);
 }
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 ?>
-<form action="apikeys.php?<?php echo $qstr; ?>" method="post" id="save">
+<form action="apikeys.php?<?php echo Http::build_query($qs); ?>" method="post" id="save">
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
diff --git a/include/staff/apikeys.inc.php b/include/staff/apikeys.inc.php
index e5430c14d1fc3e200a44a7b9116d6826c3202bf9..ddaf990966931e54b5753a4a2763d11257a239d4 100644
--- a/include/staff/apikeys.inc.php
+++ b/include/staff/apikeys.inc.php
@@ -1,7 +1,7 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff->isAdmin()) die('Access Denied');
 
-$qstr='';
+$qs = array();
 $sql='SELECT * FROM '.API_KEY_TABLE.' WHERE 1';
 $sortOptions=array('key'=>'apikey','status'=>'isactive','ip'=>'ipaddr','date'=>'created','created'=>'created','updated'=>'updated');
 $orderWays=array('DESC'=>'DESC','ASC'=>'ASC');
@@ -27,9 +27,11 @@ $order_by="$order_column $order ";
 $total=db_count('SELECT count(*) FROM '.API_KEY_TABLE.' ');
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 $pageNav=new Pagenate($total,$page,PAGE_LIMIT);
-$pageNav->setURL('apikeys.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']));
-//Ok..lets roll...create the actual query
-$qstr.='&order='.($order=='DESC'?'ASC':'DESC');
+$qstr = '&amp;'. Http::build_query($qs);
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+$pageNav->setURL('apikeys.php', $qs);
+
+$qstr.='&amp;order='.($order=='DESC'?'ASC':'DESC');
 $query="$sql ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
 $res=db_query($query);
 if($res && ($num=db_num_rows($res)))
diff --git a/include/staff/banlist.inc.php b/include/staff/banlist.inc.php
index 7c83e9b1370c149f367cdb955a5ae7612600ae89..3454ae4803342c9b2ab52c4904a0c09dc017296f 100644
--- a/include/staff/banlist.inc.php
+++ b/include/staff/banlist.inc.php
@@ -1,7 +1,7 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$filter) die('Access Denied');
 
-$qstr='';
+$qs = array();
 $select='SELECT rule.* ';
 $from='FROM '.FILTER_RULE_TABLE.' rule ';
 $where='WHERE rule.filter_id='.db_input($filter->getId());
@@ -41,8 +41,10 @@ $order_by="$order_column $order ";
 $total=db_count('SELECT count(DISTINCT rule.id) '.$from.' '.$where);
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 $pageNav=new Pagenate($total, $page, PAGE_LIMIT);
-$pageNav->setURL('banlist.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']));
-$qstr.='&order='.($order=='DESC'?'ASC':'DESC');
+$qstr = '&amp;'. Http::build_query($qs);
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+$pageNav->setURL('banlist.php', $qs);
+$qstr.='&amp;order='.($order=='DESC'?'ASC':'DESC');
 $query="$select $from $where ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
 //echo $query;
 ?>
diff --git a/include/staff/banrule.inc.php b/include/staff/banrule.inc.php
index b3fe52347ccc875025eaa05fb490bd3b4a5be7a5..4f98cd148511d0787873a81f07b75e5ddc6c8275 100644
--- a/include/staff/banrule.inc.php
+++ b/include/staff/banrule.inc.php
@@ -1,25 +1,25 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
 
-$info=array();
-$qstr='';
+$info=$qs= array();
 if($rule && $_REQUEST['a']!='add'){
     $title=__('Update Ban Rule');
     $action='update';
     $submit_text=__('Update');
     $info=$rule->getInfo();
     $info['id']=$rule->getId();
-    $qstr.='&id='.$rule->getId();
+    $qs += array('id' => $rule->getId());
 }else {
     $title=__('Add New Email Address to Ban List');
     $action='add';
     $submit_text=__('Add');
     $info['isactive']=isset($info['isactive'])?$info['isactive']:1;
-    $qstr.='&a='.urlencode($_REQUEST['a']);
+    $qs += array('a' => $_REQUEST['a']);
 }
+
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 ?>
-<form action="banlist.php?<?php echo $qstr; ?>" method="post" id="save">
+<form action="banlist.php?<?php echo Http::build_query($qs); ?>" method="post" id="save">
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
diff --git a/include/staff/cannedresponse.inc.php b/include/staff/cannedresponse.inc.php
index 57276a6df94fbb504869a5213631005a515a108b..518ad1165310fbf99ee148e5d259c6146e782677 100644
--- a/include/staff/cannedresponse.inc.php
+++ b/include/staff/cannedresponse.inc.php
@@ -1,14 +1,13 @@
 <?php
 if(!defined('OSTSCPINC') || !$thisstaff) die('Access Denied');
-$info=array();
-$qstr='';
+$info=$qs = array();
 if($canned && $_REQUEST['a']!='add'){
     $title=__('Update Canned Response');
     $action='update';
     $submit_text=__('Save Changes');
     $info=$canned->getInfo();
     $info['id']=$canned->getId();
-    $qstr.='&id='.$canned->getId();
+    $qs += array('id' => $canned->getId());
     // Replace cid: scheme with downloadable URL for inline images
     $info['response'] = $canned->getResponseWithImages();
     $info['notes'] = Format::viewableImages($info['notes']);
@@ -17,12 +16,12 @@ if($canned && $_REQUEST['a']!='add'){
     $action='create';
     $submit_text=__('Add Response');
     $info['isenabled']=isset($info['isenabled'])?$info['isenabled']:1;
-    $qstr.='&a='.$_REQUEST['a'];
+    $qs += array('a' => $_REQUEST['a']);
 }
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 
 ?>
-<form action="canned.php?<?php echo $qstr; ?>" method="post" id="save" enctype="multipart/form-data">
+<form action="canned.php?<?php echo Http::build_query($qs); ?>" method="post" id="save" enctype="multipart/form-data">
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
diff --git a/include/staff/cannedresponses.inc.php b/include/staff/cannedresponses.inc.php
index 1affca425353568f2191fbb5b04bf1e074700ff6..05d098a34fc350ad13033c6e99561f9c424e5389 100644
--- a/include/staff/cannedresponses.inc.php
+++ b/include/staff/cannedresponses.inc.php
@@ -1,7 +1,7 @@
 <?php
 if(!defined('OSTSCPINC') || !$thisstaff) die('Access Denied');
 
-$qstr='';
+$qs = array();
 $sql='SELECT canned.*, count(attach.file_id) as files, dept.name as department '.
      ' FROM '.CANNED_TABLE.' canned '.
      ' LEFT JOIN '.DEPT_TABLE.' dept ON (dept.id=canned.dept_id) '.
@@ -36,9 +36,11 @@ $order_by="$order_column $order ";
 $total=db_count('SELECT count(*) FROM '.CANNED_TABLE.' canned ');
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 $pageNav=new Pagenate($total, $page, PAGE_LIMIT);
-$pageNav->setURL('canned.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']));
+$qstr = '&amp;'. Http::build_query($qs);
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+$pageNav->setURL('canned.php', $qs);
 //Ok..lets roll...create the actual query
-$qstr.='&order='.($order=='DESC'?'ASC':'DESC');
+$qstr .= '&order='.($order=='DESC'?'ASC':'DESC');
 $query="$sql GROUP BY canned.canned_id ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
 $res=db_query($query);
 if($res && ($num=db_num_rows($res)))
diff --git a/include/staff/categories.inc.php b/include/staff/categories.inc.php
index 32ad0a8e92fa4b978c3f7194b9d35de5eda53b65..05f0b5108a7de30b33506828084a83cc534f6791 100644
--- a/include/staff/categories.inc.php
+++ b/include/staff/categories.inc.php
@@ -1,7 +1,7 @@
 <?php
 if(!defined('OSTSCPINC') || !$thisstaff) die('Access Denied');
 
-$qstr='';
+$qs = array();
 $categories = Category::objects()
     ->annotate(array('faq_count'=>SqlAggregate::COUNT('faqs')));
 $sortOptions=array('name'=>'name','type'=>'ispublic','faqs'=>'faq_count','updated'=>'updated');
@@ -25,9 +25,10 @@ $order_by="$order_column $order ";
 $total=$categories->count();
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 $pageNav=new Pagenate($total, $page, PAGE_LIMIT);
-$pageNav->setURL('categories.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']));
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+$pageNav->setURL('categories.php', $qs);
+$qstr = '&amp;order='.($order=='DESC'?'ASC':'DESC');
 $pageNav->paginate($categories);
-$qstr.='&order='.($order=='DESC'?'ASC':'DESC');
 
 if ($total)
     $showing=$pageNav->showing().' '.__('categories');
@@ -90,7 +91,7 @@ else
             <a id="selectNone" href="#ckb"><?php echo __('None');?></a>&nbsp;&nbsp;
             <a id="selectToggle" href="#ckb"><?php echo __('Toggle');?></a>&nbsp;&nbsp;
             <?php }else{
-                echo __('No FAQ categories found.');
+                echo __('No FAQ categories found!');
             } ?>
         </td>
      </tr>
diff --git a/include/staff/category.inc.php b/include/staff/category.inc.php
index 55926a0aaef1cf2a716cad851466a93da2f58c11..b73a611e9845515663c587b283ce35f44580df67 100644
--- a/include/staff/category.inc.php
+++ b/include/staff/category.inc.php
@@ -4,7 +4,7 @@ if (!defined('OSTSCPINC') || !$thisstaff
     die('Access Denied');
 
 $info=array();
-$qstr='';
+$qs = array();
 if($category && $_REQUEST['a']!='add'){
     $title=__('Update Category').': '.$category->getName();
     $action='update';
@@ -12,7 +12,7 @@ if($category && $_REQUEST['a']!='add'){
     $info=$category->getHashtable();
     $info['id']=$category->getId();
     $info['notes'] = Format::viewableImages($category->getNotes());
-    $qstr.='&id='.$category->getId();
+    $qs += array('id' => $category->getId());
     $langs = $cfg->getSecondaryLanguages();
     $translations = $category->getAllTranslations();
     foreach ($langs as $tag) {
@@ -31,12 +31,12 @@ if($category && $_REQUEST['a']!='add'){
     $title=__('Add New Category');
     $action='create';
     $submit_text=__('Add');
-    $qstr.='&a='.$_REQUEST['a'];
+    $qs += array('a' => $_REQUEST['a']);
 }
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 
 ?>
-<form action="categories.php?<?php echo $qstr; ?>" method="post" id="save">
+<form action="categories.php?<?php echo Http::build_query($qs); ?>" method="post" id="save">
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
diff --git a/include/staff/department.inc.php b/include/staff/department.inc.php
index 9b6d9d4636ef0740e6f13d5464402d6152b06fae..db23768ef4cfc9b2e8020fa34241c7777647e0fd 100644
--- a/include/staff/department.inc.php
+++ b/include/staff/department.inc.php
@@ -1,7 +1,6 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
-$info=array();
-$qstr='';
+$info = $qs = array();
 if($dept && $_REQUEST['a']!='add') {
     //Editing Department.
     $title=__('Update Department');
@@ -10,7 +9,7 @@ if($dept && $_REQUEST['a']!='add') {
     $info = $dept->getInfo();
     $info['id'] = $dept->getId();
     $info['groups'] = $dept->getAllowedGroups();
-    $qstr.='&id='.$dept->getId();
+    $qs += array('id' => $dept->getId());
 } else {
     $title=__('Add New Department');
     $action='create';
@@ -21,13 +20,12 @@ if($dept && $_REQUEST['a']!='add') {
     if (!isset($info['group_membership']))
         $info['group_membership'] = 1;
 
-    $qstr.='&a='.$_REQUEST['a'];
-
+    $qs += array('a' => $_REQUEST['a']);
 }
 
 $info = Format::htmlchars(($errors && $_POST) ? $_POST : $info);
 ?>
-<form action="departments.php?<?php echo $qstr; ?>" method="post" id="save">
+<form action="departments.php?<?php echo Http::build_query($qs); ?>" method="post" id="save">
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
diff --git a/include/staff/departments.inc.php b/include/staff/departments.inc.php
index 4b58fea69028f9636061a7cde8a041442cd24f35..443c51c00e525c74bf82569d63d79636a4ac8b75 100644
--- a/include/staff/departments.inc.php
+++ b/include/staff/departments.inc.php
@@ -2,7 +2,7 @@
 if (!defined('OSTADMININC') || !$thisstaff->isAdmin())
     die('Access Denied');
 
-$qstr='';
+$qs = array();
 $sortOptions=array(
     'name' => 'name',
     'type' => 'ispublic',
@@ -33,10 +33,11 @@ $$x=' class="'.strtolower($order).'" ';
 $page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
 $count = Dept::objects()->count();
 $pageNav = new Pagenate($count, $page, PAGE_LIMIT);
-$_qstr = $qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']);
-$pageNav->setURL('departments.php', $_qstr);
+$qstr = '&amp;'. Http::build_query($qs);
+$qstr .= '&amp;order='.($order=='DESC' ? 'ASC' : 'DESC');
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+$pageNav->setURL('departments.php', $qs);
 $showing = $pageNav->showing().' '._N('department', 'departments', $count);
-$qstr.='&order='.($order=='DESC'?'ASC':'DESC');
 ?>
 <div class="pull-left" style="width:700px;padding-top:5px;">
  <h2><?php echo __('Departments');?></h2>
@@ -128,7 +129,7 @@ $qstr.='&order='.($order=='DESC'?'ASC':'DESC');
             <a id="selectNone" href="#ckb"><?php echo __('None');?></a>&nbsp;&nbsp;
             <a id="selectToggle" href="#ckb"><?php echo __('Toggle');?></a>&nbsp;&nbsp;
             <?php }else{
-                echo __('No department found');
+                echo __('No departments found!');
             } ?>
         </td>
      </tr>
diff --git a/include/staff/directory.inc.php b/include/staff/directory.inc.php
index 74269f6ce83cc97aa921b83245400f0ac15f5d66..99c65b521549321909bdf48774258e4685e90f0d 100644
--- a/include/staff/directory.inc.php
+++ b/include/staff/directory.inc.php
@@ -1,10 +1,6 @@
 <?php
 if(!defined('OSTSTAFFINC') || !$thisstaff || !$thisstaff->isStaff()) die('Access Denied');
-$qstr='';
-$select='SELECT staff.*,CONCAT_WS(" ",firstname,lastname) as name,dept.name as dept ';
-$from='FROM '.STAFF_TABLE.' staff '.
-      'LEFT JOIN '.DEPT_TABLE.' dept ON(staff.dept_id=dept.id) ';
-$where='WHERE staff.isvisible=1 ';
+$qs = array();
 
 $agents = Staff::objects()
     ->filter(array('isvisible'=>1))
@@ -34,7 +30,7 @@ if($_REQUEST['q']) {
 
 if($_REQUEST['did'] && is_numeric($_REQUEST['did'])) {
     $agents->filter(array('dept'=>$_REQUEST['did']));
-    $qstr.='&did='.urlencode($_REQUEST['did']);
+    $qs += array('did' => $_REQUEST['did']);
 }
 
 $sortOptions=array('name'=>'firstname,lastname','email'=>'email','dept'=>'dept__name',
@@ -61,11 +57,13 @@ foreach (explode(',', $order_column) as $C) {
 $total=$agents->count();
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 $pageNav=new Pagenate($total, $page, PAGE_LIMIT);
-$pageNav->setURL('directory.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']));
+$qstr = '&amp;'. Http::build_query($qs);
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+$pageNav->setURL('directory.php', $qs);
 $pageNav->paginate($agents);
 
 //Ok..lets roll...create the actual query
-$qstr.='&order='.($order=='-'?'ASC':'DESC');
+$qstr.='&amp;order='.($order=='DESC' ? 'ASC' : 'DESC');
 
 ?>
 <h2><?php echo __('Agents');?>
diff --git a/include/staff/dynamic-form.inc.php b/include/staff/dynamic-form.inc.php
index bf68e752f874f376c1183e4dde7971b85232565b..2d6e64f496f86f99f3743d34869ed8b328c95723 100644
--- a/include/staff/dynamic-form.inc.php
+++ b/include/staff/dynamic-form.inc.php
@@ -2,7 +2,7 @@
 
 $info=array();
 if($form && $_REQUEST['a']!='add') {
-    $title = __('Update custom form section');
+    $title = __('Update form section');
     $action = 'update';
     $url = "?id=".urlencode($_REQUEST['id']);
     $submit_text=__('Save Changes');
@@ -25,14 +25,14 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
     <input type="hidden" name="do" value="<?php echo $action; ?>">
     <input type="hidden" name="a" value="<?php echo $action; ?>">
     <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
-    <h2><?php echo __('Custom Form'); ?></h2>
+    <h2><?php echo $form ? Format::htmlchars($form->getTitle()) : __('Custom Form'); ?></h2>
     <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
     <thead>
         <tr>
             <th colspan="2">
                 <h4><?php echo $title; ?></h4>
                 <em><?php echo __(
-                'Custom forms are used to allow custom data to be associated with tickets'
+                'Forms are used to allow for collection of custom data'
                 ); ?></em>
             </th>
         </tr>
diff --git a/include/staff/dynamic-list.inc.php b/include/staff/dynamic-list.inc.php
index c0e009bc46984267efebd989a398ebc3d7701eba..9fe06217332e4afc6e2d2b8a2bccf717a7faafba 100644
--- a/include/staff/dynamic-list.inc.php
+++ b/include/staff/dynamic-list.inc.php
@@ -218,7 +218,7 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
         $page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
         $count = $list->getNumItems();
         $pageNav = new Pagenate($count, $page, PAGE_LIMIT);
-        $pageNav->setURL('list.php', 'id='.urlencode($list->getId()));
+        $pageNav->setURL('list.php', array('id' => $list->getId()));
         $showing=$pageNav->showing().' '.__('list items');
         ?>
     <?php }
diff --git a/include/staff/email.inc.php b/include/staff/email.inc.php
index e556d39b643f874d21a79e50be6c02fcf9fbf0e3..27e7dc449d0318da13a714748d5ced468cd4e737 100644
--- a/include/staff/email.inc.php
+++ b/include/staff/email.inc.php
@@ -1,7 +1,6 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
-$info=array();
-$qstr='';
+$info = $qs = array();
 if($email && $_REQUEST['a']!='add'){
     $title=__('Update Email');
     $action='update';
@@ -17,7 +16,7 @@ if($email && $_REQUEST['a']!='add'){
     if($info['userpass'])
         $passwdtxt=__('To change password enter new password above.');
 
-    $qstr.='&id='.$email->getId();
+    $qs += array('id' => $email->getId());
 }else {
     $title=__('Add New Email');
     $action='create';
@@ -31,12 +30,12 @@ if($email && $_REQUEST['a']!='add'){
         $info['mail_fetchmax'] = 10;
     if (!isset($info['smtp_auth']))
         $info['smtp_auth'] = 1;
-    $qstr.='&a='.$_REQUEST['a'];
+    $qs += array('a' => $_REQUEST['a']);
 }
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 ?>
 <h2><?php echo __('Email Address');?></h2>
-<form action="emails.php?<?php echo $qstr; ?>" method="post" id="save">
+<form action="emails.php?<?php echo Http::build_query($qs); ?>" method="post" id="save">
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
diff --git a/include/staff/emails.inc.php b/include/staff/emails.inc.php
index 7b772d3e0f173802cabf564fdad4a35aedc32f6b..413cc7e74034de04b20c7d8299505df24d35b46a 100644
--- a/include/staff/emails.inc.php
+++ b/include/staff/emails.inc.php
@@ -1,7 +1,7 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff->isAdmin()) die('Access Denied');
 
-$qstr='';
+$qs = array();
 $sortOptions = array(
         'email' => 'email',
         'dept' => 'dept__name',
@@ -30,10 +30,10 @@ $$x=' class="'.strtolower($order).'" ';
 $page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
 $count = EmailModel::objects()->count();
 $pageNav = new Pagenate($count, $page, PAGE_LIMIT);
-$_qstr = $qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']);
-$pageNav->setURL('emails.php', $_qstr);
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+$pageNav->setURL('emails.php', $qs);
 $showing = $pageNav->showing().' '._N('email', 'emails', $count);
-$qstr.='&order='.($order=='DESC'?'ASC':'DESC');
+$qstr = '&amp;order='.($order=='DESC' ? 'ASC' : 'DESC');
 
 $def_dept_id = $cfg->getDefaultDeptId();
 $def_dept_name = $cfg->getDefaultDept()->getName();
@@ -107,7 +107,7 @@ $def_priority = $cfg->getDefaultPriority()->getDesc();
             <a id="selectNone" href="#ckb"><?php echo __('None');?></a>&nbsp;&nbsp;
             <a id="selectToggle" href="#ckb"><?php echo __('Toggle');?></a>&nbsp;&nbsp;
             <?php }else{
-                echo __('No help emails found');
+                echo __('No emails found!');
             } ?>
         </td>
      </tr>
diff --git a/include/staff/faq.inc.php b/include/staff/faq.inc.php
index a1bdfcc7ac36558974060b1bb7cb902dd7311e6f..cb6ffed91153a90876b202ca5d9a55bfaababfb3 100644
--- a/include/staff/faq.inc.php
+++ b/include/staff/faq.inc.php
@@ -3,8 +3,7 @@ if (!defined('OSTSCPINC') || !$thisstaff
         || !$thisstaff->getRole()->hasPerm(FAQ::PERM_MANAGE))
     die('Access Denied');
 
-$info=array();
-$qstr='';
+$info = $qs = array();
 if($faq){
     $title=__('Update FAQ').': '.$faq->getQuestion();
     $action='update';
@@ -14,7 +13,7 @@ if($faq){
     $info['topics']=$faq->getHelpTopicsIds();
     $info['answer']=Format::viewableImages($faq->getAnswer());
     $info['notes']=Format::viewableImages($faq->getNotes());
-    $qstr='id='.$faq->getId();
+    $qs += array('id' => $faq->getId());
     $langs = $cfg->getSecondaryLanguages();
     $translations = $faq->getAllTranslations();
     foreach ($langs as $tag) {
@@ -34,12 +33,13 @@ if($faq){
     $action='create';
     $submit_text=__('Add FAQ');
     if($category) {
-        $qstr='cid='.$category->getId();
+        $qs += array('cid' => $category->getId());
         $info['category_id']=$category->getId();
     }
 }
 //TODO: Add attachment support.
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
+$qstr = Http::build_query($qs);
 ?>
 <form action="faq.php?<?php echo $qstr; ?>" method="post" id="save" enctype="multipart/form-data">
  <?php csrf_token(); ?>
diff --git a/include/staff/filter.inc.php b/include/staff/filter.inc.php
index 74435b9bde6df32da9e97d41543d837728246647..f09731af64a0cc7f5455b3677b408bb208af29aa 100644
--- a/include/staff/filter.inc.php
+++ b/include/staff/filter.inc.php
@@ -4,25 +4,24 @@ if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access
 $matches=Filter::getSupportedMatches();
 $match_types=Filter::getSupportedMatchTypes();
 
-$info=array();
-$qstr='';
+$info = $qs = array();
 if($filter && $_REQUEST['a']!='add'){
     $title=__('Update Filter');
     $action='update';
     $submit_text=__('Save Changes');
     $info=array_merge($filter->getInfo(),$filter->getFlatRules());
     $info['id']=$filter->getId();
-    $qstr.='&id='.$filter->getId();
+    $qs += array('id' => $filter->getId());
 }else {
     $title=__('Add New Filter');
     $action='add';
     $submit_text=__('Add Filter');
     $info['isactive']=isset($info['isactive'])?$info['isactive']:0;
-    $qstr.='&a='.urlencode($_REQUEST['a']);
+    $qs += array('a' => $_REQUEST['a']);
 }
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 ?>
-<form action="filters.php?<?php echo $qstr; ?>" method="post" id="save">
+<form action="filters.php?<?php echo Http::build_query($qs); ?>" method="post" id="save">
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
diff --git a/include/staff/filters.inc.php b/include/staff/filters.inc.php
index 6b4c98647a20910f1a3f5688a58674ef1e5f55e4..ce55919ca6274bd0e039cb641d5da886616731a6 100644
--- a/include/staff/filters.inc.php
+++ b/include/staff/filters.inc.php
@@ -1,7 +1,7 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff->isAdmin()) die('Access Denied');
 $targets = Filter::getTargets();
-$qstr='';
+$qs = array();
 $sql='SELECT filter.*,count(rule.id) as rules '.
      'FROM '.FILTER_TABLE.' filter '.
      'LEFT JOIN '.FILTER_RULE_TABLE.' rule ON(rule.filter_id=filter.id) '.
@@ -32,9 +32,10 @@ $order_by="$order_column $order ";
 $total=db_count('SELECT count(*) FROM '.FILTER_TABLE.' filter ');
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 $pageNav=new Pagenate($total, $page, PAGE_LIMIT);
-$pageNav->setURL('filters.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']));
-//Ok..lets roll...create the actual query
-$qstr.='&order='.($order=='DESC'?'ASC':'DESC');
+$qstr = '&amp;'. Http::build_query($qs);
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+$pageNav->setURL('filters.php', $qs);
+$qstr.='&amp;order='.($order=='DESC' ? 'ASC' : 'DESC');
 $query="$sql ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
 $res=db_query($query);
 if($res && ($num=db_num_rows($res)))
diff --git a/include/staff/group.inc.php b/include/staff/group.inc.php
index 2a3a63807fc54c6c3e56edad8bc031af9fc9e0d5..e8006cade91f1a065cbd54b3a0c1ec9d5849b18e 100644
--- a/include/staff/group.inc.php
+++ b/include/staff/group.inc.php
@@ -1,6 +1,7 @@
 <?php
 
-$info=array();
+if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
+$info = $qs = array();
 if ($group) {
     $title = __('Update Group');
     $action = 'update';
@@ -9,18 +10,20 @@ if ($group) {
     $info['id'] = $group->getId();
     $info['depts'] = $group->getDepartments();
     $trans['name'] = $group->getTranslateTag('name');
+    $qs += array('id' => $group->getId());
 } else {
     $title = __('Add New Group');
     $action = 'add';
     $submit_text = __('Create Group');
     $info['isactive'] = isset($info['isactive']) ? $info['isactive'] : 1;
+    $qs += array('a' => $_REQUEST['a']);
 }
 
 $info = Format::htmlchars(($errors && $_POST) ? array_merge($info, $_POST) : $info);
 $roles = Role::getActiveRoles();
 
 ?>
-<form action="" method="post" id="save">
+<form action="groups.php?<?php echo Http::build_query($qs); ?>" method="post" id="save" name="group">
     <?php csrf_token(); ?>
     <input type="hidden" name="do" value="<?php echo $action; ?>">
     <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
diff --git a/include/staff/groups.inc.php b/include/staff/groups.inc.php
index 69e5581448700e2bdf7a70dc14147677598b8ca2..6c7b794984a0aee199e01dc769626297c1bb38b8 100644
--- a/include/staff/groups.inc.php
+++ b/include/staff/groups.inc.php
@@ -2,7 +2,7 @@
 if (!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin())
     die('Access Denied');
 
-$qstr = '';
+$qs = array();
 $sortOptions = array(
         'name'   => 'name',
         'users'  => 'members_count',
@@ -35,10 +35,11 @@ $$x=' class="'.strtolower($order).'" ';
 $page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
 $count = Group::objects()->count();
 $pageNav = new Pagenate($count, $page, PAGE_LIMIT);
-$_qstr = $qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']);
-$pageNav->setURL('groups.php', $_qstr);
+$qstr = '&amp;'. Http::build_query($qs);
+$qstr .= '&amp;order='.($order=='DESC' ? 'ASC' : 'DESC');
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+$pageNav->setURL('pages.php', $qs);
 $showing = $pageNav->showing().' '._N('group', 'groups', $count);
-$qstr.='&order='.($order=='DESC'?'ASC':'DESC');
 ?>
 <div class="pull-left" style="width:700px;padding-top:5px;">
  <h2><?php echo __('Agent Groups');?>
diff --git a/include/staff/helptopic.inc.php b/include/staff/helptopic.inc.php
index ee8616919d13d61a0b3f38d6db876f93011115f0..6fb6371bc77cd7a61e03ee1830c950cf2ea72504 100644
--- a/include/staff/helptopic.inc.php
+++ b/include/staff/helptopic.inc.php
@@ -1,7 +1,6 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
-$info=array();
-$qstr='';
+$info = $qs = array();
 if($topic && $_REQUEST['a']!='add') {
     $title=__('Update Help Topic');
     $action='update';
@@ -10,7 +9,7 @@ if($topic && $_REQUEST['a']!='add') {
     $info['id']=$topic->getId();
     $info['pid']=$topic->getPid();
     $trans['name'] = $topic->getTranslateTag('name');
-    $qstr.='&id='.$topic->getId();
+    $qs += array('id' => $topic->getId());
 } else {
     $title=__('Add New Help Topic');
     $action='create';
@@ -18,11 +17,11 @@ if($topic && $_REQUEST['a']!='add') {
     $info['isactive']=isset($info['isactive'])?$info['isactive']:1;
     $info['ispublic']=isset($info['ispublic'])?$info['ispublic']:1;
     $info['form_id'] = Topic::FORM_USE_PARENT;
-    $qstr.='&a='.$_REQUEST['a'];
+    $qs += array('a' => $_REQUEST['a']);
 }
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 ?>
-<form action="helptopics.php?<?php echo $qstr; ?>" method="post" id="save">
+<form action="helptopics.php?<?php echo Http::build_query($qs); ?>" method="post" id="save">
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
diff --git a/include/staff/orgs.inc.php b/include/staff/orgs.inc.php
index d85d42d1d087dcd16268336c79e3f2165a3f7892..f75f67f556d9e6fbf715ed374a1771e5b06c44d0 100644
--- a/include/staff/orgs.inc.php
+++ b/include/staff/orgs.inc.php
@@ -1,7 +1,7 @@
 <?php
 if(!defined('OSTSCPINC') || !$thisstaff) die('Access Denied');
 
-$qstr='';
+$qs = array();
 
 $select = 'SELECT org.*
             ,COALESCE(team.name,
@@ -27,7 +27,7 @@ if ($_REQUEST['query']) {
                     org.name LIKE \'%'.$search.'%\' OR value.value LIKE \'%'.$search.'%\'
                 )';
 
-    $qstr.='&query='.urlencode($_REQUEST['query']);
+    $qs += array('query' => $_REQUEST['query']);
 }
 
 $sortOptions = array('name' => 'org.name',
@@ -56,9 +56,10 @@ $order_by="$order_column $order ";
 $total=db_count('SELECT count(DISTINCT org.id) '.$from.' '.$where);
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 $pageNav=new Pagenate($total,$page,PAGE_LIMIT);
-$pageNav->setURL('orgs.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']));
-//Ok..lets roll...create the actual query
-$qstr.='&order='.($order=='DESC'?'ASC':'DESC');
+$qstr = '&amp;'. Http::build_query($qs);
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+$pageNav->setURL('orgs.php', $qs);
+$qstr.='&amp;order='.($order=='DESC' ? 'ASC' : 'DESC');
 
 $select .= ', count(DISTINCT user.id) as users ';
 
diff --git a/include/staff/page.inc.php b/include/staff/page.inc.php
index 028e71b9366729a7019611b64cf55c262947ae2e..cbc2d2526f1ed37e65952a3efb4e309208825c63 100644
--- a/include/staff/page.inc.php
+++ b/include/staff/page.inc.php
@@ -6,8 +6,7 @@ $pageTypes = array(
         'thank-you' => __('Thank you page'),
         'other' => __('Other'),
         );
-$info=array();
-$qstr='';
+$info = $qs = array();
 if($page && $_REQUEST['a']!='add'){
     $title=__('Update Page');
     $action='update';
@@ -17,7 +16,7 @@ if($page && $_REQUEST['a']!='add'){
     $info['notes'] = Format::viewableImages($info['notes']);
     $trans['name'] = $page->getTranslateTag('name');
     $slug = Format::slugify($info['name']);
-    $qstr.='&id='.$page->getId();
+    $qs += array('id' => $page->getId());
     $translations = CustomDataTranslation::allTranslations(
         $page->getTranslateTag('name:body'), 'article');
     foreach ($cfg->getSecondaryLanguages() as $tag) {
@@ -34,11 +33,11 @@ if($page && $_REQUEST['a']!='add'){
     $action='add';
     $submit_text=__('Add Page');
     $info['isactive']=isset($info['isactive'])?$info['isactive']:0;
-    $qstr.='&a='.urlencode($_REQUEST['a']);
+    $qs += array('a' => $_REQUEST['a']);
 }
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 ?>
-<form action="pages.php?<?php echo $qstr; ?>" method="post" id="save">
+<form action="pages.php?<?php echo Http::build_query($qs); ?>" method="post" id="save">
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
diff --git a/include/staff/pages.inc.php b/include/staff/pages.inc.php
index 5f364a768592f2c1fb5cf3b61bac752eb019392e..11a3f48b66146023b76b0d0c38176bf55b99a822 100644
--- a/include/staff/pages.inc.php
+++ b/include/staff/pages.inc.php
@@ -4,7 +4,7 @@ if(!defined('OSTADMININC') || !$thisstaff->isAdmin()) die('Access Denied');
 $pages = Page::objects()
     ->filter(array('type__in'=>array('other','landing','thank-you','offline')))
     ->annotate(array('topics'=>SqlAggregate::COUNT('topics')));
-$qstr='';
+$qs = array();
 $sortOptions=array(
         'name'=>'name', 'status'=>'isactive',
         'created'=>'created', 'updated'=>'updated',
@@ -25,10 +25,11 @@ $$x=' class="'.strtolower($order).'" ';
 $total = $pages->count();
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 $pageNav=new Pagenate($total, $page, PAGE_LIMIT);
-$pageNav->setURL('pages.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']));
+$qstr = '&amp;'. Http::build_query($qs);
+$qstr .= '&amp;order='.($order=='DESC' ? 'ASC' : 'DESC');
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+$pageNav->setURL('pages.php', $qs);
 //Ok..lets roll...create the actual query
-$qstr.='&order='.($order=='DESC'?'ASC':'DESC');
-$pages = $pages->limit($pageNav->getLimit())->offset($pageNav->getStart());
 if ($total)
     $showing=$pageNav->showing()._N('site page','site pages', $num);
 else
diff --git a/include/staff/plugin.inc.php b/include/staff/plugin.inc.php
index f76b0f27c749e889b0a8170875798a3d2f932292..f27e21333aabc78535863ccb2e9c403851ac8a01 100644
--- a/include/staff/plugin.inc.php
+++ b/include/staff/plugin.inc.php
@@ -14,10 +14,11 @@ if($plugin && $_REQUEST['a']!='add') {
     $submit_text = __('Save Changes');
     $info = $plugin->ht;
 }
-$info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
+
+$info = Format::htmlchars(($errors && $_POST) ? $_POST : $info);
 ?>
 
-<form action="?id=<?php echo urlencode($_REQUEST['id']); ?>" method="post" id="save">
+<form action="?<?php echo Http::build_query(array('id' => $_REQUEST['id'])); ?>" method="post" id="save">
     <?php csrf_token(); ?>
     <input type="hidden" name="do" value="<?php echo $action; ?>">
     <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
diff --git a/include/staff/settings-pages.inc.php b/include/staff/settings-pages.inc.php
index 9727bde4060beeaacce6169a375b87b0b5382aa9..3bf1c78776adc2bc90b9604175591bd3ff969202 100644
--- a/include/staff/settings-pages.inc.php
+++ b/include/staff/settings-pages.inc.php
@@ -135,7 +135,7 @@ $pages = Page::getPages();
                     echo $logo->getId(); ?>" <?php
                     if ($logo->getId() == $current)
                         echo 'checked="checked"'; ?>/>
-                <img src="image.php?h=<?php echo $logo->getDownloadHash(); ?>"
+                <img src="<?php echo $logo->getDownloadUrl(); ?>"
                     alt="Custom Logo" valign="middle"
                     style="box-shadow: 0 0 0.5em rgba(0,0,0,0.5);
                         margin: 0.5em; height: 5em;
diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php
index 0aef056efa472af65087bf8126a7301bad4fc645..021d24c4397e1ac632e4c974e8f9fe0cd718b607 100644
--- a/include/staff/settings-tickets.inc.php
+++ b/include/staff/settings-tickets.inc.php
@@ -207,7 +207,7 @@ if(!($maxfileuploads=ini_get('max_file_uploads')))
             </th>
         </tr>
         <tr>
-            <td width="180"><?php echo __('Ticket Attachment Settings');?>:</td>
+            <td width="180"><?php echo __('EndUser Attachment Settings');?>:</td>
             <td>
 <?php
                 $tform = TicketForm::objects()->one()->getForm();
@@ -224,7 +224,7 @@ if(!($maxfileuploads=ini_get('max_file_uploads')))
             </td>
         </tr>
         <tr>
-            <td width="180"><?php echo __('Maximum File Size');?>:</td>
+            <td width="180"><?php echo __('Agent Maximum File Size');?>:</td>
             <td>
                 <select name="max_file_size">
                     <option value="262144">&mdash; <?php echo __('Small'); ?> &mdash;</option>
diff --git a/include/staff/slaplan.inc.php b/include/staff/slaplan.inc.php
index 91dba3a2b735c40e43cf6c2b8f155176283d0326..3ca424c3044aea1d524f6ef8032b9671cfe04aca 100644
--- a/include/staff/slaplan.inc.php
+++ b/include/staff/slaplan.inc.php
@@ -1,7 +1,6 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
-$info=array();
-$qstr='';
+$info = $qs = array();
 if($sla && $_REQUEST['a']!='add'){
     $title=__('Update SLA Plan' /* SLA is abbreviation for Service Level Agreement */);
     $action='update';
@@ -9,7 +8,7 @@ if($sla && $_REQUEST['a']!='add'){
     $info=$sla->getInfo();
     $info['id']=$sla->getId();
     $trans['name'] = $sla->getTranslateTag('name');
-    $qstr.='&id='.$sla->getId();
+    $qs += array('id' => $sla->getId());
 }else {
     $title=__('Add New SLA Plan' /* SLA is abbreviation for Service Level Agreement */);
     $action='add';
@@ -17,11 +16,11 @@ if($sla && $_REQUEST['a']!='add'){
     $info['isactive']=isset($info['isactive'])?$info['isactive']:1;
     $info['enable_priority_escalation']=isset($info['enable_priority_escalation'])?$info['enable_priority_escalation']:1;
     $info['disable_overdue_alerts']=isset($info['disable_overdue_alerts'])?$info['disable_overdue_alerts']:0;
-    $qstr.='&a='.urlencode($_REQUEST['a']);
+    $qs += array('a' => $_REQUEST['a']);
 }
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 ?>
-<form action="slas.php?<?php echo $qstr; ?>" method="post" id="save">
+<form action="slas.php?<?php echo Http::build_query($qs); ?>" method="post" id="save">
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
diff --git a/include/staff/slaplans.inc.php b/include/staff/slaplans.inc.php
index 3565ea7605554c3e828071386e68e60458d5ac60..46264ec98dfa061e98f45a787da722c72d3833e5 100644
--- a/include/staff/slaplans.inc.php
+++ b/include/staff/slaplans.inc.php
@@ -1,7 +1,7 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff->isAdmin()) die('Access Denied');
 
-$qstr='';
+$qs = array();
 $sortOptions=array(
         'name' => 'name',
         'status' => 'isactive',
@@ -31,11 +31,12 @@ $x=$sort.'_sort';
 $$x=' class="'.strtolower($order).'" ';
 $page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
 $count = SLA::objects()->count();
-$pageNav = new Pagenate($count, $page, PAGE_LIMIT);
-$_qstr = $qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']);
-$pageNav->setURL('slas.php', $_qstr);
+$qstr = '&amp;'. Http::build_query($qs);
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+
+$pageNav->setURL('slas.php', $qs);
 $showing = $pageNav->showing().' '._N('SLA plan', 'SLA plans', $count);
-$qstr.='&order='.($order=='DESC'?'ASC':'DESC');
+$qstr .= '&amp;order='.($order=='DESC' ? 'ASC' : 'DESC');
 ?>
 <div class="pull-left" style="width:700px;padding-top:5px;">
  <h2><?php echo __('Service Level Agreements');?></h2>
diff --git a/include/staff/staff.inc.php b/include/staff/staff.inc.php
index f1ac113c711a521c93119409896686676a9396f1..1da74cc1aa0648324cc1003ad6003b9ce82454e3 100644
--- a/include/staff/staff.inc.php
+++ b/include/staff/staff.inc.php
@@ -1,8 +1,7 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
 
-$info=array();
-$qstr='';
+$info = $qs = array();
 if($staff && $_REQUEST['a']!='add'){
     //Editing Department.
     $title=__('Update Agent');
@@ -13,7 +12,7 @@ if($staff && $_REQUEST['a']!='add'){
     $info['id']=$staff->getId();
     $info['teams'] = $staff->getTeams();
     $info['signature'] = Format::viewableImages($info['signature']);
-    $qstr.='&id='.$staff->getId();
+    $qs += array('id' => $staff->getId());
 }else {
     $title=__('Add New Agent');
     $action='create';
@@ -25,11 +24,11 @@ if($staff && $_REQUEST['a']!='add'){
     $info['isactive']=1;
     $info['isvisible']=1;
     $info['isadmin']=0;
-    $qstr.='&a=add';
+    $qs += array('a' => 'add');
 }
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 ?>
-<form action="staff.php?<?php echo $qstr; ?>" method="post" id="save" autocomplete="off">
+<form action="staff.php?<?php echo Http::build_query($qs); ?>" method="post" id="save" autocomplete="off">
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
diff --git a/include/staff/staffmembers.inc.php b/include/staff/staffmembers.inc.php
index e1872334ec1cc67c413a2202789d5920af6b3190..dc40e747c26e06a8ef7b734bd29ec9561eb19f50 100644
--- a/include/staff/staffmembers.inc.php
+++ b/include/staff/staffmembers.inc.php
@@ -3,6 +3,7 @@ if (!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin())
     die('Access Denied');
 
 $qstr='';
+$qs = array();
 $sortOptions = array(
         'name' => 'lastname',
         'username' => 'username',
@@ -38,17 +39,17 @@ $$x=' class="'.strtolower($order).'" ';
 $filters = array();
 if ($_REQUEST['did'] && is_numeric($_REQUEST['did'])) {
     $filters += array('dept_id' => $_REQUEST['did']);
-    $qstr.='&did='.urlencode($_REQUEST['did']);
+    $qs += array('did' => $_REQUEST['did']);
 }
 
 if ($_REQUEST['gid'] && is_numeric($_REQUEST['gid'])) {
     $filters += array('group_id' => $_REQUEST['gid']);
-    $qstr.='&gid='.urlencode($_REQUEST['gid']);
+    $qs += array('gid' => $_REQUEST['gid']);
 }
 
 if ($_REQUEST['tid'] && is_numeric($_REQUEST['tid'])) {
     $filters += array('teams__team_id' => $_REQUEST['tid']);
-    $qstr.='&tid='.urlencode($_REQUEST['tid']);
+    $qs += array('tid' => $_REQUEST['tid']);
 }
 
 //agents objects
@@ -68,10 +69,11 @@ if ($filters)
 $page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
 $count = $agents->count();
 $pageNav = new Pagenate($count, $page, PAGE_LIMIT);
-$_qstr = $qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']);
-$pageNav->setURL('staff.php', $_qstr);
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+$pageNav->setURL('staff.php', $qs);
 $showing = $pageNav->showing().' '._N('agent', 'agents', $count);
-$qstr.='&order='.($order=='DESC'?'ASC':'DESC');
+$qstr = '&amp;'. Http::build_query($qs);
+$qstr .= '&amp;order='.($order=='DESC' ? 'ASC' : 'DESC');
 
 // add limits.
 $agents->limit($pageNav->getLimit())->offset($pageNav->getStart());
diff --git a/include/staff/syslogs.inc.php b/include/staff/syslogs.inc.php
index 8b610d0f60dd4d87ec61351ce4313753a6c47acd..68648f99feb37872fcb2949ef6dc386e6c03687d 100644
--- a/include/staff/syslogs.inc.php
+++ b/include/staff/syslogs.inc.php
@@ -1,9 +1,9 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
 
-$qstr='';
+$qs = array();
 if($_REQUEST['type']) {
-    $qstr.='&amp;type='.urlencode($_REQUEST['type']);
+    $qs += array('type' => $_REQUEST['type']);
 }
 $type=null;
 switch(strtolower($_REQUEST['type'])){
@@ -38,11 +38,11 @@ if( ($startTime && $startTime>time()) or ($startTime>$endTime && $endTime>0)){
 }else{
     if($startTime){
         $qwhere.=' AND created>=FROM_UNIXTIME('.$startTime.')';
-        $qstr.='&startDate='.urlencode($_REQUEST['startDate']);
+        $qs += array('startDate' => $_REQUEST['startDate']);
     }
     if($endTime){
         $qwhere.=' AND created<=FROM_UNIXTIME('.$endTime.')';
-        $qstr.='&endDate='.urlencode($_REQUEST['endDate']);
+        $qs += array('endDate' => $_REQUEST['endDate']);
     }
 }
 $sortOptions=array('id'=>'log.log_id', 'title'=>'log.title','type'=>'log_type','ip'=>'log.ip_address'
@@ -73,8 +73,9 @@ $total=db_count("SELECT count(*) $qfrom $qwhere");
 $page = ($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 //pagenate
 $pageNav=new Pagenate($total, $page, PAGE_LIMIT);
-$pageNav->setURL('logs.php',$qstr);
-$qstr.='&order='.($order=='DESC'?'ASC':'DESC');
+$pageNav->setURL('logs.php',$qs);
+$qs += array('order' => ($order=='DESC' ? 'ASC' : 'DESC'));
+$qstr = '&amp;'. Http::build_query($qs);
 $query="$qselect $qfrom $qwhere ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
 $res=db_query($query);
 if($res && ($num=db_num_rows($res)))
diff --git a/include/staff/system.inc.php b/include/staff/system.inc.php
index 2e21c21a67527331b228f673fc2b80974bc11e73..a3a337f12a56b274093b97d3b6ab5025fd6e6472 100644
--- a/include/staff/system.inc.php
+++ b/include/staff/system.inc.php
@@ -37,6 +37,10 @@ $extensions = array(
             'name' => 'intl',
             'desc' => __('Highly recommended for non western european language content')
             ),
+        'fileinfo' => array(
+            'name' => 'fileinfo',
+            'desc' => __('Used to detect file types for uploads')
+            ),
         );
 
 ?>
diff --git a/include/staff/team.inc.php b/include/staff/team.inc.php
index ec651205c6a9bf6e4529de325ba2c921e125c47d..f851fdc3a9c789277eece37f62b60436530bd984 100644
--- a/include/staff/team.inc.php
+++ b/include/staff/team.inc.php
@@ -1,7 +1,6 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
-$info=$members=array();
-$qstr='';
+$info = $members = $qs = array();
 if ($team && $_REQUEST['a']!='add') {
     //Editing Team
     $title=__('Update Team');
@@ -10,20 +9,20 @@ if ($team && $_REQUEST['a']!='add') {
     $info=$team->getInfo();
     $info['id']=$team->getId();
     $trans['name'] = $team->getTranslateTag('name');
-    $qstr.='&id='.$team->getId();
     $members = $team->getMembers();
+    $qs += array('id' => $team->getId());
 } else {
     $title=__('Add New Team');
     $action='create';
     $submit_text=__('Create Team');
     $info['isenabled']=1;
     $info['noalerts']=0;
-    $qstr.='&a='.$_REQUEST['a'];
+    $qs += array('a' => $_REQUEST['a']);
 }
 
 $info = Format::htmlchars(($errors && $_POST) ? $_POST : $info);
 ?>
-<form action="teams.php?<?php echo $qstr; ?>" method="post" id="save">
+<form action="teams.php?<?php echo Http::build_query($qs); ?>" method="post" id="save">
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
diff --git a/include/staff/teams.inc.php b/include/staff/teams.inc.php
index 22c08c2073dbf428d406df8320620d6cc7383615..5982f7c208e1e05c020db9a97dd0409c43203293 100644
--- a/include/staff/teams.inc.php
+++ b/include/staff/teams.inc.php
@@ -1,7 +1,7 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
 
-$qstr='';
+$qs = array();
 $sortOptions=array(
         'name' => 'name',
         'status' => 'isenabled',
@@ -35,10 +35,11 @@ $$x=' class="'.strtolower($order).'" ';
 $page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
 $count = Team::objects()->count();
 $pageNav = new Pagenate($count, $page, PAGE_LIMIT);
-$_qstr = $qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']);
-$pageNav->setURL('teams.php', $_qstr);
+$qstr = '&amp;'. Http::build_query($qs);
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+$pageNav->setURL('teams.php', $qs);
 $showing = $pageNav->showing().' '._N('team', 'teams', $count);
-$qstr.='&order='.($order=='DESC'?'ASC':'DESC');
+$qstr .= '&amp;order='.urlencode($order=='DESC' ? 'ASC' : 'DESC');
 
 
 ?>
diff --git a/include/staff/template.inc.php b/include/staff/template.inc.php
index e0e3727474bed4873cd7fdfb16945083973438f0..073908166ee3a3ebb5388de462cb701d72d508d9 100644
--- a/include/staff/template.inc.php
+++ b/include/staff/template.inc.php
@@ -1,26 +1,25 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
 
-$info=array();
-$qstr='';
+$info = $qs = array();
 if($template && $_REQUEST['a']!='add'){
     $title=__('Update Template');
     $action='update';
     $submit_text=__('Save Changes');
     $info=$template->getInfo();
     $info['tpl_id']=$template->getId();
-    $qstr.='&tpl_id='.$template->getId();
+    $qs += array('tpl_id' => $template->getId());
 }else {
     $title=__('Add New Template');
     $action='add';
     $submit_text=__('Add Template');
     $info['isactive']=isset($info['isactive'])?$info['isactive']:0;
     $info['lang_id'] = $cfg->getPrimaryLanguage();
-    $qstr.='&a='.urlencode($_REQUEST['a']);
+    $qs += array('a' => $_REQUEST['a']);
 }
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 ?>
-<form action="templates.php?<?php echo $qstr; ?>" method="post" id="save">
+<form action="templates.php?<?php echo Http::build_query($qs); ?>" method="post" id="save">
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
@@ -141,7 +140,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 <?php foreach($langs as $l) {
     $selected = ($info['lang_id'] == $l['code']) ? 'selected="selected"' : ''; ?>
                     <option value="<?php echo $l['code']; ?>" <?php echo $selected;
-                        ?>><?php echo $l['desc']; ?></option>
+                        ?>><?php echo Internationalization::getLanguageDescription($l['code']); ?></option>
 <?php } ?>
                 </select>
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['lang_id']; ?></span>
diff --git a/include/staff/templates.inc.php b/include/staff/templates.inc.php
index c7bcafafb76e9567817432ce0cfb0af0f039626b..2d7a972cb04176a1f538407fa93c946cfe787f62 100644
--- a/include/staff/templates.inc.php
+++ b/include/staff/templates.inc.php
@@ -1,7 +1,7 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff->isAdmin()) die('Access Denied');
 
-$qstr='';
+$qs = array();
 $sql='SELECT tpl.*,count(dept.tpl_id) as depts '.
      'FROM '.EMAIL_TEMPLATE_GRP_TABLE.' tpl '.
      'LEFT JOIN '.DEPT_TABLE.' dept USING(tpl_id) '.
@@ -30,9 +30,10 @@ $order_by="$order_column $order ";
 $total=db_count('SELECT count(*) FROM '.EMAIL_TEMPLATE_GRP_TABLE.' tpl ');
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 $pageNav=new Pagenate($total, $page, PAGE_LIMIT);
-$pageNav->setURL('templates.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']));
-//Ok..lets roll...create the actual query
-$qstr.='&order='.($order=='DESC'?'ASC':'DESC');
+$qstr = '&amp;'. Http::build_query($qs);
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+$pageNav->setURL('templates.php', $qs);
+$qstr .= '&amp;order='.($order=='DESC' ? 'ASC' : 'DESC');
 $query="$sql GROUP BY tpl.tpl_id ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
 $res=db_query($query);
 if($res && ($num=db_num_rows($res)))
diff --git a/include/staff/templates/users.tmpl.php b/include/staff/templates/users.tmpl.php
index 51f8f7e3d6b84fa57c8174aae8a4e2547fa7ea9e..b95974d214cf17d57ef2062b1d26de4bd7106307 100644
--- a/include/staff/templates/users.tmpl.php
+++ b/include/staff/templates/users.tmpl.php
@@ -1,5 +1,5 @@
 <?php
-$qstr='';
+$qs = array();
 $select = 'SELECT user.*, email.address as email ';
 
 $from = 'FROM '.USER_TABLE.' user '
@@ -33,9 +33,12 @@ $order_by="$order_column $order ";
 $total=db_count('SELECT count(DISTINCT user.id) '.$from.' '.$where);
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 $pageNav=new Pagenate($total,$page,PAGE_LIMIT);
-$pageNav->setURL('users.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']));
+$qstr = '&amp;'. Http::build_query($qs);
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+
+$pageNav->setURL('users.php', $qs);
 //Ok..lets roll...create the actual query
-$qstr.='&order='.($order=='DESC'?'ASC':'DESC');
+$qstr .= '&amp;order='.($order=='DESC' ? 'ASC' : 'DESC');
 
 $select .= ', count(DISTINCT ticket.ticket_id) as tickets ';
 
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index ce64ae1156c060a3a5bb0fb113149dc376c67281..de9ccf80153ce21876688d343459a1cb08dc6b17 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -441,7 +441,7 @@ $tcount+= $ticket->getNumNotes();
             </tr>
             <tr><td colspan="4" class="thread-body" id="thread-id-<?php
                 echo $entry['id']; ?>"><div><?php
-                echo $entry['body']->toHtml(); ?></div></td></tr>
+                echo Format::clickableurls($entry['body']->toHtml()); ?></div></td></tr>
             <?php
             $urls = null;
             if($entry['attachments']
@@ -570,6 +570,7 @@ $tcount+= $ticket->getNumNotes();
                     <label><strong><?php echo __('Response');?>:</strong></label>
                 </td>
                 <td>
+<?php if ($cfg->isCannedResponseEnabled()) { ?>
                     <select id="cannedResp" name="cannedResp">
                         <option value="0" selected="selected"><?php echo __('Select a canned response');?></option>
                         <option value='original'><?php echo __('Original Message'); ?></option>
@@ -584,7 +585,7 @@ $tcount+= $ticket->getNumNotes();
                         ?>
                     </select>
                     <br>
-                    <?php
+<?php } # endif (canned-resonse-enabled)
                     $signature = '';
                     switch ($thisstaff->getDefaultSignatureType()) {
                     case 'dept':
@@ -1043,5 +1044,19 @@ $(function() {
             }
         });
     });
+<?php
+    // Set the lock if one exists
+    if ($lock) { ?>
+!function() {
+  var setLock = setInterval(function() {
+    if (typeof(window.autoLock) === 'undefined')
+      return;
+    clearInterval(setLock);
+    autoLock.setLock({
+      id:<?php echo $lock->getId(); ?>,
+      time: <?php echo $cfg->getLockTime(); ?>}, 'acquire');
+  }, 50);
+}();
+<?php } ?>
 });
 </script>
diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php
index ad4e0864595ba4f76f4c175675292fa8fb699190..d12c5b0858a251799d0af0afdc8a631fc1fc20be 100644
--- a/include/staff/tickets.inc.php
+++ b/include/staff/tickets.inc.php
@@ -16,8 +16,8 @@ unset($args['a']);
 
 $refresh_url = $path . '?' . http_build_query($args);
 
-
-switch(strtolower($_REQUEST['status'])){ //Status is overloaded
+$queue_name = strtolower($_GET['status'] ?: $_GET['a']); //Status is overloaded
+switch ($queue_name) {
 case 'closed':
     $status='closed';
     $results_type=__('Closed Tickets');
@@ -60,6 +60,10 @@ case 'search':
         }
         break;
     }
+    elseif (isset($_GET['uid'])) {
+        // Apply user filter
+        $tickets->filter(array('user__id'=>$_GET['uid']));
+    }
     elseif (isset($_SESSION['advsearch'])) {
         // XXX: De-duplicate and simplify this code
         $form = $search->getFormFromSession('advsearch');
@@ -67,7 +71,6 @@ case 'search':
         $tickets = $search->mangleQuerySet($tickets, $form);
         $results_type=__('Advanced Search')
             . '<a class="action-button" href="?clear_filter"><i class="icon-ban-circle"></i> <em>' . __('clear') . '</em></a>';
-        unset($_REQUEST['sort']);
         break;
     }
     // Fall-through and show open tickets
@@ -115,7 +118,11 @@ $tickets->values('lock__staff_id', 'staff_id', 'isoverdue', 'team_id', 'ticket_i
 // Apply requested quick filter
 
 // Apply requested sorting
-switch ($_REQUEST['sort']) {
+$queue_sort_key = sprintf(':Q:%s:sort', $queue_name);
+
+if (isset($_GET['sort']))
+    $_SESSION[$queue_sort_key] = $_GET['sort'];
+switch ($_SESSION[$queue_sort_key]) {
 case 'number':
     $tickets->extra(array(
         'order_by'=>array(SqlExpression::times(new SqlField('number'), 1))
@@ -143,9 +150,9 @@ case 'updated':
 }
 
 // Apply requested pagination
-$pagelimit=($_GET['limit'] && is_numeric($_GET['limit']))?$_GET['limit']:PAGE_LIMIT;
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
-$pageNav=new Pagenate($tickets->count(), $page,$pagelimit);
+$pageNav=new Pagenate($tickets->count(), $page, PAGE_LIMIT);
+$pageNav->setUrl('tickets.php', $args);
 $tickets = $pageNav->paginate($tickets);
 
 TicketForm::ensureDynamicDataView();
@@ -158,10 +165,12 @@ $_SESSION[':Q:tickets'] = $tickets;
 <!-- SEARCH FORM START -->
 <div id='basic_search'>
     <form action="tickets.php" method="get">
-    <input type="hidden" name="status" value="search">
+    <input type="hidden" name="a" value="search">
     <table>
         <tr>
-            <td><input type="text" id="basic-ticket-search" name="query" size=30 value="<?php echo Format::htmlchars($_REQUEST['query']); ?>"
+            <td><input type="text" id="basic-ticket-search" name="query"
+            size=30 value="<?php echo Format::htmlchars($_REQUEST['query'],
+            true); ?>"
                 autocomplete="off" autocorrect="off" autocapitalize="off"></td>
             <td><input type="submit" class="button" value="<?php echo __('Search'); ?>"></td>
             <td>&nbsp;&nbsp;<a href="#" onclick="javascript:
@@ -191,7 +200,7 @@ $_SESSION[':Q:tickets'] = $tickets;
     'priority,due' => __('Priority + Due Soon'),
     'number' =>     __('Ticket Number'),
 ) as $mode => $desc) { ?>
-            <option value="<?php echo $mode; ?>" <?php if ($mode == $_REQUEST['sort']) echo 'selected="selected"'; ?>><?php echo $desc; ?></option>
+            <option value="<?php echo $mode; ?>" <?php if ($mode == $_SESSION[$queue_sort_key]) echo 'selected="selected"'; ?>><?php echo $desc; ?></option>
 <?php } ?>
             </select>
             </span>
@@ -212,7 +221,8 @@ $_SESSION[':Q:tickets'] = $tickets;
 <?php csrf_token(); ?>
  <input type="hidden" name="a" value="mass_process" >
  <input type="hidden" name="do" id="action" value="" >
- <input type="hidden" name="status" value="<?php echo Format::htmlchars($_REQUEST['status']); ?>" >
+ <input type="hidden" name="status" value="<?php echo
+ Format::htmlchars($_REQUEST['status'], true); ?>" >
  <table class="list" border="0" cellspacing="1" cellpadding="2" width="940">
     <thead>
         <tr>
@@ -364,10 +374,14 @@ $_SESSION[':Q:tickets'] = $tickets;
     </tfoot>
     </table>
     <?php
-    if($total>0){ //if we actually had any tickets returned.
+    if ($total>0) { //if we actually had any tickets returned.
         echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;';
-        echo '<a class="export-csv no-pjax" href="?a=export&status='
-            .$_REQUEST['status'] .'">'.__('Export').'</a>&nbsp;<i class="help-tip icon-question-sign" href="#export"></i></div>';
+        echo sprintf('<a class="export-csv no-pjax" href="?%s">%s</a>',
+                Http::build_query(array(
+                        'a' => 'export', 'h' => $hash,
+                        'status' => $_REQUEST['status'])),
+                __('Export'));
+        echo '&nbsp;<i class="help-tip icon-question-sign" href="#export"></i></div>';
     } ?>
     </form>
 </div>
diff --git a/include/staff/users.inc.php b/include/staff/users.inc.php
index 073c33a03431a9c39fd3007c95649d3733e77e92..9ff87132d2b1311b2ad620d17cd896129e656327 100644
--- a/include/staff/users.inc.php
+++ b/include/staff/users.inc.php
@@ -1,7 +1,7 @@
 <?php
 if(!defined('OSTSCPINC') || !$thisstaff) die('Access Denied');
 
-$qstr='';
+$qs = array();
 
 $users = User::objects()
     ->annotate(array('ticket_count'=>SqlAggregate::COUNT('tickets')));
@@ -14,6 +14,7 @@ if ($_REQUEST['query']) {
         'org__name__contains' => $search,
         // TODO: Add search for cdata
     )));
+    $qs += array('query' => $_REQUEST['query']);
 }
 
 $sortOptions = array('name' => 'name',
@@ -42,11 +43,12 @@ $order_by="$order_column $order ";
 $total = $users->count();
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 $pageNav=new Pagenate($total,$page,PAGE_LIMIT);
-$pageNav->setURL('users.php',$qstr.'&sort='.urlencode($_REQUEST['sort']).'&order='.urlencode($_REQUEST['order']));
 $pageNav->paginate($users);
 
-//Ok..lets roll...create the actual query
-$qstr.='&order='.($order=='DESC'?'ASC':'DESC');
+$qstr = '&amp;'. Http::build_query($qs);
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+$pageNav->setURL('users.php', $qs);
+$qstr.='&amp;order='.($order=='DESC' ? 'ASC' : 'DESC');
 
 //echo $query;
 $_SESSION[':Q:users'] = $users;
diff --git a/js/filedrop.field.js b/js/filedrop.field.js
index 7d332b575943224a0b6b9884b2a252bdbdb98b70..e451028a03fef02fa385f09497178778c125134c 100644
--- a/js/filedrop.field.js
+++ b/js/filedrop.field.js
@@ -179,11 +179,12 @@
       }
       if (file.id)
         filenode.data('fileId', file.id);
-      if (file.download)
+      if (file.download_url) {
         filenode.find('.filename').prepend(
           $('<a class="no-pjax" target="_blank"></a>').text(file.name)
-            .attr('href', 'file.php?h='+escape(file.download))
+            .attr('href', file.download_url)
         );
+      }
       else
         filenode.find('.filename').prepend($('<span>').text(file.name));
       this.$element.parent().find('.files').append(filenode);
diff --git a/kb/file.php b/kb/file.php
deleted file mode 100644
index 99664708a567a08d80eaca73ece27a7f9973f87a..0000000000000000000000000000000000000000
--- a/kb/file.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-/*********************************************************************
-    file.php
-
-    Simply downloads the file...on hash validation as follows;
-
-    * Hash must be 64 chars long.
-    * First 32 chars is the perm. file hash
-    * Next 32 chars  is md5(file_id.session_id().file_hash)
-
-    Peter Rotich <peter@osticket.com>
-    Copyright (c)  2006-2013 osTicket
-    http://www.osticket.com
-
-    Released under the GNU General Public License WITHOUT ANY WARRANTY.
-    See LICENSE.TXT for details.
-
-    vim: expandtab sw=4 ts=4 sts=4:
-**********************************************************************/
-require('kb.inc.php');
-require_once(INCLUDE_DIR.'class.file.php');
-$h=trim($_GET['h']);
-//basic checks
-if(!$h  || strlen($h)!=64  //32*2
-        || !($file=AttachmentFile::lookup(substr($h,0,32))) //first 32 is the file hash.
-        || strcasecmp($h, $file->getDownloadHash())) //next 32 is file id + session hash.
-    Http::response(404, __('Unknown or invalid file'));
-
-$file->download();
-?>
diff --git a/l.php b/l.php
deleted file mode 100644
index a0520a8cbba31e26cd743e3e9546aebd329e0131..0000000000000000000000000000000000000000
--- a/l.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-/*********************************************************************
-    l.php
-
-    Link redirection
-
-    Jared Hancock <jared@osticket.com>
-    Copyright (c)  2006-2013 osTicket
-    http://www.osticket.com
-
-    Released under the GNU General Public License WITHOUT ANY WARRANTY.
-    See LICENSE.TXT for details.
-
-    vim: expandtab sw=4 ts=4 sts=4:
-**********************************************************************/
-require 'secure.inc.php';
-//Basic url validation + token check.
-
-# PHP < 5.4.7 will not handle a URL like //host.tld/path correctly
-if (!($url=trim($_GET['url'])))
-    Http::response(422, __('Invalid URL'));
-
-$check = (strpos($url, '//') === 0) ? 'http:' . $url : $url;
-if (!Validator::is_url($check) || !$ost->validateLinkToken($_GET['auth']))
-    Http::response(403, __('URL link not authorized'));
-elseif (strpos($_SERVER['HTTP_ACCEPT'], 'text/html') === false)
-    Http::redirect($url);
-?>
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
-<html>
-<head>
-    <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
-    <meta http-equiv="refresh" content="0;URL=<?php echo $url; ?>"/>
-</head>
-<body/>
-</html>
diff --git a/scp/attachment.php b/scp/attachment.php
deleted file mode 100644
index d3283b877bd1300ad12d93d77bd7362a12943a41..0000000000000000000000000000000000000000
--- a/scp/attachment.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-/*********************************************************************
-    attachment.php
-
-    Handles attachment downloads & access validation.
-
-    Peter Rotich <peter@osticket.com>
-    Copyright (c)  2006-2013 osTicket
-    http://www.osticket.com
-
-    Released under the GNU General Public License WITHOUT ANY WARRANTY.
-    See LICENSE.TXT for details.
-
-    vim: expandtab sw=4 ts=4 sts=4:
-**********************************************************************/
-require('staff.inc.php');
-require_once(INCLUDE_DIR.'class.attachment.php');
-
-// Basic checks
-if (!$thisstaff
-        || !$_GET['id']
-        || !$_GET['h']
-        || !($attachment=Attachment::lookup($_GET['id']))
-        || !($file=$attachment->getFile())
-        || strcasecmp(trim($_GET['h']), $file->getDownloadHash())
-        || !($object=$attachment->getObject())
-        || !$object instanceof ThreadEntry
-        || !($ticket=$object->getThread()->getObject())
-        || !$ticket instanceof Ticket
-        )
-    Http::response(404, __('Unknown or invalid file'));
-
-if (!$ticket->checkStaffPerm($thisstaff))
-    die(__('Access Denied'));
-
-//Download the file..
-$file->download();
-?>
diff --git a/scp/canned.php b/scp/canned.php
index c8290c39f58b9f87494bb11f6ad436f329d598b6..d35ec00c6b58aa207b6cb68d1f6240f64d79764a 100644
--- a/scp/canned.php
+++ b/scp/canned.php
@@ -18,8 +18,8 @@ include_once(INCLUDE_DIR.'class.canned.php');
 
 /* check permission */
 if(!$thisstaff
-        ||
-        !$thisstaff->getRole()->hasPerm(CannedModel::PERM_MANAGE)) {
+        || !$thisstaff->getRole()->hasPerm(CannedModel::PERM_MANAGE)
+        || !$cfg->isCannedResponseEnabled()) {
     header('Location: kb.php');
     exit;
 }
diff --git a/scp/file.php b/scp/file.php
deleted file mode 100644
index 97687de0f0bb0a341ce895298f587891a9c179c0..0000000000000000000000000000000000000000
--- a/scp/file.php
+++ /dev/null
@@ -1,30 +0,0 @@
-<?php
-/*********************************************************************
-    file.php
-
-    Simply downloads the file...on hash validation as follows;
-
-    * Hash must be 64 chars long.
-    * First 32 chars is the perm. file hash
-    * Next 32 chars  is md5(file_id.session_id().file_hash)
-
-    Peter Rotich <peter@osticket.com>
-    Copyright (c)  2006-2013 osTicket
-    http://www.osticket.com
-
-    Released under the GNU General Public License WITHOUT ANY WARRANTY.
-    See LICENSE.TXT for details.
-
-    vim: expandtab sw=4 ts=4 sts=4:
-**********************************************************************/
-require('staff.inc.php');
-require_once(INCLUDE_DIR.'class.file.php');
-$h=trim($_GET['h']);
-//basic checks
-if(!$h  || strlen($h)!=64  //32*2
-        || !($file=AttachmentFile::lookup(substr($h,0,32))) //first 32 is the file hash.
-        || strcasecmp($file->getDownloadHash(), $h)) //next 32 is file id + session hash.
-    Http::response(404, __('Unknown or invalid file'));
-
-$file->download();
-?>
diff --git a/scp/image.php b/scp/image.php
deleted file mode 100644
index 4c4ddbfe6da36e21588c0359b96924c365fba64b..0000000000000000000000000000000000000000
--- a/scp/image.php
+++ /dev/null
@@ -1,34 +0,0 @@
-<?php
-/*********************************************************************
-    image.php
-
-    Simply downloads the file...on hash validation as follows;
-
-    * Hash must be 64 chars long.
-    * First 32 chars is the perm. file hash
-    * Next 32 chars  is md5(file_id.session_id().file_hash)
-
-    Peter Rotich <peter@osticket.com>
-    Copyright (c)  2006-2013 osTicket
-    http://www.osticket.com
-
-    Released under the GNU General Public License WITHOUT ANY WARRANTY.
-    See LICENSE.TXT for details.
-
-    vim: expandtab sw=4 ts=4 sts=4:
-**********************************************************************/
-
-require('staff.inc.php');
-require_once(INCLUDE_DIR.'class.file.php');
-$h=trim($_GET['h']);
-//basic checks
-if(!$h  || strlen($h)!=64  //32*2
-        || !($file=AttachmentFile::lookup(substr($h,0,32))) //first 32 is the file hash.
-        || strcasecmp($h, $file->getDownloadHash())) //next 32 is file id + session hash.
-    Http::response(404, __('Unknown or invalid file'));
-
-if ($_GET['s'] && is_numeric($_GET['s']))
-    $file->display($_GET['s']);
-else
-    $file->display();
-?>
diff --git a/scp/js/ticket.js b/scp/js/ticket.js
index 600d8186bf7b0e63824daf67b9fdbe174807aacf..37b828a172974f4b0bcb9265c9cd358bbd4361ad 100644
--- a/scp/js/ticket.js
+++ b/scp/js/ticket.js
@@ -15,6 +15,20 @@
 **********************************************************************/
 var autoLock = {
 
+    // Defaults
+    lockId: 0,
+    timerId: 0,
+    lasteventTime: 0,
+    lastattemptTime: 0,
+    acquireTime: 0,
+    renewTime: 0,
+    renewFreq: 0, //renewal frequency in seconds...based on returned lock time.
+    time: 0,
+    lockAttempts: 0, //Consecutive lock attempt errors
+    maxattempts: 2, //Maximum failed lock attempts before giving up.
+    warn: true,
+    retry: true,
+
     addEvent: function(elm, evType, fn, useCapture) {
         if(elm.addEventListener) {
             elm.addEventListener(evType, fn, useCapture);
@@ -111,18 +125,6 @@ var autoLock = {
         void(autoLock.tid=parseInt($(':input[name=id]',fObj).val()));
         void(autoLock.lockTime=parseInt($(':input[name=locktime]',fObj).val()));
 
-        autoLock.lockId=0;
-        autoLock.timerId=0;
-        autoLock.lasteventTime=0;
-        autoLock.lastattemptTime=0;
-        autoLock.acquireTime=0;
-        autoLock.renewTime=0;
-        autoLock.renewFreq=0; //renewal frequency in seconds...based on returned lock time.
-        autoLock.time=0;
-        autoLock.lockAttempts=0; //Consecutive lock attempt errors
-        autoLock.maxattempts=2; //Maximum failed lock attempts before giving up.
-        autoLock.warn=true;
-        autoLock.retry=true;
         autoLock.watchDocument();
         autoLock.resetTimer();
         autoLock.addEvent(window,'unload',autoLock.releaseLock,true); //Release lock regardless of any activity.
diff --git a/scp/l.php b/scp/l.php
deleted file mode 100644
index ec4705a0f2763101c2ac2b1b96c3af174793316a..0000000000000000000000000000000000000000
--- a/scp/l.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-/*********************************************************************
-    l.php
-
-    Link redirection
-
-    Jared Hancock <jared@osticket.com>
-    Copyright (c)  2006-2013 osTicket
-    http://www.osticket.com
-
-    Released under the GNU General Public License WITHOUT ANY WARRANTY.
-    See LICENSE.TXT for details.
-
-    vim: expandtab sw=4 ts=4 sts=4:
-**********************************************************************/
-require_once 'staff.inc.php';
-//Basic url validation + token check.
-
-# PHP < 5.4.7 will not handle a URL like //host.tld/path correctly
-if (!($url=trim($_GET['url'])))
-    Http::response(422, __('Invalid URL'));
-
-$check = (strpos($url, '//') === 0) ? 'http:' . $url : $url;
-if (!Validator::is_url($check) || !$ost->validateLinkToken($_GET['auth']))
-    Http::response(403, __('URL link not authorized'));
-elseif (strpos($_SERVER['HTTP_ACCEPT'], 'text/html') === false)
-    Http::redirect($url);
-?>
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
-<html>
-<head>
-    <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
-    <meta http-equiv="refresh" content="0;URL=<?php echo $url; ?>"/>
-</head>
-<body/>
-</html>
diff --git a/setup/css/wizard.css b/setup/css/wizard.css
index 838f2de0f8c3f87a153549c54dbcb4c2cc1ea93b..a742c75cbbbf0cc2c91b72ffbabf6a99abef16a0 100644
--- a/setup/css/wizard.css
+++ b/setup/css/wizard.css
@@ -3,7 +3,7 @@ body { background: url('../images/background.jpg?1312906017') top left repeat-x
 
 #wizard { background: #fff; width: 800px; margin: 30px auto; padding: 10px; border: 1px solid #2a67ac; border-right: 2px solid #2a67ac; border-bottom: 3px solid #2a67ac; overflow: hidden; margin-bottom:5px;}
 
-a { color: #2a67ac; }
+a { color: #2a67ac; display: inline-block; }
 
 /* Helpers */
 .centered {text-align: center;}
diff --git a/setup/inc/install-prereq.inc.php b/setup/inc/install-prereq.inc.php
index 4c3167e6b71e07a85a32eed73f7ef0ac4bcb57f5..12c97730a035cadf8aec02d677280f38b81c8e8a 100644
--- a/setup/inc/install-prereq.inc.php
+++ b/setup/inc/install-prereq.inc.php
@@ -9,7 +9,7 @@ if(!defined('SETUPINC')) die('Kwaheri!');
              <p><?php echo __('We are delighted you have chosen osTicket for your customer support ticketing system!');?></p>
             <p><?php echo __("The installer will guide you every step of the way in the installation process. You're minutes away from your awesome customer support system!");?></p>
             </div>
-            <h2><?php echo __('Prerequisites');?>.</h3>
+            <h2><?php echo __('Prerequisites');?></h3>
             <p><?php echo __("Before we begin, we'll check your server configuration to make sure you meet the minimum requirements to install and run osTicket.");?></p>
             <h3><?php echo __('Required');?>: <font color="red"><?php echo $errors['prereq']; ?></font></h3>
             <?php echo __('These items are necessary in order to install and use osTicket.');?>