diff --git a/README.md b/README.md
index 9b5ef64eb27fc556123194cba2b39b5f96503c8e..872b2fa8283e87e3f988544340b9dd50dfd7786c 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@ And deploy the code into somewhere in your server's www root folder, for
 instance
 
     cd osTicket-1.8
-    php setup/cli/manage.php deploy --setup /var/www/htdocs/osticket/
+    php manage.php deploy --setup /var/www/htdocs/osticket/
 
 Then you can configure your server if necessary to serve that folder, and
 visit the page and install osTicket as usual. Go ahead and even delete
@@ -52,7 +52,7 @@ later, you can fetch updates and deploy them (from the folder where you
 cloned the git repo into)
 
     git pull
-    php setup/cli/manage.php deploy -v /var/www/htdocs/osticket/
+    php manage.php deploy -v /var/www/htdocs/osticket/
 
 Upgrading
 ---------
@@ -72,11 +72,18 @@ osTicket-1.7, visit the /scp page of you ticketing system. The upgrader will
 be presented and will walk you through the rest of the process. (The couple
 clicks needed to go through the process are pretty boring to describe).
 
+### Upgrading from v1.6
 **WARNING**: If you are upgrading from osTicket 1.6, please ensure that all
     your files in your upload folder are both readable and writable to your
     http server software. Unreadable files will not be migrated to the
     database during the upgrade and will be effectively lost.
 
+After upgrading, we recommend migrating your attachments to the database or
+to the new filesystem plugin. Use the `file` command-line applet to perform
+the migration.
+
+    php manage.php file migrate --backend=6 --to=D
+
 View the UPGRADING.txt file for other todo items to complete your upgrade.
 
 Help
@@ -119,6 +126,7 @@ osTicket is supported by several magical open source projects including:
   * [Font-Awesome](http://fortawesome.github.com/Font-Awesome/)
   * [HTMLawed](http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed)
   * [jQuery dropdown](http://labs.abeautifulsite.net/jquery-dropdown/)
+  * [jsTimezoneDetect](http://pellepim.bitbucket.org/jstz/)
   * [mPDF](http://www.mpdf1.com/)
   * [PasswordHash](http://www.openwall.com/phpass/)
   * [PEAR](http://pear.php.net/package/PEAR)
diff --git a/WHATSNEW.md b/WHATSNEW.md
index 97147310bfbb3c7700c422abb38ccee08f7a37a8..4812a8892ca4cfa988bb47fe87d3a42362463f16 100644
--- a/WHATSNEW.md
+++ b/WHATSNEW.md
@@ -1,3 +1,190 @@
+osTicket 1.10
+=============
+## Major New Features
+
+### Internationalization, Phase III
+![screen shot 2014-10-18 at 11 40 38 pm](https://cloud.githubusercontent.com/assets/672074/4692086/b16b1474-574a-11e4-89e7-b871ff591802.png)
+
+Phase III of the internationalization project is the next major advancement of
+osTicket language support. The greatest improvement is that all
+administratively customizable content. While this is a great last mile for many
+multilingual support teams, we've also revisited the client interface main
+pages as well as the knowledge base on both the client and staff panels.
+
+  * Elect primary and secondary languages
+    * Any language can be the primary, any number of languages can be secondary
+    * English-US can be disabled
+    * Order of secondary languages is sortable and controls flag order
+  * All content is translatable to secondary languages
+      * Help Topics
+        * Alphabetic sorting happens after translation
+      * SLA Plans
+      * Departments
+      * Custom Forms (and all configurations such as placeholders)
+      * Custom Lists
+        * Items
+        * Properties and configurations
+      * Site Pages
+      * FAQ Categories
+      * FAQ Articles
+        * Common attachments (available for all translations)
+        * Per-language attachments
+      * Content such as welcome emails and password-reset emails
+  * Olson timezones are used instead of GMT offset
+    * Auto-detect support for agent and client timezone
+  * Time and date formats can be automatic by locale preference now.
+    * Locale preference is the default
+    * Locale preference with forced 24-hour time is also an option
+    * Advanced format is also possible using the intl library and `sprintf` as a backup
+    * Formats including the day of the week are localized
+    * Chinese and Arabic formats using alternate day, month, and year digits and separators are now automatic.
+  * Client portal has HTML headers indicate search engine links to pages of other
+    languages, as well as the Content-Header HTTP header to indicate the intended
+    audience
+  * Spell check in text boxes, textareas and rich text editors should respect the
+    language of the content being edited
+
+### Tasks
+![screen shot 2015-05-06 at 12 36 14 pm](https://cloud.githubusercontent.com/assets/672074/7616658/c5147c68-f96b-11e4-85b7-e74a3482bb4f.png)
+
+Tasks are sub-tickets which can be created and attached to tickets as well as
+created separately. Tasks have their own assignees, department routing and
+visibility, due date, and custom data. Tasks have their own threads and can
+have a list of collaborators. All in all, tasks may very well be the greatest
+advancement of osTicket since the advent of the ticket itself.
+
+### New Advanced Search
+![screen shot 2015-05-13 at 12 35 15 pm](https://cloud.githubusercontent.com/assets/672074/7616759/94616a1c-f96c-11e4-8c19-ae1ca26a85c0.png)
+
+The advanced search feature is rewritten to address several  shortcomings of
+the original feature as well as a host of new features including
+  * Search by any field, built-in or custom
+  * Save your searches
+  * Advanced search is shown as a new queue
+  * Current advanced search criteria is maintained between searches
+  * Sorting options are relevant to queue and preference remains after navigation between queues
+
+## Minor New Features
+
+### Thread editing
+![screen shot 2015-03-20 at 6 56 10 pm](https://cloud.githubusercontent.com/assets/672074/6762680/ce4e78a0-cf32-11e4-9316-c0a969e9c70a.png)
+
+Thread items can now be edited. The original entries are preserved and are
+accessible via a thread item's "History". Items can be resent with or without
+editing them, and a signature selection is available when resending.
+
+### Roles, and custom extended access
+![screen shot 2015-05-03 at 9 05 12 pm](https://cloud.githubusercontent.com/assets/672074/7448163/257ce586-f1d8-11e4-8ed8-a11324d13027.png)
+
+The group permissions component has been offloaded to a new component, named
+"Roles". Roles allow for naming a set of permissions. Agents now have a
+"Primary Role" which defines their access to global things like the user
+directory and their access for their primary department. Each department
+granted via "Groups" is allows to be linked to a distinct "Role". This allows
+granting Read-Only access to some departments, for instance.
+
+### Improved knowledge base interface
+![screen shot 2014-10-18 at 11 55 58 pm](https://cloud.githubusercontent.com/assets/672074/4692123/5ec01038-574c-11e4-80a7-7e8a8efe3963.png)
+  * "Featured" articles show on the front page
+  * Knowledge base search on front page
+  * Translatable content
+  * Locale-specific attachments
+
+### Multiple forms and disable individual fields for Help Topics
+Help Topic configuration has a new super feature. Multiple forms can now be
+associated with each help topic, and the order the forms should appear for new
+tickets and editing tickets is configurable. Previously, the custom forms were
+always rendered above the "Ticket Details" form; but now it's completely
+customizable. What's more is that individual fields **including the issue
+details** can be disabled for any help topic.
+
+### Department hierarchy
+Departments are now nestable. All departments can have a parent department, and
+the hierarchy is arbitrarily nestable. Access is cascaded so that access to a
+parent department automatically extends access to all descendent departments.
+
+### Image annotation
+![screen shot 2015-05-04 at 9 07 38 pm](https://cloud.githubusercontent.com/assets/672074/7466027/ac34575c-f2a1-11e4-9335-417960f89334.png)
+
+Images can be annotated to add simple shapes like ovals, boxes, arrows and
+text. Annotates can be committed, and a new image is created from the
+annotations; however, annotations can still be edited before the thread post is
+submitted. Annotations are supported for both clients and agents, and the
+images can be selected from the ticket thread, so images already posted can be
+easily marked up.
+
+### Variable context type-ahead
+![screen shot 2015-04-20 at 4 32 58 pm](https://cloud.githubusercontent.com/assets/672074/7240963/ee930d8c-e77a-11e4-8928-26240274db13.png)
+
+When editing content which uses variables, such as a thank-you page or an email
+template, variable placeholders now use a type-ahead feature. This new pop out
+significantly improves the connection between which variables are available in
+which templates. It also allows for adding significantly to the variable
+library without relying on exhaustive documentation to convey this information.
+Some new variables include
+  * User lists, such as department members, team members, and collaborator lists
+  * Lists can be rendered as names, emails, or both
+  * Dates are format-able to time, short, full, and long
+  * Dates can be humanized to something like *in about an hour*
+  * Dates can be auto localized and formatted to the recipients locale and time
+    zone selection
+  * Attachments to thread items and custom fields can be attached via variable
+    (e.g. `%{message.files}`)
+
+### Redesigned list management
+![Simplified, tabular, paginated view of list items, with mass actions](https://cloud.githubusercontent.com/assets/672074/5881786/3040d162-a309-11e4-9529-8ae51d358f81.png)
+
+The list management feature has a significant overhaul to accommodate larger
+lists. It also provides a heads display of list item properties as well as AJAX
+updates. CSV import and pagination have also been added as well as mass enable,
+disable, and delete.
+
+### Pluggable filter actions
+![screen shot 2015-05-04 at 8 59 32 pm](https://cloud.githubusercontent.com/assets/672074/7465977/801b4cbc-f2a0-11e4-9598-95dd52e79e82.png)
+
+Filter actions are now far more flexible allowing for more elaborate and
+creative filter actions to be created. A new filter action has been added as an
+example of future possibilities: send an email. The new feature allows for
+ticket filter actions to be defined without modification to internal table
+structures, and even allows actions to be created via plugins!
+
+Actions are also sortable and performed in the order specified, which allows
+doing something like sending an email before rejecting the ticket.
+
+### Other Improvements
+#### Custom Data
+* Fields have more granular access configuration. View, edit, and requirement
+  can be enabled individually for both agents and end users
+* Fields can be marked for required for closed. Therefore they can inhibit
+  closure of a ticket without a valid value.
+
+#### Export
+The agent's locale is considered when exporting CSV and semicolon separators
+are used where necessary
+
+#### User Interface
+The subject line and many other text fields around the system are truncated by
+the browser, which fixes early truncation for some language with long Unicode
+byte stream, such as Chinese.
+
+#### Improved lock system
+The ticket lock system uses a code now which is rotated when updates to tickets
+are submitted. This helps prevent unwanted extra posts to tickets. A new
+annoying popup is displayed when viewing the ticket and the lock is about to
+expire.
+
+#### Draft system
+The draft system has been rewritten to reduce the number of requests to the
+backend and to reduce the dreaded "Unable to save draft" popup
+
+#### ORM
+The database query system is being redesigned to use an object relational
+mapper (ORM) instead of SQL queries. This will eventually lead to fewer
+database queries to use the system, cleaner code, and will allow the use of
+database engines other than MySQL. The ORM was originally introduced in
+osTicket v1.8.0, but has seen the greatest boost in capability in this release.
+About 47% of the SQL queries are removed between v1.9.7 and v1.10
+
 osTicket v1.9.12
 ================
 ### Improvements
diff --git a/account.php b/account.php
index 3b0de2deaa8cee33adca2422aa6575e4b2f5697f..f57eefbf1a1774d253890efa4a61ee3bb01096af 100644
--- a/account.php
+++ b/account.php
@@ -39,7 +39,7 @@ elseif ($thisclient) {
     // Existing client (with an account) updating profile
     else {
         $user = User::lookup($thisclient->getId());
-        $content = Page::lookup(Page::getIdByType('registration-thanks'));
+        $content = Page::lookupByType('registration-thanks');
         $inc = isset($_GET['confirmed'])
             ? 'register.confirmed.inc.php' : 'profile.inc.php';
     }
@@ -60,7 +60,7 @@ elseif ($_POST) {
         $user_form->getField('email')->value = $thisclient->getEmail();
     }
 
-    if (!$user_form->isValid(function($f) { return !$f->get('private'); }))
+    if (!$user_form->isValid(function($f) { return !$f->isVisibleToUsers(); }))
         $errors['err'] = __('Incomplete client information');
     elseif (!$_POST['backend'] && !$_POST['passwd1'])
         $errors['passwd1'] = __('New password is required');
@@ -96,7 +96,7 @@ elseif ($_POST) {
     if (!$errors) {
         switch ($_POST['do']) {
         case 'create':
-            $content = Page::lookup(Page::getIdByType('registration-confirm'));
+            $content = Page::lookupByType('registration-confirm');
             $inc = 'register.confirm.inc.php';
             $acct->sendConfirmEmail();
             break;
diff --git a/ajax.php b/ajax.php
index 8ea5226439f971b9cd917a13f53c099579394ed1..80e3a7838693ca355859285019aa861043b0b057 100644
--- a/ajax.php
+++ b/ajax.php
@@ -33,6 +33,7 @@ $dispatcher = patterns('',
         url_post('^(?P<id>\d+)$', 'updateDraftClient'),
         url_delete('^(?P<id>\d+)$', 'deleteDraftClient'),
         url_post('^(?P<id>\d+)/attach$', 'uploadInlineImageClient'),
+        url_post('^(?P<namespace>[\w.]+)/attach$', 'uploadInlineImageEarlyClient'),
         url_get('^(?P<namespace>[\w.]+)$', 'getDraftClient'),
         url_post('^(?P<namespace>[\w.]+)$', 'createDraftClient')
     )),
diff --git a/assets/default/css/print.css b/assets/default/css/print.css
index aca3d50f9265e03c3a95d5a5dbbe78aa891b0a21..9ddb1f1cfd496f321ba9c03d0d1caf771272f17d 100644
--- a/assets/default/css/print.css
+++ b/assets/default/css/print.css
@@ -1 +1 @@
-#header,#nav,#meta,#footer,#reply,#pagination,.reload,.refresh,form,.thread,hr,#kbAttachments,.back{display:none}th{text-align:left}a{color:#000;text-decoration:none}caption{text-align:left;padding-bottom:10px;font-weight:bold}.message,.response{border-bottom:1px solid #000;margin-bottom:20px;padding-bottom:10px}.message th,.response th{font-size:12pt;font-weight:bold;padding-bottom:5px}
\ No newline at end of file
+#header,#nav,#meta,#footer,#reply,#pagination,.reload,.refresh,.redactor-toolbar,.filedrop .dropzone,.back,#loading,.buttons{display:none}th{text-align:left}a{color:#000;text-decoration:none}caption{text-align:left;padding-bottom:10px;font-weight:bold}.message,.response{border-bottom:1px solid #000;margin-bottom:20px;padding-bottom:10px}.message th,.response th{font-size:12pt;font-weight:bold;padding-bottom:5px}
diff --git a/assets/default/css/theme.css b/assets/default/css/theme.css
index 7f019f148e1d6e5ea2ffa5e5a1ab5b6dfe142a14..86e8dabb472701c7b296820ce73b77c07c0bda16 100644
--- a/assets/default/css/theme.css
+++ b/assets/default/css/theme.css
@@ -6,7 +6,7 @@ html {
 }
 body {
   margin: 0;
-  font-size: 13px;
+  font-size: 14px;
   line-height: 1.231;
   padding: 0;
 }
@@ -14,7 +14,7 @@ body,
 input,
 select,
 textarea {
-  font-family: sans-serif;
+  font-family: "Helvetica Neue", sans-serif;
   color: #000;
 }
 b,
@@ -80,12 +80,7 @@ input[type="checkbox"],
 input[type="radio"] {
   box-sizing: border-box;
 }
-input[type="search"] {
-  -webkit-appearance: textfield;
-  -moz-box-sizing: content-box;
-  -webkit-box-sizing: content-box;
-  box-sizing: content-box;
-}
+
 textarea {
   overflow: auto;
   vertical-align: top;
@@ -115,9 +110,16 @@ fieldset {
   padding: 0;
 }
 /* Typography */
-a {
+a, .link {
   color: #0072bc;
   text-decoration: none;
+  display: inline-block;
+  margin-bottom: 1px;
+}
+a:hover, .link:hover {
+    border-bottom: 1px dotted #0072bc;
+    margin-bottom: 0;
+    cursor: pointer;
 }
 h1 {
   color: #00AEEF;
@@ -201,7 +203,7 @@ h2, .subject {
   border: 1px solid #0a0;
   background: url('../images/icons/ok.png') 10px 50% no-repeat #e0ffe0;
 }
-#msg_warning {
+#msg_warning, .warning-banner {
   margin: 0;
   padding: 5px 10px 5px 36px;
   height: 16px;
@@ -238,13 +240,13 @@ h2, .subject {
 .button,
 .button:visited {
   background: #222;
+  border: none;
   display: inline-block;
   font-size: 16px;
-  padding: 8px 16px 6px 16px;
+  padding: 4px 16px 4px 16px;
   max-width: 220px;
   text-align: center;
   color: #fff;
-  font-weight: bold;
   text-decoration: none;
   border-radius: 5px;
   -moz-border-radius: 5px;
@@ -329,6 +331,7 @@ body {
   box-shadow: 0 3px 2px rgba(0, 0, 0, 0.4);
   -moz-box-shadow: 0 3px 2px rgba(0, 0, 0, 0.4);
   -webkit-box-shadow: 0 3px 2px rgba(0, 0, 0, 0.4);
+  white-space: nowrap;
 }
 #nav li {
   margin: 0;
@@ -413,13 +416,15 @@ body {
 }
 .front-page-button {
 }
+.main-content {
+  width: 565px;
+}
 #landing_page #new_ticket {
   margin-top: 40px;
   background: url('../images/new_ticket_icon.png') top left no-repeat;
 }
 #landing_page #new_ticket,
-#landing_page #check_status,
-.front-page-button {
+#landing_page #check_status {
   width: 295px;
   padding-left: 75px;
 }
@@ -427,12 +432,8 @@ body {
   margin-top: 40px;
   background: url('../images/check_status_icon.png') top left no-repeat;
 }
-.rtl #landing_page #new_ticket,
-.rtl #landing_page #check_status,
-.rtl .front-page-button {
-  padding-left: 0;
-  padding-right: 75px;
-  background-position: top right;
+#landing_page h1, #landing_page h2, #landing_page h3 {
+    margin-bottom: 10px;
 }
 /* Landing page FAQ not yet implemented. */
 #faq {
@@ -482,10 +483,7 @@ body {
   margin: 0;
   background: url(../images/kb_category_bg.png) bottom left repeat-x;
   border-bottom: 1px solid #ddd;
-}
-#kb > li h4 {
-  padding-bottom: 3px;
-  margin-bottom: 3px;
+  display: block;
 }
 #kb > li h4 span {
   color: #666;
@@ -493,7 +491,7 @@ body {
 #kb > li h4 a {
   font-size: 14px;
 }
-#kb li i {
+#kb > li > i {
   display: block;
   width: 32px;
   height: 32px;
@@ -501,6 +499,44 @@ body {
   margin-right: 6px;
   background: url(../images/kb_large_folder.png) top left no-repeat;
 }
+.featured-category {
+    margin-top: 10px;
+    width: 49.7%;
+    display: inline-block;
+    box-sizing: border-box;
+    vertical-align: top;
+}
+.category-name {
+    display: inline-block;
+    font-weight: 400;
+    font-size: 120%;
+}
+.featured-category i {
+    color: rgba(0,174,239, 0.8);
+    text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1);
+    display: inline-block;
+    vertical-align: middle;
+}
+.article-headline {
+    margin-left: 34px;
+}
+.rtl .article-headline {
+    margin-left: 0;
+    margin-right: 34px;
+}
+.article-teaser {
+    font-size: 90%;
+    line-height: 1.5em;
+    height: 3em;
+    overflow: hidden;
+}
+.article-title {
+    font-weight: 500;
+}
+.faq-content .article-title {
+    font-size: 17pt;
+    margin-top: 15px;
+}
 #kb-search {
   padding: 10px 0;
   overflow: hidden;
@@ -556,7 +592,7 @@ body {
 #clientLogin div label {
   display: block;
 }
-label.required {
+label.required, span.required {
   font-weight: bold;
 }
 #ticketForm div label.required,
@@ -693,13 +729,15 @@ label.required {
     border-radius: 4px;
 }
 #reply {
-  margin-top: 20px;
-  padding: 10px 5px;
+  margin-top: 5px;
+  padding: 10px;
   background: #f9f9f9;
   border: 1px solid #ccc;
 }
 #reply h2 {
   margin-bottom: 10px;
+  padding-bottom: 5px;
+  border-bottom: 2px dotted rgba(0,0,0,0.1);
 }
 #reply > table {
   width: auto;
@@ -754,9 +792,6 @@ label.required {
   font-size: 1em;
   background-image: url('../images/icons/thread.gif?1319556657');
 }
-.Icon:hover {
-  text-decoration: underline;
-}
 #ticketTable {
   border: 1px solid #aaa;
   border-left: none;
@@ -778,12 +813,13 @@ label.required {
   border: 1px solid #aaa;
   border-right: none;
   border-top: none;
+  padding: 0 5px;
 }
 #ticketTable th a {
   color: #000;
 }
 #ticketTable td {
-  padding: 2px;
+  padding: 3px 5px;
   border: 1px solid #aaa;
   border-right: none;
   border-top: none;
@@ -791,70 +827,29 @@ label.required {
 #ticketTable tr.alt td {
   background: #f9f9f9;
 }
-#ticketSearchForm {
-  display: inline-block;
-  float: left;
-  padding: 0 0 5px 0;
+i.refresh {
+  color: #0a0;
+  font-size: 80%;
+  vertical-align: middle;
 }
-a.refresh {
-  display: block;
-  width: auto;
-  float: right;
-  height: 20px;
-  line-height: 20px;
-  text-align: center;
-  padding: 0 10px 0 28px;
-  border: 1px solid #aaa;
-  margin-left: 10px;
-  color: #333;
-  background-position: 5px 50%;
-  background-repeat: no-repeat;
-  background-image: url('../images/icons/refresh.png');
+.states small {
+    font-size: 70%;
+}
+.active.state {
+    font-weight: bold;
+}
+.search.well {
+    padding: 10px;
+    background-color: rgba(0,0,0,0.05);
+    margin-bottom: 10px;
+    margin-top: -15px;
 }
 .infoTable {
   background: #F4FAFF;
 }
 .infoTable th {
   text-align: left;
-}
-#ticketThread table.response,
-#ticketThread table.message {
-  margin-top: 10px;
-  border: 1px solid #aaa;
-  border-bottom: 2px solid #aaa;
-}
-#ticketThread table th {
-  text-align: left;
-  border-bottom: 1px solid #aaa;
-  font-size: 12px;
-  padding: 5px;
-}
-#ticketThread table th span {
-  font-weight: normal;
-  color: #888;
-  padding-left: 20px;
-}
-#ticketThread .message th {
-  background: #d8efff;
-}
-#ticketThread .response th {
-  background: #ddd;
-}
-#ticketThread .info {
-  padding: 2px;
-  background: #f9f9f9;
-  border-top: 1px solid #ddd;
-  height: 16px;
-  line-height: 16px;
-}
-#ticketThread .info a {
-  display: inline-block;
-  margin: 5px 10px 5px 0;
-  padding-left: 24px;
-  height: 16px;
-  line-height: 16px;
-  background-position: 0 50%;
-  background-repeat: no-repeat;
+  padding: 3px 8px;
 }
 .action-button {
   -webkit-border-radius: 3px;
@@ -868,7 +863,6 @@ a.refresh {
   border: 1px solid #aaa;
   cursor: pointer;
   font-size: 11px;
-  height: 18px;
   overflow: hidden;
   background-color: #dddddd;
   background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #efefef), color-stop(100% #dddddd));
@@ -880,7 +874,6 @@ a.refresh {
   padding: 0 5px;
   text-decoration: none;
   line-height:18px;
-  float:right;
   margin-left:5px;
 }
 .action-button span,
@@ -947,3 +940,345 @@ img.sign-in-image {
 .flush-left {
     text-align: left;
 }
+.sidebar {
+    margin-bottom: 20px;
+    margin-left: 20px;
+    width: 215px;
+}
+.rtl .sidebar {
+    margin-left: 0;
+    margin-right: 20px;
+}
+.sidebar .content {
+    padding: 10px; border: 1px solid #C8DDFA; background: #F7FBFE;
+}
+.sidebar .content:empty {
+    display: none;
+}
+
+.sidebar section .header {
+    font-weight: bold;
+    margin-bottom: 0.3em;
+}
+.sidebar section + section {
+    margin-top: 15px;
+}
+.search-form {
+    padding-top: 12px;
+}
+.searchbar .search,
+.search-form .search {
+    display: inline-block;
+    width: 400px;
+    border-radius: 5px;
+    border: 1px solid #ccc;
+    padding: 5px 10px;
+    box-shadow: inset 0 1px 1px rgba(0,0,0,.075);
+}
+
+.searchbar .search {
+    width: 100%;
+    box-sizing: border-box;
+    margin-bottom: 10px;
+}
+.bleed {
+    margin: 0 !important;
+    padding: 0 !important;
+}
+.row {
+}
+.span4 {
+    display: inline-block;
+    width: 29.5%;
+    margin: 0 1%;
+    vertical-align: top;
+}
+.span8 {
+    display: inline-block;
+    width: 66.0%;
+    margin: 0 1%;
+    vertical-align: top;
+}
+.truncate {
+    display: inline-block;
+    width: auto;
+    max-width: 100%;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    vertical-align: bottom;
+}
+.image-hover a.action-button:hover,
+.image-hover a.action-button {
+    color: initial !important;
+    text-decoration: none;
+}
+table.custom-data {
+    margin-top: 10px;
+}
+table.custom-data th {
+    width: 25%;
+}
+table.custom-data th {
+    background-color: #F4FAFF;
+    padding: 3px 8px;
+}
+table .headline,
+table.custom-data .headline {
+    border-bottom: 2px solid #ddd;
+    border-bottom: 2px solid rgba(0,0,0,0.15);
+    font-weight: bold;
+    background-color: white;
+}
+#ticketInfo h1 {
+    padding-bottom: 10px;
+    margin-bottom: 5px;
+    border-bottom: 2px dotted rgba(0, 0, 0, 0.15);
+}
+#ticketInfo h1 small {
+    font-weight: normal;
+}
+.thread-entry {
+    margin-bottom: 15px;
+}
+.thread-entry.avatar {
+    margin-left: 60px;
+}
+.thread-entry.response.avatar {
+    margin-right: 60px;
+    margin-left: 0;
+}
+.thread-entry > .avatar {
+    margin-left: -60px;
+    display:inline-block;
+    width:48px;
+    height:auto;
+    border-radius: 5px;
+}
+.thread-entry.response > .avatar {
+    margin-left: initial;
+    margin-right: -60px;
+}
+img.avatar {
+    border-radius: inherit;
+}
+.avatar > img.avatar {
+    width: 100%;
+    height: auto;
+}
+.thread-entry .header {
+    padding: 8px 0.9em;
+    border: 1px solid #ccc;
+    border-color: rgba(0,0,0,0.2);
+    border-radius: 5px 5px 0 0;
+}
+.thread-entry.avatar .header:before {
+  position: absolute;
+  top: 7px;
+  right: -8px;
+  content: '';
+  border-top: 8px solid transparent;
+  border-bottom: 8px solid transparent;
+  border-left: 8px solid #b0b0b0;
+  display: inline-block;
+}
+.thread-entry.avatar .header:after {
+  position: absolute;
+  top: 7px;
+  right: -8px;
+  content: '';
+  border-top: 7px solid transparent;
+  border-bottom: 7px solid transparent;
+  display: inline-block;
+  margin-top: 1px;
+}
+
+.thread-entry.avatar .header {
+    position: relative;
+}
+
+.thread-entry.response .header {
+    background:#dddddd;
+}
+.thread-entry.avatar.response .header:after {
+    border-left: 7px solid #dddddd;
+    margin-right: 1px;
+}
+
+.thread-entry.message .header {
+    background:#C3D9FF;
+}
+.thread-entry.avatar.message .header:before {
+    top: 7px;
+    left: -8px;
+    right: initial;
+    border-left: none;
+    border-right: 8px solid #CCC;
+}
+.thread-entry.avatar.message .header:before {
+    border-right-color: #9cadcc;
+}
+.thread-entry.avatar.message .header:after {
+    top: 7px;
+    left: -8px;
+    right: initial;
+    border-left: none;
+    border-right: 7px solid #c3d9ff;
+    margin-left: 1px;
+}
+
+.thread-entry .header .title {
+    max-width: 500px;
+    vertical-align: bottom;
+    display: inline-block;
+    margin-left: 15px;
+}
+
+.thread-entry .thread-body {
+    border: 1px solid #ddd;
+    border-top: none;
+    border-bottom:2px solid #aaa;
+    border-radius: 0 0 5px 5px;
+}
+.thread-body .attachments {
+  background-color: #f4faff;
+  margin: 0 -0.9em;
+  position: relative;
+  top: 0.9em;
+  padding: 0.3em 0.9em;
+  border-top: 1px dotted #ccc;
+  border-top-color: rgba(0,0,0,0.2);
+  border-radius: 0 0 6px 6px;
+}
+.thread-body .attachments .filesize {
+  margin-left: 0.5em;
+}
+.thread-body .attachments a,
+.thread-body .attachments a:hover {
+  text-decoration: none;
+}
+.thread-body .attachment-info {
+    margin-right: 10px;
+    display: inline-block;
+    width: 48%;
+}
+.thread-body .attachment-info .filename {
+  max-width: 80%;
+  max-width: calc(100% - 70px);
+}
+.label {
+  font-size: 11px;
+  padding: 1px 4px;
+  -webkit-border-radius: 3px;
+  -moz-border-radius: 3px;
+  border-radius: 3px;
+  font-weight: bold;
+  line-height: 14px;
+  color: #ffffff;
+  vertical-align: baseline;
+  white-space: nowrap;
+  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+  background-color: #999999;
+}
+.label-bare {
+  background-color: transparent;
+  background-color: rgba(0,0,0,0);
+  border: 1px solid #999999;
+  color: #999999;
+  text-shadow: none;
+}
+.thread-event {
+    padding: 0px 2px 15px;
+    margin-left: 60px;
+}
+.type-icon {
+    border-radius: 8px;
+    background-color: #f4f4f4;
+    padding: 4px 6px;
+    margin-right: 5px;
+    text-align: center;
+    display: inline-block;
+    font-size: 1.1em;
+    border: 1px solid #eee;
+    vertical-align: top;
+}
+.type-icon.dark {
+    border-color: #666;
+    background-color: #949494;
+}
+.thread-event img.avatar {
+    vertical-align: middle;
+    border-radius: 3px;
+    width: auto;
+    max-height: 24px;
+    margin: -3px 3px 0;
+}
+.thread-event .description {
+    margin-left: -30px;
+    padding-top: 6px;
+    padding-left: 30px;
+    display: inline-block;
+    width: 642px;
+    width: calc(100% - 95px);
+    line-height: 1.4em;
+}
+.thread-event .type-icon {
+  position:relative;
+}
+.thread-event .type-icon::after {
+  content: "";
+  border: 16px solid white;
+  position: absolute;
+  top: -3px;
+  bottom: 0;
+  left: -3px;
+  right: 0;
+  z-index: -1;
+}
+.thread-entry::after {
+  content: "";
+  border-bottom: 2px solid white;
+  display: block;
+}
+.thread-entry::before {
+  content: "";
+  display: block;
+  border-top: 2px solid white;
+}
+#ticketThread::before {
+  border-left: 2px dotted #ddd;
+  border-bottom-color: rgba(0,0,0,0.1);
+  position: absolute;
+  margin-left: 74px;
+  z-index: -1;
+  content: "";
+  top: 0;
+  bottom: 0;
+  right: 0;
+  left: 0;
+}
+#ticketThread {
+  z-index: 0;
+  position: relative;
+  border-bottom: 2px solid #ddd;
+  border-bottom-color: rgba(0,0,0,0.1);
+}
+
+.freetext-files {
+    padding: 10px;
+    margin-top: 10px;
+    border: 1px dotted #ddd;
+    border-radius: 4px;
+    background-color: #f5f5f5;
+}
+.freetext-files .file {
+    margin-right: 10px;
+    display: inline-block;
+    width: 48%;
+    padding-top: 0.2em;
+}
+.freetext-files .title {
+    font-weight: bold;
+    margin-bottom: 0.3em;
+    font-size: 1.1em;
+}
diff --git a/assets/default/css/theme.min.css b/assets/default/css/theme.min.css
index 8e747a44528fe4376371c967fd867ec835f01fce..7d5d717b86251f9de925b442aeb5c00a151d253a 100644
--- a/assets/default/css/theme.min.css
+++ b/assets/default/css/theme.min.css
@@ -1 +1 @@
-html{font-size:100%;overflow-y:scroll;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0;font-size:13px;line-height:1.231;padding:0}body,input,select,textarea{font-family:sans-serif;color:#000}b,strong{font-weight:bold}blockquote{margin:1em 40px}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}small{font-size:85%}ul,ol{margin:1em 0;padding:0 0 0 30px}img{border:0;vertical-align:middle}form{margin:0}fieldset{border:0;margin:0;padding:0}label{cursor:pointer}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}input{line-height:normal;*overflow:visible}table input{*overflow:auto}input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}input[type="checkbox"],input[type="radio"]{box-sizing:border-box}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{overflow:auto;vertical-align:top;resize:vertical}table{border-collapse:collapse;border-spacing:0}th,td{vertical-align:top}th{text-align:left;font-weight:normal}h1,h2,h3,h4,h5,h6,form,fieldset{margin:0;padding:0}a{color:#0072bc;text-decoration:none}h1{color:#00aeef;font-weight:normal;font-size:20px}h3{font-size:16px}h2{font-size:16px;color:#999}.centered{text-align:center}.clear{clear:both;height:1px;visibility:none}.hidden{display:none}.faded{color:#666}#pagination{border:0;margin:0 0 40px 0;padding:0}#pagination li{border:0;margin:0;padding:0;font-size:11px;list-style:none;display:inline}#pagination li a{margin-right:2px;display:block;float:left;padding:3px 6px;text-decoration:none}#pagination li a:hover{color:#ff0084}#pagination .previousOff,#pagination .nextOff{color:#666;display:block;float:left;font-weight:bold;padding:3px 4px}#pagination .next a,#pagination .previous a{font-weight:bold}#pagination .active{color:#000;font-weight:bold;margin-right:2px;display:block;float:left;padding:3px 6px;text-decoration:none}#msg_notice{margin:0;padding:5px 10px 5px 36px;height:16px;line-height:16px;margin-bottom:10px;border:1px solid #0a0;background:url('../images/icons/ok.png') 10px 50% no-repeat #e0ffe0}#msg_warning{margin:0;padding:5px 10px 5px 36px;height:16px;line-height:16px;margin-bottom:10px;border:1px solid #f26522;background:url('../images/icons/alert.png') 10px 50% no-repeat #ffd}#msg_error{margin:0;padding:5px 10px 5px 36px;height:16px;line-height:16px;margin-bottom:10px;border:1px solid #a00;background:url('../images/icons/error.png') 10px 50% no-repeat #fff0f0}.warning{background:#ffc;font-style:italic}.warning strong{text-transform:uppercase;color:#a00;font-style:normal}.error{color:#f00}.error input{border:1px solid #f00}.button,.button:visited{background:#222;display:inline-block;font-size:16px;padding:8px 16px 6px 16px;width:160px;text-align:center;color:#fff;font-weight:bold;text-decoration:none;border-radius:5px;-moz-border-radius:5px;-webkit-border-radius:5px;box-shadow:0 1px 3px rgba(0,0,0,0.5);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.5);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.5);text-shadow:0 -1px 1px rgba(0,0,0,0.25);border-bottom:1px solid rgba(0,0,0,0.25);position:relative;cursor:pointer;font-family:helvetica,arial,sans-serif}.button:hover{background-color:#111;color:#fff}.button:active{top:1px;box-shadow:none;-moz-box-shadow:none;-webkit-box-shadow:none}.button,.button:visited,.green.button,.green.button:visited{background-color:#91bd09}.green.button:hover{background-color:#749a02}.blue.button,.blue.button:visited{background-color:#00aeef}.blue.button:hover{background-color:#0299d2}body{background:url('../images/page_bg.png') top left repeat-x #c8c8c8}#container{background:#fff;width:840px;margin:0 auto;box-shadow:0 0 6px rgba(0,0,0,0.5);-moz-box-shadow:0 0 6px rgba(0,0,0,0.5);-webkit-box-shadow:0 0 6px rgba(0,0,0,0.5)}#header{position:relative;height:71px;padding:0 20px}#header #logo{width:220px;height:71px;float:left}#header p{width:400px;text-align:right;margin:0;padding:10px 0;float:right}#nav{margin:0 20px;padding:2px 10px;height:20px;background:url('../images/nav_bg.png') top left repeat-x;border-top:1px solid #aaa;box-shadow:0 3px 2px rgba(0,0,0,0.4);-moz-box-shadow:0 3px 2px rgba(0,0,0,0.4);-webkit-box-shadow:0 3px 2px rgba(0,0,0,0.4)}#nav li{margin:0;padding:0;list-style:none;display:inline}#nav li a{display:block;width:auto;float:left;height:20px;line-height:20px;text-align:center;padding:0 10px 0 32px;margin-left:10px;color:#333;border-radius:20px;-webkit-border-radius:20px;-moz-border-radius:20px;background-position:10px 50%;background-repeat:no-repeat}#nav li a.active,#nav li a:hover{background-color:#dbefff;color:#000}#nav li a:hover{background-color:#ededed;color:#0054a6}#nav li a.home{background-image:url('../images/icons/home.png')}#nav li a.kb{background-image:url('../images/icons/kb.png')}#nav li a.new{background-image:url('../images/icons/new.png')}#nav li a.status{background-image:url('../images/icons/status.png')}#nav li a.tickets{background-image:url('../images/icons/tix.png')}#content{padding:20px 0;margin:0 20px;height:auto!important;height:350px;min-height:350px}#footer{text-align:center;font-size:11px;color:#333}#footer a{color:#333}#footer p{margin:10px 0 0 0}#footer #poweredBy{display:block;width:126px;height:23px;outline:0;text-indent:-9999px;margin:0 auto;background:url('../images/poweredby.png') top left no-repeat}#landing_page #new_ticket{margin-top:40px;width:295px;padding-left:75px;float:left;background:url('../images/new_ticket_icon.png') top left no-repeat}#landing_page #check_status{margin-top:40px;width:295px;padding-left:75px;float:right;background:url('../images/check_status_icon.png') top left no-repeat}#faq{clear:both;margin:0;padding:5px}#faq ol{font-size:15px;margin-left:0;padding-left:0;border-top:1px solid #ddd}#faq ol li{list-style:none;margin:0;padding:0;color:#999}#faq ol li a{display:block;padding:5px 0;height:auto!important;overflow:hidden;margin:0;border-bottom:1px solid #ddd;line-height:16px;padding-left:24px;background:url('../images/icons/page.png') 0 50% no-repeat}#faq ol li a:hover{background-color:#e9f5ff}#faq .article-meta{padding:5px;background:#fafafa}#kb{margin:2px 0;padding:5px;overflow:hidden}#kb>li{padding:10px;height:auto!important;overflow:hidden;margin:0;background:url(../images/kb_category_bg.png) bottom left repeat-x;border-bottom:1px solid #ddd}#kb>li h4{padding-bottom:3px;margin-bottom:3px}#kb>li h4 span{color:#666}#kb>li h4 a{font-size:14px}#kb li i{display:block;width:32px;height:32px;float:left;margin-right:6px;background:url(../images/kb_large_folder.png) top left no-repeat}#kb-search{padding:10px 0;overflow:hidden}#kb-search div{clear:both;overflow:hidden;padding-top:5px}#kb-search #query{margin:0;display:inline-block;float:left;width:200px;margin-right:5px}#kb-search #cid{margin:0;display:inline-block;float:left;width:200px;margin-right:5px;position:relative;top:2px}#kb-search #topic-id{margin:0;display:inline-block;float:left;width:410px}#kb-search #searchSubmit{margin:0;display:inline-block;float:left;position:relative;top:2px}#kb-search #breadcrumbs{color:#333;margin-bottom:15px}#kb-search #breadcrumbs #breadcrumbs a{color:#555}#ticketForm div,#clientLogin div{clear:both;padding:3px 0;overflow:hidden}#ticketForm div label,#clientLogin div label{display:block;width:140px;float:left}#ticketForm div label.required,#clientLogin div label.required{font-weight:bold;text-align:left}#ticketForm div input,#clientLogin div input,#ticketForm div textarea,#clientLogin div textarea{width:auto;border:1px solid #aaa;background:#fff;margin-right:10px;display:block;float:left}#ticketForm div input[type=file],#clientLogin div input[type=file]{border:0}#ticketForm div select,#clientLogin div select{display:block;float:left}#ticketForm div div.captchaRow,#clientLogin div div.captchaRow{line-height:31px}#ticketForm div div.captchaRow input,#clientLogin div div.captchaRow input{position:relative;top:6px}#ticketForm td textarea,#clientLogin td textarea,#ticketForm div textarea,#clientLogin div textarea{width:600px}#ticketForm td em,#clientLogin td em,#ticketForm div em,#clientLogin div em{color:#777}#ticketForm td .captcha,#clientLogin td .captcha,#ticketForm div .captcha,#clientLogin div .captcha{width:88px;height:31px;background:#000;display:block;float:left;margin-right:20px}#ticketForm td label.inline,#clientLogin td label.inline,#ticketForm div label.inline,#clientLogin div label.inline{width:auto;padding:0 10px}#ticketForm div.error input,#clientLogin div.error input{border:1px solid #a00}#ticketForm div.error label,#clientLogin div.error label{color:#a00}#ticketTable th{width:160px;font-weight:normal;text-align:left}#ticketTable th.required,#ticketTable td.required{font-weight:bold;text-align:left}#clientLogin{width:400px;margin-top:20px;padding:10px 100px 10px 10px;border:1px solid #ccc;background:url('../images/lock.png?1319655200') 440px 50% no-repeat #f6f6f6}#clientLogin p{clear:both;text-align:center}#clientLogin strong{font-size:11px;color:#d00;display:block;padding-left:140px}#clientLogin #email{width:250px;margin-right:0}#clientLogin #ticketno{width:120px;margin-right:0}#reply{margin-top:20px;padding:10px 5px;background:#f9f9f9;border:1px solid #ccc}#reply h2{margin-bottom:10px}#reply table{width:800px}#reply table td{vertical-align:top}#reply textarea{width:628px!important}#reply input[type=text],#reply #response_options textarea{border:1px solid #aaa;background:#fff}#reply .attachments .uploads div{display:inline-block;padding-right:20px}#reply .file{display:inline-block;padding-left:20px;margin-right:20px;background:url('../images/icons/file.gif') 0 50% no-repeat}.uploads{display:inline-block;padding-right:20px}.uploads label{padding:3px;padding-right:10px;width:auto!important}.Icon{width:auto;padding-left:20px;background-position:top left;background-repeat:no-repeat;color:#069;text-decoration:none}.Icon.Ticket{background-image:url('../images/icons/ticket.gif')}.Icon.webTicket{background-image:url('../images/icons/ticket_source_web.gif')}.Icon.emailTicket{background-image:url('../images/icons/ticket_source_email.gif')}.Icon.phoneTicket{background-image:url('../images/icons/ticket_source_phone.gif')}.Icon.otherTicket{background-image:url('../images/icons/ticket_source_other.gif')}.Icon.attachment{background-image:url('../images/icons/attachment.gif')}.Icon.file{background-image:url('../images/icons/attachment.gif')}.Icon.refresh{background-image:url('../images/icons/refresh.gif')}.Icon.thread{font-weight:bold;font-size:1em;background-image:url('../images/icons/thread.gif?1319556657')}.Icon:hover{text-decoration:underline}#ticketTable{border:1px solid #aaa;border-left:none;border-bottom:0}#ticketTable caption{padding:5px;text-align:left;color:#000;background:#ddd;border:1px solid #aaa;border-bottom:0;font-weight:bold}#ticketTable th{height:24px;line-height:24px;background:#e1f2ff;border:1px solid #aaa;border-right:0;border-top:0}#ticketTable th a{color:#000}#ticketTable td{padding:2px;border:1px solid #aaa;border-right:0;border-top:0}#ticketTable tr.alt td{background:#f9f9f9}#ticketSearchForm{display:inline-block;float:left;padding:0 0 5px 0}a.refresh{display:block;width:auto;float:right;height:20px;line-height:20px;text-align:center;padding:0 10px 0 28px;border:1px solid #aaa;margin-left:10px;color:#333;background-position:5px 50%;background-repeat:no-repeat;background-image:url('../images/icons/refresh.png')}.infoTable{background:#f4faff}.infoTable th{text-align:left}#ticketThread table{margin-top:10px;border:1px solid #aaa;border-bottom:2px solid #aaa}#ticketThread table th{text-align:left;border-bottom:1px solid #aaa;font-size:12px;padding:5px}#ticketThread table th span{font-weight:normal;color:#888;padding-left:20px}#ticketThread table td{padding:5px}#ticketThread .message th{background:#d8efff}#ticketThread .response th{background:#ddd}#ticketThread .info{padding:2px;background:#f9f9f9;border-top:1px solid #ddd;height:16px;line-height:16px}#ticketThread .info a{display:inline-block;margin:5px 10px 5px 0;padding-left:24px;height:16px;line-height:16px;background-position:0 50%;background-repeat:no-repeat}
\ No newline at end of file
+html{font-size:100%;overflow-y:scroll;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0;font-size:13px;line-height:1.231;padding:0}body,input,select,textarea{font-family:sans-serif;color:#000}b,strong{font-weight:bold}blockquote{margin:1em 40px}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}small{font-size:85%}ul,ol{margin:1em 0;padding:0 0 0 30px}img{border:0;vertical-align:middle}form{margin:0}fieldset{border:0;margin:0;padding:0}label{cursor:pointer}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}input{line-height:normal;*overflow:visible}table input{*overflow:auto}input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button}input[type="checkbox"],input[type="radio"]{box-sizing:border-box}input[type="text"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{overflow:auto;vertical-align:top;resize:vertical}table{border-collapse:collapse;border-spacing:0}th,td{vertical-align:top}th{text-align:left;font-weight:normal}h1,h2,h3,h4,h5,h6,form,fieldset{margin:0;padding:0}a{color:#0072bc;text-decoration:none}h1{color:#00aeef;font-weight:normal;font-size:20px}h3{font-size:16px}h2{font-size:16px;color:#999}.centered{text-align:center}.clear{clear:both;height:1px;visibility:none}.hidden{display:none}.faded{color:#666}#pagination{border:0;margin:0 0 40px 0;padding:0}#pagination li{border:0;margin:0;padding:0;font-size:11px;list-style:none;display:inline}#pagination li a{margin-right:2px;display:block;float:left;padding:3px 6px;text-decoration:none}#pagination li a:hover{color:#ff0084}#pagination .previousOff,#pagination .nextOff{color:#666;display:block;float:left;font-weight:bold;padding:3px 4px}#pagination .next a,#pagination .previous a{font-weight:bold}#pagination .active{color:#000;font-weight:bold;margin-right:2px;display:block;float:left;padding:3px 6px;text-decoration:none}#msg_notice{margin:0;padding:5px 10px 5px 36px;height:16px;line-height:16px;margin-bottom:10px;border:1px solid #0a0;background:url('../images/icons/ok.png') 10px 50% no-repeat #e0ffe0}#msg_warning{margin:0;padding:5px 10px 5px 36px;height:16px;line-height:16px;margin-bottom:10px;border:1px solid #f26522;background:url('../images/icons/alert.png') 10px 50% no-repeat #ffd}#msg_error{margin:0;padding:5px 10px 5px 36px;height:16px;line-height:16px;margin-bottom:10px;border:1px solid #a00;background:url('../images/icons/error.png') 10px 50% no-repeat #fff0f0}.warning{background:#ffc;font-style:italic}.warning strong{text-transform:uppercase;color:#a00;font-style:normal}.error{color:#f00}.error input{border:1px solid #f00}.button,.button:visited{background:#222;display:inline-block;font-size:16px;padding:8px 16px 6px 16px;width:160px;text-align:center;color:#fff;font-weight:bold;text-decoration:none;border-radius:5px;-moz-border-radius:5px;-webkit-border-radius:5px;box-shadow:0 1px 3px rgba(0,0,0,0.5);-moz-box-shadow:0 1px 3px rgba(0,0,0,0.5);-webkit-box-shadow:0 1px 3px rgba(0,0,0,0.5);text-shadow:0 -1px 1px rgba(0,0,0,0.25);border-bottom:1px solid rgba(0,0,0,0.25);position:relative;cursor:pointer;font-family:helvetica,arial,sans-serif}.button:hover{background-color:#111;color:#fff}.button:active{top:1px;box-shadow:none;-moz-box-shadow:none;-webkit-box-shadow:none}.button,.button:visited,.green.button,.green.button:visited{background-color:#91bd09}.green.button:hover{background-color:#749a02}.blue.button,.blue.button:visited{background-color:#00aeef}.blue.button:hover{background-color:#0299d2}body{background:url('../images/page_bg.png') top left repeat-x #c8c8c8}#container{background:#fff;width:840px;margin:0 auto;box-shadow:0 0 6px rgba(0,0,0,0.5);-moz-box-shadow:0 0 6px rgba(0,0,0,0.5);-webkit-box-shadow:0 0 6px rgba(0,0,0,0.5)}#header{position:relative;height:71px;padding:0 20px}#header #logo{width:220px;height:71px;float:left}#header p{width:400px;text-align:right;margin:0;padding:10px 0;float:right}#nav{margin:0 20px;padding:2px 10px;height:20px;background:url('../images/nav_bg.png') top left repeat-x;border-top:1px solid #aaa;box-shadow:0 3px 2px rgba(0,0,0,0.4);-moz-box-shadow:0 3px 2px rgba(0,0,0,0.4);-webkit-box-shadow:0 3px 2px rgba(0,0,0,0.4)}#nav li{margin:0;padding:0;list-style:none;display:inline}#nav li a{display:block;width:auto;float:left;height:20px;line-height:20px;text-align:center;padding:0 10px 0 32px;margin-left:10px;color:#333;border-radius:20px;-webkit-border-radius:20px;-moz-border-radius:20px;background-position:10px 50%;background-repeat:no-repeat}#nav li a.active,#nav li a:hover{background-color:#dbefff;color:#000}#nav li a:hover{background-color:#ededed;color:#0054a6}#nav li a.home{background-image:url('../images/icons/home.png')}#nav li a.kb{background-image:url('../images/icons/kb.png')}#nav li a.new{background-image:url('../images/icons/new.png')}#nav li a.status{background-image:url('../images/icons/status.png')}#nav li a.tickets{background-image:url('../images/icons/tix.png')}#content{padding:20px 0;margin:0 20px;height:auto!important;height:350px;min-height:350px}#footer{text-align:center;font-size:11px;color:#333}#footer a{color:#333}#footer p{margin:10px 0 0 0}#footer #poweredBy{display:block;width:126px;height:23px;outline:0;text-indent:-9999px;margin:0 auto;background:url('../images/poweredby.png') top left no-repeat}#landing_page #new_ticket{margin-top:40px;width:295px;padding-left:75px;float:left;background:url('../images/new_ticket_icon.png') top left no-repeat}#landing_page #check_status{margin-top:40px;width:295px;padding-left:75px;float:right;background:url('../images/check_status_icon.png') top left no-repeat}#faq{clear:both;margin:0;padding:5px}#faq ol{font-size:15px;margin-left:0;padding-left:0;border-top:1px solid #ddd}#faq ol li{list-style:none;margin:0;padding:0;color:#999}#faq ol li a{display:block;padding:5px 0;height:auto!important;overflow:hidden;margin:0;border-bottom:1px solid #ddd;line-height:16px;padding-left:24px;background:url('../images/icons/page.png') 0 50% no-repeat}#faq ol li a:hover{background-color:#e9f5ff}#faq .article-meta{padding:5px;background:#fafafa}#kb{margin:2px 0;padding:5px;overflow:hidden}#kb>li{padding:10px;height:auto!important;overflow:hidden;margin:0;background:url(../images/kb_category_bg.png) bottom left repeat-x;border-bottom:1px solid #ddd}#kb>li h4{padding-bottom:3px;margin-bottom:3px}#kb>li h4 span{color:#666}#kb>li h4 a{font-size:14px}#kb li i{display:block;width:32px;height:32px;float:left;margin-right:6px;background:url(../images/kb_large_folder.png) top left no-repeat}#kb-search{padding:10px 0;overflow:hidden}#kb-search div{clear:both;overflow:hidden;padding-top:5px}#kb-search #query{margin:0;display:inline-block;float:left;width:200px;margin-right:5px}#kb-search #cid{margin:0;display:inline-block;float:left;width:200px;margin-right:5px;position:relative;top:2px}#kb-search #topic-id{margin:0;display:inline-block;float:left;width:410px}#kb-search #searchSubmit{margin:0;display:inline-block;float:left;position:relative;top:2px}#kb-search #breadcrumbs{color:#333;margin-bottom:15px}#kb-search #breadcrumbs #breadcrumbs a{color:#555}#ticketForm div,#clientLogin div{clear:both;padding:3px 0;overflow:hidden}#ticketForm div label,#clientLogin div label{display:block;width:140px;float:left}#ticketForm div label.required,#clientLogin div label.required{font-weight:bold;text-align:left}#ticketForm div input,#clientLogin div input,#ticketForm div textarea,#clientLogin div textarea{width:auto;border:1px solid #aaa;background:#fff;margin-right:10px;display:block;float:left}#ticketForm div input[type=file],#clientLogin div input[type=file]{border:0}#ticketForm div select,#clientLogin div select{display:block;float:left}#ticketForm div div.captchaRow,#clientLogin div div.captchaRow{line-height:31px}#ticketForm div div.captchaRow input,#clientLogin div div.captchaRow input{position:relative;top:6px}#ticketForm td textarea,#clientLogin td textarea,#ticketForm div textarea,#clientLogin div textarea{width:600px}#ticketForm td em,#clientLogin td em,#ticketForm div em,#clientLogin div em{color:#777}#ticketForm td .captcha,#clientLogin td .captcha,#ticketForm div .captcha,#clientLogin div .captcha{width:88px;height:31px;background:#000;display:block;float:left;margin-right:20px}#ticketForm td label.inline,#clientLogin td label.inline,#ticketForm div label.inline,#clientLogin div label.inline{width:auto;padding:0 10px}#ticketForm div.error input,#clientLogin div.error input{border:1px solid #a00}#ticketForm div.error label,#clientLogin div.error label{color:#a00}#ticketTable th{width:160px;font-weight:normal;text-align:left}#ticketTable th.required,#ticketTable td.required{font-weight:bold;text-align:left}#clientLogin{width:400px;margin-top:20px;padding:10px 100px 10px 10px;border:1px solid #ccc;background:url('../images/lock.png?1319655200') 440px 50% no-repeat #f6f6f6}#clientLogin p{clear:both;text-align:center}#clientLogin strong{font-size:11px;color:#d00;display:block;padding-left:140px}#clientLogin #email{width:250px;margin-right:0}#clientLogin #ticketno{width:120px;margin-right:0}#reply{margin-top:20px;padding:10px 5px;background:#f9f9f9;border:1px solid #ccc}#reply h2{margin-bottom:10px}#reply table{width:800px}#reply table td{vertical-align:top}#reply textarea{width:628px!important}#reply input[type=text],#reply #response_options textarea{border:1px solid #aaa;background:#fff}#reply .attachments .uploads div{display:inline-block;padding-right:20px}#reply .file{display:inline-block;padding-left:20px;margin-right:20px;background:url('../images/icons/file.gif') 0 50% no-repeat}.uploads{display:inline-block;padding-right:20px}.uploads label{padding:3px;padding-right:10px;width:auto!important}.Icon{width:auto;padding-left:20px;background-position:top left;background-repeat:no-repeat;color:#069;text-decoration:none}.Icon.Ticket{background-image:url('../images/icons/ticket.gif')}.Icon.webTicket{background-image:url('../images/icons/ticket_source_web.gif')}.Icon.emailTicket{background-image:url('../images/icons/ticket_source_email.gif')}.Icon.phoneTicket{background-image:url('../images/icons/ticket_source_phone.gif')}.Icon.otherTicket{background-image:url('../images/icons/ticket_source_other.gif')}.Icon.attachment{background-image:url('../images/icons/attachment.gif')}.Icon.file{background-image:url('../images/icons/attachment.gif')}.Icon.refresh{background-image:url('../images/icons/refresh.gif')}.Icon.thread{font-weight:bold;font-size:1em;background-image:url('../images/icons/thread.gif?1319556657')}.Icon:hover{text-decoration:underline}#ticketTable{border:1px solid #aaa;border-left:none;border-bottom:0}#ticketTable caption{padding:5px;text-align:left;color:#000;background:#ddd;border:1px solid #aaa;border-bottom:0;font-weight:bold}#ticketTable th{height:24px;line-height:24px;background:#e1f2ff;border:1px solid #aaa;border-right:0;border-top:0}#ticketTable th a{color:#000}#ticketTable td{padding:2px;border:1px solid #aaa;border-right:0;border-top:0}#ticketTable tr.alt td{background:#f9f9f9}#ticketSearchForm{display:inline-block;float:left;padding:0 0 5px 0}a.refresh{display:block;width:auto;float:right;height:20px;line-height:20px;text-align:center;padding:0 10px 0 28px;border:1px solid #aaa;margin-left:10px;color:#333;background-position:5px 50%;background-repeat:no-repeat;background-image:url('../images/icons/refresh.png')}.infoTable{background:#f4faff}.infoTable th{text-align:left}#ticketThread table{margin-top:10px;border:1px solid #aaa;border-bottom:2px solid #aaa}#ticketThread table th{text-align:left;border-bottom:1px solid #aaa;font-size:12px;padding:5px}#ticketThread table th span{font-weight:normal;color:#888;padding-left:20px}#ticketThread table td{padding:5px}#ticketThread .message th{background:#d8efff}#ticketThread .response th{background:#ddd}#ticketThread .info{padding:2px;background:#f9f9f9;border-top:1px solid #ddd;height:16px;line-height:16px}#ticketThread .info a{display:inline-block;margin:5px 10px 5px 0;padding-left:24px;height:16px;line-height:16px;background-position:0 50%;background-repeat:no-repeat}
diff --git a/assets/default/less/reset.less b/assets/default/less/reset.less
index 8cf9581eb2516d2c21e5b7b2be0bf614e85d9592..b8392cd5dfc306b48fc2614f9f6db15508451781 100644
--- a/assets/default/less/reset.less
+++ b/assets/default/less/reset.less
@@ -87,13 +87,6 @@ input[type="checkbox"], input[type="radio"] {
   box-sizing: border-box;
 }
 
-input[type="search"] {
-  -webkit-appearance: textfield;
-  -moz-box-sizing: content-box;
-  -webkit-box-sizing: content-box;
-  box-sizing: content-box;
-}
-
 textarea {
   overflow: auto;
   vertical-align: top;
@@ -114,4 +107,4 @@ th { text-align: left; font-weight: normal; }
 h1, h2, h3, h4, h5, h6, form, fieldset {
   margin: 0;
   padding: 0;
-}
\ No newline at end of file
+}
diff --git a/avatar.php b/avatar.php
new file mode 100644
index 0000000000000000000000000000000000000000..77c0a7fbeb566b8b3b91226a1d254a2d64b2c0d1
--- /dev/null
+++ b/avatar.php
@@ -0,0 +1,36 @@
+<?php
+/*********************************************************************
+    avatar.php
+
+    Simple download utility for internally-generated avatars
+
+    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');
+
+if (!isset($_GET['uid']) || !isset($_GET['mode']))
+    Http::response(400, '`uid` and `mode` parameters are required');
+
+require_once INCLUDE_DIR . 'class.avatar.php';
+
+try {
+    $ra = new RandomAvatar($_GET['mode']);
+    $avatar = $ra->makeAvatar($_GET['uid']);
+
+    Http::response(200, false, 'image/png', false);
+    Http::cacheable($_GET['uid'], false, 86400);
+    imagepng($avatar, null, 1);
+    imagedestroy($avatar);
+    exit;
+}
+catch (InvalidArgumentException $ex) {
+    Http::response(422, 'No such avatar image set');
+}
diff --git a/bootstrap.php b/bootstrap.php
index 55848778c4670d48a6c8f91fbb7ec24cf907cf29..e70c79a6bb9801b06c49e4aaa5349a93e84b6de6 100644
--- a/bootstrap.php
+++ b/bootstrap.php
@@ -43,6 +43,7 @@ class Bootstrap {
                 ini_set('date.timezone', 'America/New_York');
             }
         }
+        date_default_timezone_set('UTC');
 
         if (!isset($_SERVER['REMOTE_ADDR']))
             $_SERVER['REMOTE_ADDR'] = '';
@@ -56,7 +57,7 @@ class Bootstrap {
                 && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https');
     }
 
-    function defineTables($prefix) {
+    static function defineTables($prefix) {
         #Tables being used sytem wide
         define('SYSLOG_TABLE',$prefix.'syslog');
         define('SESSION_TABLE',$prefix.'session');
@@ -68,35 +69,46 @@ class Bootstrap {
         define('FILE_CHUNK_TABLE',$prefix.'file_chunk');
 
         define('ATTACHMENT_TABLE',$prefix.'attachment');
+
         define('USER_TABLE',$prefix.'user');
+        define('USER_CDATA_TABLE', $prefix.'user__cdata');
         define('USER_EMAIL_TABLE',$prefix.'user_email');
         define('USER_ACCOUNT_TABLE',$prefix.'user_account');
 
         define('ORGANIZATION_TABLE', $prefix.'organization');
+        define('ORGANIZATION_CDATA_TABLE', $prefix.'organization__cdata');
+
         define('NOTE_TABLE', $prefix.'note');
 
         define('STAFF_TABLE',$prefix.'staff');
         define('TEAM_TABLE',$prefix.'team');
         define('TEAM_MEMBER_TABLE',$prefix.'team_member');
         define('DEPT_TABLE',$prefix.'department');
-        define('GROUP_TABLE',$prefix.'groups');
-        define('GROUP_DEPT_TABLE', $prefix.'group_dept_access');
+        define('STAFF_DEPT_TABLE', $prefix.'staff_dept_access');
+        define('ROLE_TABLE', $prefix.'role');
 
         define('FAQ_TABLE',$prefix.'faq');
         define('FAQ_TOPIC_TABLE',$prefix.'faq_topic');
         define('FAQ_CATEGORY_TABLE',$prefix.'faq_category');
 
         define('DRAFT_TABLE',$prefix.'draft');
+
+        define('THREAD_TABLE', $prefix.'thread');
+        define('THREAD_ENTRY_TABLE', $prefix.'thread_entry');
+        define('THREAD_ENTRY_EMAIL_TABLE', $prefix.'thread_entry_email');
+
+        define('LOCK_TABLE',$prefix.'lock');
+
         define('TICKET_TABLE',$prefix.'ticket');
-        define('TICKET_THREAD_TABLE',$prefix.'ticket_thread');
-        define('TICKET_ATTACHMENT_TABLE',$prefix.'ticket_attachment');
-        define('TICKET_LOCK_TABLE',$prefix.'ticket_lock');
-        define('TICKET_EVENT_TABLE',$prefix.'ticket_event');
-        define('TICKET_EMAIL_INFO_TABLE',$prefix.'ticket_email_info');
-        define('TICKET_COLLABORATOR_TABLE', $prefix.'ticket_collaborator');
+        define('TICKET_CDATA_TABLE', $prefix.'ticket__cdata');
+        define('THREAD_EVENT_TABLE',$prefix.'thread_event');
+        define('THREAD_COLLABORATOR_TABLE', $prefix.'thread_collaborator');
         define('TICKET_STATUS_TABLE', $prefix.'ticket_status');
         define('TICKET_PRIORITY_TABLE',$prefix.'ticket_priority');
 
+        define('TASK_TABLE', $prefix.'task');
+        define('TASK_CDATA_TABLE', $prefix.'task__cdata');
+
         define('PRIORITY_TABLE',TICKET_PRIORITY_TABLE);
 
 
@@ -110,6 +122,7 @@ class Bootstrap {
         define('FORM_ANSWER_TABLE',$prefix.'form_entry_values');
 
         define('TOPIC_TABLE',$prefix.'help_topic');
+        define('TOPIC_FORM_TABLE',$prefix.'help_topic_form');
         define('SLA_TABLE', $prefix.'sla');
 
         define('EMAIL_TABLE',$prefix.'email');
@@ -118,9 +131,12 @@ class Bootstrap {
 
         define('FILTER_TABLE', $prefix.'filter');
         define('FILTER_RULE_TABLE', $prefix.'filter_rule');
+        define('FILTER_ACTION_TABLE', $prefix.'filter_action');
 
         define('PLUGIN_TABLE', $prefix.'plugin');
         define('SEQUENCE_TABLE', $prefix.'sequence');
+        define('TRANSLATION_TABLE', $prefix.'translation');
+        define('QUEUE_TABLE', $prefix.'queue');
 
         define('API_KEY_TABLE',$prefix.'api_key');
         define('TIMEZONE_TABLE',$prefix.'timezone');
@@ -180,15 +196,14 @@ class Bootstrap {
     function loadCode() {
         #include required files
         require_once INCLUDE_DIR.'class.util.php';
-        require(INCLUDE_DIR.'class.signal.php');
+        require_once INCLUDE_DIR.'class.translation.php';
+        require_once(INCLUDE_DIR.'class.signal.php');
+        require(INCLUDE_DIR.'class.model.php');
         require(INCLUDE_DIR.'class.user.php');
         require(INCLUDE_DIR.'class.auth.php');
         require(INCLUDE_DIR.'class.pagenate.php'); //Pagenate helper!
         require(INCLUDE_DIR.'class.log.php');
         require(INCLUDE_DIR.'class.crypto.php');
-        require(INCLUDE_DIR.'class.timezone.php');
-        require_once(INCLUDE_DIR.'class.signal.php');
-        require(INCLUDE_DIR.'class.nav.php');
         require(INCLUDE_DIR.'class.page.php');
         require_once(INCLUDE_DIR.'class.format.php'); //format helpers
         require_once(INCLUDE_DIR.'class.validator.php'); //Class to help with basic form input validation...please help improve it.
@@ -298,13 +313,14 @@ define('SETUP_DIR',ROOT_DIR.'setup/');
 
 define('UPGRADE_DIR', INCLUDE_DIR.'upgrader/');
 define('I18N_DIR', INCLUDE_DIR.'i18n/');
+define('CLI_DIR', INCLUDE_DIR.'cli/');
 
 /*############## Do NOT monkey with anything else beyond this point UNLESS you really know what you are doing ##############*/
 
 #Current version && schema signature (Changes from version to version)
 define('THIS_VERSION','1.8-git'); //Shown on admin panel
 define('GIT_VERSION','$git');
-define('MAJOR_VERSION', '1.9');
+define('MAJOR_VERSION', '1.10');
 //Path separator
 if(!defined('PATH_SEPARATOR')){
     if(strpos($_ENV['OS'],'Win')!==false || !strcasecmp(substr(PHP_OS, 0, 3),'WIN'))
diff --git a/client.inc.php b/client.inc.php
index 001a63ebd5c94ea95075e6cb5bc08a52415b1d24..2ad4d4702139d68e589dd917acfcedd4ca28f30d 100644
--- a/client.inc.php
+++ b/client.inc.php
@@ -48,8 +48,7 @@ $nav=null;
 $thisclient = UserAuthenticationBackend::getUser();
 
 if (isset($_GET['lang']) && $_GET['lang']) {
-    if (Internationalization::getLanguageInfo($_GET['lang']))
-        $_SESSION['client:lang'] = $_GET['lang'];
+    Internationalization::setCurrentLanguage($_GET['lang']);
 }
 
 // Bootstrap gettext translations as early as possible, but after attempting
@@ -77,6 +76,7 @@ $ost->addExtraHeader('<meta name="csrf_token" content="'.$ost->getCSRFToken().'"
 /* Client specific defaults */
 define('PAGE_LIMIT', DEFAULT_PAGE_LIMIT);
 
+require(INCLUDE_DIR.'class.nav.php');
 $nav = new UserNav($thisclient, 'home');
 
 $exempt = in_array(basename($_SERVER['SCRIPT_NAME']), array('logout.php', 'ajax.php', 'logs.php', 'upgrade.php'));
diff --git a/css/filedrop.css b/css/filedrop.css
index 96dca339e9736b699b9249ef44c799c0584eacd7..370db784a332b0c0f9bf34f7e32e2d949e06e200 100644
--- a/css/filedrop.css
+++ b/css/filedrop.css
@@ -25,6 +25,7 @@
     padding: 5px 10px 5px 20px;
     margin: 0;
     border-radius: 5px;
+    height:25px;
 }
 .rtl .filedrop .files .file {
     padding-left: 10px;
@@ -37,6 +38,10 @@
   margin: 0 1em;
   color: #999;
 }
+.filedrop .files .file > span {
+    padding:4px 0 0 0;
+    display:block;
+}
 .filedrop .files .file .upload-rate {
   margin: 0 10px;
   color: #aaa;
@@ -53,8 +58,8 @@
 .filedrop .preview {
   width: auto;
   height: auto;
-  max-width: 60px;
-  max-height: 40px;
+  max-width: 100px;
+  max-height: 25px;
   display: inline-block;
   float: left;
   padding-right: 10px;
@@ -64,9 +69,9 @@
   padding-left: 10px;
   float: right;
 }
-.redactor_box + .filedrop .dropzone,
-.redactor_box + div > .filedrop .dropzone,
-.redactor_box + div > .filedrop .files {
+.redactor-box + .filedrop .dropzone,
+.redactor-box + div > .filedrop .dropzone,
+.redactor-box + div > .filedrop .files {
     border-top-width: 1px;
     border-top-left-radius: 0;
     border-top-right-radius: 0;
@@ -75,6 +80,7 @@
 .tooltip-preview img {
     max-width: 300px;
     max-height: 300px;
+    z-index:11;
 }
 
 /* Bootstrap 3.2 progress-bar */
diff --git a/css/jquery.multiselect.css b/css/jquery.multiselect.css
deleted file mode 100644
index 8a08e22b75ba758290971dbadb1a903bddca534e..0000000000000000000000000000000000000000
--- a/css/jquery.multiselect.css
+++ /dev/null
@@ -1,23 +0,0 @@
-.ui-multiselect { padding:2px 0 2px 4px; text-align:left }
-.ui-multiselect span.ui-icon { float:right }
-.ui-multiselect-single .ui-multiselect-checkboxes input { position:absolute !important; top: auto !important; left:-9999px; }
-.ui-multiselect-single .ui-multiselect-checkboxes label { padding:5px !important }
-
-.ui-multiselect-header { margin-bottom:3px; padding:3px 0 3px 4px }
-.ui-multiselect-header ul { font-size:0.9em }
-.ui-multiselect-header ul li { float:left; padding:0 10px 0 0 }
-.ui-multiselect-header a { text-decoration:none }
-.ui-multiselect-header a:hover { text-decoration:underline }
-.ui-multiselect-header span.ui-icon { float:left }
-.ui-multiselect-header li.ui-multiselect-close { float:right; text-align:right; padding-right:0 }
-
-.ui-multiselect-menu { display:none; padding:3px; position:absolute; z-index:10000; text-align: left }
-.ui-multiselect-checkboxes { position:relative /* fixes bug in IE6/7 */; overflow-y:auto }
-.ui-multiselect-checkboxes label { cursor:default; display:block; border:1px solid transparent; padding:3px 1px }
-.ui-multiselect-checkboxes label input { position:relative; top:1px }
-.ui-multiselect-checkboxes li { clear:both; font-size:0.9em; padding-right:3px }
-.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label { text-align:center; font-weight:bold; border-bottom:1px solid }
-.ui-multiselect-checkboxes li.ui-multiselect-optgroup-label a { display:block; padding:3px; margin:1px 0; text-decoration:none }
-
-/* remove label borders in IE6 because IE6 does not support transparency */
-* html .ui-multiselect-checkboxes label { border:none }
diff --git a/css/redactor.css b/css/redactor.css
index beedbecee4966d7bb5fb2a0caa2c93239231a52b..052eec5b37c1de720864bbc5561917c0089ca744 100644
--- a/css/redactor.css
+++ b/css/redactor.css
@@ -1,320 +1,147 @@
 /*
 	Icon font
 */
-@font-face {
-  font-family: 'RedactorFont';
-  src: url('redactor-font.eot');
-}
 @font-face {
   font-family: 'RedactorFont';
   src: url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMggi/NUAAAC8AAAAYGNtYXAaVcx2AAABHAAAAExnYXNwAAAAEAAAAWgAAAAIZ2x5Zm8dIFkAAAFwAAATSGhlYWQACVb9AAAUuAAAADZoaGVhA+ECBQAAFPAAAAAkaG10eEEBA94AABUUAAAAkGxvY2FVlFE8AAAVpAAAAEptYXhwAC8AkgAAFfAAAAAgbmFtZRHEcG0AABYQAAABZnBvc3QAAwAAAAAXeAAAACAAAwIAAZAABQAAAUwBZgAAAEcBTAFmAAAA9QAZAIQAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADmHwHg/+D/4AHgACAAAAABAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEADgAAAAKAAgAAgACAAEAIOYf//3//wAAAAAAIOYA//3//wAB/+MaBAADAAEAAAAAAAAAAAAAAAEAAf//AA8AAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAwAAACUCAAGSAAQACQANAAA3EQURBQEFEQURATUXBwACAP4AAdv+SgG2/tySkiUBbgH+lAEBSgH+3AEBJv7/3G9sAAAGAAAASQIAAW4ABAAJAA4AEwAYAB0AABMhFSE1FSEVITUVIRUhNSczFSM1FTMVIzUVMxUjNZIBbv6SAW7+kgFu/pKSSUlJSUlJAW5JSW5JSW5JSdxJSW5JSW5JSQAAAAACAAAAJQH3AZIAFgAuAAAlLgMnBzIuAic+AyMXNh4CByUnMg4CBx4DIxcnHgMXNi4CBwH3Dik/XUABAR04Vjg+WDUYAQFNeEcZEv7MAQENHDMlHzIfEQEBASZUTDYHCSBIZj4lGCQaEARqFi5HLzJFKhJqDC1RZSzVPQoWIxkbJBQID0wCCQ4VDxo4KA8PAAACAG4AJQGSAZIABAAzAAA3IQchJzceAzMyPgI3PgMnNyMXDgMHDgMjIi4CJy4DNycjBx4DF24BJQH+3QFABRIUGg0QGBUQCAYKBgQBAUABAQEEBAQCCAgKBQYJCQcEAgUCAwEBPwEBAwcJCEkkJD8HCgYEBAYKBwcRFRkPtcMGCQkHAwMFAwEBAwUDAwcJCQbDtQ8ZFREHAAUAAP//AgABtwAGAA4AFgBHAF8AAAEzFTMVIzUfAQc1IzUzNS8BNxUzFSMVFx4DFRwBDgEHDgMHMh4CFx4DHwEjJzwBJjQjLgMrARUjNTMyHgIXBzMyPgI3PgM1NC4CJy4DKwEVAUkjS24mkZFvb96RkW9vDAMFAwECAwICBQUGBAECAgIBAQICAgEbIBMBAQIEBQUCCh0qCAwKCQM3DgMFBQMCAQIBAQEBAgECAwQGAw4BtpYgtv9cXEolSUhcXEklSlUDCAoNBwQJBwcCAwUDAgEBAQIBAQMEBANCLgEBAQIGBwYCSLYBAwUDRAECAgECBAQGAwQFBQQBAgIBATIAAAAAAwBtAAABkgGTAAMADAARAAAlIzcXBzM3MxczAyMDFyEVITUBI0YjI7ZKF2MXSmVbZQEBJP7c5nh4eUlJASb+2iRJSQAKAAAAJQIAAZIABAAJAA4AEwAYAB0AIgAnACwAMQAANxEFEQU3FzUHFTU3NScVJwcVFzUVJxU3NRUHFRc1NxUXNQclBxUXNRUnFTc1FQcVFzUAAgD+ALeSkpKSJW1tbW1tbSWSkgEkbW1tbW1tJQFuAf6UASUBSgFIbQFIAUq4AUgBSm8BSgFIbQFIAUrbSAFKAQEBSAFKbwFKAUhtAUgBSgAAAAIACQAlAgABkgAWAC4AACUOAxU1DgMHJj4CFzU0HgIXBT4DNxU1FD4CNy4DNRUmDgIXAgA5VTkcQVxAKA8RGEh3Thc2Vz/+PAY3S1UlECAxICYyHQw9Z0chCt8wRi8VAWsFDxsjGS1kUiwLaQETKUYxYBAUDwgDTRABCRMlGhoiFwkBPhAQJzkZAAAAAgBJAEkBtwFuAEcAjwAAAQ4DFRQeAhceAxc+Azc+AzU0LgInLgMHJg4CBwYiBiYHNAYmIicwLgE0NTQ+Ajc+Azc1DgMHJw4DFRQeAhceAxc+Azc+AzU0LgInLgMHJg4CBwYiBiYVJgYmIjUiJjQmNTQ+Ajc+Azc1DgMHATkJDQkEAwYKBgcOEBAJCA4NDAUGCAUDAwQHBQUKCgwGBQoICAMBAgIBAQEBAQEBAQMGCgYGDxITCxMhHBYJzQkNCQQDBwkHBg4QEQgIDg0MBgUIBQMCBQcFBAoLDAYFCQkIAwECAgEBAQEBAQEBAwcJBgcPERQLEyEcFwkBIgwYHBsQCxgUEgcICwgDAQECBggGBQ0MDwYIDA0KBgUIBAQBAQICBQECAgEBAQECAQQCBQEKEhQRCggQDAwDFwgQFBQNAQwYHBsQCxgUEgcICwgDAQECBggGBQ0MDwYIDA0KBgUIBAQBAQICBQECAgEBAQECAQQCBQEKEhQRCggQDAwDFwgQFBQNAAT//wBJAgABbgAEAAkADgASAAATIRUhNRchFSE1FSEVITUHNQcXAAIA/gC3AUn+twFJ/rclk5MBbklJbklJbklJSbdcWwAAAAUAAABJAgABbgAEAAkADgAaAG0AABMhFSE1FSEVITUVIRUhNSczNSM1IwcVNxUjFRc+Azc+Azc0PgE0NTQuAicuAyMiBioBByIOAiMVPgM3Mj4BMjM6AR4BFx4CFBUcAQYUBw4DBw4DDwEVMzUjPgM3MZIBbv6SAW7+kgFu/pKNRBgUFhYYIAUHBQMBAgICAQEBAQEDBAICBgcHBQEEAwQCAgMEBAICBAQDAgIDAwMCAgMDAwEBAgEBAQEBAgICAQQGCQULRC0BAwQEAgFuSUluSUluSUlrFF0GFAZJFJEFBwYEAQIDBAMBAgMDAwIDBwUFAgIEAgEBAQEBAhUBAgIBAQEBAQIBAQIDBAIBAgMCAQICAwMCAQUHCQYNExQBBAMFAgADAAAASQIAAW4ALAAxAGwAACUiLgInNTMeAzMyPgI1NC4CIyIOAgcjNT4DMzIeAhUUDgIjJzMVIzUnIg4CByMVDgMVFB4CFxUzHgMzMj4CNzMVDgMjIi4CNTQ+AjMyHgIXFSMuAyMBbgoUEhEIHgUKCwsGEyEZDg4ZIRMGCwsKBR4IERIUCh41KBcXKDUet5KSJQYLCwoFHgQHBQICBQcEHgUKCwsGBgsLCgUeCBESFAoeNSgXFyg1HgoUEhEIHgUKCwsGSQMGBwU0AgQDAQ0XHhESHhcNAQMEAjQFBwYDFyg1Hx41KBe3SUkvAQMEAhgFCw0OBwcNDQsGFwIEAwEBAwQCNAUHBgMXKDUeHzUoFwMGBwU0AgQDAQAAAAEAAAC3AgABAAAEAAATIRUhNQACAP4AAQBJSQABAJIASQGSAZIADAAAAQ8CFzcHNxc3DwEXAQcpQQvBC0ApQAvBC0EBWdYBOAE6AdgBOgE4AQAAAAQAAABJAgABbgAEAAkADgASAAATIRUhNRchFSE1FSEVITUHNRcHAAIA/gC3AUn+twFJ/re3k5MBbklJbklJbklJSbdcWwAAAAMAAAAlAgABkgAEAAkAEgAANxEFEQUBBREFEQc/ARcVJTU3FwACAP4AAdv+SgG2tiQwPv6Sbm4lAW4B/pQBAUoB/twBASa4AV5eSgFIk5MABAAlAAAB2wG3AAMAGgAeADUAAAEVJzMHHgIGDwEOAS4BJy4BNDY/AT4BHgEXARcnFTceATI2PwE+AS4BJy4CBg8BDgEeARcB29vbKgMDAQICcwIGBgYCAwMBAnQCBQYGAv5029sqAwYGBQJzAgEBAgMCBgYGAnICAgEDAgG33NwrAgYGBgJzAgEBAgMDBQYGAnMCAQECA/51AdvaKgMDAQJzAgUGBgMCAwECAnMCBQYGAgAABAAA/9sCAAHbAAMAGgAeADUAACU1Fwc3LgI2PwE+AR4BFx4BFAYPAQ4BIiYnBycXNQcuASIGDwEOAR4BFx4CNj8BPgEuAScBJdvbKgMDAQICcwIGBgYCAwMBAnQCBQYGAnTb2yoDBgYFAnMCAQECAwIGBgYCcgICAQMC/9zbASwCBgYGAnICAgEDAgMGBgUCcwIBAwN1AdzbKgMDAQJzAgUGBgMCAwECAnICBgYGAgABAG4AJQFuAZIAEgAAJREjESM1Ii4CNTQ+AjsBESMBSSRKFigeEREeKBaTJSUBSf63khEeKBcWKB4R/pMAAAAAAwAlAAEB3AG2AAoAVwB4AAAlMwcnMzUjNxcjFQcOAwcOAyMiLgInLgM1ND4CNz4DOwE1NC4CJy4DIyIOAgcOAwc1PgM3PgIyMzIeAhceAx0BIzU1IyIOAgcOAxUUHgIXHgMzMj4CNz4DPQEBkkpcXEpKXFxK6wIGBgcEAwgICQUIDw4LBQUHBQIDBQkGBQ8SFAwlAQMDAgMFBwgFBAoJCQQFCQkJBQQJCQkEBQkKCQUNFRENBQUIBQI0FQgMCggDAwUDAQECAwICBQUHAwUJCQcCAwUCApKRkZORkZMHBAYFBQECAwIBAgUHBQULDQ8JCRANCwQFBgUCCQMGBQQCAgICAQEBAgEBAwQFAy8CAwMCAQEBAQIFCAUGDhIXDXgYSwECAwICBgYIBQQGBgUCAgMCAQIEBgQECgsOBwQAAAAEACUASgHbAW4AAwAMAC0AegAANyM3FwczNzMXMwMjAyUVFA4CBw4DIyIuAicuAzU0PgI3PgM7ATcuAyMqAQ4BBw4DBxU+Azc+AzMyHgIXHgMdASMiDgIHDgMVFB4CFx4DMzI+Ajc+AzcVMzU0LgInrjUbGok4EUsSOE1ETQF/AQMFAwMHCQoFBAYGBQIDAwIBAgMEAwMJCw0IFiIFDhIWDQYKCgoFBAoJCgQFCgoJBQUJCgoFBAkHBgIDAwMBJg0WEw8GBgkGAwIFCAUFDA4QCQUJCQgEBAcHBgI3AgUIBsV1dXZHRwEf/uFlBAcOCwsEBAYEAwICAwICBQYHAwUJBwUCAgMCAWIFCAYCAQEBAQMCBAIwAwUEAwIBAgEBAQIDAQIEBgYDCQMEBwQFCw4QCgkPDgsFBQcFAgEBAwICBQUHAxh7DhcTDwUAAAIASQBJAbcBkwAEAIEAABMhFSE1Fx4DFx4DFRQOAgcOAyMiLgInLgMnFR4DFx4DMzI+Ajc+AzU0LgInLgMvAS4DJy4DNTQ+Ajc+AzMyHgIXHgMXNS4DJy4DIyIOAgcOAxUUHgIXHgMfAUkBbv6SvwQIBgYCAgMDAQIDBQQDCAkLBgYNDAwGBg0NDQYGCwwNBgYNDAwHDxoXEggHCwgDAgUHBAUMDxIKHAcNCQcDAgMDAQIDBQMDCAkKBgYLCgsGBQsLCgYGCwwLBgYLDAsGDBcUEQcICwcDAgQHBAUMERUNIAEAJSUxAgMFBAMDBgYHAwUICAYDAgQDAQECAwMCBQcIBEEDBAUDAgECAQEDBgkGBQ8SFQwJEA8NBgYKCggDCwIFBQQDAgUFBgMFBwcFAwIDAwEBAgMCAgQGBgM9AgUDBAEBAgEBAwcJBgYPERMLCA8ODAQFCgoJBQsAAAQAAABJAgABbgAEAAkADgATAAA/ARcHJxc3FwcnJScHFzcXJwcXNwAltiO4AbYluCMB/yO4JbYBuCO2Jdsdkh6TAZQekhwBHZIekwGUHpIcAAAAAAUAAP/bAgAB2wAEAAkADgATABgAABcRIREhASERIREHITUhFRUhNSEVFSE1IRUAAgD+AAHb/koBtkn+3AEk/twBJP7cASQlAgD+AAHc/kkBt5JJSW5JSW5JSQAAAwCTAEkBbQGSABcALwBbAAA3Mh4CFx4DFwYUDgEHDgMrATczNzIeAhceAhQXBhQOAQcOAysBNzMDMzI+Ajc+Ayc2LgInLgMnPgM3PgMnNi4CJy4DKwED+AcNCQkDBAMEAQEBBAQEAgkKDQcqASgBBQsIBwIDAwQBAQQCBAEICAsFKgEoZGQRGRgRCAYLBgQBAQMEBwQGCg8OCggMDQgFAwcDAwEBBAYLBgcQFBcOZAHeAQMEAwMICQwHBgsJCAIDBAMBYYECAgMDAgYHCQUFCQcGAgIEAgFN/uoDBQgGBQ4RFQsKEQ8NBgUJBgQBAQMFBwUECwwOCAsSDw0FBggFAv63AAADACUAAAHbAbcABAANABEAADcRIREhEyMDMzczFzMDBxcjNyUBtv5K/URMOBBLETdLIho0GgABt/5JAW7+20hIASU1eHgAAAACAEIAHwG8AZkAIQBLAAAlBycOAS4BJwcXBw4BIiYvAS4BNDY/AT4BMhYfAR4BFAYHJy4BIgYPAQ4BFBYXHgE+AT8BLgMnLgI2PwE+AhYXBxc3PgE0JicBvJQEBQsMCwYhHg8PJygnDw8PDw8P1w8nKCcPDw8QEA8lCxscHAvFCwwLCgsbHRsLJwMFBgUCCgwDBQhSBg8QEgl+JoYLCwoL9pQEAQECAwMgHg8PDw8PDxAmKCcP1w8QEA8PDycoJw9+CwoLC8YLGx0bCwoLAQsLJgIDBAUCChcXFQhSBgYBBAV9JYYLHBwbCwAAAAMAAABJAgABbgAEAAkADgAAEyEVITUXIRUhNRczFSM1AAIA/gCSAW7+kpPb2wFuSUluSUluSUkAAwAAAEkCAAFuAAQACQAOAAATIRUhNRUhFSE1FTMVIzUAAgD+AAFt/pPc3AFuSUluSUluSUkAAAADAAAASQIAAW4ABAAJAA4AABMhFSE1FSEVITUVIRUhNQAB//4BAf/+AQIA/gABbklJbklJbklJAAMAAABJAgABbgAEAAkADgAAEyEHIScHIRchNxchByEnbgElAf7dAW0B/wH9/wFtASUB/t0BAW5JSW5JSW5JSQAGAAAAJwIAAZUACAANABQAGAAdACEAADc1IxEhFTMRIQEhFSE1FyMVIRUhNQcjNxcXITUhFScXIzdJSQG3Sf5JAUn+kwFtSiX+twFu27hcXG3+2wElKSlJICdJASVK/twBSdzcSbcl3EltbSUlJW5JSQAAAAEAAAABAADCHXSvXw889QALAgAAAAAAz3WLJQAAAADPdYsl////2wIAAdsAAAAIAAIAAAAAAAAAAQAAAeD/4AAAAgD//wAAAgAAAQAAAAAAAAAAAAAAAAAAACQAAAAAAAAAAAAAAAABAAAAAgAAAAIAAAACAAAAAgAAbgIAAAACAABtAgAAAAIAAAkCAABJAgD//wIAAAACAAAAAgAAAAIAAJICAAAAAgAAAAIAACUCAAAAAgAAbgIAACUCAAAlAgAASQIAAAACAAAAAgAAkwIAACUCAABCAgAAAAIAAAACAAAAAgAAAAIAAAAAAAAAAAoAFAAeAEAAcAC4AQQBhgGoAfoCQAMCAyYDuARGBFQEcASUBLwFFgVuBY4GLgbUB4IHrAfaCFwIgAj2CRIJLglKCWoJpAAAAAEAAAAkAJAACgAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAOAK4AAQAAAAAAAQAYAAAAAQAAAAAAAgAOAGoAAQAAAAAAAwAYAC4AAQAAAAAABAAYAHgAAQAAAAAABQAWABgAAQAAAAAABgAMAEYAAQAAAAAACgAoAJAAAwABBAkAAQAYAAAAAwABBAkAAgAOAGoAAwABBAkAAwAYAC4AAwABBAkABAAYAHgAAwABBAkABQAWABgAAwABBAkABgAYAFIAAwABBAkACgAoAJAAUgBlAGQAYQBjAHQAbwByAEYAbwBuAHQAVgBlAHIAcwBpAG8AbgAgADEALgAwAFIAZQBkAGEAYwB0AG8AcgBGAG8AbgB0UmVkYWN0b3JGb250AFIAZQBkAGEAYwB0AG8AcgBGAG8AbgB0AFIAZQBnAHUAbABhAHIAUgBlAGQAYQBjAHQAbwByAEYAbwBuAHQARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==) format('truetype'), url(data:application/font-woff;charset=utf-8;base64,d09GRk9UVE8AABIoAAoAAAAAEeAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAAA9AAADgEAAA4Bg0Rie09TLzIAAA74AAAAYAAAAGAIIvzVY21hcAAAD1gAAABMAAAATBpVzHZnYXNwAAAPpAAAAAgAAAAIAAAAEGhlYWQAAA+sAAAANgAAADYACVb9aGhlYQAAD+QAAAAkAAAAJAPhAgVobXR4AAAQCAAAAJAAAACQQQED3m1heHAAABCYAAAABgAAAAYAJFAAbmFtZQAAEKAAAAFmAAABZhHEcG1wb3N0AAASCAAAACAAAAAgAAMAAAEABAQAAQEBDVJlZGFjdG9yRm9udAABAgABADr4HAL4GwP4GAQeCgAZU/+Lix4KABlT/4uLDAeKZviU+HQFHQAAAT8PHQAAAUQRHQAAAAkdAAAN+BIAJQEBDRkbHSAlKi80OT5DSE1SV1xhZmtwdXp/hImOk5idoqessba7wFJlZGFjdG9yRm9udFJlZGFjdG9yRm9udHUwdTF1MjB1RTYwMHVFNjAxdUU2MDJ1RTYwM3VFNjA0dUU2MDV1RTYwNnVFNjA3dUU2MDh1RTYwOXVFNjBBdUU2MEJ1RTYwQ3VFNjBEdUU2MEV1RTYwRnVFNjEwdUU2MTF1RTYxMnVFNjEzdUU2MTR1RTYxNXVFNjE2dUU2MTd1RTYxOHVFNjE5dUU2MUF1RTYxQnVFNjFDdUU2MUR1RTYxRXVFNjFGAAACAYkAIgAkAgABAAQABwAKAA0AQQCYAPEBSQH6Ai8CxwMhA98EGwTXBYEFkQW0BfEGLwagBxEHOgf0CLUJaQmsCfwKhAq5C0QLdAuiC9AMAQxo/JQO/JQO/JQO+5QOi7AVi/gB+JSLi/wB/JSLBfhv990V/EqLi/u5+EqLi/e5Bfu4+5QVi/dv9yb7Avsm+wEFDvcm+AIV+AKLi0L8AouL1AWL+wIV+AKLi0L8AouL1AWL+wIV+AKLi0L8AouL1AX7JvdwFdSLi0JCi4vUBYv7AhXUi4tCQouL1AWL+wIV1IuLQkKLi9QFDviLsBVky0yq+0KWCIshBYuLQMb7LPcT9z33GsW4i4sIiyEF92Wr9wT7QV77Cgj7yfdpFYvIBYuLb3ImSOFBtnqLiwiLfIvXBe6F9yJ7nGSl0PsO6Ps2YwgO9wLUFfe4i4tn+7iLi68FysoVnHmngrGLsounlJydnJ2Up4uyCIv3SUyLi/tXBYt8hoCDg4ODgId8i32Lf4+Dk4OTh5aLmgiL91dLi4v7SQWLZJRvnXkIDvfd+EoVrouL+yrWi4tr+wKLi/dKBbH7kxX3JS/7JS+L1fsDi4uw9wOLi9QF+3LTFfsl5/cl54tC9wOLi2b7A4uLQQWXNhWTg499i3iLf4mBhoSGg4SHgYmOio6KjYiNiI6GjoQIpklri3i5BYuMio2KjYaZhZKEiwiBi4tDbouL90q1iwWfi5mHk4MIVEcVmYsFk4uRjY+Pjo+NkYuUi5SJkoiOh4+FjYOLCH2Li1kFDve393oVRYuu9wyu+wwF+0r7DRXVi6LU7ouiQtWLJve6MIsm+7oFjGcV97iLi0L7uIuL1AUOi7AVi/gB+JSLi/wB/JSLBfdLrxX3JouL1Psmi4tCBYv3AhX3JouL1Psmi4tCBWb3SxX7AYuLQvcBi4vUBYv7AhX7AYuLQvcBi4vUBYv7AhX7AYuLQvcBi4vUBbD3cBWLQvcmi4vU+yaLBfe4ixX7AYuLQvcBi4vUBYv7AhX7AYuLQvcBi4vUBYv7AhX7AYuLQvcBi4vUBQ74lPdzFfss+xNAUIuLCIv1BftCgExsZEte9wr3BPdB92VrCIv1BYuLxV73PfsaCPxYLBWcsvcim+6RCIs/i5oFi4u2nOHVJs5vpIuLCItOBfs2s/sOLqVGCA73zfe2FXNsgGiLY4tpk3Ccd513n4Gji6CLnJKZmpqakpyLn4uehZt+mH+ZfJJ7i32LgIeChQiIiYmKiYuKi4mMioyKjoqPi5GLpJOknKOco6KcqJYIi6EFWXhlcnRrCPthixV0bH9oi2OLaZNwnXecd6CBoougi5ySmpqZmpKci5+LnoWbfph/mX2Seot+i3+IgoQIiImJioqLiYuKjIqMiY6Kj4uRi6SUpJujnKOinKmWCIuhBVh4ZnJzawgOi/gCFfiUi4tC/JSLi9QF90v7AhX33YuLQvvdi4vUBYv7AhX33YuLQvvdi4vUBWZCFYv3S/snL/cnMAUO9yb4AhX4AouLQvwCi4vUBYv7AhX4AouLQvwCi4vUBYv7AhX4AouLQvwCi4vUBfsh9hXPi4ufc4uL6HeLdYWLd6GRi0Jzi4t3Bav7JRWXl5KTjY6PkI2PjY+Mj4yPi5CLlIiThJCFkYKOf4uHi4aKhoqGioaKhokIi3YFkI6QjZCNkIyPjI+LkIuPio6IjoiMh4uGi4iLiImIiYeJh4eHiIiDgX18CIB+i3jPi4ufXosFjo+QkJGRCIuLBQ74AtQVcItyk3aYCIu/qYsFmIWZh5uLvYu0sIu5i7pisFmLe4t9h36FCG2Li78FoJikk6aL3IvMSYs6iztKSTqLCPtL90sV9yaLi0L7JouL1AVmuhV8i3yHfoUIbYuLcwWAfYR6i3iLeZJ5ln0Ii3SpiwWYhZqHmoubi5mPmJEIqYuLVwV2fnKDcIs6i0rNi9uL3MzN3Iumi6SDoH4Ii1dtiwV+kX2Pe4sIDov3lBX4lIuLQvyUi4vUBQ73m/ftFWL7a0qLgFL3VYuWxEuLtPdry4uWxPtVi4BSzIsFDov4AhX4lIuLQvyUi4vUBfdL+wIV992Li0L73YuL1AWL+wIV992Li0L73YuL1AX7S0IVi/dL9ycv+ycwBQ6LsBWL+AH4lIuL/AH8lIsF+G/33RX8SouL+7n4SouL97kF+0r7SxWvi7vqySyLQvwCi4vU9wL3JvcC+yYFDvhv+EsVi/tw+2/3cPdviwVhYBWShIyChoUI+wf7BwWFhoKMhJKEkoqUkJEI9wj3BwWQkJWKkYQI/CD8HxX3b4r7b/dvi/tuBbW1FZKElYqQkAj3B/cHBZCQipWEkoSRgo2FhQj7BvsHBYWGjYGRhQgO97n3kxWL93D3b/tv+2+KBbW3FYSSipSQkQj3B/cGBZGRlIqShJKEjIGGhgj7CPsHBYaGgYyFkgj7CPsJFftvjPdv+3CL928FYWEVhJKBjIaGCPsH+wcFhoaMgZKEkoSUipGRCPcG9wYFkZGJlIWSCA733bAVi/fdZ4uL+91Bi4v3JgVPi1q8i8iLx7y8x4sI9yeLi/wBZosFDvgm9yYV1Ysv+yUv9yXVi4v3J0GL5/cl5/slQYuL+ycF+3+EFYWCgoSBhoGGgIh/i3WLeZF+mH6XhZ2Looujkp2blpqXopGriwiwi4uUBYuUiJKFj4SQgo1/i3+Lf4l/iH+If4V+hAiLugWWkJeOl46XjZiMmIusi6KEmH6ZfZFyi2gIi/sMV4uLowWL1hV2iwV3i32IhIaDhoeCi36LgY6EkIWQhpOIlIuZi5aQkpaTlo+ai58Ii48FDvdC91kVVoum9wml+wkF+x37ChXDi5zS1oudRMOLPvezR4s++7MF+BPwFYuHBYt3h3uDgIOAf4V9i4GLg46GkYWRiJOLlIuYj5WTkJSQmY6giwihiwWt7RV9mXOSaYt8i36Kfol/iH6Hf4YIi1sFmJOYkJiPl46YjZmLl4uViJGHkoaOhIuCCIuCZYsFaYtyhXt/e3+DeItyi3SReZl+mH6ehaOLmIuXjZWQlpCTk5KUCItzwouL9w8Fi6+EpX2ZCA7U95QV+AKLi2b8AouLsAX3U1oVloeUhZGEkYSOgouCi36GgYKEgoR/iHuLe4t6jnuRepB6lHqXCItKBZqEm4Wch5yIm4mci7OLqZOfm5+alKOLq4ujhZ9/mn6bd5dwlAhvlgV3kX6ShZGFkIiTi5OLl4+UlJGTkZeOm4uai5mImoaZhpqEmYIIi8gFfJF8kHuPfI58jXuLaYtxg3h6d3uCdItui3WQeZd+l32hf61+CKuABQ6L928Vr6n3S/snZ277S/cmBYuLFfdL9yevbvtL+ydnqAX4lIsVZ6n7S/snr273S/cmBYuLFftL9ydnbvdL+yevqAUOi2YVi/iU+JSLi/yU/JSLBfhv+HAV/EqLi/xL+EqLi/hLBUL7JhX7uIuL1Pe4i4tCBYv7AhX7uIuL1Pe4i4tCBYv7AhX7uIuL1Pe4i4tCBQ73jPdyFZ6LmYiUg5ODj36LeYt6h3+DhIOEfYd3iwhii4vstIsFi/cVFZuLloiShJKFjoKLfYt+iIGEhYSFgIh7iwhii4vYtIsFJvuqFfCLBbWLqJKemp2ZlKKLqoulhZ9/mn+ZeZRzjZ+NmpKVl5aXkJuLoIungqB5mHqZcJJoiwgmi4v73QUOsIsVi/hL+EqLi/xL/EqLBfeR+AIVR4s/+7nDi5vT1oucQ8KLQPe5BWlWFaX7DFeLpfcMBQ74UPeKFfso+yiHjwV9h3uNfJMIamupbXx8BWJiSYtitAh8mgVitIvNtLQI92v3awW0tM2LtGIImnwFtGKLSWJiCGb3EhVuqFyKbm4I+1n7WgVtbotcp26ob7qLqKkIsrEFg4+EkIWScKaGsJ+gCN3dBZuapIyifwj7EvsRsWb3GvcaBaiojLpuqAgOi/gCFfiUi4tC/JSLi9QF9yb7AhX4AouLQvwCi4vUBfcn+wIV92+Li0L7b4uL1AUOi/gCFfiUi4tC/JSLi9QFi/sCFfgBi4tC/AGLi9QFi/sCFfdwi4tC+3CLi9QFDov4AhX4k4uLQvyTi4vUBYv7AhX4k4uLQvyTi4vUBYv7AhX4lIuLQvyUi4vUBQ73AvgCFfe4i4tC+7iLi9QF+wL7AhX4lIuLQvyUi4vUBfcC+wIV97iLi0L7uIuL1AUO1LIVi9RCi4v3ufhLi4tB1IuL+7j8S4sF99333RX8AYuL+3D4AYuL93AF1UIVZouL+0v73YuLZvgCi4v3cAX7b0IV+0yL5/cB5/sBBfcBZhX7uYuLsPe5i4tmBWL3AhW0QkKLq9QFDviUFPiUFYsMCgAAAAADAgABkAAFAAABTAFmAAAARwFMAWYAAAD1ABkAhAAAAAAAAAAAAAAAAAAAAAEQAAAAAAAAAAAAAAAAAAAAAEAAAOYfAeD/4P/gAeAAIAAAAAEAAAAAAAAAAAAAACAAAAAAAAIAAAADAAAAFAADAAEAAAAUAAQAOAAAAAoACAACAAIAAQAg5h///f//AAAAAAAg5gD//f//AAH/4xoEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAQAAhlBJsl8PPPUACwIAAAAAAM91iyUAAAAAz3WLJf///9sCAAHbAAAACAACAAAAAAAAAAEAAAHg/+AAAAIA//8AAAIAAAEAAAAAAAAAAAAAAAAAAAAkAAAAAAAAAAAAAAAAAQAAAAIAAAACAAAAAgAAAAIAAG4CAAAAAgAAbQIAAAACAAAJAgAASQIA//8CAAAAAgAAAAIAAAACAACSAgAAAAIAAAACAAAlAgAAAAIAAG4CAAAlAgAAJQIAAEkCAAAAAgAAAAIAAJMCAAAlAgAAQgIAAAACAAAAAgAAAAIAAAACAAAAAABQAAAkAAAAAAAOAK4AAQAAAAAAAQAYAAAAAQAAAAAAAgAOAGoAAQAAAAAAAwAYAC4AAQAAAAAABAAYAHgAAQAAAAAABQAWABgAAQAAAAAABgAMAEYAAQAAAAAACgAoAJAAAwABBAkAAQAYAAAAAwABBAkAAgAOAGoAAwABBAkAAwAYAC4AAwABBAkABAAYAHgAAwABBAkABQAWABgAAwABBAkABgAYAFIAAwABBAkACgAoAJAAUgBlAGQAYQBjAHQAbwByAEYAbwBuAHQAVgBlAHIAcwBpAG8AbgAgADEALgAwAFIAZQBkAGEAYwB0AG8AcgBGAG8AbgB0UmVkYWN0b3JGb250AFIAZQBkAGEAYwB0AG8AcgBGAG8AbgB0AFIAZQBnAHUAbABhAHIAUgBlAGQAYQBjAHQAbwByAEYAbwBuAHQARwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAAAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==) format('woff');
   font-weight: normal;
   font-style: normal;
 }
-/* =Selection
------------------------------------------------------------------------------*/
-.redactor_box ::selection {
-  background: #ffff9e;
-}
-.redactor_box ::-moz-selection {
-  background: #ffff9e;
-}
-.redactor_box img::selection {
-  background: transparent;
-}
-.redactor_box img::-moz-selection {
-  background: transparent;
-}
 /*
-	BOX
+	Box
 */
-.redactor_box {
+.redactor-box {
   position: relative;
   overflow: visible;
-  background: #fff;
+  overflow-x: hidden;
+  border: 1px solid #ddd;
 }
-.redactor_box iframe {
+.redactor-box textarea {
   display: block;
-  margin: 0;
-  padding: 0;
-  border: 1px solid #eee;
-}
-.redactor_box textarea {
   position: relative;
-  display: block;
-  overflow: auto;
   margin: 0;
   padding: 0;
   width: 100%;
+  overflow: auto;
   outline: none;
   border: none;
-  background-color: #222;
+  background-color: #111;
   box-shadow: none;
   color: #ccc;
   font-size: 13px;
-  font-family: Menlo, Monaco, monospace, sans-serif;
+  font-family: Menlo, Monaco, monospace, sans-serif !important;
   resize: none;
 }
-.redactor_box textarea:focus {
+.redactor-box textarea:focus {
   outline: none;
 }
-.redactor_box,
-.redactor_box textarea {
-  z-index: auto !important;
+.redactor-editor,
+.redactor-box {
+  background: #fff;
+}
+/*
+	Z-index setup
+*/
+.redactor-editor,
+.redactor-box,
+.redactor-box textarea {
+  z-index: auto;
+}
+.redactor-box-fullscreen {
+  z-index: 99;
+}
+.redactor-toolbar {
+  z-index: 1 !important;
 }
-.redactor_box_fullscreen {
-  z-index: 99 !important;
+.redactor-dropdown {
+  z-index: 15;
 }
-#redactor_modal_overlay,
-#redactor_modal,
-.redactor_dropdown {
-  z-index: 100 !important;
+#redactor-modal-overlay,
+#redactor-modal-box,
+#redactor-modal {
+  z-index: 11 !important;
 }
 /*
-	AIR
-
+	Resize
 */
-body .redactor_air {
-  position: absolute;
-  z-index: 502;
+.redactor-resize {
+  background: #f4f4f4;
+  padding: 4px 0 3px 0;
+  cursor: move;
+  border: 1px solid #e3e3e3;
+  border-top: none;
+}
+.redactor-resize div {
+  width: 30px;
+  margin: auto;
+  border-top: 1px solid #bbb;
+  border-bottom: 1px solid #fff;
 }
 /*
-	FULLSCREEN
+	Fullscreen
 */
-body .redactor_box_fullscreen {
+body .redactor-box-fullscreen {
   position: fixed;
   top: 0;
   left: 0;
   width: 100%;
 }
 /*
-	LINK TOOLTIP
+	Utils
 */
-.redactor-link-tooltip {
+.redactor-scrollbar-measure {
   position: absolute;
-  z-index: 49999;
-  padding: 10px;
-  line-height: 1;
-  display: inline-block;
-  background-color: #000;
-  color: #555 !important;
-}
-.redactor-link-tooltip,
-.redactor-link-tooltip a {
-  font-size: 12px;
-  font-family: Arial, Helvetica, Verdana, Tahoma, sans-serif;
-}
-.redactor-link-tooltip a {
-  color: #ccc;
-  margin: 0 5px;
-  text-decoration: none;
-}
-.redactor-link-tooltip a:hover {
-  color: #fff;
+  top: -9999px;
+  width: 50px;
+  height: 50px;
+  overflow: scroll;
 }
 /*
-	IMAGE BOX
+	Editor
 */
-#redactor-image-box img {
-  width: 100%;
-}
-.redactor_editor {
+.redactor-editor {
   position: relative;
   overflow: auto;
   margin: 0 !important;
-  padding: 10px 10px;
-  padding-bottom: 5px;
+  padding: 10px;
+  min-height: 80px;
   outline: none;
-  background: none;
-  background: #fff;
-  box-shadow: none !important;
   white-space: normal;
-  border: 1px solid #eee;
-}
-.redactor_editor:focus {
-  outline: none;
-}
-.redactor_editor div,
-.redactor_editor p,
-.redactor_editor ul,
-.redactor_editor ol,
-.redactor_editor table,
-.redactor_editor dl,
-.redactor_editor blockquote,
-.redactor_editor pre,
-.redactor_editor h1,
-.redactor_editor h2,
-.redactor_editor h3,
-.redactor_editor h4,
-.redactor_editor h5,
-.redactor_editor h6 {
-  font-family: Arial, Helvetica, Verdana, Tahoma, sans-serif;
-}
-.redactor_editor code,
-.redactor_editor pre {
-  font-family: Menlo, Monaco, monospace, sans-serif;
-}
-.redactor_editor div,
-.redactor_editor p,
-.redactor_editor ul,
-.redactor_editor ol,
-.redactor_editor table,
-.redactor_editor dl,
-.redactor_editor blockquote,
-.redactor_editor pre {
+  font-family: Arial, Helvetica, Verdana, Tahoma, sans-serif !important;
   font-size: 14px;
   line-height: 1.6em;
 }
-.redactor_editor a {
-  color: #15c !important;
-  text-decoration: underline !important;
-}
-.redactor_editor .redactor_placeholder {
-  color: #999 !important;
-  display: block !important;
+.redactor-editor[style*='width:'] {
+  border-right: 1px dashed #999;
 }
-/*
-	TYPOGRAPHY
-*/
-.redactor_editor object,
-.redactor_editor embed,
-.redactor_editor video,
-.redactor_editor img {
-  max-width: 100%;
-  width: auto;
-}
-.redactor_editor video,
-.redactor_editor img {
-  height: auto;
-}
-.redactor_editor p,
-.redactor_editor ul,
-.redactor_editor ol,
-.redactor_editor table,
-.redactor_editor dl,
-.redactor_editor blockquote,
-.redactor_editor pre {
-  margin: 0;
-  margin-bottom: 10px !important;
-  border: none;
-  background: none !important;
-  box-shadow: none !important;
-}
-.redactor_editor iframe,
-.redactor_editor object,
-.redactor_editor hr {
-  margin-bottom: 15px !important;
-}
-.redactor_editor blockquote {
-  color: #777;
-  padding: 10px 20px;
-  font-style: italic !important;
-  border-left: 5px solid #eeeeee;
-}
-[dir="rtl"] .redactor_editor blockquote {
-  border-left: none;
-  border-right: 5px solid #eeeeee;
-}
-.redactor_editor ul,
-.redactor_editor ol {
-  padding-left: 2em !important;
-}
-.redactor_editor ul ul,
-.redactor_editor ol ol,
-.redactor_editor ul ol,
-.redactor_editor ol ul {
-  margin: 2px !important;
-  padding: 0 !important;
-  padding-left: 2em !important;
-  border: none;
-}
-.redactor_editor dl dt {
-  font-weight: bold;
-}
-.redactor_editor dd {
-  margin-left: 1em;
-}
-.redactor_editor table {
-  border-collapse: collapse;
-  font-size: 1em !important;
-}
-.redactor_editor table td {
-  padding: 5px !important;
-  border: 1px solid #ddd;
-  vertical-align: top;
-}
-.redactor_editor table thead td {
-  border-bottom: 2px solid #000 !important;
-  font-weight: bold !important;
-}
-.redactor_editor code {
-  background-color: #d8d7d7 !important;
+.redactor-editor:focus {
+  outline: none;
 }
-.redactor_editor pre {
-  overflow: auto;
-  padding: 1em !important;
-  border: 1px solid #ddd !important;
-  border-radius: 3px !important;
-  background: #f8f8f8 !important;
-  white-space: pre;
-  font-size: 90% !important;
-}
-.redactor_editor hr {
-  display: block;
-  height: 1px;
-  border: 0;
-  border-top: 1px solid #ccc;
+.toolbar-fixed-box + .redactor-editor {
+  padding-top: 42px !important;
 }
 /*
-	HEADERS
+	Placeholder
 */
-.redactor_editor h1,
-.redactor_editor h2,
-.redactor_editor h3,
-.redactor_editor h4,
-.redactor_editor h5,
-.redactor_editor h6 {
-  margin-top: 0 !important;
-  padding: 0 !important;
-  background: none;
-  color: #000;
-  font-weight: bold;
+.redactor-placeholder:after {
+  position: absolute;
+  top: 10px;
+  left: 10px;
+  content: attr(placeholder);
+  display: block;
+  /* For Firefox */
+  color: #999 !important;
+  font-weight: normal !important;
 }
-.redactor_editor h1 {
-  font-size: 36px !important;
-  line-height: 1.111em !important;
-  margin-bottom: .15em !important;
-}
-.redactor_editor h2 {
-  font-size: 30px !important;
-  line-height: 1.111em !important;
-  margin-bottom: .25em !important;
-}
-.redactor_editor h3 {
-  font-size: 24px !important;
-  line-height: 1.333em !important;
-  margin-bottom: .2em !important;
-}
-.redactor_editor h4 {
-  font-size: 18px !important;
-  line-height: 1.5em !important;
-  margin-bottom: .2em !important;
-}
-.redactor_editor h5 {
-  font-size: 1em !important;
-  line-height: 1.6em !important;
-  margin-bottom: .25em !important;
-}
-.redactor_editor h6 {
-  font-size: .8em !important;
-  line-height: 1.6em !important;
-  text-transform: uppercase;
-  margin-bottom: .3em !important;
+/* Placeholder in linebreaks mode */
+.redactor-linebreaks.redactor-placeholder:after {
+  top: 10px;
+  left: 10px;
 }
 /*
-	TOOLBAR
+	Toolbar
 */
-.redactor_toolbar {
+.redactor-toolbar {
   position: relative;
   top: 0;
   left: 0;
@@ -322,31 +149,28 @@ body .redactor_box_fullscreen {
   padding: 0 !important;
   list-style: none !important;
   font-size: 14px !important;
-  font-family: Arial, Helvetica, Verdana, Tahoma, sans-serif;
   line-height: 1 !important;
   background: #fff;
   border: none;
-  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+  box-shadow: 0 1px 4px -2px rgba(0, 0, 0, 0.4);
   z-index: 1 !important;
 }
-.redactor_toolbar:after {
+.redactor-toolbar:after {
   content: "";
   display: table;
-  clear: both;
+  /* clear: both; */
 }
-.redactor_toolbar.redactor-toolbar-overflow {
+.redactor-toolbar.redactor-toolbar-overflow {
   overflow-y: auto;
   height: 29px;
   white-space: nowrap;
 }
-.redactor_toolbar.redactor-toolbar-external {
+.redactor-toolbar.redactor-toolbar-external {
+  z-index: 999;
   box-shadow: none;
   border: 1px solid rgba(0, 0, 0, 0.1);
 }
-body .redactor_air .redactor_toolbar {
-  padding-right: 2px !important;
-}
-.redactor_toolbar li {
+.redactor-toolbar li {
   vertical-align: top;
   display: inline-block;
   margin: 0 !important;
@@ -357,7 +181,7 @@ body .redactor_air .redactor_toolbar {
   -moz-box-sizing: content-box;
   box-sizing: content-box;
 }
-.redactor_toolbar li a {
+.redactor-toolbar li a {
   display: block;
   color: #333;
   text-align: center;
@@ -371,57 +195,55 @@ body .redactor_air .redactor_toolbar {
   -moz-box-sizing: content-box;
   box-sizing: content-box;
 }
-.redactor_toolbar li a:hover {
+.redactor-toolbar li a:hover {
   outline: none;
   background-color: #1f78d8;
   color: #fff;
-  text-decoration: none;
 }
-.redactor_toolbar li a:hover i:before {
+.redactor-toolbar li a:hover i:before {
   color: #fff;
 }
-.redactor_toolbar li a:active,
-.redactor_toolbar li a.redactor_act {
+.redactor-toolbar li a:active,
+.redactor-toolbar li a.redactor-act {
   outline: none;
   background-color: #ccc;
   color: #444;
 }
-.redactor_toolbar li a.redactor-btn-image {
+.redactor-toolbar li a.redactor-btn-image {
   width: 14px;
   height: 14px;
   background-position: center center;
   background-repeat: no-repeat;
 }
-.redactor_button_disabled {
+.redactor-toolbar li a.fa-redactor-btn {
+  display: inline-block;
+  padding: 9px 10px 8px 10px;
+  line-height: 1;
+}
+.redactor-toolbar li a.redactor-button-disabled {
   filter: alpha(opacity=30);
   -moz-opacity: 0.3;
   opacity: 0.3;
 }
-.redactor_button_disabled:hover {
+.redactor-toolbar li a.redactor-button-disabled:hover {
+  color: #333;
   outline: none;
   background-color: transparent !important;
   cursor: default;
 }
-.redactor_toolbar li a.fa-redactor-btn {
-  display: inline-block;
-  padding: 9px 10px 8px 10px;
-  line-height: 1;
-}
-.redactor_toolbar.redactor-toolbar-typewriter {
-  box-shadow: none;
-  background: rgba(240, 240, 240, 0.9);
-}
-.redactor_toolbar.redactor-toolbar-typewriter li a:hover {
-  outline: none;
-  background-color: #1f78d8;
+.redactor-toolbar li a.redactor-button-focus {
   color: #fff;
+  background: #000;
 }
-.redactor_toolbar.redactor-toolbar-typewriter li a:active,
-.redactor_toolbar.redactor-toolbar-typewriter li a.redactor_act {
-  outline: none;
-  background-color: #ccc;
-  color: #444;
+/*
+	CodeMirror
+*/
+.redactor-box .CodeMirror {
+  display: none;
 }
+/*
+	Icons
+*/
 .re-icon {
   font-family: 'RedactorFont';
   speak: none;
@@ -535,192 +357,209 @@ body .redactor_air .redactor_toolbar {
   content: "\e61f";
 }
 /*
-	Toolbar classes
+	Toolbar tooltip
 */
-.redactor_format_blockquote {
-  padding-left: 10px;
-  color: #666 !important;
-  font-style: italic;
+.redactor-toolbar-tooltip {
+  position: absolute;
+  z-index: 1054;
+  text-align: center;
+  top: 0;
+  left: 0;
+  background: #000;
+  color: #fff;
+  padding: 5px 8px;
+  line-height: 1;
+  font-family: Arial, Helvetica, Verdana, Tahoma, sans-serif !important;
+  font-size: 12px;
+  border-radius: 2px;
 }
-.redactor_format_pre {
-  font-family: monospace, sans-serif;
+/*
+	Dropdown
+*/
+.redactor-dropdown {
+  position: absolute;
+  top: 28px;
+  left: 0;
+  padding: 0;
+  min-width: 220px;
+  max-height: 254px;
+  overflow: auto;
+  background-color: #fff;
+  box-shadow: 0 1px 7px rgba(0, 0, 0, 0.25);
+  font-size: 14px;
+  font-family: Arial, Helvetica, Verdana, Tahoma, sans-serif !important;
+  line-height: 1.6em;
 }
-.redactor_format_h1,
-.redactor_format_h2,
-.redactor_format_h3,
-.redactor_format_h4,
-.redactor_format_h5 {
-  font-weight: bold;
+.redactor-dropdown a {
+  display: block;
+  padding: 10px 15px;
+  color: #000;
+  text-decoration: none;
+  border-bottom: 1px solid rgba(0, 0, 0, 0.07);
 }
-.redactor_format_h1 {
-  font-size: 30px;
-  line-height: 36px;
+.redactor-dropdown a:last-child {
+  border-bottom: none;
 }
-.redactor_format_h2 {
-  font-size: 24px;
-  line-height: 36px;
+.redactor-dropdown a:hover {
+  background-color: #1f78d8;
+  color: #fff !important;
+  text-decoration: none;
 }
-.redactor_format_h3 {
-  font-size: 20px;
-  line-height: 30px;
+.redactor-dropdown a.selected {
+  background-color: #000;
+  color: #fff;
 }
-.redactor_format_h4 {
-  font-size: 16px;
-  line-height: 26px;
+.redactor-dropdown a.redactor-dropdown-link-inactive,
+.redactor-dropdown a.redactor-dropdown-link-inactive:hover {
+  background: none;
+  cursor: default;
+  color: #000 !important;
+  filter: alpha(opacity=40);
+  -moz-opacity: 0.4;
+  opacity: 0.4;
 }
-.redactor_format_h5 {
-  font-size: 14px;
-  line-height: 23px;
+.redactor-dropdown a.redactor-dropdown-link-selected {
+  color: #fff;
+  background: #000;
 }
-.redactor-toolbar-typewriter .redactor_dropdown .redactor_format_h1,
-.redactor-toolbar-typewriter .redactor_dropdown .redactor_format_h2,
-.redactor-toolbar-typewriter .redactor_dropdown .redactor_format_h3,
-.redactor-toolbar-typewriter .redactor_dropdown .redactor_format_h4,
-.redactor-toolbar-typewriter .redactor_dropdown .redactor_format_h5 {
-  font-size: 1em;
-  line-height: 1.6em;
-  text-transform: uppercase;
+/*
+	IMAGE BOX
+*/
+#redactor-image-box {
+  position: relative;
+  max-width: 100%;
+  display: inline-block;
+  line-height: 0;
+  outline: 1px dashed rgba(0, 0, 0, 0.6);
+}
+#redactor-image-editter {
+  position: absolute;
+  z-index: 5;
+  top: 50%;
+  left: 50%;
+  margin-top: -11px;
+  margin-left: -18px;
+  line-height: 1;
+  background-color: #000;
+  color: #fff;
+  font-size: 11px;
+  padding: 7px 10px;
+  cursor: pointer;
 }
-.redactor-toolbar-typewriter .redactor_dropdown .redactor_format_h2 {
-  font-size: .85em;
+#redactor-image-resizer {
+  position: absolute;
+  z-index: 2;
+  line-height: 1;
+  cursor: nw-resize;
+  bottom: -4px;
+  right: -5px;
+  border: 1px solid #fff;
+  background-color: #000;
+  width: 8px;
+  height: 8px;
 }
 /*
-	Typewriter
+	LINK TOOLTIP
 */
-.redactor_editor.redactor-editor-typewriter {
-  background: #f5f5f5 !important;
-  padding: 25px 50px !important;
-}
-.redactor_editor.redactor-editor-typewriter div,
-.redactor_editor.redactor-editor-typewriter p,
-.redactor_editor.redactor-editor-typewriter ul,
-.redactor_editor.redactor-editor-typewriter ol,
-.redactor_editor.redactor-editor-typewriter table,
-.redactor_editor.redactor-editor-typewriter dl,
-.redactor_editor.redactor-editor-typewriter blockquote,
-.redactor_editor.redactor-editor-typewriter pre,
-.redactor_editor.redactor-editor-typewriter h1,
-.redactor_editor.redactor-editor-typewriter h2,
-.redactor_editor.redactor-editor-typewriter h3,
-.redactor_editor.redactor-editor-typewriter h4,
-.redactor_editor.redactor-editor-typewriter h5,
-.redactor_editor.redactor-editor-typewriter h6 {
-  font-family: 'Courier New', 'Lucida Console', Consolas, Monaco, monospace, sans-serif;
-  font-size: 18px !important;
-  line-height: 1.5em !important;
-  margin-bottom: 1.5em !important;
-}
-.redactor_editor.redactor-editor-typewriter h2 {
-  font-size: 14px !important;
+.redactor-link-tooltip {
+  position: absolute;
+  z-index: 99;
+  padding: 10px;
+  line-height: 1;
+  display: inline-block;
+  background-color: #000;
+  color: #555 !important;
 }
-.redactor_editor.redactor-editor-typewriter h1,
-.redactor_editor.redactor-editor-typewriter h2,
-.redactor_editor.redactor-editor-typewriter h3,
-.redactor_editor.redactor-editor-typewriter h4,
-.redactor_editor.redactor-editor-typewriter h5,
-.redactor_editor.redactor-editor-typewriter h6 {
-  text-transform: uppercase;
+.redactor-link-tooltip,
+.redactor-link-tooltip a {
+  font-size: 12px;
+  font-family: Arial, Helvetica, Verdana, Tahoma, sans-serif !important;
 }
-.redactor_editor.redactor-editor-typewriter a {
-  color: #000 !important;
-  text-decoration: underline !important;
+.redactor-link-tooltip a {
+  color: #ccc;
+  margin: 0 5px;
+  text-decoration: none;
+}
+.redactor-link-tooltip a:hover {
+  color: #fff;
 }
 /*
-	WYM
+	DROPAREA
 */
-.redactor_editor.redactor_editor_wym {
-  padding: 10px 7px 0 7px !important;
-  background: #f6f6f6 !important;
-}
-.redactor_editor.redactor_editor_wym div,
-.redactor_editor.redactor_editor_wym p,
-.redactor_editor.redactor_editor_wym ul,
-.redactor_editor.redactor_editor_wym ol,
-.redactor_editor.redactor_editor_wym table,
-.redactor_editor.redactor_editor_wym dl,
-.redactor_editor.redactor_editor_wym pre,
-.redactor_editor.redactor_editor_wym h1,
-.redactor_editor.redactor_editor_wym h2,
-.redactor_editor.redactor_editor_wym h3,
-.redactor_editor.redactor_editor_wym h4,
-.redactor_editor.redactor_editor_wym h5,
-.redactor_editor.redactor_editor_wym h6,
-.redactor_editor.redactor_editor_wym blockquote {
-  margin-top: 0;
-  margin-bottom: 5px !important;
-  padding: 10px !important;
-  border: 1px solid #e4e4e4 !important;
-  background-color: #fff !important;
-  z-index: 0;
-}
-.redactor_editor.redactor_editor_wym blockquote:before {
-  content: '';
-}
-.redactor_editor.redactor_editor_wym img {
+#redactor-droparea {
   position: relative;
-  z-index: 1;
+  overflow: hidden;
+  padding: 140px 20px;
+  border: 3px dashed rgba(0, 0, 0, 0.1);
 }
-.redactor_editor.redactor_editor_wym div {
-  border: 1px dotted #aaa !important;
+#redactor-droparea.drag-hover {
+  background: rgba(200, 222, 250, 0.75);
 }
-.redactor_editor.redactor_editor_wym pre {
-  border: 2px dashed #e4e4e4 !important;
-  background-color: #f8f8f8 !important;
+#redactor-droparea.drag-drop {
+  background: rgba(250, 248, 200, 0.5);
 }
-.redactor_editor.redactor_editor_wym ul,
-.redactor_editor.redactor_editor_wym ol {
-  padding-left: 2em !important;
-}
-.redactor_editor.redactor_editor_wym ul li ul,
-.redactor_editor.redactor_editor_wym ol li ul,
-.redactor_editor.redactor_editor_wym ul li ol,
-.redactor_editor.redactor_editor_wym ol li ol {
-  border: none !important;
+#redactor-droparea-placeholder {
+  text-align: center;
+  font-size: 12px;
+  color: rgba(0, 0, 0, 0.7);
 }
 /*
-	DROPDOWN
+	PROGRESS
 */
-.redactor_dropdown {
-  position: absolute;
-  top: 28px;
+#redactor-progress {
+  position: fixed;
+  top: 0;
   left: 0;
-  padding: 10px;
-  width: 200px;
-  background-color: #fff;
-  box-shadow: 0 1px 5px #bbb;
-  font-size: 13px;
-  font-family: Helvetica, Arial, Verdana, Tahoma, sans-serif;
-  line-height: 21px;
+  width: 100%;
+  z-index: 1000000;
+  height: 10px;
 }
-.redactor-toolbar-typewriter .redactor_dropdown {
-  font-family: 'Courier New', 'Lucida Console', Consolas, Monaco, monospace, sans-serif;
-  background-color: #f5f5f5;
+#redactor-progress span {
+  display: block;
+  width: 100%;
+  height: 100%;
+  background-color: #3d58a8;
+  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);
+  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);
+  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);
+  -webkit-animation: progress-bar-stripes 2s linear infinite;
+  -o-animation: progress-bar-stripes 2s linear infinite;
+  animation: progress-bar-stripes 2s linear infinite;
+  background-size: 40px 40px;
 }
-.redactor_separator_drop {
-  padding: 0 !important;
-  border-top: 1px solid #ddd;
-  font-size: 0;
-  line-height: 0;
+@-webkit-keyframes progress-bar-stripes {
+  from {
+    background-position: 40px 0;
+  }
+  to {
+    background-position: 0 0;
+  }
 }
-.redactor_dropdown a {
-  display: block;
-  padding: 3px 5px;
-  color: #000;
-  text-decoration: none;
+@-o-keyframes progress-bar-stripes {
+  from {
+    background-position: 40px 0;
+  }
+  to {
+    background-position: 0 0;
+  }
 }
-.redactor_dropdown a:hover {
-  background-color: #dde4ef;
-  color: #444 !important;
-  text-decoration: none;
+@keyframes progress-bar-stripes {
+  from {
+    background-position: 40px 0;
+  }
+  to {
+    background-position: 0 0;
+  }
 }
 /*
 	MODAL
 */
-#redactor_modal_overlay {
+#redactor-modal-overlay {
   position: fixed;
   top: 0;
   left: 0;
   margin: auto;
+  overflow: auto;
   width: 100%;
   height: 100%;
   background-color: #000 !important;
@@ -728,140 +567,169 @@ body .redactor_air .redactor_toolbar {
   -moz-opacity: 0.3;
   opacity: 0.3;
 }
-#redactor_modal {
+#redactor-modal-box {
   position: fixed;
-  top: 50%;
-  left: 50%;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+#redactor-modal {
+  outline: 0;
+  position: relative;
+  margin: auto;
+  margin-bottom: 20px;
   padding: 0;
   background: #fff;
   color: #000;
-  font-size: 12px !important;
-  font-family: Arial, Helvetica, Verdana, Tahoma, sans-serif;
-  box-shadow: 0 1px 10px rgba(0, 0, 0, 0.5);
+  font-size: 14px !important;
+  font-family: Arial, Helvetica, Verdana, Tahoma, sans-serif !important;
+  box-shadow: 0 1px 70px rgba(0, 0, 0, 0.5);
 }
-#redactor_modal header {
-  padding: 20px 30px 5px 30px;
-  font-size: 16px;
+#redactor-modal header {
+  padding: 30px 40px 5px 40px;
+  font-size: 18px;
+  font-weight: bold;
 }
-#redactor_modal section {
-  padding: 20px 30px;
+#redactor-modal section {
+  padding: 30px 40px 50px 40px;
 }
-#redactor_modal label {
-  display: block !important;
+#redactor-modal label {
+  display: block;
   float: none !important;
-  margin: 10px 0 3px 0 !important;
-  padding: 0 !important;
-  font-size: 12px !important;
-}
-#redactor_modal footer:after {
-  content: "";
-  display: table;
-  clear: both;
-}
-#redactor_modal footer div {
-  float: left;
+  margin: 15px 0 3px 0 !important;
+  padding: 0;
 }
-#redactor_modal input[type="radio"],
-#redactor_modal input[type="checkbox"] {
+#redactor-modal input[type="radio"],
+#redactor-modal input[type="checkbox"] {
   position: relative;
   top: -1px;
 }
-#redactor_modal input[type="text"],
-#redactor_modal input[type="password"],
-#redactor_modal input[type="email"],
-#redactor_modal textarea {
+#redactor-modal select {
+  width: 100%;
+}
+#redactor-modal input[type="text"],
+#redactor-modal input[type="password"],
+#redactor-modal input[type="email"],
+#redactor-modal input[type="url"],
+#redactor-modal textarea {
   position: relative;
   z-index: 2;
   margin: 0;
-  padding: 1px 2px;
-  height: 23px;
+  padding: 5px 4px;
+  height: 28px;
   border: 1px solid #ccc;
   border-radius: 1px;
   background-color: white;
   box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2) inset;
   color: #333;
-  font-size: 13px;
-  font-family: Arial, Helvetica, Verdana, Tahoma, sans-serif;
-  line-height: 1;
+  width: 100%;
+  font-size: 14px;
+  font-family: Arial, Helvetica, Verdana, Tahoma, sans-serif !important;
   -moz-transition: border 0.3s ease-in;
   transition: border 0.3s ease-in;
 }
-#redactor_modal textarea {
-  display: block;
-  margin-top: 4px;
-  line-height: 1.4em;
-}
-#redactor_modal input:focus,
-#redactor_modal textarea:focus {
+#redactor-modal input[type="text"]:focus,
+#redactor-modal input[type="password"]:focus,
+#redactor-modal input[type="email"]:focus,
+#redactor-modal input[type="url"]:focus,
+#redactor-modal textarea:focus {
   outline: none;
   border-color: #5ca9e4;
   box-shadow: 0 0 0 2px rgba(70, 161, 231, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2) inset;
 }
-#redactor_modal_close {
-  position: absolute;
-  top: 5px;
-  right: 3px;
-  width: 20px;
-  height: 20px;
-  color: #999;
-  font-size: 26px;
-  cursor: pointer;
+#redactor-modal input[type="text"].redactor-input-error,
+#redactor-modal input[type="password"].redactor-input-error,
+#redactor-modal input[type="email"].redactor-input-error,
+#redactor-modal input[type="url"].redactor-input-error,
+#redactor-modal textarea.redactor-input-error {
+  border-color: #e82f2f;
+  box-shadow: 0 0 0 2px rgba(232, 47, 47, 0.3), 0 1px 2px rgba(0, 0, 0, 0.2) inset;
 }
-#redactor_modal_close:hover {
-  color: #000;
+#redactor-modal textarea {
+  display: block;
+  margin-top: 4px;
+  line-height: 1.4em;
 }
-.redactor_input {
-  width: 99%;
-  font-size: 14px;
+/*
+	Tabs in Modal
+*/
+#redactor-modal-tabber {
+  margin-bottom: 15px;
+  font-size: 12px;
 }
-.redactor_modal_box {
-  overflow: auto;
-  margin-bottom: 10px;
-  height: 350px;
+#redactor-modal-tabber a {
+  border: 1px solid #ddd;
+  line-height: 1;
+  padding: 8px 15px;
+  margin-right: -1px;
+  text-decoration: none;
+  color: #000;
 }
-#redactor_image_box {
-  overflow: auto;
-  margin-bottom: 10px;
-  height: 270px;
+#redactor-modal-tabber a:hover {
+  background-color: #1f78d8;
+  border-color: #1f78d8;
+  color: #fff;
 }
-#redactor_image_box_select {
-  display: block;
-  margin-bottom: 15px !important;
-  width: 200px;
+#redactor-modal-tabber a.active {
+  cursor: default;
+  background-color: #ddd;
+  border-color: #ddd;
+  color: rgba(0, 0, 0, 0.6);
 }
-#redactor_image_box img {
-  margin-right: 10px;
-  margin-bottom: 10px;
-  max-width: 100px;
-  cursor: pointer;
+/*
+	List in Modal
+*/
+#redactor-modal #redactor-modal-list {
+  margin-left: 0;
+  padding-left: 0;
+  list-style: none;
+  max-height: 250px;
+  overflow-x: auto;
 }
-#redactor_tabs {
-  margin-bottom: 18px;
+#redactor-modal #redactor-modal-list li {
+  border-bottom: 1px solid #ddd;
 }
-#redactor_tabs a {
-  display: inline-block;
-  margin-right: 2px;
-  padding: 4px 14px;
-  border: 1px solid #d2d2d2;
-  border-radius: 3px;
-  background: #fff;
+#redactor-modal #redactor-modal-list li:last-child {
+  border-bottom: none;
+}
+#redactor-modal #redactor-modal-list a {
+  padding: 10px 5px;
   color: #000;
   text-decoration: none;
-  line-height: 1;
+  font-size: 13px;
+  display: block;
+  position: relative;
 }
-#redactor_tabs a:hover,
-#redactor_tabs a.redactor_tabs_act {
-  border-color: #eee;
-  color: #999 !important;
-  text-decoration: none !important;
+#redactor-modal #redactor-modal-list a:hover {
+  background-color: #eee;
 }
-.redactor_modal_btn_hidden {
-  display: none;
+#redactor-modal-close {
+  position: absolute;
+  top: 10px;
+  right: 10px;
+  width: 30px;
+  height: 30px;
+  text-align: right;
+  color: #bbb;
+  font-size: 30px;
+  font-weight: 300;
+  cursor: pointer;
+  -webkit-appearance: none;
+  padding: 0;
+  border: 0;
+  background: 0;
+  outline: none;
 }
-#redactor_modal footer button {
+#redactor-modal-close:hover {
+  color: #000;
+}
+#redactor-modal footer button {
   position: relative;
   width: 100%;
-  padding: 10px 16px;
+  padding: 14px 16px;
   margin: 0;
   outline: none;
   border: none;
@@ -871,114 +739,252 @@ body .redactor_air .redactor_toolbar {
   text-decoration: none;
   font-weight: normal;
   font-size: 12px;
-  font-family: Arial, Helvetica, Verdana, Tahoma, sans-serif;
+  font-family: Arial, Helvetica, Verdana, Tahoma, sans-serif !important;
   line-height: 1;
   cursor: pointer;
 }
-#redactor_modal footer button:hover {
+#redactor-modal footer button:hover {
   color: #777;
   background: none;
   background: #bbb;
   text-decoration: none;
 }
-#redactor_modal footer button.redactor_modal_delete_btn {
+#redactor-modal footer button.redactor-modal-delete-btn {
   background: none;
   color: #fff;
   background-color: #b52525;
 }
-#redactor_modal footer button.redactor_modal_delete_btn:hover {
+#redactor-modal footer button.redactor-modal-delete-btn:hover {
   color: rgba(255, 255, 255, 0.6);
   background-color: #881b1b;
 }
-#redactor_modal footer button.redactor_modal_action_btn {
+#redactor-modal footer button.redactor-modal-action-btn {
   background: none;
   color: #fff;
   background-color: #2461b5;
 }
-#redactor_modal footer button.redactor_modal_action_btn:hover {
+#redactor-modal footer button.redactor-modal-action-btn:hover {
   color: rgba(255, 255, 255, 0.6);
   background-color: #1a4580;
 }
-/* Drag and Drop Area */
-.redactor_droparea {
-  position: relative;
-  margin: auto;
-  margin-bottom: 5px;
-  width: 100%;
+/*
+	##############################################
+
+	DROPDOWN FORMATTING
+
+	##############################################
+*/
+.redactor-dropdown .redactor-formatting-blockquote {
+  color: rgba(0, 0, 0, 0.4);
+  font-style: italic;
 }
-.redactor_droparea .redactor_dropareabox {
-  position: relative;
-  z-index: 1;
-  padding: 60px 0;
-  width: 99%;
-  border: 1px dashed #ddd;
-  background: #fff;
-  text-align: center;
+.redactor-dropdown .redactor-formatting-pre {
+  font-family: monospace, sans-serif;
 }
-.redactor_droparea .redactor_dropareabox,
-.redactor_dropalternative {
-  color: #555;
-  font-size: 12px;
+.redactor-dropdown .redactor-formatting-h1 {
+  font-size: 36px;
+  line-height: 36px;
+  font-weight: bold;
 }
-.redactor_dropalternative {
-  margin: 4px 0 2px 0;
+.redactor-dropdown .redactor-formatting-h2 {
+  font-size: 24px;
+  line-height: 36px;
+  font-weight: bold;
 }
-.redactor_dropareabox.hover {
-  border-color: #aaa;
-  background: #efe3b8;
+.redactor-dropdown .redactor-formatting-h3 {
+  font-size: 21px;
+  line-height: 30px;
+  font-weight: bold;
 }
-.redactor_dropareabox.error {
-  border-color: #dcc3c3;
-  background: #f7e5e5;
+.redactor-dropdown .redactor-formatting-h4 {
+  font-size: 18px;
+  line-height: 26px;
+  font-weight: bold;
 }
-.redactor_dropareabox.drop {
-  border-color: #e0e5d6;
-  background: #f4f4ee;
+.redactor-dropdown .redactor-formatting-h5 {
+  font-size: 16px;
+  line-height: 23px;
+  font-weight: bold;
 }
-/* =ProgressBar
------------------------------------------------------------------------------*/
-#redactor-progress {
-  position: fixed;
-  top: 0;
-  left: 0;
+/*
+	##############################################
+
+	 CONTENT STYLES
+
+	##############################################
+*/
+.redactor-editor code,
+.redactor-editor pre {
+  font-family: Menlo, Monaco, monospace, sans-serif !important;
+  cursor: text;
+}
+.redactor-editor div,
+.redactor-editor p,
+.redactor-editor ul,
+.redactor-editor ol,
+.redactor-editor table,
+.redactor-editor dl,
+.redactor-editor blockquote,
+.redactor-editor pre {
+  font-size: 14px;
+  line-height: 1.6em;
+}
+.redactor-editor a {
+  color: #15c;
+  text-decoration: underline;
+}
+.redactor-editor object,
+.redactor-editor embed,
+.redactor-editor video,
+.redactor-editor img {
+  max-width: 100%;
+  width: auto;
+}
+.redactor-editor video,
+.redactor-editor img {
+  height: auto;
+}
+.redactor-editor div,
+.redactor-editor p,
+.redactor-editor ul,
+.redactor-editor ol,
+.redactor-editor table,
+.redactor-editor dl,
+.redactor-editor figure,
+.redactor-editor blockquote,
+.redactor-editor pre {
+  margin: 0;
+  margin-bottom: 15px;
+  border: none;
+  background: none;
+  box-shadow: none;
+}
+.redactor-editor iframe,
+.redactor-editor object,
+.redactor-editor hr {
+  margin-bottom: 15px;
+}
+.redactor-editor blockquote {
+  margin-left: 1.6em !important;
+  padding-left: 0;
+  color: #777;
+  font-style: italic;
+}
+.redactor-editor ul,
+.redactor-editor ol {
+  padding-left: 2em;
+}
+.redactor-editor ul ul,
+.redactor-editor ol ol,
+.redactor-editor ul ol,
+.redactor-editor ol ul {
+  margin: 2px;
+  padding: 0;
+  padding-left: 2em;
+  border: none;
+}
+.redactor-editor ol ol li {
+  list-style-type: lower-alpha;
+}
+.redactor-editor ol ol ol li {
+  list-style-type: lower-roman;
+}
+.redactor-editor dl dt {
+  font-weight: bold;
+}
+.redactor-editor dd {
+  margin-left: 1em;
+}
+.redactor-editor table {
+  border-collapse: collapse;
+  font-size: 1em;
   width: 100%;
-  z-index: 1000000;
-  height: 10px;
 }
-#redactor-progress span {
+.redactor-editor table td,
+.redactor-editor table th {
+  padding: 5px;
+  border: 1px solid #ddd;
+  vertical-align: top;
+}
+.redactor-editor table thead td,
+.redactor-editor table th {
+  font-weight: bold;
+  border-bottom-color: #888;
+}
+.redactor-editor code {
+  background-color: #d8d7d7;
+}
+.redactor-editor pre {
+  padding: 1em;
+  border: 1px solid #ddd;
+  border-radius: 3px;
+  background: #f8f8f8;
+  font-size: 90%;
+}
+.redactor-editor hr {
   display: block;
-  width: 100%;
-  height: 100%;
-  background-color: #3d58a8;
-  background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);
-  background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);
-  background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.2) 50%, rgba(255, 255, 255, 0.2) 75%, transparent 75%, transparent);
-  -webkit-animation: progress-bar-stripes 2s linear infinite;
-  -o-animation: progress-bar-stripes 2s linear infinite;
-  animation: progress-bar-stripes 2s linear infinite;
-  background-size: 40px 40px;
+  height: 1px;
+  border: 0;
+  border-top: 1px solid #ccc;
 }
-@-webkit-keyframes progress-bar-stripes {
-  from {
-    background-position: 40px 0;
-  }
-  to {
-    background-position: 0 0;
-  }
+.redactor-editor[dir=rtl] {
+  direction: rtl;
+  text-align: right;
+  unicode-bidi: embed;
+}
+.redactor-editor h1,
+.redactor-editor h2,
+.redactor-editor h3,
+.redactor-editor h4,
+.redactor-editor h5,
+.redactor-editor h6 {
+  font-weight: bold;
+  color: #000;
+  padding: 0;
+  background: none;
+  text-rendering: optimizeLegibility;
+  margin: 0 0 .5em 0;
 }
-@-o-keyframes progress-bar-stripes {
-  from {
-    background-position: 40px 0;
-  }
-  to {
-    background-position: 0 0;
-  }
+.redactor-editor h1,
+.redactor-editor h2,
+.redactor-editor h3,
+.redactor-editor h4 {
+  line-height: 1.3;
 }
-@keyframes progress-bar-stripes {
-  from {
-    background-position: 40px 0;
-  }
-  to {
-    background-position: 0 0;
-  }
+.redactor-editor h1 {
+  font-size: 2.2em;
+}
+.redactor-editor h2 {
+  font-size: 1.9em;
+  margin-bottom: .7em;
+}
+.redactor-editor h3 {
+  font-size: 1.5em;
+}
+.redactor-editor h4 {
+  font-size: 1.3em;
+}
+.redactor-editor h5 {
+  font-size: 1.15em;
+}
+.redactor-editor h6 {
+  font-size: 1em;
+  text-transform: uppercase;
+}
+.redactor.color-swatch {
+  float: left;
+  font-size: 0;
+  border: 2px solid #fff;
+  padding: 0;
+  margin: 0;
+  width: 22px;
+  height: 22px;
+  box-sizing: border-box;
+}
+.redactor.uncolor {
+  display: block;
+  clear: both;
+  padding: 5px;
+  font-size: 12px;
+  line-height: 1;
 }
diff --git a/css/rtl.css b/css/rtl.css
index a8a42fd0ddd02c59b352c9779a4f2c3087fa105c..aa5dfec8c6361d8eab32cbf109f208ec47885b16 100644
--- a/css/rtl.css
+++ b/css/rtl.css
@@ -147,3 +147,7 @@
     padding-left: 8px;
     padding-right: initial;
 }
+.rtl .left-tabs {
+    margin-left: auto;
+    margin-right: 45px;
+}
diff --git a/css/select2.min.css b/css/select2.min.css
new file mode 100644
index 0000000000000000000000000000000000000000..1c7234426a473d3e017340ecda88f880dc1e3476
--- /dev/null
+++ b/css/select2.min.css
@@ -0,0 +1 @@
+.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle;}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none;}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px;}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none;}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap;}.select2-container .select2-search--inline{float:left;}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none;}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051;}.select2-results{display:block;}.select2-results__options{list-style:none;margin:0;padding:0;}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none;}.select2-results__option[aria-selected]{cursor:pointer;}.select2-container--open .select2-dropdown{left:0;}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0;}.select2-search--dropdown{display:block;padding:4px;}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box;}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none;}.select2-search--dropdown.select2-search--hide{display:none;}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0);}.select2-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px;}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px;}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999;}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px;}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0;}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left;}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto;}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default;}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none;}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px;}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%;}.select2-container--default .select2-selection--multiple .select2-selection__placeholder{color:#999;margin-top:5px;float:left;}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px;}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px;}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px;}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333;}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder{float:right;}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto;}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto;}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0;}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default;}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none;}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0;}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0;}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa;}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto;}.select2-container--default .select2-results__option[role=group]{padding:0;}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999;}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd;}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em;}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0;}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em;}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em;}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em;}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em;}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em;}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white;}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px;}.select2-container--classic .select2-selection--single{background-color:#f6f6f6;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #ffffff 50%, #eeeeee 100%);background-image:-o-linear-gradient(top, #ffffff 50%, #eeeeee 100%);background-image:linear-gradient(to bottom, #ffffff 50%, #eeeeee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0);}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb;}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px;}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px;}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999;}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eeeeee 50%, #cccccc 100%);background-image:-o-linear-gradient(top, #eeeeee 50%, #cccccc 100%);background-image:linear-gradient(to bottom, #eeeeee 50%, #cccccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#cccccc', GradientType=0);}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0;}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left;}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto;}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb;}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none;}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px;}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #ffffff 0%, #eeeeee 50%);background-image:-o-linear-gradient(top, #ffffff 0%, #eeeeee 50%);background-image:linear-gradient(to bottom, #ffffff 0%, #eeeeee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0);}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eeeeee 50%, #ffffff 100%);background-image:-o-linear-gradient(top, #eeeeee 50%, #ffffff 100%);background-image:linear-gradient(to bottom, #eeeeee 50%, #ffffff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0);}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0;}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb;}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px;}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none;}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px;}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px;}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555;}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right;}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto;}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto;}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb;}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0;}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0;}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;}.select2-container--classic .select2-dropdown{background-color:white;border:1px solid transparent;}.select2-container--classic .select2-dropdown--above{border-bottom:none;}.select2-container--classic .select2-dropdown--below{border-top:none;}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto;}.select2-container--classic .select2-results__option[role=group]{padding:0;}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey;}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:white;}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px;}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb;}
\ No newline at end of file
diff --git a/css/thread.css b/css/thread.css
index 5d5e3334732ca649fb80b573c7c1859b253d0be6..bb79ede494126d2581bc4bb7ce778e91b117f5c1 100644
--- a/css/thread.css
+++ b/css/thread.css
@@ -30,7 +30,7 @@
   color: #333333;
   background-color: #ffffff;
   margin: 0;
-  padding: 0.5em;
+  padding: 0.9em;
   word-wrap: break-word;
   overflow-x: auto;
 }
@@ -68,7 +68,7 @@
 .thread-body kbd,
 .thread-body pre,
 .thread-body samp {
-  font-family: monospace, serif;
+  font-family: 'Source Code Pro', 'Monaco', 'Consolas', monospace, serif;
   font-size: 1em;
 }
 .thread-body pre {
@@ -110,16 +110,16 @@
   -moz-box-sizing: border-box;
   box-sizing: border-box;
 }
-.thread-body a {
-  color: #428bca;
-  text-decoration: none;
+.thread-body a:not(.button) {
+  color: #428bca !important;
+  text-decoration: underline;
 }
-.thread-body a:hover,
-.thread-body a:focus {
+.thread-body a:not(.button):hover,
+.thread-body a:not(.button):focus {
   color: #2a6496;
   text-decoration: underline;
 }
-.thread-body a:focus {
+.thread-body a:not(.button):focus {
   outline: thin dotted #333;
   outline: 5px auto -webkit-focus-ring-color;
   outline-offset: -2px;
@@ -210,15 +210,15 @@
 }
 .thread-body h1,
 .thread-body .h1 {
-  font-size: 36px;
+  font-size: 30px;
 }
 .thread-body h2,
 .thread-body .h2 {
-  font-size: 30px;
+  font-size: 25px;
 }
 .thread-body h3,
 .thread-body .h3 {
-  font-size: 24px;
+  font-size: 21px;
 }
 .thread-body h4,
 .thread-body .h4 {
@@ -396,7 +396,7 @@
 .thread-body blockquote,
 .thread-body pre {
 	font-size: 14px;
-	line-height: 1.4rem;
+	line-height: 1.5rem;
 }
 
 /* Adjust plain/text messages posted as <pre> in the thread body to show in
@@ -422,11 +422,18 @@
 	margin: 0;
 	margin-bottom: 10px;
 	border: none;
-	background: none !important;
+    background: none;
 	box-shadow: none !important;
     text-indent: 0 !important;
 }
 
+.thread-body pre {
+    background: #f5f5f5;
+    background-color: rgba(0,0,0,0.05);
+    border-radius: 5px;
+    padding: 0.5em;
+}
+
 .thread-body iframe,
 .thread-body object,
 .thread-body hr {
@@ -469,9 +476,6 @@ table.thread-entry {
 table.thread-entry th div span {
     vertical-align: middle;
 }
-table.thread-entry th div :not(.title) {
-    font-weight: 600;
-}
 table.thread-entry th div .title {
     font-weight: 400;
 }
@@ -486,23 +490,18 @@ table.thread-entry th .textra {
     max-width: 100%; /* Ensure image hovered is resized */
 }
 .image-hover .caption {
-    background-color: rgba(0,0,0,0.5);
-    min-width: 20em;
-    color: white;
-    padding: 1em;
-    display: none;
-    width: 100%;
     position: absolute;
-    bottom: 0;
-    left: 0;
-    font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
-    font-size: 14px;
-    -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box;
-}
-.image-hover .caption .filename {
-    display: inline-block;
-    max-width: 60%;
-    overflow: hidden;
+    right: 3px;
+    bottom: 5px;
+
+    visibility: hidden;
+    opacity: 0.5;
+    transition: visibility 0s linear, opacity 0.2s ease-in;
+}
+.image-hover:hover .caption {
+    visibility: visible;
+    opacity: 1;
+    transition-delay: 0.2s;
 }
 
 /* Additional style for the mighty Microsoft Office emails "standard" style */
diff --git a/file.php b/file.php
index 22cc8094be840bc25b322a6d571921f697beb8f8..fda45974ed942b2e342a9a1efc8c0eaa06ce99c1 100644
--- a/file.php
+++ b/file.php
@@ -29,11 +29,16 @@ if (!$_GET['key']
 // 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);
+    try {
+        if (($s = @$_GET['s']) && strpos($file->getType(), 'image/') === 0)
+            return $file->display($s);
 
-    // Download the file..
-    $file->download(@$_GET['disposition'] ?: false, $_GET['expires']);
+        // Download the file..
+        $file->download(@$_GET['disposition'] ?: false, $_GET['expires']);
+    }
+    catch (Exception $x) {
+        Http::response(500, 'Unable to find that file: '.$ex->getMessage());
+    }
 }
 // else
 Http::response(404, __('Unknown or invalid file'));
diff --git a/images/avatar-sprite-ateam.png b/images/avatar-sprite-ateam.png
new file mode 100644
index 0000000000000000000000000000000000000000..0e16f01f7505840da760de8fcd46057f731a7aad
Binary files /dev/null and b/images/avatar-sprite-ateam.png differ
diff --git a/images/mystery-oscar.png b/images/mystery-oscar.png
new file mode 100644
index 0000000000000000000000000000000000000000..3aa3b83b69ec8d1b18f1b0f9676cc62d818e6795
Binary files /dev/null and b/images/mystery-oscar.png differ
diff --git a/include/JSON.php b/include/JSON.php
index e75eba65b8ed62d0c8648fe4bd322177a7ffab46..efa9424976e76bcba3eb96b8ede3e0453c259376 100644
--- a/include/JSON.php
+++ b/include/JSON.php
@@ -129,7 +129,7 @@ class Services_JSON
     *                                   bubble up with an error, so all return values
     *                                   from encode() should be checked with isError()
     */
-    function Services_JSON($use = 0)
+    function __construct($use = 0)
     {
         $this->use = $use;
     }
@@ -779,10 +779,10 @@ if (class_exists('PEAR_Error')) {
 
     class Services_JSON_Error extends PEAR_Error
     {
-        function Services_JSON_Error($message = 'unknown error', $code = null,
+        function __construct($message = 'unknown error', $code = null,
                                      $mode = null, $options = null, $userinfo = null)
         {
-            parent::PEAR_Error($message, $code, $mode, $options, $userinfo);
+            parent::__construct($message, $code, $mode, $options, $userinfo);
         }
     }
 
@@ -793,7 +793,7 @@ if (class_exists('PEAR_Error')) {
      */
     class Services_JSON_Error
     {
-        function Services_JSON_Error($message = 'unknown error', $code = null,
+        function __construct($message = 'unknown error', $code = null,
                                      $mode = null, $options = null, $userinfo = null)
         {
 
diff --git a/include/PasswordHash.php b/include/PasswordHash.php
index b5b8efcadbb70123ddedc61ae673c15342f94955..745c93ba2038adbcaf474d8679e59e852d2ed9cb 100644
--- a/include/PasswordHash.php
+++ b/include/PasswordHash.php
@@ -30,7 +30,7 @@ class PasswordHash {
 	var $portable_hashes;
 	var $random_state;
 
-	function PasswordHash($iteration_count_log2, $portable_hashes)
+	function __construct($iteration_count_log2, $portable_hashes)
 	{
 		$this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
 
diff --git a/include/Spyc.php b/include/Spyc.php
index 1c0fc31a8c90db69aeaefe4291590fe3c4aea96d..e92a4becea0016682299529c3fe290a44cdd21d2 100644
--- a/include/Spyc.php
+++ b/include/Spyc.php
@@ -617,6 +617,8 @@ class Spyc {
 
     if (is_numeric($value)) {
       if ($value === '0') return 0;
+      if (stripos($value, '0x') === 0)
+        $value = hexdec($value);
       if (rtrim ($value, 0) === $value)
         $value = (float)$value;
       return $value;
diff --git a/include/ajax.admin.php b/include/ajax.admin.php
new file mode 100644
index 0000000000000000000000000000000000000000..ddeb7c5db090d30078925e202db7de47e36bd475
--- /dev/null
+++ b/include/ajax.admin.php
@@ -0,0 +1,193 @@
+<?php
+
+require_once(INCLUDE_DIR . 'class.dept.php');
+require_once(INCLUDE_DIR . 'class.role.php');
+require_once(INCLUDE_DIR . 'class.team.php');
+
+class AdminAjaxAPI extends AjaxController {
+
+    /**
+     * Ajax: GET /admin/add/department
+     *
+     * Uses a dialog to add a new department
+     *
+     * Returns:
+     * 200 - HTML form for addition
+     * 201 - {id: <id>, name: <name>}
+     *
+     * Throws:
+     * 403 - Not logged in
+     * 403 - Not an administrator
+     */
+    function addDepartment() {
+        global $ost, $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login required');
+        if (!$thisstaff->isAdmin())
+            Http::response(403, 'Access denied');
+
+        $form = new DepartmentQuickAddForm($_POST);
+
+        if ($_POST && $form->isValid()) {
+            $dept = Dept::create();
+            $errors = array();
+            $vars = $form->getClean();
+            $vars += array(
+                'group_membership' => Dept::ALERTS_DEPT_AND_EXTENDED,
+            );
+            if ($dept->update($vars, $errors)) {
+                Http::response(201, $this->encode(array(
+                    'id' => $dept->id,
+                    'name' => $dept->name,
+                ), 'application/json'));
+            }
+            foreach ($errors as $name=>$desc)
+                if ($F = $form->getField($name))
+                    $F->addError($desc);
+        }
+
+        $title = __("Add New Department");
+        $path = ltrim($ost->get_path_info(), '/');
+
+        include STAFFINC_DIR . 'templates/quick-add.tmpl.php';
+    }
+
+    /**
+     * Ajax: GET /admin/add/team
+     *
+     * Uses a dialog to add a new team
+     *
+     * Returns:
+     * 200 - HTML form for addition
+     * 201 - {id: <id>, name: <name>}
+     *
+     * Throws:
+     * 403 - Not logged in
+     * 403 - Not an adminitrator
+     */
+    function addTeam() {
+        global $ost, $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login required');
+        if (!$thisstaff->isAdmin())
+            Http::response(403, 'Access denied');
+
+        $form = new TeamQuickAddForm($_POST);
+
+        if ($_POST && $form->isValid()) {
+            $team = Team::create();
+            $errors = array();
+            $vars = $form->getClean();
+            $vars += array(
+                'isenabled' => true,
+            );
+            if ($team->update($vars, $errors)) {
+                Http::response(201, $this->encode(array(
+                    'id' => $team->getId(),
+                    'name' => $team->name,
+                ), 'application/json'));
+            }
+            foreach ($errors as $name=>$desc)
+                if ($F = $form->getField($name))
+                    $F->addError($desc);
+        }
+
+        $title = __("Add New Team");
+        $path = ltrim($ost->get_path_info(), '/');
+
+        include STAFFINC_DIR . 'templates/quick-add.tmpl.php';
+    }
+
+    /**
+     * Ajax: GET /admin/add/role
+     *
+     * Uses a dialog to add a new role
+     *
+     * Returns:
+     * 200 - HTML form for addition
+     * 201 - {id: <id>, name: <name>}
+     *
+     * Throws:
+     * 403 - Not logged in
+     * 403 - Not an adminitrator
+     */
+    function addRole() {
+        global $ost, $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login required');
+        if (!$thisstaff->isAdmin())
+            Http::response(403, 'Access denied');
+
+        $form = new RoleQuickAddForm($_POST);
+
+        if ($_POST && $form->isValid()) {
+            $role = Role::create();
+            $errors = array();
+            $vars = $form->getClean();
+            if ($role->update($vars, $errors)) {
+                Http::response(201, $this->encode(array(
+                    'id' => $role->getId(),
+                    'name' => $role->name,
+                ), 'application/json'));
+            }
+            foreach ($errors as $name=>$desc)
+                if ($F = $form->getField($name))
+                    $F->addError($desc);
+        }
+
+        $title = __("Add New Role");
+        $path = ltrim($ost->get_path_info(), '/');
+
+        include STAFFINC_DIR . 'templates/quick-add-role.tmpl.php';
+    }
+
+    function getRolePerms($id) {
+        global $ost, $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login required');
+        if (!$thisstaff->isAdmin())
+            Http::response(403, 'Access denied');
+        if (!($role = Role::lookup($id)))
+            Http::response(404, 'No such role');
+
+        return $this->encode($role->getPermissionInfo());
+    }
+
+    function addStaff() {
+        global $ost, $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login required');
+        if (!$thisstaff->isAdmin())
+            Http::response(403, 'Access denied');
+
+        $form = new StaffQuickAddForm($_POST);
+
+        if ($_POST && $form->isValid()) {
+            $staff = Staff::create();
+            $errors = array();
+            if ($staff->update($form->getClean(), $errors)) {
+                Http::response(201, $this->encode(array(
+                    'id' => $staff->getId(),
+                    'name' => (string) $staff->getName(),
+                ), 'application/json'));
+            }
+            foreach ($errors as $name=>$desc) {
+                if ($F = $form->getField($name)) {
+                    $F->addError($desc);
+                    unset($errors[$name]);
+                }
+            }
+            $errors['err'] = implode(", ", $errors);
+        }
+
+        $title = __("Add New Agent");
+        $path = ltrim($ost->get_path_info(), '/');
+
+        include STAFFINC_DIR . 'templates/quick-add.tmpl.php';
+    }
+}
diff --git a/include/ajax.config.php b/include/ajax.config.php
index 093914a6d5791640b02310bd7ee38e0f7f34c6f2..1ac326e98d5ec5b8bb4a873b6fb891cbf1d0c6e8 100644
--- a/include/ajax.config.php
+++ b/include/ajax.config.php
@@ -13,6 +13,7 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
+require_once INCLUDE_DIR . 'class.ajax.php';
 
 if(!defined('INCLUDE_DIR')) die('!');
 
@@ -23,6 +24,7 @@ class ConfigAjaxAPI extends AjaxController {
         global $cfg, $thisstaff;
 
         $lang = Internationalization::getCurrentLanguage();
+        $info = Internationalization::getLanguageInfo($lang);
         list($sl, $locale) = explode('_', $lang);
 
         $rtl = false;
@@ -31,19 +33,27 @@ class ConfigAjaxAPI extends AjaxController {
                 $rtl = true;
         }
 
+        $primary = $cfg->getPrimaryLanguage();
+        $primary_info = Internationalization::getLanguageInfo($primary);
+        list($primary_sl, $primary_locale) = explode('_', $primary);
+
         $config=array(
-              'lock_time'       => ($cfg->getLockTime()*3600),
-              'html_thread'     => (bool) $cfg->isHtmlThreadEnabled(),
-              'date_format'     => ($cfg->getDateFormat()),
+              'lock_time'       => $cfg->getTicketLockMode() == Lock::MODE_DISABLED ? 0 : ($cfg->getLockTime()*60),
+              'html_thread'     => (bool) $cfg->isRichTextEnabled(),
+              'date_format'     => $cfg->getDateFormat(true),
               'lang'            => $lang,
               'short_lang'      => $sl,
               'has_rtl'         => $rtl,
-              'page_size'       => $thisstaff->getPageLimit(),
+              'lang_flag'       => strtolower($info['flag'] ?: $locale ?: $sl),
+              'primary_lang_flag' => strtolower($primary_info['flag'] ?: $primary_locale ?: $primary_sl),
+              'primary_language' => Internationalization::rfc1766($primary),
+              'secondary_languages' => $cfg->getSecondaryLanguages(),
+              'page_size'       => $thisstaff->getPageLimit() ?: PAGE_LIMIT,
         );
         return $this->json_encode($config);
     }
 
-    function client() {
+    function client($headers=true) {
         global $cfg;
 
         $lang = Internationalization::getCurrentLanguage();
@@ -56,15 +66,19 @@ class ConfigAjaxAPI extends AjaxController {
         }
 
         $config=array(
-            'html_thread'     => (bool) $cfg->isHtmlThreadEnabled(),
+            'html_thread'     => (bool) $cfg->isRichTextEnabled(),
             'lang'            => $lang,
             'short_lang'      => $sl,
             'has_rtl'         => $rtl,
+            'primary_language' => Internationalization::rfc1766($cfg->getPrimaryLanguage()),
+            'secondary_languages' => $cfg->getSecondaryLanguages(),
         );
 
         $config = $this->json_encode($config);
-        Http::cacheable(md5($config), $cfg->lastModified());
-        header('Content-Type: application/json; charset=UTF-8');
+        if ($headers) {
+            Http::cacheable(md5($config), $cfg->lastModified());
+            header('Content-Type: application/json; charset=UTF-8');
+        }
 
         return $config;
     }
@@ -78,10 +92,40 @@ class ConfigAjaxAPI extends AjaxController {
             array('name'=>'End-User Login Page', 'url'=> '%{url}/login.php'),
         ));
 
-        Http::cacheable(md5($links), filemtime(__file__));
+        Http::cacheable(md5($links));
         header('Content-Type: application/json; charset=UTF-8');
 
         return $links;
     }
+
+    /**
+     * Ajax: GET /config/date-format?format=<format>
+     *
+     * Formats the user's current date and time according to the given
+     * format in INTL codes.
+     *
+     * Get-Arguments:
+     * format - (string) format string used to format the current date and
+     *      time (from the user's perspective)
+     *
+     * Returns:
+     * (string) Current sequence number, optionally formatted
+     *
+     * Throws:
+     * 403 - Not logged in
+     * 400 - ?format missing
+     */
+    function dateFormat() {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
+        elseif (!isset($_GET['format']))
+            Http::response(400, '?format is required');
+
+        return Format::htmlchars(Format::__formatDate(
+            Misc::gmtime(), $_GET['format'], false, null, null, '', 'UTC'
+        ));
+    }
 }
 ?>
diff --git a/include/ajax.content.php b/include/ajax.content.php
index 830d799f01dc57ae558cea0ae30f05e0057eb2fe..c12d56ab00bdd63863b2dee5e96ccac82a0bc592 100644
--- a/include/ajax.content.php
+++ b/include/ajax.content.php
@@ -13,9 +13,10 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
-
 if(!defined('INCLUDE_DIR')) die('!');
 
+require_once INCLUDE_DIR.'class.ajax.php';
+
 class ContentAjaxAPI extends AjaxController {
 
     function log($id) {
@@ -28,7 +29,7 @@ class ContentAjaxAPI extends AjaxController {
                     $log->getTitle(),
                     Format::display(str_replace(',',', ',$log->getText())),
                     __('Log Date'),
-                    Format::db_daydatetime($log->getCreateDate()),
+                    Format::daydatetime($log->getCreateDate()),
                     __('IP Address'),
                     $log->getIP());
         }else {
@@ -119,6 +120,11 @@ class ContentAjaxAPI extends AjaxController {
         switch ($type) {
         case 'none':
             break;
+        case 'agent':
+            if (!($staff = Staff::lookup($id)))
+                Http::response(404, 'No such staff member');
+            echo Format::viewableImages($staff->getSignature());
+            break;
         case 'mine':
             echo Format::viewableImages($thisstaff->getSignature());
             break;
@@ -134,24 +140,42 @@ class ContentAjaxAPI extends AjaxController {
     }
 
     function manageContent($id, $lang=false) {
-        global $thisstaff;
+        global $thisstaff, $cfg;
 
         if (!$thisstaff)
             Http::response(403, 'Login Required');
 
         $content = Page::lookup($id, $lang);
-        $info = $content->getHashtable();
+
+        $langs = Internationalization::getConfiguredSystemLanguages();
+        $translations = $content->getAllTranslations();
+        $info = array(
+            'title' => $content->getName(),
+            'body' => $content->getBody(),
+        );
+        foreach ($translations as $t) {
+            if (!($data = $t->getComplex()))
+                continue;
+            $info['trans'][$t->lang] = array(
+                'title' => $data['name'],
+                'body' => $data['body'],
+            );
+        }
+
         include STAFFINC_DIR . 'templates/content-manage.tmpl.php';
     }
 
     function manageNamedContent($type, $lang=false) {
-        global $thisstaff;
+        global $thisstaff, $cfg;
 
         if (!$thisstaff)
             Http::response(403, 'Login Required');
 
-        $content = Page::lookup(Page::getIdByType($type, $lang));
+        $langs = $cfg->getSecondaryLanguages();
+
+        $content = Page::lookupByType($type, $lang);
         $info = $content->getHashtable();
+
         include STAFFINC_DIR . 'templates/content-manage.tmpl.php';
     }
 
@@ -168,8 +192,9 @@ class ContentAjaxAPI extends AjaxController {
 
         $vars = array_merge($content->getHashtable(), $_POST);
         $errors = array();
+
         // Allow empty content for the staff banner
-        if ($content->save($id, $vars, $errors,
+        if ($content->update($vars, $errors,
             $content->getType() == 'banner-staff')
         ) {
             Http::response(201, 'Have a great day!');
@@ -180,5 +205,22 @@ class ContentAjaxAPI extends AjaxController {
         $errors = Format::htmlchars($errors);
         include STAFFINC_DIR . 'templates/content-manage.tmpl.php';
     }
+
+    function context() {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login Required');
+        if (!$_GET['root'])
+            Http::response(400, '`root` is required parameter');
+
+        $items = VariableReplacer::getContextForRoot($_GET['root']);
+
+        if (!$items)
+            Http::response(422, 'No such context');
+
+        header('Content-Type: application/json');
+        return $this->encode($items);
+    }
 }
 ?>
diff --git a/include/ajax.draft.php b/include/ajax.draft.php
index 091632891e0d5d065048bdd8859095f867b1880e..c18dca63d08689b1d9d44cc079cb813601ef64e7 100644
--- a/include/ajax.draft.php
+++ b/include/ajax.draft.php
@@ -7,58 +7,40 @@ require_once(INCLUDE_DIR.'class.draft.php');
 class DraftAjaxAPI extends AjaxController {
 
     function _createDraft($vars) {
-        $field_list = array('response', 'note', 'answer', 'body',
-             'message', 'issue');
-        foreach ($field_list as $field) {
-            if (isset($_POST[$field])) {
-                $vars['body'] = urldecode($_POST[$field]);
-                break;
-            }
-        }
-        if (!isset($vars['body']))
-            return Http::response(422, "Draft body not found in request");
-
-        $errors = array();
-        if (!($draft = Draft::create($vars, $errors)))
-            Http::response(500, print_r($errors, true));
-
-        // If the draft is created from an existing document, ensure inline
-        // attachments from the cloned document are attachned to the draft
-        // XXX: Actually, I think this is just wasting time, because the
-        //     other object already has the items attached, so the database
-        //     won't clean up the files. They don't have to be attached to
-        //     the draft for Draft::getAttachmentIds to return the id of the
-        //     attached file
-        //$draft->syncExistingAttachments();
+        if (false === ($vars['body'] = self::_findDraftBody($_POST)))
+            return JsonDataEncoder::encode(array(
+                'error' => __("Draft body not found in request"),
+                'code' => 422,
+                ));
+
+        if (!($draft = Draft::create($vars)) || !$draft->save())
+            Http::response(500, 'Unable to create draft');
 
         echo JsonDataEncoder::encode(array(
             'draft_id' => $draft->getId(),
         ));
     }
 
-    function _getDraft($id) {
-        if (!($draft = Draft::lookup($id)))
+    function _getDraft($draft) {
+        if (!$draft || !$draft instanceof Draft)
             Http::response(205, "Draft not found. Create one first");
 
         $body = Format::viewableImages($draft->getBody());
 
         echo JsonDataEncoder::encode(array(
             'body' => $body,
-            'draft_id' => (int)$id,
+            'draft_id' => $draft->getId(),
         ));
     }
 
     function _updateDraft($draft) {
-        $field_list = array('response', 'note', 'answer', 'body',
-             'message', 'issue');
-        foreach ($field_list as $field) {
-            if (isset($_POST[$field])) {
-                $body = urldecode($_POST[$field]);
-                break;
-            }
-        }
-        if (!isset($body))
-            return Http::response(422, "Draft body not found in request");
+        if (false === ($body = self::_findDraftBody($_POST)))
+            return JsonDataEncoder::encode(array(
+                'error' => array(
+                    'message' => "Draft body not found in request",
+                    'code' => 422,
+                )
+            ));
 
         if (!$draft->setBody($body))
             return Http::response(500, "Unable to update draft body");
@@ -79,6 +61,21 @@ class DraftAjaxAPI extends AjaxController {
             unset($_FILES['file']);
 
             $file = AttachmentFile::format($_FILES['image']);
+            # Allow for data-uri uploaded files
+            $fp = fopen($file[0]['tmp_name'], 'rb');
+            if (fread($fp, 5) == 'data:') {
+                $data = 'data:';
+                while ($block = fread($fp, 8192))
+                  $data .= $block;
+                $file[0] = Format::parseRfc2397($data);
+                list(,$ext) = explode('/', $file[0]['type'], 2);
+                $file[0] += array(
+                    'name' => Misc::randCode(8).'.'.$ext,
+                    'size' => strlen($file[0]['data']),
+                );
+            }
+            fclose($fp);
+
             # TODO: Detect unacceptable attachment extension
             # TODO: Verify content-type and check file-content to ensure image
             $type = $file[0]['type'];
@@ -97,8 +94,13 @@ class DraftAjaxAPI extends AjaxController {
                     ))
                 );
 
+            // Paste uploads in Chrome will have a name of 'blob'
+            if ($file[0]['name'] == 'blob')
+                $file[0]['name'] = 'screenshot-'.Misc::randCode(4);
 
-            if (!($ids = $draft->attachments->upload($file))) {
+            $ids = $draft->attachments->upload($file);
+
+            if (!$ids) {
                 if ($file[0]['error']) {
                     return Http::response(403,
                         JsonDataEncoder::encode(array(
@@ -110,7 +112,7 @@ class DraftAjaxAPI extends AjaxController {
                     return Http::response(500, 'Unable to attach image');
             }
 
-            $id = $ids[0];
+            $id = (is_array($ids)) ? $ids[0] : $ids;
         }
         else {
             $type = explode('/', $_POST['contentType']);
@@ -129,6 +131,8 @@ class DraftAjaxAPI extends AjaxController {
 
         echo JsonDataEncoder::encode(array(
             'content_id' => 'cid:'.$f->getKey(),
+            // Return draft_id to connect the auto draft creation
+            'draft_id' => $draft->getId(),
             'filelink' => $f->getDownloadUrl(false, 'inline'),
         ));
     }
@@ -141,30 +145,35 @@ class DraftAjaxAPI extends AjaxController {
             Http::response(403, "Valid session required");
 
         $vars = array(
-            'staff_id' => ($thisclient) ? $thisclient->getId() : 0,
             'namespace' => $namespace,
         );
 
-        $info = self::_createDraft($vars);
-        $info['draft_id'] = $namespace;
+        return self::_createDraft($vars);
     }
 
     function getDraftClient($namespace) {
         global $thisclient;
 
         if ($thisclient) {
-            if (!($id = Draft::findByNamespaceAndStaff($namespace,
-                    $thisclient->getId())))
+            try {
+                $draft = Draft::lookupByNamespaceAndStaff($namespace,
+                    $thisclient->getId());
+            }
+            catch (DoesNotExist $e) {
                 Http::response(205, "Draft not found. Create one first");
+            }
         }
         else {
             if (substr($namespace, -12) != substr(session_id(), -12))
                 Http::response(404, "Draft not found");
-            elseif (!($id = Draft::findByNamespaceAndStaff($namespace, 0)))
+            try {
+                $draft = Draft::lookupByNamespaceAndStaff($namespace, 0);
+            }
+            catch (DoesNotExist $e) {
                 Http::response(205, "Draft not found. Create one first");
+            }
         }
-
-        return self::_getDraft($id);
+        return self::_getDraft($draft);
     }
 
     function updateDraftClient($id) {
@@ -220,6 +229,21 @@ class DraftAjaxAPI extends AjaxController {
         return self::_uploadInlineImage($draft);
     }
 
+    function uploadInlineImageEarlyClient($namespace) {
+        global $thisclient;
+
+        if (!$thisclient && substr($namespace, -12) != substr(session_id(), -12))
+            Http::response(403, "Valid session required");
+
+        $draft = Draft::create(array(
+            'namespace' => $namespace,
+        ));
+        if (!$draft->save())
+            Http::response(500, 'Unable to create draft');
+
+        return $this->uploadInlineImageClient($draft->getId());
+    }
+
     // Staff interface for drafts ========================================
     function createDraft($namespace) {
         global $thisstaff;
@@ -228,7 +252,6 @@ class DraftAjaxAPI extends AjaxController {
             Http::response(403, "Login required for draft creation");
 
         $vars = array(
-            'staff_id' => $thisstaff->getId(),
             'namespace' => $namespace,
         );
 
@@ -240,11 +263,15 @@ class DraftAjaxAPI extends AjaxController {
 
         if (!$thisstaff)
             Http::response(403, "Login required for draft creation");
-        elseif (!($id = Draft::findByNamespaceAndStaff($namespace,
-                $thisstaff->getId())))
+        try {
+            $draft = Draft::lookupByNamespaceAndStaff($namespace,
+                $thisstaff->getId());
+        }
+        catch (DoesNotExist $e) {
             Http::response(205, "Draft not found. Create one first");
+        }
 
-        return self::_getDraft($id);
+        return self::_getDraft($draft);
     }
 
     function updateDraft($id) {
@@ -273,6 +300,21 @@ class DraftAjaxAPI extends AjaxController {
         return self::_uploadInlineImage($draft);
     }
 
+    function uploadInlineImageEarly($namespace) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, "Login required for image upload");
+
+        $draft = Draft::create(array(
+            'namespace' => $namespace
+        ));
+        if (!$draft->save())
+            Http::response(500, 'Unable to create draft');
+
+        return $this->uploadInlineImage($draft->getId());
+    }
+
     function deleteDraft($id) {
         global $thisstaff;
 
@@ -292,11 +334,23 @@ class DraftAjaxAPI extends AjaxController {
         if (!$thisstaff)
             Http::response(403, "Login required for file queries");
 
-        $sql = 'SELECT distinct f.id, COALESCE(a.type, f.ft) FROM '.FILE_TABLE
+        if (isset($_GET['threadId']) && is_numeric($_GET['threadId'])
+            && ($thread = Thread::lookup($_GET['threadId']))
+            && ($object = $thread->getObject())
+            && ($thisstaff->canAccess($object))
+        ) {
+            $union = ' UNION SELECT f.id, a.`type`, a.`name` FROM '.THREAD_TABLE.' t
+                JOIN '.THREAD_ENTRY_TABLE.' th ON (th.thread_id = t.id)
+                JOIN '.ATTACHMENT_TABLE.' a ON (a.object_id = th.id AND a.`type` = \'H\')
+                JOIN '.FILE_TABLE.' f ON (a.file_id = f.id)
+                WHERE a.`inline` = 1 AND t.id='.db_input($_GET['threadId']);
+        }
+
+        $sql = 'SELECT distinct f.id, COALESCE(a.type, f.ft), a.`name` FROM '.FILE_TABLE
             .' f LEFT JOIN '.ATTACHMENT_TABLE.' a ON (a.file_id = f.id)
-            WHERE (a.`type` IN (\'C\', \'F\', \'T\', \'P\') OR f.ft = \'L\')
-                AND f.`type` LIKE \'image/%\'';
-        if (!($res = db_query($sql)))
+            WHERE ((a.`type` IN (\'C\', \'F\', \'T\', \'P\') AND a.`inline` = 1) OR f.ft = \'L\')'
+                .' AND f.`type` LIKE \'image/%\'';
+        if (!($res = db_query($sql.$union)))
             Http::response(500, 'Unable to lookup files');
 
         $files = array();
@@ -306,19 +360,45 @@ class DraftAjaxAPI extends AjaxController {
             'T' => __('Email Templates'),
             'L' => __('Logos'),
             'P' => __('Pages'),
+            'H' => __('This Thread'),
         );
-        while (list($id, $type) = db_fetch_row($res)) {
-            $f = AttachmentFile::lookup($id);
+        while (list($id, $type, $name) = db_fetch_row($res)) {
+            $f = AttachmentFile::lookup((int) $id);
             $url = $f->getDownloadUrl();
             $files[] = array(
-                'thumb'=>$url.'&s=128',
+                // Don't send special sizing for thread items 'cause they
+                // should be cached already by the client
+                'thumb'=>$url.($type != 'H' ? '&s=128' : ''),
                 'image'=>$url,
-                'title'=>$f->getName(),
+                'title'=>$name ?: $f->getName(),
                 'folder'=>$folders[$type]
             );
         }
         echo JsonDataEncoder::encode($files);
     }
 
+    function _findDraftBody($vars) {
+        if (isset($vars['name'])) {
+            $parts = array();
+            // Support nested `name`, like trans[lang]
+            if (preg_match('`(\w+)(?:\[(\w+)\])?(?:\[(\w+)\])?`', $_POST['name'], $parts)) {
+                array_shift($parts);
+                $focus = $vars;
+                foreach ($parts as $p)
+                    $focus = $focus[$p];
+                return $focus;
+            }
+        }
+        $field_list = array('response', 'note', 'answer', 'body',
+             'message', 'issue', 'description');
+        foreach ($field_list as $field) {
+            if (isset($vars[$field])) {
+                return $vars[$field];
+            }
+        }
+
+        return false;
+    }
+
 }
 ?>
diff --git a/include/ajax.filter.php b/include/ajax.filter.php
new file mode 100644
index 0000000000000000000000000000000000000000..5ef5ec82cc95fbec05d727dfdae8ba071ba32af3
--- /dev/null
+++ b/include/ajax.filter.php
@@ -0,0 +1,28 @@
+<?php
+
+require_once(INCLUDE_DIR . 'class.filter.php');
+
+class FilterAjaxAPI extends AjaxController {
+
+    function getFilterActionForm($type) {
+        if (!($A = FilterAction::lookupByType($type)))
+            Http::response(404, 'No such filter action type');
+
+        $form = $A->getConfigurationForm();
+        ?>
+        <div style="position:relative">
+            <div class="pull-right" style="position:absolute;top:2px;right:2px;">
+                <a href="#" title="<?php echo __('clear'); ?>" onclick="javascript:
+        if (!confirm(__('You sure?')))
+            return false;
+        $(this).closest('tr').fadeOut(400, function() { $(this).hide(); });
+        return false;"><i class="icon-trash"></i></a>
+            </div>
+        <?php
+        include STAFFINC_DIR . 'templates/dynamic-form-simple.tmpl.php';
+        ?>
+        </div>
+        <?php
+    }
+
+}
diff --git a/include/ajax.forms.php b/include/ajax.forms.php
index ca33e246c64c973b69106a4ef63b43232aa60e33..f99870a9770d436cc36c028c068a5d25ba9061d9 100644
--- a/include/ajax.forms.php
+++ b/include/ajax.forms.php
@@ -24,13 +24,15 @@ class DynamicFormsAjaxAPI extends AjaxController {
             $_SESSION[':form-data'] = array_merge($_SESSION[':form-data'], $_GET);
         }
 
-        if ($form = $topic->getForm()) {
+        foreach ($topic->getForms() as $form) {
+            if (!$form->hasAnyVisibleFields())
+                continue;
             ob_start();
             $form->getForm($_SESSION[':form-data'])->render(!$client);
-            $html = ob_get_clean();
+            $html .= ob_get_clean();
             ob_start();
             print $form->getMedia();
-            $media = ob_get_clean();
+            $media .= ob_get_clean();
         }
         return $this->encode(array(
             'media' => $media,
@@ -48,14 +50,38 @@ class DynamicFormsAjaxAPI extends AjaxController {
     }
 
     function saveFieldConfiguration($field_id) {
-        $field = DynamicFormField::lookup($field_id);
-        if (!$field->setConfiguration()) {
-            include STAFFINC_DIR . 'templates/dynamic-field-config.tmpl.php';
-            return;
+
+        if (!($field = DynamicFormField::lookup($field_id)))
+            Http::response(404, 'No such field');
+
+        $DFF = 'DynamicFormField';
+
+        // Capture flags which should remain unchanged
+        $p_mask = $DFF::MASK_MASK_ALL;
+        if ($field->isPrivacyForced()) {
+            $p_mask |= $DFF::FLAG_CLIENT_VIEW | $DFF::FLAG_AGENT_VIEW;
+        }
+        if ($field->isRequirementForced()) {
+            $p_mask |= $DFF::FLAG_CLIENT_REQUIRED | $DFF::FLAG_AGENT_REQUIRED;
+        }
+        if ($field->hasFlag($DFF::FLAG_MASK_DISABLE)) {
+            $p_mask |= $DFF::FLAG_ENABLED;
         }
-        else
+
+        // Capture current state of immutable flags
+        $preserve = $field->flags & $p_mask;
+
+        // Set admin-configured flag states
+        $flags = array_reduce($_POST['flags'],
+            function($a, $b) { return $a | $b; }, 0);
+        $field->flags = $flags | $preserve;
+
+        if ($field->setConfiguration($_POST)) {
             $field->save();
-        Http::response(201, 'Field successfully updated');
+            Http::response(201, 'Field successfully updated');
+        }
+
+        include STAFFINC_DIR . 'templates/dynamic-field-config.tmpl.php';
     }
 
     function deleteAnswer($entry_id, $field_id) {
@@ -72,29 +98,248 @@ class DynamicFormsAjaxAPI extends AjaxController {
         $ent->delete();
     }
 
-    function getListItemProperties($list_id, $item_id) {
+
+    function getListItem($list_id, $item_id) {
 
         $list = DynamicList::lookup($list_id);
-        if (!$list || !($item = $list->getItem( (int) $item_id)))
+        if (!$list)
             Http::response(404, 'No such list item');
 
+        $list = CustomListHandler::forList($list);
+        if (!($item = $list->getItem( (int) $item_id)))
+            Http::response(404, 'No such list item');
+
+        $action = "#list/{$list->getId()}/item/{$item->getId()}/update";
+        $item_form = $list->getListItemBasicForm($item->ht, $item);
+
         include(STAFFINC_DIR . 'templates/list-item-properties.tmpl.php');
     }
 
-    function saveListItemProperties($list_id, $item_id) {
+    function getListItems($list_id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
+
+        if (!($list = DynamicList::lookup($list_id)))
+            Http::response(404, 'No such list');
+
+        $pjax_container = '#items';
+        include(STAFFINC_DIR . 'templates/list-items.tmpl.php');
+    }
+
+    function saveListItem($list_id, $item_id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
 
         $list = DynamicList::lookup($list_id);
         if (!$list || !($item = $list->getItem( (int) $item_id)))
             Http::response(404, 'No such list item');
 
-        if (!$item->setConfiguration()) {
+        $item_form = $list->getListItemBasicForm($_POST, $item);
+
+        if ($valid = $item_form->isValid()) {
+            // Update basic information
+            $basic = $item_form->getClean();
+            $item->extra = $basic['extra'];
+            $item->value = $basic['value'];
+
+            if ($_item = DynamicListItem::lookup(array(
+                            'list_id' => $list->getId(), 'value'=>$item->value)))
+                if ($_item && $_item->id != $item->id)
+                    $item_form->getField('value')->addError(
+                        __('Value already in use'));
+        }
+
+        // Context
+        $action = "#list/{$list->getId()}/item/{$item->getId()}/update";
+        $icon = ($list->get('sort_mode') == 'SortCol')
+            ? '<i class="icon-sort"></i>&nbsp;' : '';
+
+        if (!$valid || !$item->setConfiguration($_POST)) {
             include STAFFINC_DIR . 'templates/list-item-properties.tmpl.php';
             return;
         }
-        else
+        else {
             $item->save();
+        }
+
+        Http::response(201, $this->encode(array(
+            'id' => $item->getId(),
+            'row' => $this->_renderListItem($item, $list),
+            'success' => true,
+        )));
+    }
+
+    function _renderListItem($item, $list=false) {
+        $list = $list ?: $item->getList();
+
+        // Send the whole row back
+        $prop_fields = $list->getSummaryFields();
+        $icon = ($list->get('sort_mode') == 'SortCol')
+            ? '<i class="icon-sort"></i>&nbsp;' : '';
+        ob_start();
+        $item->_config = null;
+        include STAFFINC_DIR . 'templates/list-item-row.tmpl.php';
+        return ob_get_clean();
+    }
 
-        Http::response(201, 'Successfully updated record');
+    function searchListItems($list_id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
+        elseif (!($list = DynamicList::lookup($list_id)))
+            Http::response(404, 'No such list');
+        elseif (!($q = $_GET['q']))
+            Http::response(400, '"q" query arg is required');
+
+        $list = CustomListHandler::forList($list);
+        $items = $list->search($q);
+
+        $results = array();
+        foreach ($items as $I) {
+            $display = $I->getValue();
+            if (isset($I->extra))
+              $display .= " ({$I->extra})";
+            $results[] = array(
+                'value' => $I->getValue(),
+                'display' => $display,
+                'id' => $I->id,
+                'list_id' => $list->getId(),
+            );
+        }
+        return $this->encode($results);
+    }
+
+    function addListItem($list_id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
+        elseif (!($list = DynamicList::lookup($list_id)))
+            Http::response(404, 'No such list');
+
+        $list = CustomListHandler::forList($list);
+        $action = "#list/{$list->getId()}/item/add";
+        $item_form = $list->getListItemBasicForm($_POST ?: null);
+        $errors = array();
+
+        if ($_POST && ($valid = $item_form->isValid())) {
+            $data = $item_form->getClean();
+            if ($list->isItemUnique($data)) {
+                $item = $list->addItem($data, $errors);
+                if ($item->setConfiguration($_POST, $errors)) {
+                    Http::response(201, $this->encode(array(
+                        'success' => true,
+                        'row' => $this->_renderListItem($item, $list)
+                    )));
+                }
+            }
+            else {
+                $item_form->getField('value')->addError(
+                    __('Value already in use'));
+            }
+        }
+
+        include(STAFFINC_DIR . 'templates/list-item-properties.tmpl.php');
+    }
+
+    function importListItems($list_id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
+        elseif (!($list = DynamicList::lookup($list_id)))
+            Http::response(404, 'No such list');
+
+        $list = CustomListHandler::forList($list);
+        $info = array(
+            'title' => sprintf('%s &mdash; %s',
+                $list->getName(), __('Import Items')),
+            'action' => "#list/{$list_id}/import",
+            'upload_url' => "lists.php?id={$list_id}&amp;do=import-items",
+        );
+
+        if ($_POST) {
+            $status = $list->importFromPost($_FILES['import'] ?: $_POST['pasted']);
+            if ($status && is_numeric($status))
+                Http::response(201, $this->encode( array('success' => true, 'count' => $status)));
+
+            $info['error'] = $status;
+            $info['pasted'] = Format::htmlchars($_POST['pasted']);
+        }
+
+        include(STAFFINC_DIR . 'templates/list-import.tmpl.php');
+    }
+
+    function disableItems($list_id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
+        elseif (!($list = DynamicList::lookup($list_id)))
+            Http::response(404, 'No such list');
+        elseif (!$_POST['ids'])
+            Http::response(422, 'Send `ids` parameter');
+
+        $list = CustomListHandler::forList($list);
+        foreach ($_POST['ids'] as $id) {
+            if ($item = $list->getItem( (int) $id)) {
+                $item->disable();
+                $item->save();
+            }
+            else {
+                Http::response(404, 'No such list item');
+            }
+        }
+        Http::response(200, $this->encode(array('success' => true)));
+    }
+
+    function undisableItems($list_id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
+        elseif (!($list = DynamicList::lookup($list_id)))
+            Http::response(404, 'No such list');
+        elseif (!$_POST['ids'])
+            Http::response(422, 'Send `ids` parameter');
+
+        $list = CustomListHandler::forList($list);
+        foreach ($_POST['ids'] as $id) {
+            if ($item = $list->getItem( (int) $id)) {
+                $item->enable();
+                $item->save();
+            }
+            else {
+                Http::response(404, 'No such list item');
+            }
+        }
+        Http::response(200, $this->encode(array('success' => true)));
+    }
+
+    function deleteItems($list_id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
+        elseif (!($list = DynamicList::lookup($list_id)))
+            Http::response(404, 'No such list');
+        elseif (!$_POST['ids'])
+            Http::response(422, 'Send `ids` parameter');
+
+        foreach ($_POST['ids'] as $id) {
+            if ($item = $list->getItem( (int) $id)) {
+                $item->delete();
+            }
+            else {
+                Http::response(404, 'No such list item');
+            }
+        }
+        Http::response(200, $this->encode(array('success' => true)));
     }
 
     function upload($id) {
@@ -116,5 +361,24 @@ class DynamicFormsAjaxAPI extends AjaxController {
             array('id'=>$field->ajaxUpload())
         );
     }
+
+    function getAllFields($id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Login required');
+        elseif (!$form = DynamicForm::lookup($id))
+            Http::response(400, 'No such form');
+
+        // XXX: Fetch the form via the list!
+        ob_start();
+        include STAFFINC_DIR . 'templates/dynamic-form-fields-view.tmpl.php';
+        $html = ob_get_clean();
+
+        return $this->encode(array(
+            'success'=>true,
+            'html' => $html,
+        ));
+    }
 }
 ?>
diff --git a/include/ajax.i18n.php b/include/ajax.i18n.php
index 3aeaefa135b8448db40e97fc8fb2b0ce1a13ac6e..4665f3ab2c18d0a8f1bd8d82e779921470a7e82a 100644
--- a/include/ajax.i18n.php
+++ b/include/ajax.i18n.php
@@ -37,5 +37,112 @@ class i18nAjaxAPI extends AjaxController {
         Http::cacheable(md5($data), $cfg->lastModified());
         echo $data;
     }
+
+    function getTranslations($tag) {
+        $t = CustomDataTranslation::allTranslations($tag);
+        $phrases = array();
+        $lm = 0;
+        foreach ($t as $translation) {
+            $phrases[$translation->lang] = $translation->text;
+            $lm = max($lm, strtotime($translation->updated));
+        }
+        $json = JsonDataEncoder::encode($phrases) ?: '{}';
+        //Http::cacheable(md5($json), $lm);
+
+        return $json;
+    }
+
+    function updateTranslations($tag) {
+        global $thisstaff, $cfg;
+
+        if (!$thisstaff)
+            Http::response(403, "Agent login required");
+        if (!$_POST)
+            Http::response(422, "No translations found to update");
+
+        $t = CustomDataTranslation::allTranslations($tag);
+        $phrases = array();
+        foreach ($t as $translation) {
+            $phrases[$translation->lang] = $translation;
+        }
+        foreach ($_POST as $lang => $phrase) {
+            if (isset($phrases[$lang])) {
+                $p = $phrases[$lang];
+                if (!$phrase) {
+                    $p->delete();
+                }
+                else {
+                    // Avoid XSS injection
+                    $p->text = trim(Format::striptags($phrase));
+                    $p->agent_id = $thisstaff->getId();
+                }
+            }
+            elseif (in_array($lang, $cfg->getSecondaryLanguages())) {
+                if (!$phrase)
+                    continue;
+                $phrases[$lang] = CustomDataTranslation::create(array(
+                    'lang'          => $lang,
+                    'text'          => $phrase,
+                    'object_hash'   => $tag,
+                    'type'          => 'phrase',
+                    'agent_id'      => $thisstaff->getId(),
+                    'updated'       => new SqlFunction('NOW'),
+                ));
+            }
+            else {
+                Http::response(400,
+                    sprintf("%s: Must be a secondary language", $lang));
+            }
+        }
+        // Commit.
+        foreach ($phrases as $p)
+            if (!$p->save())
+                Http::response(500, sprintf("%s: Unable to commit language"));
+    }
+
+    function getConfiguredLanguages() {
+        global $cfg;
+
+        $primary = $cfg->getPrimaryLanguage();
+        $info = Internationalization::getLanguageInfo($primary);
+        $langs = array(
+            $primary => array(
+                'name' => Internationalization::getLanguageDescription($primary),
+                'flag' => strtolower($info['flag']),
+                'direction' => $info['direction'] ?: 'ltr',
+            ),
+        );
+
+        foreach ($cfg->getSecondaryLanguages() as $l) {
+            $info = Internationalization::getLanguageInfo($l);
+            $langs[$l] = array(
+                'name' => Internationalization::getLanguageDescription($l),
+                'flag' => strtolower($info['flag']),
+                'direction' => $info['direction'] ?: 'ltr',
+            );
+        }
+        $json = JsonDataEncoder::encode($langs);
+        Http::cacheable(md5($json), $cfg->lastModified());
+
+        return $json;
+    }
+
+    function getSecondaryLanguages() {
+        global $cfg;
+
+        $langs = array();
+        foreach ($cfg->getSecondaryLanguages() as $l) {
+            $info = Internationalization::getLanguageInfo($l);
+            $langs[$l] = array(
+                'name' => Internationalization::getLanguageDescription($l),
+                'flag' => strtolower($info['flag']),
+                'direction' => $info['direction'] ?: 'ltr',
+            );
+        }
+        $json = JsonDataEncoder::encode($langs);
+        Http::cacheable(md5($json), $cfg->lastModified());
+
+        return $json;
+    }
 }
 ?>
diff --git a/include/ajax.kbase.php b/include/ajax.kbase.php
index 16e961ad94b8b2afad49e677c38c1af330edbb66..3fd1409666cef380540a2578176b34973bd20d1f 100644
--- a/include/ajax.kbase.php
+++ b/include/ajax.kbase.php
@@ -26,7 +26,7 @@ class KbaseAjaxAPI extends AjaxController {
         if(!$id || !($canned=Canned::lookup($id)) || !$canned->isEnabled())
             Http::response(404, 'No such premade reply');
 
-        if (!$cfg->isHtmlThreadEnabled())
+        if (!$cfg->isRichTextEnabled())
             $format .= '.plain';
 
         return $canned->getFormattedResponse($format);
@@ -50,11 +50,12 @@ class KbaseAjaxAPI extends AjaxController {
                  <a href="faq.php?id=%d">'.__('View').'</a> | <a href="faq.php?id=%d">'.__('Attachments (%d)').'</a>',
                 $faq->getQuestion(),
                 $faq->getAnswerWithImages(),
-                Format::db_daydatetime($faq->getUpdateDate()),
+                Format::daydatetime($faq->getUpdateDate()),
                 $faq->getId(),
                 $faq->getId(),
                 $faq->getNumAttachments());
-        if($thisstaff && $thisstaff->canManageFAQ()) {
+        if($thisstaff
+                && $thisstaff->hasPerm(FAQ::PERM_MANAGE)) {
             $resp.=sprintf(' | <a href="faq.php?id=%d&a=edit">'.__('Edit').'</a>',$faq->getId());
 
         }
@@ -62,5 +63,31 @@ class KbaseAjaxAPI extends AjaxController {
 
         return $resp;
     }
+
+    function manageFaqAccess($id) {
+        global $ost, $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login required');
+        if (!$thisstaff->hasPerm(FAQ::PERM_MANAGE))
+            Http::response(403, 'Access denied');
+        if (!($faq = FAQ::lookup($id)))
+            Http::response(404, 'No such faq article');
+
+        $form = new FaqAccessMgmtForm($_POST ?: $faq->getHashtable());
+
+        if ($_POST && $form->isValid()) {
+            $clean = $form->getClean();
+            $faq->ispublished = $clean['ispublished'];
+            $faq->save();
+            Http::response(201, 'Have a nice day');
+        }
+
+        $title = __("Manage FAQ Access");
+        $verb = __('Update');
+        $path = ltrim($ost->get_path_info(), '/');
+
+        include STAFFINC_DIR . 'templates/quick-add.tmpl.php';
+    }
 }
 ?>
diff --git a/include/ajax.note.php b/include/ajax.note.php
index 8e179ab163e14f15b40878d9254cffaa5debaa29..8980a561109256c83959a9173e9c4205da9f2b43 100644
--- a/include/ajax.note.php
+++ b/include/ajax.note.php
@@ -54,14 +54,14 @@ class NoteAjaxAPI extends AjaxController {
             Http::response(403, "Login required");
         elseif (!isset($_POST['note']) || !$_POST['note'])
             Http::response(422, "Send `note` parameter");
-        elseif (!($note = QuickNote::create(array(
-                'staff_id' => $thisstaff->getId(),
-                'body' => Format::sanitize($_POST['note']),
-                'created' => new SqlFunction('NOW'),
-                'ext_id' => $ext_id,
-                ))))
-            Http::response(500, "Unable to create new note");
-        elseif (!$note->save(true))
+
+        $note = new QuickNote(array(
+            'staff_id' => $thisstaff->getId(),
+            'body' => Format::sanitize($_POST['note']),
+            'created' => new SqlFunction('NOW'),
+            'ext_id' => $ext_id,
+        ));
+        if (!$note->save(true))
             Http::response(500, "Unable to create new note");
 
         $show_options = true;
diff --git a/include/ajax.orgs.php b/include/ajax.orgs.php
index bcc44f25c8d6a538f89e344936d854c34cc65cf4..1a1c8d19ca060a22e139135cae4d5af4965510f4 100644
--- a/include/ajax.orgs.php
+++ b/include/ajax.orgs.php
@@ -15,6 +15,7 @@
 
 if(!defined('INCLUDE_DIR')) die('403');
 
+require_once INCLUDE_DIR . 'class.organization.php';
 include_once(INCLUDE_DIR.'class.ticket.php');
 
 class OrgsAjaxAPI extends AjaxController {
@@ -23,28 +24,40 @@ class OrgsAjaxAPI extends AjaxController {
 
         if(!isset($_REQUEST['q'])) {
             Http::response(400, 'Query argument is required');
-        }
+        } 
+        
+        if (!$_REQUEST['q'])
+            return $this->json_encode(array());
 
+        $q = $_REQUEST['q'];
         $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
-        $orgs=array();
-
-        $escaped = db_input(strtolower($_REQUEST['q']), false);
-        $sql='SELECT DISTINCT org.id, org.name '
-            .' FROM '.ORGANIZATION_TABLE.' org '
-            .' LEFT JOIN '.FORM_ENTRY_TABLE.' entry ON (entry.object_type=\'O\' AND entry.object_id = org.id)
-               LEFT JOIN '.FORM_ANSWER_TABLE.' value ON (value.entry_id=entry.id) '
-            .' WHERE org.name LIKE \'%'.$escaped.'%\' OR value.value LIKE \'%'.$escaped.'%\''
-            .' ORDER BY org.created '
-            .' LIMIT '.$limit;
-
-        if(($res=db_query($sql)) && db_num_rows($res)){
-            while(list($id, $name)=db_fetch_row($res)) {
-                $orgs[] = array('name' => Format::htmlchars($name), 'info' => $name,
-                    'id' => $id, '/bin/true' => $_REQUEST['q']);
-            }
+
+        if (strlen($q) < 3)
+            return $this->encode(array());
+
+        $orgs = Organization::objects()
+            ->values_flat('id', 'name')
+            ->limit($limit);
+
+        global $ost;
+        $orgs = $ost->searcher->find($q, $orgs);
+        $orgs->order_by(new SqlCode('__relevance__'), QuerySet::DESC)
+            ->distinct('id');
+
+        if (!count($orgs) && preg_match('`\w$`u', $q)) {
+            // Do wildcard full-text search
+            $_REQUEST['q'] = $q."*";
+            return $this->search($type);
+        }
+
+        $matched = array();
+        foreach ($orgs as $O) {
+            list($id, $name) = $O;
+            $matched[] = array('name' => Format::htmlchars($name), 'info' => $name,
+                'id' => $id, '/bin/true' => $_REQUEST['q']);
         }
 
-        return $this->json_encode(array_values($orgs));
+        return $this->json_encode(array_values($matched));
 
     }
 
@@ -53,6 +66,8 @@ class OrgsAjaxAPI extends AjaxController {
 
         if(!$thisstaff)
             Http::response(403, 'Login Required');
+        elseif (!$thisstaff->hasPerm(Organization::PERM_EDIT))
+            Http::response(403, 'Permission Denied');
         elseif(!($org = Organization::lookup($id)))
             Http::response(404, 'Unknown organization');
 
@@ -71,6 +86,8 @@ class OrgsAjaxAPI extends AjaxController {
 
         if(!$thisstaff)
             Http::response(403, 'Login Required');
+        elseif (!$thisstaff->hasPerm(Organization::PERM_EDIT))
+            Http::response(403, 'Permission Denied');
         elseif(!($org = Organization::lookup($id)))
             Http::response(404, 'Unknown organization');
 
@@ -96,6 +113,8 @@ class OrgsAjaxAPI extends AjaxController {
 
         if (!$thisstaff)
             Http::response(403, 'Login Required');
+        elseif (!$thisstaff->hasPerm(Organization::PERM_DELETE))
+            Http::response(403, 'Permission Denied');
         elseif (!($org = Organization::lookup($id)))
             Http::response(404, 'Unknown organization');
 
@@ -115,6 +134,8 @@ class OrgsAjaxAPI extends AjaxController {
 
         if (!$thisstaff)
             Http::response(403, 'Login Required');
+        elseif (!$thisstaff->hasPerm(User::PERM_EDIT))
+            Http::response(403, 'Permission Denied');
         elseif (!($org = Organization::lookup($id)))
             Http::response(404, 'Unknown organization');
 
@@ -135,7 +156,8 @@ class OrgsAjaxAPI extends AjaxController {
                             Format::htmlchars($user->getName()));
             } else { //Creating new  user
                 $form = UserForm::getUserForm()->getForm($_POST);
-                if (!($user = User::fromForm($form)))
+                $can_create = $thisstaff->hasPerm(User::PERM_CREATE);
+                if (!($user = User::fromForm($form, $can_create)))
                     $info['error'] = __('Error adding user - try again!');
             }
 
@@ -173,6 +195,8 @@ class OrgsAjaxAPI extends AjaxController {
 
         if (!$thisstaff)
             Http::response(403, 'Login Required');
+        elseif (!$thisstaff->hasPerm(Organization::PERM_CREATE))
+            Http::response(403, 'Permission Denied');
         elseif (!($org = Organization::lookup($org_id)))
             Http::response(404, 'No such organization');
 
@@ -196,6 +220,10 @@ class OrgsAjaxAPI extends AjaxController {
     }
 
     function addOrg() {
+        global $thisstaff;
+
+        if (!$thisstaff->hasPerm(Organization::PERM_CREATE))
+            Http::response(403, 'Permission Denied');
 
         $info = array();
 
@@ -258,7 +286,7 @@ class OrgsAjaxAPI extends AjaxController {
     }
 
     function manageForms($org_id) {
-        $forms = DynamicFormEntry::forOrganization($org_id);
+        $forms = DynamicFormEntry::forObject($org_id, 'O');
         $info = array('action' => '#orgs/'.Format::htmlchars($org_id).'/forms/manage');
         include(STAFFINC_DIR . 'templates/form-manage.tmpl.php');
     }
@@ -268,13 +296,15 @@ class OrgsAjaxAPI extends AjaxController {
 
         if (!$thisstaff)
             Http::response(403, "Login required");
+        elseif (!$thisstaff->hasPerm(Organization::PERM_EDIT))
+            Http::response(403, 'Permission Denied');
         elseif (!($org = Organization::lookup($org_id)))
             Http::response(404, "No such ticket");
         elseif (!isset($_POST['forms']))
             Http::response(422, "Send updated forms list");
 
         // Add new forms
-        $forms = DynamicFormEntry::forOrganization($org_id);
+        $forms = DynamicFormEntry::forObject($org_id, 'O');
         foreach ($_POST['forms'] as $sort => $id) {
             $found = false;
             foreach ($forms as $e) {
diff --git a/include/ajax.reports.php b/include/ajax.reports.php
deleted file mode 100644
index e9e660a8c69b36f0c876ca63724b8735789aae94..0000000000000000000000000000000000000000
--- a/include/ajax.reports.php
+++ /dev/null
@@ -1,239 +0,0 @@
-<?php
-/*********************************************************************
-    ajax.reports.php
-
-    AJAX interface for reports -- both plot and tabular data are retrievable
-    in JSON format from this utility. Please put plumbing in /scp/ajax.php
-    pattern rules.
-
-    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:
-**********************************************************************/
-
-if(!defined('INCLUDE_DIR')) die('403');
-
-include_once(INCLUDE_DIR.'class.ticket.php');
-
-/**
- * Overview Report
- *
- * The overview report allows for the display of basic ticket statistics in
- * both graphical and tabular formats.
- */
-class OverviewReportAjaxAPI extends AjaxController {
-    function enumTabularGroups() {
-        return $this->encode(array("dept"=>__("Department"), "topic"=>__("Topics"),
-            # XXX: This will be relative to permissions based on the
-            # logged-in-staff. For basic staff, this will be 'My Stats'
-            "staff"=>__("Agent")));
-    }
-
-    function getData() {
-        global $thisstaff;
-
-        list($start, $stop) = $this->_getDateRange();
-
-        $groups = array(
-            "dept" => array(
-                "table" => DEPT_TABLE,
-                "pk" => "dept_id",
-                "sort" => 'T1.dept_name',
-                "fields" => 'T1.dept_name',
-                "headers" => array(__('Department')),
-                "filter" => ('T1.dept_id IN ('.implode(',', db_input($thisstaff->getDepts())).')')
-            ),
-            "topic" => array(
-                "table" => TOPIC_TABLE,
-                "pk" => "topic_id",
-                "sort" => 'name',
-                "fields" => "CONCAT_WS(' / ',"
-                    ."(SELECT P.topic FROM ".TOPIC_TABLE." P WHERE P.topic_id = T1.topic_pid),"
-                    ."T1.topic) as name ",
-                "headers" => array(__('Help Topic')),
-                "filter" => '1'
-            ),
-            "staff" => array(
-                "table" => STAFF_TABLE,
-                "pk" => 'staff_id',
-                "sort" => 'name',
-                "fields" => "CONCAT_WS(' ', T1.firstname, T1.lastname) as name",
-                "headers" => array(__('Agent')),
-                "filter" =>
-                    ('T1.staff_id=S1.staff_id
-                      AND
-                      (T1.staff_id='.db_input($thisstaff->getId())
-                        .(($depts=$thisstaff->getManagedDepartments())?
-                            (' OR T1.dept_id IN('.implode(',', db_input($depts)).')'):'')
-                        .(($thisstaff->canViewStaffStats())?
-                            (' OR T1.dept_id IN('.implode(',', db_input($thisstaff->getDepts())).')'):'')
-                     .')'
-                     )
-            )
-        );
-        $group = $this->get('group', 'dept');
-        $info = isset($groups[$group])?$groups[$group]:$groups['dept'];
-
-        # XXX: Die if $group not in $groups
-
-        $queries=array(
-            array(5, 'SELECT '.$info['fields'].',
-                COUNT(*)-COUNT(NULLIF(A1.state, "created")) AS Opened,
-                COUNT(*)-COUNT(NULLIF(A1.state, "assigned")) AS Assigned,
-                COUNT(*)-COUNT(NULLIF(A1.state, "overdue")) AS Overdue,
-                COUNT(*)-COUNT(NULLIF(A1.state, "closed")) AS Closed,
-                COUNT(*)-COUNT(NULLIF(A1.state, "reopened")) AS Reopened
-            FROM '.$info['table'].' T1
-                LEFT JOIN '.TICKET_EVENT_TABLE.' A1
-                    ON (A1.'.$info['pk'].'=T1.'.$info['pk'].'
-                         AND NOT annulled
-                         AND (A1.timestamp BETWEEN '.$start.' AND '.$stop.'))
-                LEFT JOIN '.STAFF_TABLE.' S1 ON (S1.staff_id=A1.staff_id)
-            WHERE '.$info['filter'].'
-            GROUP BY T1.'.$info['pk'].'
-            ORDER BY '.$info['sort']),
-
-            array(1, 'SELECT '.$info['fields'].',
-                FORMAT(AVG(DATEDIFF(T2.closed, T2.created)),1) AS ServiceTime
-            FROM '.$info['table'].' T1
-                LEFT JOIN '.TICKET_TABLE.' T2 ON (T2.'.$info['pk'].'=T1.'.$info['pk'].')
-                LEFT JOIN '.STAFF_TABLE.' S1 ON (S1.staff_id=T2.staff_id)
-            WHERE '.$info['filter'].' AND T2.closed BETWEEN '.$start.' AND '.$stop.'
-            GROUP BY T1.'.$info['pk'].'
-            ORDER BY '.$info['sort']),
-
-            array(1, 'SELECT '.$info['fields'].',
-                FORMAT(AVG(DATEDIFF(B2.created, B1.created)),1) AS ResponseTime
-            FROM '.$info['table'].' T1
-                LEFT JOIN '.TICKET_TABLE.' T2 ON (T2.'.$info['pk'].'=T1.'.$info['pk'].')
-                LEFT JOIN '.TICKET_THREAD_TABLE.' B2 ON (B2.ticket_id = T2.ticket_id
-                    AND B2.thread_type="R")
-                LEFT JOIN '.TICKET_THREAD_TABLE.' B1 ON (B2.pid = B1.id)
-                LEFT JOIN '.STAFF_TABLE.' S1 ON (S1.staff_id=B2.staff_id)
-            WHERE '.$info['filter'].' AND B1.created BETWEEN '.$start.' AND '.$stop.'
-            GROUP BY T1.'.$info['pk'].'
-            ORDER BY '.$info['sort'])
-        );
-        $rows = array();
-        $cols = 1;
-        foreach ($queries as $q) {
-            list($c, $sql) = $q;
-            $res = db_query($sql);
-            $cols += $c;
-            while ($row = db_fetch_row($res)) {
-                $found = false;
-                foreach ($rows as &$r) {
-                    if ($r[0] == $row[0]) {
-                        $r = array_merge($r, array_slice($row, -$c));
-                        $found = true;
-                        break;
-                    }
-                }
-                if (!$found)
-                    $rows[] = array_merge(array($row[0]), array_slice($row, -$c));
-            }
-            # Make sure each row has the same number of items
-            foreach ($rows as &$r)
-                while (count($r) < $cols)
-                    $r[] = null;
-        }
-        return array("columns" => array_merge($info['headers'],
-                        array(__('Opened'),__('Assigned'),__('Overdue'),__('Closed'),__('Reopened'),
-                              __('Service Time'),__('Response Time'))),
-                     "data" => $rows);
-    }
-
-    function getTabularData() {
-        return $this->encode($this->getData());
-    }
-
-    function downloadTabularData() {
-        $data = $this->getData();
-        $csv = '"' . implode('","',$data['columns']) . '"';
-        foreach ($data['data'] as $row)
-            $csv .= "\n" . '"' . implode('","', $row) . '"';
-        Http::download(
-            sprintf('%s-report.csv', $this->get('group', __('Department'))),
-            'text/csv', $csv);
-    }
-
-    function _getDateRange() {
-        global $cfg;
-
-        if(($start = $this->get('start', 'last month'))) {
-            $stop = $this->get('period', 'now');
-        } else {
-            $start = 'last month';
-            $stop = $this->get('period', 'now');
-        }
-
-        if ($start != 'last month')
-            $start = DateTime::createFromFormat($cfg->getDateFormat(),
-                $start)->format('U');
-        else
-            $start = strtotime($start);
-
-        if (substr($stop, 0, 1) == '+')
-            $stop = strftime('%Y-%m-%d ', $start) . $stop;
-
-        $start = 'FROM_UNIXTIME('.$start.')';
-        $stop = 'FROM_UNIXTIME('.strtotime($stop).')';
-
-        return array($start, $stop);
-    }
-
-    function getPlotData() {
-        list($start, $stop) = $this->_getDateRange();
-
-        # Fetch all types of events over the timeframe
-        $res = db_query('SELECT DISTINCT(state) FROM '.TICKET_EVENT_TABLE
-            .' WHERE timestamp BETWEEN '.$start.' AND '.$stop
-                .' ORDER BY 1');
-        $events = array();
-        while ($row = db_fetch_row($res)) $events[] = $row[0];
-
-        # TODO: Handle user => db timezone offset
-        # XXX: Implement annulled column from the %ticket_event table
-        $res = db_query('SELECT state, DATE_FORMAT(timestamp, \'%Y-%m-%d\'), '
-                .'COUNT(ticket_id)'
-            .' FROM '.TICKET_EVENT_TABLE
-            .' WHERE timestamp BETWEEN '.$start.' AND '.$stop
-            .' AND NOT annulled'
-            .' GROUP BY state, DATE_FORMAT(timestamp, \'%Y-%m-%d\')'
-            .' ORDER BY 2, 1');
-        # Initialize array of plot values
-        $plots = array();
-        foreach ($events as $e) { $plots[$e] = array(); }
-
-        $time = null; $times = array();
-        # Iterate over result set, adding zeros for missing ticket events
-        $slots = array();
-        while ($row = db_fetch_row($res)) {
-            $row_time = strtotime($row[1]);
-            if ($time != $row_time) {
-                # New time (and not the first), figure out which events did
-                # not have any tickets associated for this time slot
-                if ($time !== null) {
-                    # Not the first record -- add zeros all the arrays that
-                    # did not have at least one entry for the timeframe
-                    foreach (array_diff($events, $slots) as $slot)
-                        $plots[$slot][] = 0;
-                }
-                $slots = array();
-                $times[] = $time = $row_time;
-            }
-            # Keep track of states for this timeframe
-            $slots[] = $row[0];
-            $plots[$row[0]][] = (int)$row[2];
-        }
-        foreach (array_diff($events, $slots) as $slot)
-            $plots[$slot][] = 0;
-        return $this->encode(array("times" => $times, "plots" => $plots,
-            "events"=>$events));
-    }
-}
diff --git a/include/ajax.search.php b/include/ajax.search.php
new file mode 100644
index 0000000000000000000000000000000000000000..3342ac46d2e69d7a3dd5023e873fc92989693b73
--- /dev/null
+++ b/include/ajax.search.php
@@ -0,0 +1,234 @@
+<?php
+/*********************************************************************
+    ajax.search.php
+
+    AJAX interface for searches, queue management, etc.
+
+    Jared Hancock <jared@osticket.com>
+    Peter Rotich <peter@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:
+**********************************************************************/
+
+if(!defined('INCLUDE_DIR')) die('403');
+
+include_once(INCLUDE_DIR.'class.ticket.php');
+require_once(INCLUDE_DIR.'class.ajax.php');
+
+class SearchAjaxAPI extends AjaxController {
+
+    function getAdvancedSearchDialog() {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login required');
+
+        $search = SavedSearch::create();
+        $form = $search->getFormFromSession('advsearch') ?: $search->getForm();
+        $matches = self::_getSupportedTicketMatches();
+
+        include STAFFINC_DIR . 'templates/advanced-search.tmpl.php';
+    }
+
+    function addField($name) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login required');
+
+        @list($type, $id) = explode('!', $name, 2);
+
+        switch (strtolower($type)) {
+        case ':ticket':
+        case ':user':
+        case ':organization':
+        case ':field':
+            // Support nested field ids for list properties and such
+            if (strpos($id, '.') !== false)
+                list(,$id) = explode('!', $id, 2);
+            if (!($field = DynamicFormField::lookup($id)))
+                Http::response(404, 'No such field: ', print_r($id, true));
+
+            $impl = $field->getImpl();
+            $impl->set('label', sprintf('%s / %s',
+                $field->form->getLocal('title'), $field->getLocal('label')
+            ));
+            break;
+
+        default:
+            $extended = SavedSearch::getExtendedTicketFields();
+
+            if (isset($extended[$name])) {
+                $impl = $extended[$name];
+                break;
+            }
+            Http::response(400, 'No such field type');
+        }
+
+        $fields = SavedSearch::getSearchField($impl, $name);
+        $form = new SimpleForm($fields);
+        // Check the box to search the field by default
+        if ($F = $form->getField("{$name}+search"))
+            $F->value = true;
+
+        ob_start();
+        include STAFFINC_DIR . 'templates/advanced-search-field.tmpl.php';
+        $html = ob_get_clean();
+
+        return $this->encode(array(
+            'success' => true,
+            'html' => $html,
+            // Send the current formfield UID to be resent with the next
+            // addField request and set above
+            'ff_uid' => FormField::$uid,
+        ));
+    }
+
+    function doSearch() {
+        global $thisstaff;
+
+        $search = SavedSearch::create();
+
+        $form = $search->getForm($_POST);
+        if (!$form->isValid()) {
+            $matches = self::_getSupportedTicketMatches();
+            include STAFFINC_DIR . 'templates/advanced-search.tmpl.php';
+            return;
+        }
+        $_SESSION['advsearch'] = $form->getState();
+
+        Http::response(200, $this->encode(array(
+            'redirect' => 'tickets.php?advanced',
+        )));
+    }
+
+    function saveSearch($id) {
+        global $thisstaff;
+
+        $search = SavedSearch::lookup($id);
+        if (!$search || !$search->checkAccess($thisstaff))
+            Http::response(404, 'No such saved search');
+        elseif (!$thisstaff)
+            Http::response(403, 'Agent login is required');
+
+        return self::_saveSearch($search);
+    }
+
+    function _saveSearch($search) {
+        $data = array();
+        foreach ($_POST['form'] as $id=>$info) {
+            $name = $info['name'];
+            if (substr($name, -2) == '[]')
+                $data[substr($name, 0, -2)][] = $info['value'];
+            else
+                $data[$name] = $info['value'];
+        }
+        $form = $search->getForm($data);
+        $form->setSource($data);
+        if (!$data || !$form->isValid()) {
+            Http::response(422, 'Validation errors exist on form');
+        }
+
+        $search->config = JsonDataEncoder::encode($form->getState());
+        if (isset($_POST['name']))
+            $search->title = $_POST['name'];
+        elseif ($search->__new__)
+            Http::response(400, 'A name is required');
+        if (!$search->save()) {
+            Http::response(500, 'Internal error. Unable to update search');
+        }
+        Http::response(201, $this->encode(array(
+            'id' => $search->id,
+            'title' => $search->title,
+        )));
+    }
+
+    function _getSupportedTicketMatches() {
+        // User information
+        $matches = array(
+            __('Ticket Built-In') => SavedSearch::getExtendedTicketFields(),
+            __('Custom Forms') => array()
+        );
+        foreach (array('ticket'=>'TicketForm', 'user'=>'UserForm', 'organization'=>'OrganizationForm') as $k=>$F) {
+            $form = $F::objects()->one();
+            $fields = &$matches[$form->getLocal('title')];
+            foreach ($form->getFields() as $f) {
+                if (!$f->hasData() || $f->isPresentationOnly())
+                    continue;
+                $fields[":$k!".$f->get('id')] = __(ucfirst($k)).' / '.$f->getLocal('label');
+                /* TODO: Support matches on list item properties
+                if (($fi = $f->getImpl()) && $fi->hasSubFields()) {
+                    foreach ($fi->getSubFields() as $p) {
+                        $fields[":$k.".$f->get('id').'.'.$p->get('id')]
+                            = __(ucfirst($k)).' / '.$f->getLocal('label').' / '.$p->getLocal('label');
+                    }
+                }
+                */
+            }
+        }
+        $fields = &$matches[__('Custom Forms')];
+        foreach (DynamicForm::objects()->filter(array('type'=>'G')) as $form) {
+            foreach ($form->getFields() as $f) {
+                if (!$f->hasData() || $f->isPresentationOnly())
+                    continue;
+                $key = sprintf(':field!%d', $f->get('id'), $f->get('id'));
+                $fields[$key] = $form->getLocal('title').' / '.$f->getLocal('label');
+            }
+        }
+        return $matches;
+    }
+
+    function createSearch() {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login is required');
+
+        $search = SavedSearch::create();
+        $search->staff_id = $thisstaff->getId();
+        return self::_saveSearch($search);
+    }
+
+    function loadSearch($id) {
+        global $thisstaff;
+
+        if (!$thisstaff) {
+            Http::response(403, 'Agent login is required');
+        }
+        elseif (!($search = SavedSearch::lookup($id))) {
+            Http::response(404, 'No such saved search');
+        }
+
+        if ($state = JsonDataParser::parse($search->config)) {
+            $form = $search->loadFromState($state);
+            $form->loadState($state);
+        }
+        $matches = self::_getSupportedTicketMatches();
+
+        include STAFFINC_DIR . 'templates/advanced-search.tmpl.php';
+    }
+
+    function deleteSearch($id) {
+        global $thisstaff;
+
+        if (!$thisstaff) {
+            Http::response(403, 'Agent login is required');
+        }
+        elseif (!($search = SavedSearch::lookup($id))) {
+            Http::response(404, 'No such saved search');
+        }
+        elseif (!$search->delete()) {
+            Http::response(500, 'Unable to delete search');
+        }
+
+        Http::response(200, $this->encode(array(
+            'id' => $search->id,
+            'success' => true,
+        )));
+    }
+}
diff --git a/include/ajax.staff.php b/include/ajax.staff.php
new file mode 100644
index 0000000000000000000000000000000000000000..5356e9ccf6c52efb3f550da04c6cb3fb3ced1d75
--- /dev/null
+++ b/include/ajax.staff.php
@@ -0,0 +1,232 @@
+<?php
+
+require_once(INCLUDE_DIR . 'class.staff.php');
+
+class StaffAjaxAPI extends AjaxController {
+
+  /**
+   * Ajax: GET /staff/<id>/set-password
+   *
+   * Uses a dialog to add a new department
+   *
+   * Returns:
+   * 200 - HTML form for addition
+   * 201 - {id: <id>, name: <name>}
+   *
+   * Throws:
+   * 403 - Not logged in
+   * 403 - Not an administrator
+   * 404 - No such agent exists
+   */
+  function setPassword($id) {
+      global $ost, $thisstaff;
+
+      if (!$thisstaff)
+          Http::response(403, 'Agent login required');
+      if (!$thisstaff->isAdmin())
+          Http::response(403, 'Access denied');
+      if ($id && !($staff = Staff::lookup($id)))
+          Http::response(404, 'No such agent');
+
+      $form = new PasswordResetForm($_POST);
+      $errors = array();
+      if (!$_POST && isset($_SESSION['new-agent-passwd']))
+          $form->data($_SESSION['new-agent-passwd']);
+
+      if ($_POST && $form->isValid()) {
+          $clean = $form->getClean();
+          if ($id == 0) {
+              // Stash in the session later when creating the user
+              $_SESSION['new-agent-passwd'] = $clean;
+              Http::response(201, 'Carry on');
+          }
+          try {
+              if ($clean['welcome_email']) {
+                  $staff->sendResetEmail();
+              }
+              else {
+                  $staff->setPassword($clean['passwd1'], null);
+                  if ($clean['change_passwd'])
+                      $staff->change_passwd = 1;
+              }
+              if ($staff->save())
+                  Http::response(201, 'Successfully updated');
+          }
+          catch (BadPassword $ex) {
+              $passwd1 = $form->getField('passwd1');
+              $passwd1->addError($ex->getMessage());
+          }
+          catch (PasswordUpdateFailed $ex) {
+              $errors['err'] = __('Password update failed:').' '.$ex->getMessage();
+          }
+      }
+
+      $title = __("Set Agent Password");
+      $verb = $id == 0 ? __('Set') : __('Update');
+      $path = ltrim($ost->get_path_info(), '/');
+
+      include STAFFINC_DIR . 'templates/quick-add.tmpl.php';
+  }
+
+    function changePassword($id) {
+        global $cfg, $ost, $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login required');
+        if (!$id || $thisstaff->getId() != $id)
+            Http::response(404, 'No such agent');
+
+        $form = new PasswordChangeForm($_POST);
+        $errors = array();
+
+        if ($_POST && $form->isValid()) {
+            $clean = $form->getClean();
+            if (($rtoken = $_SESSION['_staff']['reset-token'])) {
+                $_config = new Config('pwreset');
+                if ($_config->get($rtoken) != $thisstaff->getId())
+                    $errors['err'] =
+                        __('Invalid reset token. Logout and try again');
+                elseif (!($ts = $_config->lastModified($rtoken))
+                        && ($cfg->getPwResetWindow() < (time() - strtotime($ts))))
+                    $errors['err'] =
+                        __('Invalid reset token. Logout and try again');
+            }
+            if (!$errors) {
+                try {
+                    $thisstaff->setPassword($clean['passwd1'], @$clean['current']);
+                    if ($thisstaff->save()) {
+                        if ($rtoken) {
+                            $thisstaff->cancelResetTokens();
+                            Http::response(200, $this->encode(array(
+                                'redirect' => 'index.php'
+                            )));
+                        }
+                        Http::response(201, 'Successfully updated');
+                    }
+                }
+                catch (BadPassword $ex) {
+                    $passwd1 = $form->getField('passwd1');
+                    $passwd1->addError($ex->getMessage());
+                }
+                catch (PasswordUpdateFailed $ex) {
+                    $errors['err'] = __('Password update failed:').' '.$ex->getMessage();
+                }
+            }
+        }
+
+        $title = __("Change Password");
+        $verb = __('Update');
+        $path = ltrim($ost->get_path_info(), '/');
+
+        include STAFFINC_DIR . 'templates/quick-add.tmpl.php';
+    }
+
+    function getAgentPerms($id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login required');
+        if (!$thisstaff->isAdmin())
+            Http::response(403, 'Access denied');
+        if (!($staff = Staff::lookup($id)))
+            Http::response(404, 'No such agent');
+
+        return $this->encode($staff->getPermissionInfo());
+    }
+
+    function resetPermissions() {
+        global $ost, $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login required');
+        if (!$thisstaff->isAdmin())
+            Http::response(403, 'Access denied');
+
+        $form = new ResetAgentPermissionsForm($_POST);
+
+        if (@is_array($_GET['ids'])) {
+            $perms = new RolePermission();
+            $selected = Staff::objects()->filter(array('staff_id__in' => $_GET['ids']));
+            foreach ($selected as $staff)
+                // XXX: This maybe should be intersection rather than union
+                $perms->merge($staff->getPermission());
+            $form->getField('perms')->setValue($perms->getInfo());
+        }
+
+        if ($_POST && $form->isValid()) {
+            $clean = $form->getClean();
+            Http::response(201, $this->encode(array('perms' => $clean['perms'])));
+        }
+
+        $title = __("Reset Agent Permissions");
+        $verb = __("Continue");
+        $path = ltrim($ost->get_path_info(), '/');
+
+        include STAFFINC_DIR . 'templates/reset-agent-permissions.tmpl.php';
+    }
+
+    function changeDepartment() {
+        global $ost, $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login required');
+        if (!$thisstaff->isAdmin())
+            Http::response(403, 'Access denied');
+
+        $form = new ChangeDepartmentForm($_POST);
+
+        // Preselect reasonable dept and role based on the current  settings
+        // of the received staff ids
+        if (@is_array($_GET['ids'])) {
+            $dept_id = null;
+            $role_id = null;
+            $selected = Staff::objects()->filter(array('staff_id__in' => $_GET['ids']));
+            foreach ($selected as $staff) {
+                if (!isset($dept_id)) {
+                    $dept_id = $staff->dept_id;
+                    $role_id = $staff->role_id;
+                }
+                elseif ($dept_id != $staff->dept_id)
+                    $dept_id = 0;
+                elseif ($role_id != $staff->role_id)
+                    $role_id = 0;
+            }
+            $form->getField('dept_id')->setValue($dept_id);
+            $form->getField('role_id')->setValue($role_id);
+        }
+
+        if ($_POST && $form->isValid()) {
+            $clean = $form->getClean();
+            Http::response(201, $this->encode($clean));
+        }
+
+        $title = __("Change Primary Department");
+        $verb = __("Continue");
+        $path = ltrim($ost->get_path_info(), '/');
+
+        include STAFFINC_DIR . 'templates/quick-add.tmpl.php';
+    }
+
+    function setAvatar($id) {
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, 'Agent login required');
+        if ($id != $thisstaff->getId() && !$thisstaff->isAdmin())
+            Http::response(403, 'Access denied');
+        if ($id == $thisstaff->getId())
+            $staff = $thisstaff;
+        else
+            $staff = Staff::lookup((int) $id);
+
+        if (!($avatar = $staff->getAvatar()))
+            Http::response(404, 'User does not have an avatar');
+
+        if ($code = $avatar->toggle())
+          return $this->encode(array(
+            'img' => (string) $avatar,
+            // XXX: This is very inflexible
+            'code' => $code,
+          ));
+    }
+}
diff --git a/include/ajax.tasks.php b/include/ajax.tasks.php
new file mode 100644
index 0000000000000000000000000000000000000000..6fa9e052633b41fae05d27ac3d9caa0b40b4f932
--- /dev/null
+++ b/include/ajax.tasks.php
@@ -0,0 +1,768 @@
+<?php
+/*********************************************************************
+    ajax.tasks.php
+
+    AJAX interface for tasks
+
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  20014 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:
+**********************************************************************/
+
+if(!defined('INCLUDE_DIR')) die('403');
+
+include_once(INCLUDE_DIR.'class.ticket.php');
+require_once(INCLUDE_DIR.'class.ajax.php');
+require_once(INCLUDE_DIR.'class.task.php');
+
+class TasksAjaxAPI extends AjaxController {
+
+    function lookup() {
+        global $thisstaff;
+
+        $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
+        $tasks = array();
+
+        $visibility = Q::any(array(
+            'staff_id' => $thisstaff->getId(),
+            'team_id__in' => $thisstaff->teams->values_flat('team_id'),
+        ));
+
+        if (!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts())) {
+            $visibility->add(array('dept_id__in' => $depts));
+        }
+
+
+        $hits = TaskModel::objects()
+            ->filter(Q::any(array(
+                'number__startswith' => $_REQUEST['q'],
+            )))
+            ->filter($visibility)
+            ->values('number')
+            ->annotate(array('tasks' => SqlAggregate::COUNT('id')))
+            ->order_by('-created')
+            ->limit($limit);
+
+        foreach ($hits as $T) {
+            $tasks[] = array('id'=>$T['number'], 'value'=>$T['number'],
+                'info'=>"{$T['number']}",
+                'matches'=>$_REQUEST['q']);
+        }
+
+        return $this->json_encode($tasks);
+    }
+
+    function add() {
+        global $thisstaff;
+
+        $info=$errors=array();
+        if ($_POST) {
+            Draft::deleteForNamespace('task.add', $thisstaff->getId());
+            // Default form
+            $form = TaskForm::getInstance();
+            $form->setSource($_POST);
+            // Internal form
+            $iform = TaskForm::getInternalForm($_POST);
+            $isvalid = true;
+            if (!$iform->isValid())
+                $isvalid = false;
+            if (!$form->isValid())
+                $isvalid = false;
+
+            if ($isvalid) {
+                $vars = $_POST;
+                $vars['default_formdata'] = $form->getClean();
+                $vars['internal_formdata'] = $iform->getClean();
+                $desc = $form->getField('description');
+                if ($desc
+                        && $desc->isAttachmentsEnabled()
+                        && ($attachments=$desc->getWidget()->getAttachments()))
+                    $vars['cannedattachments'] = $attachments->getClean();
+                $vars['staffId'] = $thisstaff->getId();
+                $vars['poster'] = $thisstaff;
+                $vars['ip_address'] = $_SERVER['REMOTE_ADDR'];
+                if (($task=Task::create($vars, $errors)))
+                    Http::response(201, $task->getId());
+            }
+
+            $info['error'] = __('Error adding task - try again!');
+        }
+
+        include STAFFINC_DIR . 'templates/task.tmpl.php';
+    }
+
+
+    function preview($tid) {
+        global $thisstaff;
+
+        // No perm. check -- preview allowed for staff
+        // XXX: perhaps force preview via parent object?
+        if(!$thisstaff || !($task=Task::lookup($tid)))
+            Http::response(404, __('No such task'));
+
+        include STAFFINC_DIR . 'templates/task-preview.tmpl.php';
+    }
+
+    function edit($tid) {
+        global $thisstaff;
+
+        if(!($task=Task::lookup($tid)))
+            Http::response(404, __('No such task'));
+
+        if (!$task->checkStaffPerm($thisstaff, Task::PERM_EDIT))
+            Http::response(403, __('Permission Denied'));
+
+        $info = $errors = array();
+        $forms = DynamicFormEntry::forObject($task->getId(),
+                ObjectModel::OBJECT_TYPE_TASK);
+
+        if ($_POST && $forms) {
+            // TODO: Validate internal form
+
+            // Validate dynamic meta-data
+            if ($task->update($forms, $_POST, $errors)) {
+                Http::response(201, 'Task updated successfully');
+            } elseif(!$errors['err']) {
+                $errors['err']=__('Unable to update the task. Correct the errors below and try again!');
+            }
+            $info = Format::htmlchars($_POST);
+        }
+
+        include STAFFINC_DIR . 'templates/task-edit.tmpl.php';
+    }
+
+    function massProcess($action, $w=null)  {
+        global $thisstaff, $cfg;
+
+        $actions = array(
+                'transfer' => array(
+                    'verbed' => __('transferred'),
+                    ),
+                'assign' => array(
+                    'verbed' => __('assigned'),
+                    ),
+                'claim' => array(
+                    'verbed' => __('claimed'),
+                    ),
+                'delete' => array(
+                    'verbed' => __('deleted'),
+                    ),
+                'reopen' => array(
+                    'verbed' => __('reopen'),
+                    ),
+                'close' => array(
+                    'verbed' => __('closed'),
+                    ),
+                );
+
+        if (!isset($actions[$action]))
+            Http::response(404, __('Unknown action'));
+
+
+        $info = $errors = $e = array();
+        $inc = null;
+        $i = $count = 0;
+        if ($_POST) {
+            if (!$_POST['tids'] || !($count=count($_POST['tids'])))
+                $errors['err'] = sprintf(
+                        __('You must select at least %s.'),
+                        __('one task'));
+        } else {
+            $count  =  $_REQUEST['count'];
+        }
+
+        switch ($action) {
+        case 'claim':
+            $w = 'me';
+        case 'assign':
+            $inc = 'assign.tmpl.php';
+            $info[':action'] = "#tasks/mass/assign/$w";
+            $info[':title'] = sprintf('Assign %s',
+                    _N('selected task', 'selected tasks', $count));
+
+            $form = AssignmentForm::instantiate($_POST);
+
+            $assignCB = function($t, $f, $e) {
+                return $t->assign($f, $e);
+            };
+
+            $assignees = null;
+            switch ($w) {
+            case 'agents':
+                $depts = array();
+                $tids = $_POST['tids'] ?: array_filter(
+                        explode(',', @$_REQUEST['tids'] ?: ''));
+                if ($tids) {
+                    $tasks = Task::objects()
+                        ->distinct('dept_id')
+                        ->filter(array('id__in' => $tids));
+                    $depts = $tasks->values_flat('dept_id');
+                }
+
+                $members = Staff::objects()
+                    ->distinct('staff_id')
+                    ->filter(array(
+                                'onvacation' => 0,
+                                'isactive' => 1,
+                                )
+                            );
+
+                if ($depts) {
+                    $members->filter(Q::any( array(
+                                    'dept_id__in' => $depts,
+                                    Q::all(array(
+                                        'dept_access__dept__id__in' => $depts,
+                                        Q::not(array('dept_access__dept__flags__hasbit'
+                                            => Dept::FLAG_ASSIGN_MEMBERS_ONLY))
+                                        ))
+                                    )));
+                }
+
+                switch ($cfg->getAgentNameFormat()) {
+                case 'last':
+                case 'lastfirst':
+                case 'legal':
+                    $members->order_by('lastname', 'firstname');
+                    break;
+
+                default:
+                    $members->order_by('firstname', 'lastname');
+                }
+
+                $prompt  = __('Select an Agent');
+                $assignees = array();
+                foreach ($members as $member)
+                     $assignees['s'.$member->getId()] = $member->getName();
+
+                if (!$assignees)
+                    $info['warn'] =  __('No agents available for assignment');
+                break;
+            case 'teams':
+                $assignees = array();
+                $prompt = __('Select a Team');
+                foreach (Team::getActiveTeams() as $id => $name)
+                    $assignees['t'.$id] = $name;
+
+                if (!$assignees)
+                    $info['warn'] =  __('No teams available for assignment');
+                break;
+            case 'me':
+                $info[':action'] = '#tasks/mass/claim';
+                $info[':title'] = sprintf('Claim %s',
+                        _N('selected task', 'selected tasks', $count));
+                $info['warn'] = sprintf(
+                        __('Are you sure you want to CLAIM %s?'),
+                        _N('selected task', 'selected tasks', $count));
+                $verb = sprintf('%s, %s', __('Yes'), __('Claim'));
+                $id = sprintf('s%s', $thisstaff->getId());
+                $assignees = array($id => $thisstaff->getName());
+                $vars = $_POST ?: array('assignee' => array($id));
+                $form = ClaimForm::instantiate($vars);
+                $assignCB = function($t, $f, $e) {
+                    return $t->claim($f, $e);
+                };
+                break;
+            }
+
+            if ($assignees != null)
+                $form->setAssignees($assignees);
+
+            if ($prompt && ($f=$form->getField('assignee')))
+                $f->configure('prompt', $prompt);
+
+            if ($_POST && $form->isValid() && !$errors) {
+                foreach ($_POST['tids'] as $tid) {
+                    if (($t=Task::lookup($tid))
+                            // Make sure the agent is allowed to
+                            // access and assign the task.
+                            && $t->checkStaffPerm($thisstaff, Task::PERM_ASSIGN)
+                            // Do the assignment
+                            && $assignCB($t, $form, $e)
+                            )
+                        $i++;
+                }
+
+                if (!$i) {
+                    $info['error'] = sprintf(
+                            __('Unable to %1$s %2$s'),
+                            __('assign'),
+                            _N('selected task', 'selected tasks', $count));
+                }
+            }
+            break;
+        case 'transfer':
+            $inc = 'transfer.tmpl.php';
+            $info[':action'] = '#tasks/mass/transfer';
+            $info[':title'] = sprintf('Transfer %s',
+                    _N('selected task', 'selected tasks', $count));
+            $form = TransferForm::instantiate($_POST);
+            if ($_POST && $form->isValid()) {
+                foreach ($_POST['tids'] as $tid) {
+                    if (($t=Task::lookup($tid))
+                            // Make sure the agent is allowed to
+                            // access and transfer the task.
+                            && $t->checkStaffPerm($thisstaff, Task::PERM_TRANSFER)
+                            // Do the transfer
+                            && $t->transfer($form, $e)
+                            )
+                        $i++;
+                }
+
+                if (!$i) {
+                    $info['error'] = sprintf(
+                            __('Unable to %1$s %2$s'),
+                            __('transfer'),
+                            _N('selected task', 'selected tasks', $count));
+                }
+            }
+            break;
+        case 'reopen':
+            $info['status'] = 'open';
+        case 'close':
+            $inc = 'task-status.tmpl.php';
+            $info[':action'] = "#tasks/mass/$action";
+            $info['status'] = $info['status'] ?: 'closed';
+            $perm = $action = '';
+            switch ($info['status']) {
+            case 'open':
+                // If an agent can create a task then they're allowed to
+                // reopen closed ones.
+                $perm = Task::PERM_CREATE;
+                $info[':title'] = sprintf('Reopen %s',
+                         _N('selected task', 'selected tasks', $count));
+
+                $info['warn'] = sprintf(__('Are you sure you want to %s?'),
+                        sprintf(__('REOPEN %s'),
+                             _N('selected task', 'selected tasks', $count)
+                             ));
+                break;
+            case 'closed':
+                $perm = Task::PERM_CLOSE;
+                $info[':title'] = sprintf('Close %s',
+                         _N('selected task', 'selected tasks', $count));
+
+                $info['warn'] = sprintf(__('Are you sure you want to %s?'),
+                        sprintf(__('CLOSE %s'),
+                             _N('selected task', 'selected tasks', $count)
+                             ));
+                break;
+            default:
+                Http::response(404, __('Unknown action'));
+            }
+            // Check generic permissions --  department specific permissions
+            // will be checked below.
+            if ($perm && !$thisstaff->hasPerm($perm, false))
+                $errors['err'] = sprintf(
+                        __('You do not have permission to %s %s'),
+                        __($action),
+                        __('tasks'));
+
+            if ($_POST && !$errors) {
+                if (!$_POST['status']
+                        || !in_array($_POST['status'], array('open', 'closed')))
+                    $errors['status'] = __('Status selection required');
+                else {
+                    foreach ($_POST['tids'] as $tid) {
+                        if (($t=Task::lookup($tid))
+                                && $t->checkStaffPerm($thisstaff, $perm ?: null)
+                                && $t->setStatus($_POST['status'], $_POST['comments'])
+                                )
+                            $i++;
+                    }
+
+                    if (!$i) {
+                        $info['error'] = sprintf(
+                                __('Unable to change status of %1$s'),
+                                _N('selected task', 'selected tasks', $count));
+                    }
+                }
+            }
+            break;
+        case 'delete':
+            $inc = 'delete.tmpl.php';
+            $info[':action'] = '#tasks/mass/delete';
+            $info[':title'] = sprintf('Delete %s',
+                    _N('selected task', 'selected tasks', $count));
+            $info[':placeholder'] = sprintf(__(
+                        'Optional reason for deleting %s'),
+                    _N('selected task', 'selected tasks', $count));
+            $info['warn'] = sprintf(__(
+                        'Are you sure you want to DELETE %s?'),
+                    _N('selected task', 'selected tasks', $count));
+            $info[':extra'] = sprintf('<strong>%s</strong>',
+                        __('Deleted tasks CANNOT be recovered, including any associated attachments.')
+                        );
+
+            if ($_POST && !$errors) {
+                foreach ($_POST['tids'] as $tid) {
+                    if (($t=Task::lookup($tid))
+                            && $t->getDeptId() != $_POST['dept_id']
+                            && $t->checkStaffPerm($thisstaff, Task::PERM_DELETE)
+                            && $t->delete($_POST, $e)
+                            )
+                        $i++;
+                }
+
+                if (!$i) {
+                    $info['error'] = sprintf(
+                            __('Unable to %1$s %2$s'),
+                            __('delete'),
+                            _N('selected task', 'selected tasks', $count));
+                }
+            }
+            break;
+        default:
+            Http::response(404, __('Unknown action'));
+        }
+
+
+        if ($_POST && $i) {
+
+            // Assume success
+            if ($i==$count) {
+                $msg = sprintf(__('Successfully %s %s.'),
+                        $actions[$action]['verbed'],
+                        sprintf(__('%1$d %2$s'),
+                            $count,
+                            _N('selected task', 'selected tasks', $count))
+                        );
+                $_SESSION['::sysmsgs']['msg'] = $msg;
+            } else {
+                $warn = sprintf(
+                        __('%1$d of %2$d %3$s %4$s'), $i, $count,
+                        _N('selected task', 'selected tasks',
+                            $count),
+                        $actions[$action]['verbed']);
+                $_SESSION['::sysmsgs']['warn'] = $warn;
+            }
+            Http::response(201, 'processed');
+        } elseif($_POST && !isset($info['error'])) {
+            $info['error'] = $errors['err'] ?: sprintf(
+                    __('Unable to %1$s  %2$s'),
+                    __('process'),
+                    _N('selected task', 'selected tasks', $count));
+        }
+
+        if ($_POST)
+            $info = array_merge($info, Format::htmlchars($_POST));
+
+
+        include STAFFINC_DIR . "templates/$inc";
+        //  Copy checked tasks to the form.
+        echo "
+        <script type=\"text/javascript\">
+        $(function() {
+            $('form#tasks input[name=\"tids[]\"]:checkbox:checked')
+            .each(function() {
+                $('<input>')
+                .prop('type', 'hidden')
+                .attr('name', 'tids[]')
+                .val($(this).val())
+                .appendTo('form.mass-action');
+            });
+        });
+        </script>";
+    }
+
+    function transfer($tid) {
+        global $thisstaff;
+
+        if(!($task=Task::lookup($tid)))
+            Http::response(404, __('No such task'));
+
+        if (!$task->checkStaffPerm($thisstaff, Task::PERM_TRANSFER))
+            Http::response(403, __('Permission Denied'));
+
+        $errors = array();
+
+        $info = array(
+                ':title' => sprintf(__('Task #%s: %s'),
+                    $task->getNumber(),
+                    __('Transfer')),
+                ':action' => sprintf('#tasks/%d/transfer',
+                    $task->getId())
+                );
+
+        $form = $task->getTransferForm($_POST);
+        if ($_POST && $form->isValid()) {
+            if ($task->transfer($form, $errors)) {
+                $_SESSION['::sysmsgs']['msg'] = sprintf(
+                        __('%s successfully'),
+                        sprintf(
+                            __('%s transferred to %s department'),
+                            __('Task'),
+                            $task->getDept()
+                            )
+                        );
+                Http::response(201, $task->getId());
+            }
+
+            $form->addErrors($errors);
+            $info['error'] = $errors['err'] ?: __('Unable to transfer task');
+        }
+
+        $info['dept_id'] = $info['dept_id'] ?: $task->getDeptId();
+
+        include STAFFINC_DIR . 'templates/transfer.tmpl.php';
+    }
+
+    function assign($tid, $target=null) {
+        global $thisstaff;
+
+        if (!($task=Task::lookup($tid)))
+            Http::response(404, __('No such task'));
+
+        if (!$task->checkStaffPerm($thisstaff, Task::PERM_ASSIGN)
+                || !($form=$task->getAssignmentForm($_POST, array(
+                            'target' => $target))))
+            Http::response(403, __('Permission Denied'));
+
+        $errors = array();
+        $info = array(
+                ':title' => sprintf(__('Task #%s: %s'),
+                    $task->getNumber(),
+                    $task->isAssigned() ? __('Reassign') :  __('Assign')),
+                ':action' => sprintf('#tasks/%d/assign%s',
+                    $task->getId(),
+                    $target ? "/$target" : ''),
+                );
+        if ($task->isAssigned()) {
+            $info['notice'] = sprintf(__('%s is currently assigned to %s'),
+                    __('Task'),
+                    $task->getAssigned());
+        }
+
+        if ($_POST && $form->isValid()) {
+            if ($task->assign($form, $errors)) {
+                $_SESSION['::sysmsgs']['msg'] = sprintf(
+                        __('%s successfully'),
+                        sprintf(
+                            __('%s assigned to %s'),
+                            __('Task'),
+                            $form->getAssignee())
+                        );
+                Http::response(201, $task->getId());
+            }
+
+            $form->addErrors($errors);
+            $info['error'] = $errors['err'] ?: __('Unable to assign task');
+        }
+
+        include STAFFINC_DIR . 'templates/assign.tmpl.php';
+    }
+
+    function claim($tid) {
+
+        global $thisstaff;
+
+        if (!($task=Task::lookup($tid)))
+            Http::response(404, __('No such task'));
+
+        // Check for premissions and such
+        if (!$task->checkStaffPerm($thisstaff, Task::PERM_ASSIGN)
+                || !($form = $task->getClaimForm($_POST)))
+            Http::response(403, __('Permission Denied'));
+
+        $errors = array();
+        $info = array(
+                ':title' => sprintf(__('Task #%s: %s'),
+                    $task->getNumber(),
+                    __('Claim')),
+                ':action' => sprintf('#tasks/%d/claim',
+                    $task->getId()),
+
+                );
+
+        if ($task->isAssigned()) {
+            if ($task->getStaffId() == $thisstaff->getId())
+                $assigned = __('you');
+            else
+                $assigneed = $task->getAssigned();
+
+            $info['error'] = sprintf(__('%s is currently assigned to <b>%s</b>'),
+                    __('This task'),
+                    $assigned);
+        } else {
+            $info['warn'] = sprintf(__('Are you sure you want to CLAIM %s?'),
+                    __('this task'));
+        }
+
+        if ($_POST && $form->isValid()) {
+            if ($task->claim($form, $errors)) {
+                $_SESSION['::sysmsgs']['msg'] = sprintf(
+                        __('%s successfully'),
+                        sprintf(
+                            __('%s assigned to %s'),
+                            __('Task'),
+                            __('you'))
+                        );
+                Http::response(201, $task->getId());
+            }
+
+            $form->addErrors($errors);
+            $info['error'] = $errors['err'] ?: __('Unable to claim task');
+        }
+
+        $verb = sprintf('%s, %s', __('Yes'), __('Claim'));
+
+        include STAFFINC_DIR . 'templates/assign.tmpl.php';
+
+    }
+
+   function delete($tid) {
+        global $thisstaff;
+
+        if(!($task=Task::lookup($tid)))
+            Http::response(404, __('No such task'));
+
+        if (!$task->checkStaffPerm($thisstaff, Task::PERM_DELETE))
+            Http::response(403, __('Permission Denied'));
+
+        $errors = array();
+        $info = array(
+                ':title' => sprintf(__('Task #%s: %s'),
+                    $task->getNumber(),
+                    __('Delete')),
+                ':action' => sprintf('#tasks/%d/delete',
+                    $task->getId()),
+                );
+
+        if ($_POST) {
+            if ($task->delete($_POST,  $errors)) {
+                $_SESSION['::sysmsgs']['msg'] = sprintf(
+                            __('%s #%s deleted successfully'),
+                            __('Task'),
+                            $task->getNumber(),
+                            $task->getDept());
+                Http::response(201, 0);
+            }
+            $info = array_merge($info, Format::htmlchars($_POST));
+            $info['error'] = $errors['err'] ?: __('Unable to delete task');
+        }
+        $info[':placeholder'] = sprintf(__(
+                    'Optional reason for deleting %s'),
+                __('this task'));
+        $info['warn'] = sprintf(__(
+                    'Are you sure you want to DELETE %s?'),
+                    __('this task'));
+        $info[':extra'] = sprintf('<strong>%s</strong>',
+                    __('Deleted tasks CANNOT be recovered, including any associated attachments.')
+                    );
+
+        include STAFFINC_DIR . 'templates/delete.tmpl.php';
+    }
+
+   function changeStatus($tid, $status) {
+        global $thisstaff;
+        $statuses = array(
+                'open' => __('Reopen'),
+                'closed' => __('Close'),
+                );
+
+        if(!($task=Task::lookup($tid)) || !$task->checkStaffPerm($thisstaff))
+            Http::response(404, __('No such task'));
+
+        $perm = null;
+        $info = $errors = array();
+        switch ($status) {
+        case 'open':
+            $perm = Task::PERM_CREATE;
+            $info = array(
+                    ':title' => sprintf(__('Reopen Task #%s'),
+                        $task->getNumber()),
+                    ':action' => sprintf('#tasks/%d/reopen',
+                        $task->getId())
+                    );
+            break;
+        case 'closed':
+            $perm = Task::PERM_CLOSE;
+            $info = array(
+                    ':title' => sprintf(__('Close Task #%s'),
+                        $task->getNumber()),
+                    ':action' => sprintf('#tasks/%d/close',
+                        $task->getId())
+                    );
+
+            if (($m=$task->isCloseable()) !== true)
+                $errors['err'] = $info['error'] = $m;
+            else
+                $info['warn'] = sprintf(__('Are you sure you want to %s?'),
+                        sprintf(__('change status of %s'), __('this task')));
+            break;
+        default:
+            Http::response(404, __('Unknown status'));
+        }
+
+        if (!$errors && (!$perm || !$task->checkStaffPerm($thisstaff, $perm)))
+            $errors['err'] = sprintf(
+                        __('You do not have permission to %s %s'),
+                        $statuses[$status], __('tasks'));
+
+        if ($_POST && !$errors) {
+            if ($task->setStatus($status, $_POST['comments'], $errors))
+                Http::response(201, 0);
+
+            $info['error'] = $errors['err'] ?: __('Unable to change status of the task');
+        }
+
+        $info['status'] = $status;
+
+        include STAFFINC_DIR . 'templates/task-status.tmpl.php';
+   }
+
+   function reopen($tid) {
+       return $this->changeStatus($tid, 'open');
+   }
+
+   function close($tid) {
+       return $this->changeStatus($tid, 'closed');
+   }
+
+    function task($tid) {
+        global $thisstaff;
+
+        if (!($task=Task::lookup($tid))
+                || !$task->checkStaffPerm($thisstaff))
+            Http::response(404, __('No such task'));
+
+        $info=$errors=array();
+        $note_form = new SimpleForm(array(
+            'attachments' => new FileUploadField(array('id'=>'attach',
+            'name'=>'attach:note',
+            'configuration' => array('extensions'=>'')))
+            ));
+
+        if ($_POST) {
+
+            switch ($_POST['a']) {
+            case 'postnote':
+                $vars = $_POST;
+                $attachments = $note_form->getField('attachments')->getClean();
+                $vars['cannedattachments'] = array_merge(
+                    $vars['cannedattachments'] ?: array(), $attachments);
+                if(($note=$task->postNote($vars, $errors, $thisstaff))) {
+                    $msg=__('Note posted successfully');
+                    // Clear attachment list
+                    $note_form->setSource(array());
+                    $note_form->getField('attachments')->reset();
+                    Draft::deleteForNamespace('task.note.'.$task->getId(),
+                            $thisstaff->getId());
+                } else {
+                    if(!$errors['err'])
+                        $errors['err'] = __('Unable to post the note - missing or invalid data.');
+                }
+                break;
+            default:
+                $errors['err'] = __('Unknown action');
+            }
+        }
+
+        include STAFFINC_DIR . 'templates/task-view.tmpl.php';
+    }
+}
+?>
diff --git a/include/ajax.thread.php b/include/ajax.thread.php
new file mode 100644
index 0000000000000000000000000000000000000000..470f9276e24a6d3c4c15a7d054422799db70c9b1
--- /dev/null
+++ b/include/ajax.thread.php
@@ -0,0 +1,292 @@
+<?php
+/*********************************************************************
+    ajax.thread.php
+
+    AJAX interface for thread
+
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  2015 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:
+**********************************************************************/
+
+if(!defined('INCLUDE_DIR')) die('403');
+
+include_once(INCLUDE_DIR.'class.ticket.php');
+require_once(INCLUDE_DIR.'class.ajax.php');
+require_once(INCLUDE_DIR.'class.note.php');
+include_once INCLUDE_DIR . 'class.thread_actions.php';
+
+class ThreadAjaxAPI extends AjaxController {
+
+    function lookup() {
+        global $thisstaff;
+
+        if(!is_numeric($_REQUEST['q']))
+            return self::lookupByEmail();
+
+
+        $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
+        $tickets=array();
+
+        $visibility = Q::any(array(
+            'staff_id' => $thisstaff->getId(),
+            'team_id__in' => $thisstaff->teams->values_flat('team_id'),
+        ));
+        if (!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts())) {
+            $visibility->add(array('dept_id__in' => $depts));
+        }
+
+
+        $hits = TicketModel::objects()
+            ->filter(Q::any(array(
+                'number__startswith' => $_REQUEST['q'],
+            )))
+            ->filter($visibility)
+            ->values('number', 'user__emails__address')
+            ->annotate(array('tickets' => SqlAggregate::COUNT('ticket_id')))
+            ->order_by('-created')
+            ->limit($limit);
+
+        foreach ($hits as $T) {
+            $tickets[] = array('id'=>$T['number'], 'value'=>$T['number'],
+                'info'=>"{$T['number']} — {$T['user__emails__address']}",
+                'matches'=>$_REQUEST['q']);
+        }
+        if (!$tickets)
+            return self::lookupByEmail();
+
+        return $this->json_encode($tickets);
+    }
+
+
+    function addRemoteCollaborator($tid, $bk, $id) {
+        global $thisstaff;
+
+        if (!($thread=Thread::lookup($tid))
+                || !($object=$thread->getObject())
+                || !$object->checkStaffPerm($thisstaff))
+            Http::response(404, 'No such thread');
+        elseif (!$bk || !$id)
+            Http::response(422, 'Backend and user id required');
+        elseif (!($backend = StaffAuthenticationBackend::getBackend($bk)))
+            Http::response(404, 'User not found');
+
+        $user_info = $backend->lookup($id);
+        $form = UserForm::getUserForm()->getForm($user_info);
+        $info = array();
+        if (!$user_info)
+            $info['error'] = __('Unable to find user in directory');
+
+        return self::_addcollaborator($thread, null, $form, $info);
+    }
+
+    //Collaborators utils
+    function addCollaborator($tid, $uid=0) {
+        global $thisstaff;
+
+        if (!($thread=Thread::lookup($tid))
+                || !($object=$thread->getObject())
+                || !$object->checkStaffPerm($thisstaff))
+            Http::response(404, __('No such thread'));
+
+
+        $user = $uid? User::lookup($uid) : null;
+
+        //If not a post then assume new collaborator form
+        if(!$_POST)
+            return self::_addcollaborator($thread, $user);
+
+        $user = $form = null;
+        if (isset($_POST['id']) && $_POST['id']) { //Existing user/
+            $user =  User::lookup($_POST['id']);
+        } else { //We're creating a new user!
+            $form = UserForm::getUserForm()->getForm($_POST);
+            $user = User::fromForm($form);
+        }
+
+        $errors = $info = array();
+        if ($user) {
+            // FIXME: Refuse to add ticket owner??
+            if (($c=$thread->addCollaborator($user,
+                            array('isactive'=>1), $errors))) {
+                $info = array('msg' => sprintf(__('%s added as a collaborator'),
+                            Format::htmlchars($c->getName())));
+                return self::_collaborators($thread, $info);
+            }
+        }
+
+        if($errors && $errors['err']) {
+            $info +=array('error' => $errors['err']);
+        } else {
+            $info +=array('error' =>__('Unable to add collaborator. Internal error'));
+        }
+
+        return self::_addcollaborator($thread, $user, $form, $info);
+    }
+
+    function updateCollaborator($tid, $cid) {
+        global $thisstaff;
+
+        if (!($thread=Thread::lookup($tid))
+                || !($object=$thread->getObject())
+                || !$object->checkStaffPerm($thisstaff))
+            Http::response(405, 'No such thread');
+
+
+        if (!($c=Collaborator::lookup(array(
+                            'id' => $cid,
+                            'thread_id' => $thread->getId())))
+                || !($user=$c->getUser()))
+            Http::response(406, 'Unknown collaborator');
+
+        $errors = array();
+        if(!$user->updateInfo($_POST, $errors))
+            return self::_collaborator($c ,$user->getForms($_POST), $errors);
+
+        $info = array('msg' => sprintf('%s updated successfully',
+                    Format::htmlchars($c->getName())));
+
+        return self::_collaborators($thread, $info);
+    }
+
+    function viewCollaborator($tid, $cid) {
+        global $thisstaff;
+
+        if (!($thread=Thread::lookup($tid))
+                || !($object=$thread->getObject())
+                || !$object->checkStaffPerm($thisstaff))
+            Http::response(404, 'No such thread');
+
+
+        if (!($collaborator=Collaborator::lookup(array(
+                            'id' => $cid,
+                            'thread_id' => $thread->getId()))))
+            Http::response(404, 'Unknown collaborator');
+
+        return self::_collaborator($collaborator);
+    }
+
+    function showCollaborators($tid) {
+        global $thisstaff;
+
+        if(!($thread=Thread::lookup($tid))
+                || !($object=$thread->getObject())
+                || !$object->checkStaffPerm($thisstaff))
+            Http::response(404, 'No such thread');
+
+        if ($thread->getCollaborators())
+            return self::_collaborators($thread);
+
+        return self::_addcollaborator($thread);
+    }
+
+    function previewCollaborators($tid) {
+        global $thisstaff;
+
+        if (!($thread=Thread::lookup($tid))
+                || !($object=$thread->getObject())
+                || !$object->checkStaffPerm($thisstaff))
+            Http::response(404, 'No such thread');
+
+        ob_start();
+        include STAFFINC_DIR . 'templates/collaborators-preview.tmpl.php';
+        $resp = ob_get_contents();
+        ob_end_clean();
+
+        return $resp;
+    }
+
+    function _addcollaborator($thread, $user=null, $form=null, $info=array()) {
+        global $thisstaff;
+
+        $info += array(
+                    'title' => __('Add a collaborator'),
+                    'action' => sprintf('#thread/%d/add-collaborator',
+                        $thread->getId()),
+                    'onselect' => sprintf('ajax.php/thread/%d/add-collaborator/',
+                        $thread->getId()),
+                    );
+
+        ob_start();
+        include STAFFINC_DIR . 'templates/user-lookup.tmpl.php';
+        $resp = ob_get_contents();
+        ob_end_clean();
+
+        return $resp;
+    }
+
+    function updateCollaborators($tid) {
+        global $thisstaff;
+
+        if (!($thread=Thread::lookup($tid))
+                || !($object=$thread->getObject())
+                || !$object->checkStaffPerm($thisstaff))
+            Http::response(404, 'No such thread');
+
+        $errors = $info = array();
+        if ($thread->updateCollaborators($_POST, $errors))
+            Http::response(201, sprintf('Recipients (%d of %d)',
+                        $thread->getNumActiveCollaborators(),
+                        $thread->getNumCollaborators()));
+
+        if($errors && $errors['err'])
+            $info +=array('error' => $errors['err']);
+
+        return self::_collaborators($thread, $info);
+    }
+
+
+
+    function _collaborator($collaborator, $form=null, $info=array()) {
+        global $thisstaff;
+
+        $info += array('action' => sprintf('#thread/%d/collaborators/%d',
+                    $collaborator->thread_id, $collaborator->getId()));
+
+        $user = $collaborator->getUser();
+
+        ob_start();
+        include(STAFFINC_DIR . 'templates/user.tmpl.php');
+        $resp = ob_get_contents();
+        ob_end_clean();
+
+        return $resp;
+    }
+
+    function _collaborators($thread, $info=array()) {
+
+        ob_start();
+        include(STAFFINC_DIR . 'templates/collaborators.tmpl.php');
+        $resp = ob_get_contents();
+        ob_end_clean();
+
+        return $resp;
+    }
+
+    function triggerThreadAction($ticket_id, $thread_id, $action) {
+        $thread = ThreadEntry::lookup($thread_id);
+        if (!$thread)
+            Http::response(404, 'No such ticket thread entry');
+        if ($thread->getThread()->getObjectId() != $ticket_id)
+            Http::response(404, 'No such ticket thread entry');
+
+        $valid = false;
+        foreach ($thread->getActions() as $group=>$list) {
+            foreach ($list as $name=>$A) {
+                if ($A->getId() == $action) {
+                    $valid = true; break;
+                }
+            }
+        }
+        if (!$valid)
+            Http::response(400, 'Not a valid action for this thread');
+
+        $thread->triggerAction($action);
+    }
+}
+?>
diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php
index 21e93290c87ae7fb7ae45d7704a52ea14ed516f4..c20abba7ec0120fbaceffca81f762c5e5eb0c650 100644
--- a/include/ajax.tickets.php
+++ b/include/ajax.tickets.php
@@ -19,280 +19,101 @@ if(!defined('INCLUDE_DIR')) die('403');
 include_once(INCLUDE_DIR.'class.ticket.php');
 require_once(INCLUDE_DIR.'class.ajax.php');
 require_once(INCLUDE_DIR.'class.note.php');
+include_once INCLUDE_DIR . 'class.thread_actions.php';
 
 class TicketsAjaxAPI extends AjaxController {
 
     function lookup() {
         global $thisstaff;
 
-        if(!is_numeric($_REQUEST['q']))
-            return self::lookupByEmail();
-
-
         $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
         $tickets=array();
+        // Bail out of query is empty
+        if (!$_REQUEST['q'])
+            return $this->json_encode($tickets);
 
-        $sql='SELECT DISTINCT `number`, email.address AS email'
-            .' FROM '.TICKET_TABLE.' ticket'
-            .' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id'
-            .' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id'
-            .' WHERE `number` LIKE \''.db_input($_REQUEST['q'], false).'%\'';
-
-        $sql.=' AND ( staff_id='.db_input($thisstaff->getId());
-
-        if(($teams=$thisstaff->getTeams()) && count(array_filter($teams)))
-            $sql.=' OR team_id IN('.implode(',', db_input(array_filter($teams))).')';
-
-        if(!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts()))
-            $sql.=' OR dept_id IN ('.implode(',', db_input($depts)).')';
-
-        $sql.=' )  '
-            .' ORDER BY ticket.created LIMIT '.$limit;
+        $visibility = Q::any(array(
+            'staff_id' => $thisstaff->getId(),
+            'team_id__in' => $thisstaff->teams->values_flat('team_id'),
+        ));
 
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while(list($id, $email)=db_fetch_row($res)) {
-                $info = "$id - $email";
-                $tickets[] = array('id'=>$id, 'email'=>$email, 'value'=>$id,
-                    'info'=>$info, 'matches'=>$_REQUEST['q']);
-            }
+        if (!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts())) {
+            $visibility->add(array('dept_id__in' => $depts));
         }
-        if (!$tickets)
-            return self::lookupByEmail();
-
-        return $this->json_encode($tickets);
-    }
-
-    function lookupByEmail() {
-        global $thisstaff;
 
-
-        $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
-        $tickets=array();
-
-        $sql='SELECT email.address AS email, count(ticket.ticket_id) as tickets '
-            .' FROM '.TICKET_TABLE.' ticket'
-            .' JOIN '.USER_TABLE.' user ON user.id = ticket.user_id'
-            .' JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id'
-            .' WHERE (email.address LIKE \'%'.db_input(strtolower($_REQUEST['q']), false).'%\'
-                OR user.name LIKE \'%'.db_input($_REQUEST['q'], false).'%\')';
-
-        $sql.=' AND ( staff_id='.db_input($thisstaff->getId());
-
-        if(($teams=$thisstaff->getTeams()) && count(array_filter($teams)))
-            $sql.=' OR team_id IN('.implode(',', db_input(array_filter($teams))).')';
-
-        if(!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts()))
-            $sql.=' OR dept_id IN ('.implode(',', db_input($depts)).')';
-
-        $sql.=' ) '
-            .' GROUP BY email.address '
-            .' ORDER BY ticket.created  LIMIT '.$limit;
-
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while(list($email, $count)=db_fetch_row($res))
-                $tickets[] = array('email'=>$email, 'value'=>$email,
-                    'info'=>"$email ($count)", 'matches'=>$_REQUEST['q']);
-        }
-
-        return $this->json_encode($tickets);
-    }
-
-    function _search($req) {
-        global $thisstaff, $cfg, $ost;
-
-        $result=array();
-        $criteria = array();
-
-        $select = 'SELECT ticket.ticket_id';
-        $from = ' FROM '.TICKET_TABLE.' ticket
-                  LEFT JOIN '.TICKET_STATUS_TABLE.' status
-                    ON (status.id = ticket.status_id) ';
-        //Access control.
-        $where = ' WHERE ( (ticket.staff_id='.db_input($thisstaff->getId())
-                    .' AND status.state="open" )';
-
-        if(($teams=$thisstaff->getTeams()) && count(array_filter($teams)))
-            $where.=' OR (ticket.team_id IN ('.implode(',', db_input(array_filter($teams)))
-                   .' ) AND status.state="open" )';
-
-        if(!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts()))
-            $where.=' OR ticket.dept_id IN ('.implode(',', db_input($depts)).')';
-
-        $where.=' ) ';
-
-        //Department
-        if ($req['deptId']) {
-            $where.=' AND ticket.dept_id='.db_input($req['deptId']);
-            $criteria['dept_id'] = $req['deptId'];
-        }
-
-        //Help topic
-        if($req['topicId']) {
-            $where.=' AND ticket.topic_id='.db_input($req['topicId']);
-            $criteria['topic_id'] = $req['topicId'];
+        $hits = TicketModel::objects()
+            ->filter($visibility)
+            ->values('user__default_email__address')
+            ->annotate(array(
+                'number' => new SqlCode('null'),
+                'tickets' => SqlAggregate::COUNT('ticket_id', true)))
+            ->limit($limit);
+
+        $q = $_REQUEST['q'];
+
+        if (strlen($q) < 3)
+            return $this->encode(array());
+
+        global $ost;
+        $hits = $ost->searcher->find($q, $hits)
+            ->order_by(new SqlCode('__relevance__'), QuerySet::DESC);
+
+        if (preg_match('/\d{2,}[^*]/', $q, $T = array())) {
+            $hits = TicketModel::objects()
+                ->values('user__default_email__address', 'number')
+                ->annotate(array(
+                    'tickets' => new SqlCode('1'),
+                    '__relevance__' => new SqlCode(1)
+                ))
+                ->filter($visibility)
+                ->filter(array('number__startswith' => $q))
+                ->limit($limit)
+                ->union($hits);
         }
-
-        // Status
-        if ($req['statusId']
-                && ($status=TicketStatus::lookup($req['statusId']))) {
-            $where .= sprintf(' AND status.id="%d" ',
-                    $status->getId());
-            $criteria['status_id'] = $status->getId();
+        elseif (!count($hits) && preg_match('`\w$`u', $q)) {
+            // Do wild-card fulltext search
+            $_REQUEST['q'] = $q.'*';
+            return $this->lookup();
         }
 
-        // Flags
-        if ($req['flag']) {
-            switch (strtolower($req['flag'])) {
-                case 'answered':
-                    $where .= ' AND ticket.isanswered =1 ';
-                    $criteria['isanswered'] = 1;
-                    $criteria['state'] = 'open';
-                    $where .= ' AND status.state="open" ';
-                    break;
-                case 'overdue':
-                    $where .= ' AND ticket.isoverdue =1 ';
-                    $criteria['isoverdue'] = 1;
-                    $criteria['state'] = 'open';
-                    $where .= ' AND status.state="open" ';
-                    break;
+        foreach ($hits as $T) {
+            $email = $T['user__default_email__address'];
+            $count = $T['tickets'];
+            if ($T['number']) {
+                $tickets[] = array('id'=>$T['number'], 'value'=>$T['number'],
+                    'info'=>"{$T['number']} — {$email}",
+                    'matches'=>$_REQUEST['q']);
             }
-        }
-
-        //Assignee
-        if($req['assignee'] && strcasecmp($req['status'], 'closed'))  { # assigned-to
-            $id=preg_replace("/[^0-9]/", "", $req['assignee']);
-            $assignee = $req['assignee'];
-            $where.= ' AND ( ( status.state="open" ';
-            if($assignee[0]=='t') {
-                $where.=' AND ticket.team_id='.db_input($id);
-                $criteria['team_id'] = $id;
-            }
-            elseif($assignee[0]=='s' || is_numeric($id)) {
-                $where.=' AND ticket.staff_id='.db_input($id);
-                $criteria['staff_id'] = $id;
-            }
-
-            $where.=')';
-
-            if($req['staffId'] && !$req['status']) //Assigned TO + Closed By
-                $where.= ' OR (ticket.staff_id='.db_input($req['staffId']).
-                    ' AND status.state IN("closed")) ';
-            elseif($req['staffId']) // closed by any
-                $where.= ' OR status.state IN("closed") ';
-
-            $where.= ' ) ';
-        } elseif($req['staffId']) { # closed-by
-            $where.=' AND (ticket.staff_id='.db_input($req['staffId']).' AND
-                status.state IN("closed")) ';
-            $criteria['state__in'] = array('closed');
-            $criteria['staff_id'] = $req['staffId'];
-        }
-
-        //dates
-        $startTime  =($req['startDate'] && (strlen($req['startDate'])>=8))?strtotime($req['startDate']):0;
-        $endTime    =($req['endDate'] && (strlen($req['endDate'])>=8))?strtotime($req['endDate']):0;
-        if ($endTime)
-            // $endTime should be the last second of the day, not the first like $startTime
-            $endTime += (60 * 60 * 24) - 1;
-        if( ($startTime && $startTime>time()) or ($startTime>$endTime && $endTime>0))
-            $startTime=$endTime=0;
-
-        if($startTime) {
-            $where.=' AND ticket.created>=FROM_UNIXTIME('.$startTime.')';
-            $criteria['created__gte'] = $startTime;
-        }
-
-        if($endTime) {
-            $where.=' AND ticket.created<=FROM_UNIXTIME('.$endTime.')';
-            $criteria['created__lte'] = $startTime;
-        }
-
-        // Dynamic fields
-        $cdata_search = false;
-        foreach (TicketForm::getInstance()->getFields() as $f) {
-            if (isset($req[$f->getFormName()])
-                    && ($val = $req[$f->getFormName()])) {
-                $name = $f->get('name') ? $f->get('name')
-                    : 'field_'.$f->get('id');
-                if (is_array($val)) {
-                    $cwhere = '(' . implode(' OR ', array_map(
-                        function($k) use ($name) {
-                            return sprintf('FIND_IN_SET(%s, `%s`)', db_input($k), $name);
-                        }, $val)
-                    ) . ')';
-                    $criteria["cdata.{$name}"] = $val;
-                }
-                else {
-                    $cwhere = "cdata.`$name` LIKE '%".db_real_escape($val)."%'";
-                    $criteria["cdata.{$name}"] = $val;
-                }
-                $where .= ' AND ('.$cwhere.')';
-                $cdata_search = true;
+            else {
+                $tickets[] = array('email'=>$email, 'value'=>$email,
+                    'info'=>"$email ($count)", 'matches'=>$_REQUEST['q']);
             }
         }
-        if ($cdata_search)
-            $from .= 'LEFT JOIN '.TABLE_PREFIX.'ticket__cdata '
-                    ." cdata ON (cdata.ticket_id = ticket.ticket_id)";
-
-        //Query
-        $joins = array();
-        if($req['query']) {
-            // Setup sets of joins and queries
-            if ($s = $ost->searcher)
-               return $s->find($req['query'], $criteria, 'Ticket');
-        }
-
-        $sections = array();
-        foreach ($joins as $j) {
-            $sections[] = "$select $from {$j['from']} $where AND ({$j['where']})";
-        }
-        if (!$joins)
-            $sections[] = "$select $from $where";
-
-        $sql=implode(' union ', $sections);
-        if (!($res = db_query($sql)))
-            return TicketForm::dropDynamicDataView();
-
-        $tickets = array();
-        while ($row = db_fetch_row($res))
-            $tickets[] = $row[0];
 
-        return $tickets;
-    }
-
-    function search() {
-        $tickets = self::_search($_REQUEST);
-        $result = array();
-
-        if (count($tickets)) {
-            $uid = md5($_SERVER['QUERY_STRING']);
-            $_SESSION["adv_$uid"] = $tickets;
-            $result['success'] = sprintf(__("Search criteria matched %s"),
-                    sprintf(_N('%d ticket', '%d tickets', count($tickets)), count($tickets)
-                ))
-                . " - <a href='tickets.php?advsid=$uid'>".__('view')."</a>";
-        } else {
-            $result['fail']=__('No tickets found matching your search criteria.');
-        }
-
-        return $this->json_encode($result);
+        return $this->json_encode($tickets);
     }
 
     function acquireLock($tid) {
-        global $cfg,$thisstaff;
+        global $cfg, $thisstaff;
 
-        if(!$tid || !is_numeric($tid) || !$thisstaff || !$cfg || !$cfg->getLockTime())
+        if(!$cfg || !$cfg->getLockTime() || $cfg->getTicketLockMode() == Lock::MODE_DISABLED)
+            Http::response(418, $this->encode(array('id'=>0, 'retry'=>false)));
+
+        if(!$tid || !is_numeric($tid) || !$thisstaff)
             return 0;
 
-        if(!($ticket = Ticket::lookup($tid)) || !$ticket->checkStaffAccess($thisstaff))
-            return $this->json_encode(array('id'=>0, 'retry'=>false, 'msg'=>__('Lock denied!')));
+        if (!($ticket = Ticket::lookup($tid)) || !$ticket->checkStaffPerm($thisstaff))
+            return $this->encode(array('id'=>0, 'retry'=>false, 'msg'=>__('Lock denied!')));
 
         //is the ticket already locked?
-        if($ticket->isLocked() && ($lock=$ticket->getLock()) && !$lock->isExpired()) {
+        if ($ticket->isLocked() && ($lock=$ticket->getLock()) && !$lock->isExpired()) {
             /*Note: Ticket->acquireLock does the same logic...but we need it here since we need to know who owns the lock up front*/
             //Ticket is locked by someone else.??
-            if($lock->getStaffId()!=$thisstaff->getId())
-                return $this->json_encode(array('id'=>0, 'retry'=>false, 'msg'=>__('Unable to acquire lock.')));
+            if ($lock->getStaffId() != $thisstaff->getId())
+                return $this->json_encode(array('id'=>0, 'retry'=>false,
+                    'msg' => sprintf(__('Currently locked by %s'),
+                        $lock->getStaff()->getAvatarAndName())
+                    ));
 
             //Ticket already locked by staff...try renewing it.
             $lock->renew(); //New clock baby!
@@ -302,248 +123,84 @@ class TicketsAjaxAPI extends AjaxController {
             return $this->json_encode(array('id'=>0, 'retry'=>true));
         }
 
-        return $this->json_encode(array('id'=>$lock->getId(), 'time'=>$lock->getTime()));
+        return $this->json_encode(array(
+            'id'=>$lock->getId(), 'time'=>$lock->getTime(),
+            'code' => $lock->getCode()
+        ));
     }
 
-    function renewLock($tid, $id) {
+    function renewLock($id, $ticketId) {
         global $thisstaff;
 
-        if(!$tid || !is_numeric($tid) || !$id || !is_numeric($id) || !$thisstaff)
-            return $this->json_encode(array('id'=>0, 'retry'=>true));
-
-        $lock= TicketLock::lookup($id, $tid);
-        if(!$lock || !$lock->getStaffId() || $lock->isExpired()) //Said lock doesn't exist or is is expired
-            return self::acquireLock($tid); //acquire the lock
-
-        if($lock->getStaffId()!=$thisstaff->getId()) //user doesn't own the lock anymore??? sorry...try to next time.
-            return $this->json_encode(array('id'=>0, 'retry'=>false)); //Give up...
-
-        //Renew the lock.
-        $lock->renew(); //Failure here is not an issue since the lock is not expired yet.. client need to check time!
-
-        return $this->json_encode(array('id'=>$lock->getId(), 'time'=>$lock->getTime()));
+        if (!$id || !is_numeric($id) || !$thisstaff)
+            Http::response(403, $this->encode(array('id'=>0, 'retry'=>false)));
+        if (!($lock = Lock::lookup($id)))
+            Http::response(404, $this->encode(array('id'=>0, 'retry'=>'acquire')));
+        if (!($ticket = Ticket::lookup($ticketId)) || $ticket->lock_id != $lock->lock_id)
+            // Ticket / Lock mismatch
+            Http::response(400, $this->encode(array('id'=>0, 'retry'=>false)));
+
+        if (!$lock->getStaffId() || $lock->isExpired())
+            // Said lock doesn't exist or is is expired — fetch a new lock
+            return self::acquireLock($ticket->getId());
+
+        if ($lock->getStaffId() != $thisstaff->getId())
+            // user doesn't own the lock anymore??? sorry...try to next time.
+            Http::response(403, $this->encode(array('id'=>0, 'retry'=>false,
+                'msg' => sprintf(__('Currently locked by %s'),
+                    $lock->getStaff->getAvatarAndName())
+            ))); //Give up...
+
+        // Ensure staff still has access
+        if (!$ticket->checkStaffPerm($thisstaff))
+            Http::response(403, $this->encode(array('id'=>0, 'retry'=>false,
+                'msg' => sprintf(__('You no longer have access to #%s.'),
+                $ticket->getNumber())
+            )));
+
+        // Renew the lock.
+        // Failure here is not an issue since the lock is not expired yet.. client need to check time!
+        $lock->renew();
+
+        return $this->encode(array('id'=>$lock->getId(), 'time'=>$lock->getTime(),
+            'code' => $lock->getCode()));
     }
 
-    function releaseLock($tid, $id=0) {
+    function releaseLock($id) {
         global $thisstaff;
 
-        if($id && is_numeric($id)){ //Lock Id provided!
-
-            $lock = TicketLock::lookup($id, $tid);
-            //Already gone?
-            if(!$lock || !$lock->getStaffId() || $lock->isExpired()) //Said lock doesn't exist or is is expired
-                return 1;
-
-            //make sure the user actually owns the lock before releasing it.
-            return ($lock->getStaffId()==$thisstaff->getId() && $lock->release())?1:0;
+        if (!$id || !is_numeric($id) || !$thisstaff)
+            Http::response(403, $this->encode(array('id'=>0, 'retry'=>true)));
+        if (!($lock = Lock::lookup($id)))
+            Http::response(404, $this->encode(array('id'=>0, 'retry'=>true)));
 
-        }elseif($tid){ //release all the locks the user owns on the ticket.
-            return TicketLock::removeStaffLocks($thisstaff->getId(),$tid)?1:0;
+        // You have to own the lock
+        if ($lock->getStaffId() != $thisstaff->getId()) {
+            return 0;
         }
-
-        return 0;
+        // Can't be expired
+        if ($lock->isExpired()) {
+            return 1;
+        }
+        return $lock->release() ? 1 : 0;
     }
 
     function previewTicket ($tid) {
-
         global $thisstaff;
 
-        if(!$thisstaff || !($ticket=Ticket::lookup($tid)) || !$ticket->checkStaffAccess($thisstaff))
+        if(!$thisstaff || !($ticket=Ticket::lookup($tid))
+                || !$ticket->checkStaffPerm($thisstaff))
             Http::response(404, __('No such ticket'));
 
         include STAFFINC_DIR . 'templates/ticket-preview.tmpl.php';
     }
 
-    function addRemoteCollaborator($tid, $bk, $id) {
-        global $thisstaff;
-
-        if (!($ticket=Ticket::lookup($tid))
-                || !$ticket->checkStaffAccess($thisstaff))
-            Http::response(404, 'No such ticket');
-        elseif (!$bk || !$id)
-            Http::response(422, 'Backend and user id required');
-        elseif (!($backend = StaffAuthenticationBackend::getBackend($bk)))
-            Http::response(404, 'User not found');
-
-        $user_info = $backend->lookup($id);
-        $form = UserForm::getUserForm()->getForm($user_info);
-        $info = array();
-        if (!$user_info)
-            $info['error'] = __('Unable to find user in directory');
-
-        return self::_addcollaborator($ticket, null, $form, $info);
-    }
-
-    //Collaborators utils
-    function addCollaborator($tid, $uid=0) {
-        global $thisstaff;
-
-        if (!($ticket=Ticket::lookup($tid))
-                || !$ticket->checkStaffAccess($thisstaff))
-            Http::response(404, __('No such ticket'));
-
-
-        $user = $uid? User::lookup($uid) : null;
-
-        //If not a post then assume new collaborator form
-        if(!$_POST)
-            return self::_addcollaborator($ticket, $user);
-
-        $user = $form = null;
-        if (isset($_POST['id']) && $_POST['id']) { //Existing user/
-            $user =  User::lookup($_POST['id']);
-        } else { //We're creating a new user!
-            $form = UserForm::getUserForm()->getForm($_POST);
-            $user = User::fromForm($form);
-        }
-
-        $errors = $info = array();
-        if ($user) {
-            if ($user->getId() == $ticket->getOwnerId())
-                $errors['err'] = sprintf(__('Ticket owner, %s, is a collaborator by default!'),
-                        Format::htmlchars($user->getName()));
-            elseif (($c=$ticket->addCollaborator($user,
-                            array('isactive'=>1), $errors))) {
-                $note = Format::htmlchars(sprintf(__('%s <%s> added as a collaborator'),
-                            Format::htmlchars($c->getName()), $c->getEmail()));
-                $ticket->logNote(__('New Collaborator Added'), $note,
-                    $thisstaff, false);
-                $info = array('msg' => sprintf(__('%s added as a collaborator'),
-                            Format::htmlchars($c->getName())));
-                return self::_collaborators($ticket, $info);
-            }
-        }
-
-        if($errors && $errors['err']) {
-            $info +=array('error' => $errors['err']);
-        } else {
-            $info +=array('error' =>__('Unable to add collaborator. Internal error'));
-        }
-
-        return self::_addcollaborator($ticket, $user, $form, $info);
-    }
-
-    function updateCollaborator($cid) {
-        global $thisstaff;
-
-        if(!($c=Collaborator::lookup($cid))
-                || !($user=$c->getUser())
-                || !($ticket=$c->getTicket())
-                || !$ticket->checkStaffAccess($thisstaff)
-                )
-            Http::response(404, 'Unknown collaborator');
-
-        $errors = array();
-        if(!$user->updateInfo($_POST, $errors))
-            return self::_collaborator($c ,$user->getForms($_POST), $errors);
-
-        $info = array('msg' => sprintf('%s updated successfully',
-                    Format::htmlchars($c->getName())));
-
-        return self::_collaborators($ticket, $info);
-    }
-
-    function viewCollaborator($cid) {
-        global $thisstaff;
-
-        if(!($collaborator=Collaborator::lookup($cid))
-                || !($ticket=$collaborator->getTicket())
-                || !$ticket->checkStaffAccess($thisstaff))
-            Http::response(404, 'Unknown collaborator');
-
-        return self::_collaborator($collaborator);
-    }
-
-    function showCollaborators($tid) {
-        global $thisstaff;
-
-        if(!($ticket=Ticket::lookup($tid))
-                || !$ticket->checkStaffAccess($thisstaff))
-            Http::response(404, 'No such ticket');
-
-        if($ticket->getCollaborators())
-            return self::_collaborators($ticket);
-
-        return self::_addcollaborator($ticket);
-    }
-
-    function previewCollaborators($tid) {
-        global $thisstaff;
-
-        if (!($ticket=Ticket::lookup($tid))
-                || !$ticket->checkStaffAccess($thisstaff))
-            Http::response(404, 'No such ticket');
-
-        ob_start();
-        include STAFFINC_DIR . 'templates/collaborators-preview.tmpl.php';
-        $resp = ob_get_contents();
-        ob_end_clean();
-
-        return $resp;
-    }
-
-    function _addcollaborator($ticket, $user=null, $form=null, $info=array()) {
-
-        $info += array(
-                    'title' => sprintf(__('Ticket #%s: Add a collaborator'), $ticket->getNumber()),
-                    'action' => sprintf('#tickets/%d/add-collaborator', $ticket->getId()),
-                    'onselect' => sprintf('ajax.php/tickets/%d/add-collaborator/', $ticket->getId()),
-                    );
-        return self::_userlookup($user, $form, $info);
-    }
-
-
-    function updateCollaborators($tid) {
-        global $thisstaff;
-
-        if(!($ticket=Ticket::lookup($tid))
-                || !$ticket->checkStaffAccess($thisstaff))
-            Http::response(404, 'No such ticket');
-
-        $errors = $info = array();
-        if ($ticket->updateCollaborators($_POST, $errors))
-            Http::response(201, sprintf('Recipients (%d of %d)',
-                        $ticket->getNumActiveCollaborators(),
-                        $ticket->getNumCollaborators()));
-
-        if($errors && $errors['err'])
-            $info +=array('error' => $errors['err']);
-
-        return self::_collaborators($ticket, $info);
-    }
-
-
-
-    function _collaborator($collaborator, $form=null, $info=array()) {
-
-        $info += array('action' => '#collaborators/'.$collaborator->getId());
-
-        $user = $collaborator->getUser();
-
-        ob_start();
-        include(STAFFINC_DIR . 'templates/user.tmpl.php');
-        $resp = ob_get_contents();
-        ob_end_clean();
-
-        return $resp;
-    }
-
-    function _collaborators($ticket, $info=array()) {
-
-        ob_start();
-        include(STAFFINC_DIR . 'templates/collaborators.tmpl.php');
-        $resp = ob_get_contents();
-        ob_end_clean();
-
-        return $resp;
-    }
-
     function viewUser($tid) {
         global $thisstaff;
 
         if(!$thisstaff
                 || !($ticket=Ticket::lookup($tid))
-                || !$ticket->checkStaffAccess($thisstaff))
+                || !$ticket->checkStaffPerm($thisstaff))
             Http::response(404, 'No such ticket');
 
 
@@ -570,7 +227,7 @@ class TicketsAjaxAPI extends AjaxController {
 
         if(!$thisstaff
                 || !($ticket=Ticket::lookup($tid))
-                || !$ticket->checkStaffAccess($thisstaff)
+                || !$ticket->checkStaffPerm($thisstaff)
                 || !($user = User::lookup($ticket->getOwnerId())))
             Http::response(404, 'No such ticket/user');
 
@@ -597,7 +254,7 @@ class TicketsAjaxAPI extends AjaxController {
 
         if(!$thisstaff
                 || !($ticket=Ticket::lookup($tid))
-                || !$ticket->checkStaffAccess($thisstaff))
+                || !$ticket->checkStaffPerm($thisstaff))
             Http::response(404, 'No such ticket');
 
 
@@ -611,6 +268,7 @@ class TicketsAjaxAPI extends AjaxController {
     }
 
     function _userlookup($user, $form, $info) {
+        global $thisstaff;
 
         ob_start();
         include(STAFFINC_DIR . 'templates/user-lookup.tmpl.php');
@@ -621,8 +279,17 @@ class TicketsAjaxAPI extends AjaxController {
     }
 
     function manageForms($ticket_id) {
-        $forms = DynamicFormEntry::forTicket($ticket_id);
-        $info = array('action' => '#tickets/'.Format::htmlchars($ticket_id).'/forms/manage');
+        global $thisstaff;
+
+        if (!$thisstaff)
+            Http::response(403, "Login required");
+        elseif (!($ticket = Ticket::lookup($ticket_id)))
+            Http::response(404, "No such ticket");
+        elseif (!$ticket->checkStaffPerm($thisstaff, Ticket::PERM_EDIT))
+            Http::response(403, "Access Denied");
+
+        $forms = DynamicFormEntry::forTicket($ticket->getId());
+        $info = array('action' => '#tickets/'.$ticket->getId().'/forms/manage');
         include(STAFFINC_DIR . 'templates/form-manage.tmpl.php');
     }
 
@@ -633,7 +300,7 @@ class TicketsAjaxAPI extends AjaxController {
             Http::response(403, "Login required");
         elseif (!($ticket = Ticket::lookup($ticket_id)))
             Http::response(404, "No such ticket");
-        elseif (!$ticket->checkStaffAccess($thisstaff))
+        elseif (!$ticket->checkStaffPerm($thisstaff, Ticket::PERM_EDIT))
             Http::response(403, "Access Denied");
         elseif (!isset($_POST['forms']))
             Http::response(422, "Send updated forms list");
@@ -672,7 +339,7 @@ class TicketsAjaxAPI extends AjaxController {
         global $thisstaff, $cfg;
 
         if (!($ticket = Ticket::lookup($tid))
-                || !$ticket->checkStaffAccess($thisstaff))
+                || !$ticket->checkStaffPerm($thisstaff))
             Http::response(404, 'Unknown ticket ID');
 
 
@@ -684,7 +351,7 @@ class TicketsAjaxAPI extends AjaxController {
             $response = "<br/><blockquote>{$response->asVar()}</blockquote><br/>";
 
             //  Return text if html thread is not enabled
-            if (!$cfg->isHtmlThreadEnabled())
+            if (!$cfg->isRichTextEnabled())
                 $response = Format::html2text($response, 90);
             else
                 $response = Format::viewableImages($response);
@@ -693,7 +360,7 @@ class TicketsAjaxAPI extends AjaxController {
             return Format::json_encode(array('response' => $response));
         }
 
-        if (!$cfg->isHtmlThreadEnabled())
+        if (!$cfg->isRichTextEnabled())
             $format.='.plain';
 
         $varReplacer = function (&$var) use($ticket) {
@@ -707,6 +374,442 @@ class TicketsAjaxAPI extends AjaxController {
         return $canned->getFormattedResponse($format, $varReplacer);
     }
 
+    function transfer($tid) {
+        global $thisstaff;
+
+        if (!($ticket=Ticket::lookup($tid)))
+            Http::response(404, __('No such ticket'));
+
+        if (!$ticket->checkStaffPerm($thisstaff, Ticket::PERM_TRANSFER))
+            Http::response(403, __('Permission Denied'));
+
+        $errors = array();
+
+        $info = array(
+                ':title' => sprintf(__('Ticket #%s: %s'),
+                    $ticket->getNumber(),
+                    __('Transfer')),
+                ':action' => sprintf('#tickets/%d/transfer',
+                    $ticket->getId())
+                );
+
+        $form = $ticket->getTransferForm($_POST);
+        if ($_POST && $form->isValid()) {
+            if ($ticket->transfer($form, $errors)) {
+                $_SESSION['::sysmsgs']['msg'] = sprintf(
+                        __('%s successfully'),
+                        sprintf(
+                            __('%s transferred to %s department'),
+                            __('Ticket'),
+                            $ticket->getDept()
+                            )
+                        );
+                Http::response(201, $ticket->getId());
+            }
+
+            $form->addErrors($errors);
+            $info['error'] = $errors['err'] ?: __('Unable to transfer ticket');
+        }
+
+        $info['dept_id'] = $info['dept_id'] ?: $ticket->getDeptId();
+
+        include STAFFINC_DIR . 'templates/transfer.tmpl.php';
+    }
+
+
+    function assign($tid, $target=null) {
+        global $thisstaff;
+
+        if (!($ticket=Ticket::lookup($tid)))
+            Http::response(404, __('No such ticket'));
+
+        if (!$ticket->checkStaffPerm($thisstaff, Ticket::PERM_ASSIGN)
+                || !($form = $ticket->getAssignmentForm($_POST,
+                        array('target' => $target))))
+            Http::response(403, __('Permission Denied'));
+
+        $errors = array();
+        $info = array(
+                ':title' => sprintf(__('Ticket #%s: %s'),
+                    $ticket->getNumber(),
+                    sprintf('%s %s',
+                        $ticket->isAssigned() ?
+                            __('Reassign') :  __('Assign'),
+                        !strcasecmp($target, 'agents') ?
+                            __('to an Agent') : __('to a Team')
+                    )),
+                ':action' => sprintf('#tickets/%d/assign%s',
+                    $ticket->getId(),
+                    ($target  ? "/$target": '')),
+                );
+
+        if ($ticket->isAssigned()) {
+            if ($ticket->getStaffId() == $thisstaff->getId())
+                $assigned = __('you');
+            else
+                $assigned = $ticket->getAssigned();
+
+            $info['notice'] = sprintf(__('%s is currently assigned to <b>%s</b>'),
+                    __('This ticket'),
+                    Format::htmlchars($assigned)
+                    );
+        }
+
+        if ($_POST && $form->isValid()) {
+            if ($ticket->assign($form, $errors)) {
+                $_SESSION['::sysmsgs']['msg'] = sprintf(
+                        __('%s successfully'),
+                        sprintf(
+                            __('%s assigned to %s'),
+                            __('Ticket'),
+                            $form->getAssignee())
+                        );
+                Http::response(201, $ticket->getId());
+            }
+
+            $form->addErrors($errors);
+            $info['error'] = $errors['err'] ?: __('Unable to assign ticket');
+        }
+
+        include STAFFINC_DIR . 'templates/assign.tmpl.php';
+    }
+
+    function claim($tid) {
+
+        global $thisstaff;
+
+        if (!($ticket=Ticket::lookup($tid)))
+            Http::response(404, __('No such ticket'));
+
+        // Check for premissions and such
+        if (!$ticket->checkStaffPerm($thisstaff, Ticket::PERM_ASSIGN)
+                || !$ticket->isOpen() // Claim only open
+                || $ticket->getStaff() // cannot claim assigned ticket
+                || !($form = $ticket->getClaimForm($_POST)))
+            Http::response(403, __('Permission Denied'));
+
+        $errors = array();
+        $info = array(
+                ':title' => sprintf(__('Ticket #%s: %s'),
+                    $ticket->getNumber(),
+                    __('Claim')),
+                ':action' => sprintf('#tickets/%d/claim',
+                    $ticket->getId()),
+
+                );
+
+        if ($ticket->isAssigned()) {
+            if ($ticket->getStaffId() == $thisstaff->getId())
+                $assigned = __('you');
+            else
+                $assigneed = $ticket->getAssigned();
+
+            $info['error'] = sprintf(__('%s is currently assigned to <b>%s</b>'),
+                    __('This ticket'),
+                    $assigned);
+        } else {
+            $info['warn'] = sprintf(__('Are you sure you want to CLAIM %s?'),
+                    __('this ticket'));
+        }
+
+        if ($_POST && $form->isValid()) {
+            if ($ticket->claim($form, $errors)) {
+                $_SESSION['::sysmsgs']['msg'] = sprintf(
+                        __('%s successfully'),
+                        sprintf(
+                            __('%s assigned to %s'),
+                            __('Ticket'),
+                            __('you'))
+                        );
+                Http::response(201, $ticket->getId());
+            }
+
+            $form->addErrors($errors);
+            $info['error'] = $errors['err'] ?: __('Unable to claim ticket');
+        }
+
+        $verb = sprintf('%s, %s', __('Yes'), __('Claim'));
+
+        include STAFFINC_DIR . 'templates/assign.tmpl.php';
+
+    }
+
+    function massProcess($action, $w=null)  {
+        global $thisstaff, $cfg;
+
+        $actions = array(
+                'transfer' => array(
+                    'verbed' => __('transferred'),
+                    ),
+                'assign' => array(
+                    'verbed' => __('assigned'),
+                    ),
+                'claim' => array(
+                    'verbed' => __('assigned'),
+                    ),
+                'delete' => array(
+                    'verbed' => __('deleted'),
+                    ),
+                'reopen' => array(
+                    'verbed' => __('reopen'),
+                    ),
+                'close' => array(
+                    'verbed' => __('closed'),
+                    ),
+                );
+
+        if (!isset($actions[$action]))
+            Http::response(404, __('Unknown action'));
+
+
+        $info = $errors = $e = array();
+        $inc = null;
+        $i = $count = 0;
+        if ($_POST) {
+            if (!$_POST['tids'] || !($count=count($_POST['tids'])))
+                $errors['err'] = sprintf(
+                        __('You must select at least %s.'),
+                        __('one ticket'));
+        } else {
+            $count  =  $_REQUEST['count'];
+        }
+        switch ($action) {
+        case 'claim':
+            $w = 'me';
+        case 'assign':
+            $inc = 'assign.tmpl.php';
+            $info[':action'] = "#tickets/mass/assign/$w";
+            $info[':title'] = sprintf('Assign %s',
+                    _N('selected ticket', 'selected tickets', $count));
+
+            $form = AssignmentForm::instantiate($_POST);
+
+            $assignCB = function($t, $f, $e) {
+                return $t->assign($f, $e);
+            };
+
+            $assignees = null;
+            switch ($w) {
+                case 'agents':
+                    $depts = array();
+                    $tids = $_POST['tids'] ?: array_filter(explode(',', $_REQUEST['tids']));
+                    if ($tids) {
+                        $tickets = TicketModel::objects()
+                            ->distinct('dept_id')
+                            ->filter(array('ticket_id__in' => $tids));
+
+                        $depts = $tickets->values_flat('dept_id');
+                    }
+                    $members = Staff::objects()
+                        ->distinct('staff_id')
+                        ->filter(array(
+                                    'onvacation' => 0,
+                                    'isactive' => 1,
+                                    )
+                                );
+
+                    if ($depts) {
+                        $members->filter(Q::any( array(
+                                        'dept_id__in' => $depts,
+                                        Q::all(array(
+                                            'dept_access__dept__id__in' => $depts,
+                                            Q::not(array('dept_access__dept__flags__hasbit'
+                                                => Dept::FLAG_ASSIGN_MEMBERS_ONLY))
+                                            ))
+                                        )));
+                    }
+
+                    switch ($cfg->getAgentNameFormat()) {
+                    case 'last':
+                    case 'lastfirst':
+                    case 'legal':
+                        $members->order_by('lastname', 'firstname');
+                        break;
+
+                    default:
+                        $members->order_by('firstname', 'lastname');
+                    }
+
+                    $prompt  = __('Select an Agent');
+                    $assignees = array();
+                    foreach ($members as $member)
+                         $assignees['s'.$member->getId()] = $member->getName();
+
+                    if (!$assignees)
+                        $info['warn'] =  __('No agents available for assignment');
+                    break;
+                case 'teams':
+                    $assignees = array();
+                    $prompt = __('Select a Team');
+                    foreach (Team::getActiveTeams() as $id => $name)
+                        $assignees['t'.$id] = $name;
+
+                    if (!$assignees)
+                        $info['warn'] =  __('No teams available for assignment');
+                    break;
+                case 'me':
+                    $info[':action'] = '#tickets/mass/claim';
+                    $info[':title'] = sprintf('Claim %s',
+                            _N('selected ticket', 'selected tickets', $count));
+                    $info['warn'] = sprintf(
+                            __('Are you sure you want to CLAIM %s?'),
+                            _N('selected ticket', 'selected tickets', $count));
+                    $verb = sprintf('%s, %s', __('Yes'), __('Claim'));
+                    $id = sprintf('s%s', $thisstaff->getId());
+                    $assignees = array($id => $thisstaff->getName());
+                    $vars = $_POST ?: array('assignee' => array($id));
+                    $form = ClaimForm::instantiate($vars);
+                    $assignCB = function($t, $f, $e) {
+                        return $t->claim($f, $e);
+                    };
+                    break;
+            }
+
+            if ($assignees != null)
+                $form->setAssignees($assignees);
+
+            if ($prompt && ($f=$form->getField('assignee')))
+                $f->configure('prompt', $prompt);
+
+            if ($_POST && $form->isValid()) {
+                foreach ($_POST['tids'] as $tid) {
+                    if (($t=Ticket::lookup($tid))
+                            // Make sure the agent is allowed to
+                            // access and assign the task.
+                            && $t->checkStaffPerm($thisstaff, Ticket::PERM_ASSIGN)
+                            // Do the assignment
+                            && $assignCB($t, $form, $e)
+                            )
+                        $i++;
+                }
+
+                if (!$i) {
+                    $info['error'] = sprintf(
+                            __('Unable to %1$s %2$s'),
+                            __('assign'),
+                            _N('selected ticket', 'selected tickets', $count));
+                }
+            }
+            break;
+        case 'transfer':
+            $inc = 'transfer.tmpl.php';
+            $info[':action'] = '#tickets/mass/transfer';
+            $info[':title'] = sprintf('Transfer %s',
+                    _N('selected ticket', 'selected tickets', $count));
+            $form = TransferForm::instantiate($_POST);
+            if ($_POST && $form->isValid()) {
+                foreach ($_POST['tids'] as $tid) {
+                    if (($t=Ticket::lookup($tid))
+                            // Make sure the agent is allowed to
+                            // access and transfer the task.
+                            && $t->checkStaffPerm($thisstaff, Ticket::PERM_TRANSFER)
+                            // Do the transfer
+                            && $t->transfer($form, $e)
+                            )
+                        $i++;
+                }
+
+                if (!$i) {
+                    $info['error'] = sprintf(
+                            __('Unable to %1$s %2$s'),
+                            __('transfer'),
+                            _N('selected ticket', 'selected tickets', $count));
+                }
+            }
+            break;
+        case 'delete':
+            $inc = 'delete.tmpl.php';
+            $info[':action'] = '#tickets/mass/delete';
+            $info[':title'] = sprintf('Delete %s',
+                    _N('selected ticket', 'selected tickets', $count));
+
+            $info[':placeholder'] = sprintf(__(
+                        'Optional reason for deleting %s'),
+                    _N('selected ticket', 'selected tickets', $count));
+            $info['warn'] = sprintf(__(
+                        'Are you sure you want to DELETE %s?'),
+                    _N('selected ticket', 'selected tickets', $count));
+            $info[':extra'] = sprintf('<strong>%s</strong>',
+                        __('Deleted tickets CANNOT be recovered, including any associated attachments.')
+                        );
+
+            // Generic permission check.
+            if (!$thisstaff->hasPerm(Ticket::PERM_DELETE, false))
+                $errors['err'] = sprintf(
+                        __('You do not have permission to %s %s'),
+                        __('delete'),
+                        __('tickets'));
+
+
+            if ($_POST && !$errors) {
+                foreach ($_POST['tids'] as $tid) {
+                    if (($t=Ticket::lookup($tid))
+                            && $t->checkStaffPerm($thisstaff, Ticket::PERM_DELETE)
+                            && $t->delete($_POST['comments'], $e)
+                            )
+                        $i++;
+                }
+
+                if (!$i) {
+                    $info['error'] = sprintf(
+                            __('Unable to %1$s %2$s'),
+                            __('delete'),
+                            _N('selected ticket', 'selected tickets', $count));
+                }
+            }
+            break;
+        default:
+            Http::response(404, __('Unknown action'));
+        }
+
+        if ($_POST && $i) {
+
+            // Assume success
+            if ($i==$count) {
+                $msg = sprintf(__('Successfully %s %s.'),
+                        $actions[$action]['verbed'],
+                        sprintf(__('%1$d %2$s'),
+                            $count,
+                            _N('selected ticket', 'selected tickets', $count))
+                        );
+                $_SESSION['::sysmsgs']['msg'] = $msg;
+            } else {
+                $warn = sprintf(
+                        __('%1$d of %2$d %3$s %4$s'), $i, $count,
+                        _N('selected ticket', 'selected tickets',
+                            $count),
+                        $actions[$action]['verbed']);
+                $_SESSION['::sysmsgs']['warn'] = $warn;
+            }
+            Http::response(201, 'processed');
+        } elseif($_POST && !isset($info['error'])) {
+            $info['error'] = $errors['err'] ?: sprintf(
+                    __('Unable to %1$s  %2$s'),
+                    __('process'),
+                    _N('selected ticket', 'selected tickets', $count));
+        }
+
+        if ($_POST)
+            $info = array_merge($info, Format::htmlchars($_POST));
+
+        include STAFFINC_DIR . "templates/$inc";
+        //  Copy checked tickets to the form.
+        echo "
+        <script type=\"text/javascript\">
+        $(function() {
+            $('form#tickets input[name=\"tids[]\"]:checkbox:checked')
+            .each(function() {
+                $('<input>')
+                .prop('type', 'hidden')
+                .attr('name', 'tids[]')
+                .val($(this).val())
+                .appendTo('form.mass-action');
+            });
+        });
+        </script>";
+
+    }
+
     function changeTicketStatus($tid, $status, $id=0) {
         global $thisstaff;
 
@@ -714,9 +817,11 @@ class TicketsAjaxAPI extends AjaxController {
             Http::response(403, 'Access denied');
         elseif (!$tid
                 || !($ticket=Ticket::lookup($tid))
-                || !$ticket->checkStaffAccess($thisstaff))
+                || !$ticket->checkStaffPerm($thisstaff))
             Http::response(404, 'Unknown ticket #');
 
+        $role = $thisstaff->getRole($ticket->getDeptId());
+
         $info = array();
         $state = null;
         switch($status) {
@@ -725,12 +830,17 @@ class TicketsAjaxAPI extends AjaxController {
                 $state = 'open';
                 break;
             case 'close':
-                if (!$thisstaff->canCloseTickets())
+                if (!$role->hasPerm(TicketModel::PERM_CLOSE))
                     Http::response(403, 'Access denied');
                 $state = 'closed';
+
+                // Check if ticket is closeable
+                if (is_string($closeable=$ticket->isCloseable()))
+                    $info['warn'] =  $closeable;
+
                 break;
             case 'delete':
-                if (!$thisstaff->canDeleteTickets())
+                if (!$role->hasPerm(TicketModel::PERM_DELETE))
                     Http::response(403, 'Access denied');
                 $state = 'deleted';
                 break;
@@ -752,7 +862,7 @@ class TicketsAjaxAPI extends AjaxController {
             Http::response(403, 'Access denied');
         elseif (!$tid
                 || !($ticket=Ticket::lookup($tid))
-                || !$ticket->checkStaffAccess($thisstaff))
+                || !$ticket->checkStaffPerm($thisstaff))
             Http::response(404, 'Unknown ticket #');
 
         $errors = $info = array();
@@ -763,22 +873,22 @@ class TicketsAjaxAPI extends AjaxController {
         elseif ($status->getId() == $ticket->getStatusId())
             $errors['err'] = sprintf(__('Ticket already set to %s status'),
                     __($status->getName()));
-        else {
+        elseif (($role = $thisstaff->getRole($ticket->getDeptId()))) {
             // Make sure the agent has permission to set the status
             switch(mb_strtolower($status->getState())) {
                 case 'open':
-                    if (!$thisstaff->canCloseTickets()
-                            && !$thisstaff->canCreateTickets())
+                    if (!$role->hasPerm(TicketModel::PERM_CLOSE)
+                            && !$role->hasPerm(TicketModel::PERM_CREATE))
                         $errors['err'] = sprintf(__('You do not have permission %s.'),
                                 __('to reopen tickets'));
                     break;
                 case 'closed':
-                    if (!$thisstaff->canCloseTickets())
+                    if (!$role->hasPerm(TicketModel::PERM_CLOSE))
                         $errors['err'] = sprintf(__('You do not have permission %s.'),
                                 __('to resolve/close tickets'));
                     break;
                 case 'deleted':
-                    if (!$thisstaff->canDeleteTickets())
+                    if (!$role->hasPerm(TicketModel::PERM_DELETE))
                         $errors['err'] = sprintf(__('You do not have permission %s.'),
                                 __('to archive/delete tickets'));
                     break;
@@ -786,11 +896,13 @@ class TicketsAjaxAPI extends AjaxController {
                     $errors['err'] = sprintf('%s %s',
                             __('Unknown or invalid'), __('status'));
             }
+        } else {
+            $errors['err'] = __('Access denied');
         }
 
         $state = strtolower($status->getState());
 
-        if (!$errors && $ticket->setStatus($status, $_REQUEST['comments'])) {
+        if (!$errors && $ticket->setStatus($status, $_REQUEST['comments'], $errors)) {
 
             if ($state == 'deleted') {
                 $msg = sprintf('%s %s',
@@ -836,12 +948,12 @@ class TicketsAjaxAPI extends AjaxController {
                 $state = 'open';
                 break;
             case 'close':
-                if (!$thisstaff->canCloseTickets())
+                if (!$thisstaff->hasPerm(TicketModel::PERM_CLOSE, false))
                     Http::response(403, 'Access denied');
                 $state = 'closed';
                 break;
             case 'delete':
-                if (!$thisstaff->canDeleteTickets())
+                if (!$thisstaff->hasPerm(TicketModel::PERM_DELETE, false))
                     Http::response(403, 'Access denied');
 
                 $state = 'deleted';
@@ -875,18 +987,18 @@ class TicketsAjaxAPI extends AjaxController {
             // Make sure the agent has permission to set the status
             switch(mb_strtolower($status->getState())) {
                 case 'open':
-                    if (!$thisstaff->canCloseTickets()
-                            && !$thisstaff->canCreateTickets())
+                    if (!$thisstaff->hasPerm(TicketModel::PERM_CLOSE, false)
+                            && !$thisstaff->hasPerm(TicketModel::PERM_CREATE, false))
                         $errors['err'] = sprintf(__('You do not have permission %s.'),
                                 __('to reopen tickets'));
                     break;
                 case 'closed':
-                    if (!$thisstaff->canCloseTickets())
+                    if (!$thisstaff->hasPerm(TicketModel::PERM_CLOSE, false))
                         $errors['err'] = sprintf(__('You do not have permission %s.'),
                                 __('to resolve/close tickets'));
                     break;
                 case 'deleted':
-                    if (!$thisstaff->canDeleteTickets())
+                    if (!$thisstaff->hasPerm(TicketModel::PERM_DELETE, false))
                         $errors['err'] = sprintf(__('You do not have permission %s.'),
                                 __('to archive/delete tickets'));
                     break;
@@ -901,16 +1013,19 @@ class TicketsAjaxAPI extends AjaxController {
             $i = 0;
             $comments = $_REQUEST['comments'];
             foreach ($_REQUEST['tids'] as $tid) {
+
                 if (($ticket=Ticket::lookup($tid))
                         && $ticket->getStatusId() != $status->getId()
-                        && $ticket->checkStaffAccess($thisstaff)
-                        && $ticket->setStatus($status, $comments))
+                        && $ticket->checkStaffPerm($thisstaff)
+                        && $ticket->setStatus($status, $comments, $errors))
                     $i++;
             }
 
-            if (!$i)
-                $errors['err'] = sprintf(__('Unable to change status for %s'),
+            if (!$i) {
+                $errors['err'] = $errors['err']
+                    ?: sprintf(__('Unable to change status for %s'),
                         _N('the selected ticket', 'any of the selected tickets', $count));
+            }
             else {
                 // Assume success
                 if ($i==$count) {
@@ -956,14 +1071,36 @@ class TicketsAjaxAPI extends AjaxController {
         return self::_changeSelectedTicketsStatus($state, $info, $errors);
     }
 
+    function triggerThreadAction($ticket_id, $thread_id, $action) {
+        $thread = ThreadEntry::lookup($thread_id);
+        if (!$thread)
+            Http::response(404, 'No such ticket thread entry');
+        if ($thread->getThread()->getObjectId() != $ticket_id)
+            Http::response(404, 'No such ticket thread entry');
+
+        $valid = false;
+        foreach ($thread->getActions() as $group=>$list) {
+            foreach ($list as $name=>$A) {
+                if ($A->getId() == $action) {
+                    $valid = true; break;
+                }
+            }
+        }
+        if (!$valid)
+            Http::response(400, 'Not a valid action for this thread');
+
+        $thread->triggerAction($action);
+    }
+
     private function _changeSelectedTicketsStatus($state, $info=array(), $errors=array()) {
 
         $count = $_REQUEST['count'] ?:
             ($_REQUEST['tids'] ?  count($_REQUEST['tids']) : 0);
 
-        $info['title'] = sprintf(__('%1$s Tickets &mdash; %2$d selected'),
-                TicketStateField::getVerb($state),
-                 $count);
+        $info['title'] = sprintf(__('Change Status &mdash; %1$d %2$s selected'),
+                 $count,
+                 _N('ticket', 'tickets', $count)
+                 );
 
         if (!strcasecmp($state, 'deleted')) {
 
@@ -1030,5 +1167,140 @@ class TicketsAjaxAPI extends AjaxController {
 
         include(STAFFINC_DIR . 'templates/ticket-status.tmpl.php');
     }
+
+    function tasks($tid) {
+        global $thisstaff;
+
+        if (!($ticket=Ticket::lookup($tid))
+                || !$ticket->checkStaffPerm($thisstaff))
+            Http::response(404, 'Unknown ticket');
+
+         include STAFFINC_DIR . 'ticket-tasks.inc.php';
+    }
+
+    function addTask($tid) {
+        global $thisstaff;
+
+        if (!($ticket=Ticket::lookup($tid)))
+            Http::response(404, 'Unknown ticket');
+
+        if (!$ticket->checkStaffPerm($thisstaff, Task::PERM_CREATE))
+            Http::response(403, 'Permission denied');
+
+        $info=$errors=array();
+
+        if ($_POST) {
+            Draft::deleteForNamespace(
+                    sprintf('ticket.%d.task', $ticket->getId()),
+                    $thisstaff->getId());
+            // Default form
+            $form = TaskForm::getInstance();
+            $form->setSource($_POST);
+            // Internal form
+            $iform = TaskForm::getInternalForm($_POST);
+            $isvalid = true;
+            if (!$iform->isValid())
+                $isvalid = false;
+            if (!$form->isValid())
+                $isvalid = false;
+
+            if ($isvalid) {
+                $vars = $_POST;
+                $vars['object_id'] = $ticket->getId();
+                $vars['object_type'] = ObjectModel::OBJECT_TYPE_TICKET;
+                $vars['default_formdata'] = $form->getClean();
+                $vars['internal_formdata'] = $iform->getClean();
+                $desc = $form->getField('description');
+                if ($desc
+                        && $desc->isAttachmentsEnabled()
+                        && ($attachments=$desc->getWidget()->getAttachments()))
+                    $vars['cannedattachments'] = $attachments->getClean();
+                $vars['staffId'] = $thisstaff->getId();
+                $vars['poster'] = $thisstaff;
+                $vars['ip_address'] = $_SERVER['REMOTE_ADDR'];
+                if (($task=Task::create($vars, $errors)))
+                    Http::response(201, $task->getId());
+            }
+
+            $info['error'] = __('Error adding task - try again!');
+        }
+
+        $info['action'] = sprintf('#tickets/%d/add-task', $ticket->getId());
+        $info['title'] = sprintf(
+                __( 'Ticket #%1$s: %2$s'),
+                $ticket->getNumber(),
+                _('Add New Task')
+                );
+
+         include STAFFINC_DIR . 'templates/task.tmpl.php';
+    }
+
+    function task($tid, $id) {
+        global $thisstaff;
+
+        if (!($ticket=Ticket::lookup($tid))
+                || !$ticket->checkStaffPerm($thisstaff))
+            Http::response(404, 'Unknown ticket');
+
+        // Lookup task and check access
+        if (!($task=Task::lookup($id))
+                || !$task->checkStaffPerm($thisstaff))
+            Http::response(404, 'Unknown task');
+
+        $info = $errors = array();
+        $note_attachments_form = new SimpleForm(array(
+            'attachments' => new FileUploadField(array('id'=>'attach',
+                'name'=>'attach:note',
+                'configuration' => array('extensions'=>'')))
+        ));
+
+        $reply_attachments_form = new SimpleForm(array(
+            'attachments' => new FileUploadField(array('id'=>'attach',
+                'name'=>'attach:reply',
+                'configuration' => array('extensions'=>'')))
+        ));
+
+        if ($_POST) {
+            $vars = $_POST;
+            switch ($_POST['a']) {
+            case 'postnote':
+                $attachments = $note_attachments_form->getField('attachments')->getClean();
+                $vars['cannedattachments'] = array_merge(
+                    $vars['cannedattachments'] ?: array(), $attachments);
+                if (($note=$task->postNote($vars, $errors, $thisstaff))) {
+                    $msg=__('Note posted successfully');
+                    // Clear attachment list
+                    $note_attachments_form->setSource(array());
+                    $note_attachments_form->getField('attachments')->reset();
+                    Draft::deleteForNamespace('task.note.'.$task->getId(),
+                            $thisstaff->getId());
+                } else {
+                    if (!$errors['err'])
+                        $errors['err'] = __('Unable to post the note - missing or invalid data.');
+                }
+                break;
+            case 'postreply':
+                $attachments = $reply_attachments_form->getField('attachments')->getClean();
+                $vars['cannedattachments'] = array_merge(
+                    $vars['cannedattachments'] ?: array(), $attachments);
+                if (($response=$task->postReply($vars, $errors))) {
+                    $msg=__('Update posted successfully');
+                    // Clear attachment list
+                    $reply_attachments_form->setSource(array());
+                    $reply_attachments_form->getField('attachments')->reset();
+                    Draft::deleteForNamespace('task.reply.'.$task->getId(),
+                            $thisstaff->getId());
+                } else {
+                    if (!$errors['err'])
+                        $errors['err'] = __('Unable to post the reply - missing or invalid data.');
+                }
+                break;
+            default:
+                $errors['err'] = __('Unknown action');
+            }
+        }
+
+        include STAFFINC_DIR . 'templates/task-view.tmpl.php';
+    }
 }
 ?>
diff --git a/include/ajax.users.php b/include/ajax.users.php
index 7e00ebbba9691fb74179ad355a9e8e2fb8ab0d7a..dd01ac1deb8bfb4ea261e20d9da37a33aa18d44a 100644
--- a/include/ajax.users.php
+++ b/include/ajax.users.php
@@ -22,61 +22,90 @@ require_once INCLUDE_DIR.'class.note.php';
 
 class UsersAjaxAPI extends AjaxController {
 
-    /* Assumes search by emal for now */
-    function search($type = null) {
+    /* Assumes search by basic info for now */
+    function search($type = null, $fulltext=false) {
 
         if(!isset($_REQUEST['q'])) {
             Http::response(400, __('Query argument is required'));
         }
 
+        $matches = array();
+        if (!$_REQUEST['q'])
+            return $this->json_encode($matches);
+
+        $q = $_REQUEST['q'];
         $limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
         $users=array();
         $emails=array();
+        $matches = array();
+
+        if (strlen($q) < 3)
+            return $this->encode(array());
 
         if (!$type || !strcasecmp($type, 'remote')) {
-            foreach (AuthenticationBackend::searchUsers($_REQUEST['q']) as $u) {
-                $name = new PersonsName(array('first' => $u['first'], 'last' => $u['last']));
-                $users[] = array('email' => $u['email'], 'name'=>(string) $name,
+            foreach (AuthenticationBackend::searchUsers($q) as $u) {
+                if (!trim($u['email']))
+                    // Email is required currently
+                    continue;
+                $name = new UsersName(array('first' => $u['first'], 'last' => $u['last']));
+                $matches[] = array('email' => $u['email'], 'name'=>(string) $name,
                     'info' => "{$u['email']} - $name (remote)",
-                    'id' => "auth:".$u['id'], "/bin/true" => $_REQUEST['q']);
+                    'id' => "auth:".$u['id'], "/bin/true" => $q);
                 $emails[] = $u['email'];
             }
         }
 
         if (!$type || !strcasecmp($type, 'local')) {
-            $remote_emails = ($emails = array_filter($emails))
-                ? ' OR email.address IN ('.implode(',',db_input($emails)).') '
-                : '';
-
-            $q = str_replace(' ', '%', $_REQUEST['q']);
-            $escaped = db_input($q, false);
-            $sql='SELECT DISTINCT user.id, email.address, name '
-                .' FROM '.USER_TABLE.' user '
-                .' JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id '
-                .' LEFT JOIN '.FORM_ENTRY_TABLE.' entry ON (entry.object_type=\'U\' AND entry.object_id = user.id)
-                   LEFT JOIN '.FORM_ANSWER_TABLE.' value ON (value.entry_id=entry.id) '
-                .' WHERE email.address LIKE \'%'.$escaped.'%\'
-                   OR user.name LIKE \'%'.$escaped.'%\'
-                   OR value.value LIKE \'%'.$escaped.'%\''.$remote_emails
-                .' LIMIT '.$limit;
-
-            if(($res=db_query($sql)) && db_num_rows($res)){
-                while(list($id,$email,$name)=db_fetch_row($res)) {
-                    foreach ($users as $i=>$u) {
-                        if ($u['email'] == $email) {
-                            unset($users[$i]);
-                            break;
-                        }
+
+            $users = User::objects()
+                ->values_flat('id', 'name', 'default_email__address')
+                ->limit($limit);
+
+            if ($fulltext) {
+                global $ost;
+                $users = $ost->searcher->find($q, $users);
+                $users->order_by(new SqlCode('__relevance__'), QuerySet::DESC)
+                    ->distinct('id');
+
+                if (!count($emails) && !count($users) && preg_match('`\w$`u', $q)) {
+                    // Do wildcard full-text search
+                    $_REQUEST['q'] = $q."*";
+                    return $this->search($type, $fulltext);
+                }
+            } else {
+                $users->filter(Q::any(array(
+                    'emails__address__contains' => $q,
+                    'name__contains' => $q,
+                    'org__name__contains' => $q,
+                )));
+            }
+
+            // Omit already-imported remote users
+            if ($emails = array_filter($emails)) {
+                $users->union(User::objects()
+                    ->values_flat('id', 'name', 'default_email__address')
+                    ->annotate(array('__relevance__' => new SqlCode(1)))
+                    ->filter(array(
+                        'emails__address__in' => $emails
+                )));
+            }
+
+            foreach ($users as $U) {
+                list($id, $name, $email) = $U;
+                foreach ($matches as $i=>$u) {
+                    if ($u['email'] == $email) {
+                        unset($matches[$i]);
+                        break;
                     }
-                    $name = Format::htmlchars(new PersonsName($name));
-                    $users[] = array('email'=>$email, 'name'=>$name, 'info'=>"$email - $name",
-                        "id" => $id, "/bin/true" => $_REQUEST['q']);
                 }
+                $name = Format::htmlchars(new UsersName($name));
+                $matches[] = array('email'=>$email, 'name'=>$name, 'info'=>"$email - $name",
+                    "id" => $id, "/bin/true" => $_REQUEST['q']);
             }
-            usort($users, function($a, $b) { return strcmp($a['name'], $b['name']); });
+            usort($matches, function($a, $b) { return strcmp($a['name'], $b['name']); });
         }
 
-        return $this->json_encode(array_values($users));
+        return $this->json_encode(array_values($matches));
 
     }
 
@@ -110,6 +139,8 @@ class UsersAjaxAPI extends AjaxController {
 
         if(!$thisstaff)
             Http::response(403, 'Login Required');
+        elseif (!$thisstaff->hasPerm(User::PERM_EDIT))
+            Http::response(403, 'Permission Denied');
         elseif(!($user = User::lookup($id)))
             Http::response(404, 'Unknown user');
 
@@ -126,6 +157,8 @@ class UsersAjaxAPI extends AjaxController {
 
         if(!$thisstaff)
             Http::response(403, 'Login Required');
+        elseif (!$thisstaff->hasPerm(User::PERM_EDIT))
+            Http::response(403, 'Permission Denied');
         elseif(!($user = User::lookup($id)))
             Http::response(404, 'Unknown user');
 
@@ -142,6 +175,8 @@ class UsersAjaxAPI extends AjaxController {
 
         if (!$thisstaff)
             Http::response(403, 'Login Required');
+        elseif (!$thisstaff->hasPerm(User::PERM_MANAGE))
+            Http::response(403, 'Permission Denied');
         elseif (!($user = User::lookup($id)))
             Http::response(404, 'Unknown user');
 
@@ -169,6 +204,8 @@ class UsersAjaxAPI extends AjaxController {
 
         if (!$thisstaff)
             Http::response(403, 'Login Required');
+        elseif (!$thisstaff->hasPerm(User::PERM_MANAGE))
+            Http::response(403, 'Permission Denied');
         elseif (!($user = User::lookup($id)))
             Http::response(404, 'Unknown user');
 
@@ -201,17 +238,17 @@ class UsersAjaxAPI extends AjaxController {
 
         if (!$thisstaff)
             Http::response(403, 'Login Required');
+        elseif (!$thisstaff->hasPerm(User::PERM_DELETE))
+            Http::response(403, 'Permission Denied');
         elseif (!($user = User::lookup($id)))
             Http::response(404, 'Unknown user');
 
         $info = array();
         if ($_POST) {
             if ($user->tickets->count()) {
-                if (!$thisstaff->canDeleteTickets()) {
-                    $info['error'] = __('You do not have permission to delete a user with tickets!');
-                } elseif ($_POST['deletetickets']) {
-                    foreach($user->tickets as $ticket)
-                        $ticket->delete();
+                if ($_POST['deletetickets']) {
+                    if (!$user->deleteAllTickets())
+                        $info['error'] = __('You do not have permission to delete a user with tickets!');
                 } else {
                     $info['error'] = __('You cannot delete a user with tickets!');
                 }
@@ -241,6 +278,7 @@ class UsersAjaxAPI extends AjaxController {
     }
 
     function addUser() {
+        global $thisstaff;
 
         $info = array();
 
@@ -248,6 +286,9 @@ class UsersAjaxAPI extends AjaxController {
             $info['lookup'] = 'local';
 
         if ($_POST) {
+            if (!$thisstaff->hasPerm(User::PERM_CREATE))
+                Http::response(403, 'Permission Denied');
+
             $info['title'] = __('Add New User');
             $form = UserForm::getUserForm()->getForm($_POST);
             if (($user = User::fromForm($form)))
@@ -264,6 +305,8 @@ class UsersAjaxAPI extends AjaxController {
 
         if (!$thisstaff)
             Http::response(403, 'Login Required');
+        elseif (!$thisstaff->hasPerm(User::PERM_CREATE))
+            Http::response(403, 'Permission Denied');
         elseif (!$bk || !$id)
             Http::response(422, 'Backend and user id required');
         elseif (!($backend = AuthenticationBackend::getSearchDirectoryBackend($bk))
@@ -285,6 +328,8 @@ class UsersAjaxAPI extends AjaxController {
 
         if (!$thisstaff)
             Http::response(403, 'Login Required');
+        elseif (!$thisstaff->hasPerm(User::PERM_CREATE))
+            Http::response(403, 'Permission Denied');
 
         $info = array(
             'title' => __('Import Users'),
@@ -305,6 +350,7 @@ class UsersAjaxAPI extends AjaxController {
     }
 
     function selectUser($id) {
+        global $thisstaff;
 
         if ($id)
             $user = User::lookup($id);
@@ -320,9 +366,14 @@ class UsersAjaxAPI extends AjaxController {
     }
 
     static function _lookupform($form=null, $info=array()) {
+        global $thisstaff;
 
-        if (!$info or !$info['title'])
-            $info += array('title' => __('Lookup or create a user'));
+        if (!$info or !$info['title']) {
+            if ($thisstaff->hasPerm(User::PERM_CREATE))
+                $info += array('title' => __('Lookup or create a user'));
+            else
+                $info += array('title' => __('Lookup a user'));
+        }
 
         ob_start();
         include(STAFFINC_DIR . 'templates/user-lookup.tmpl.php');
@@ -413,7 +464,7 @@ class UsersAjaxAPI extends AjaxController {
     }
 
     function manageForms($user_id) {
-        $forms = DynamicFormEntry::forUser($user_id);
+        $forms = DynamicFormEntry::forObject($user_id, 'U');
         $info = array('action' => '#users/'.Format::htmlchars($user_id).'/forms/manage');
         include(STAFFINC_DIR . 'templates/form-manage.tmpl.php');
     }
@@ -423,13 +474,15 @@ class UsersAjaxAPI extends AjaxController {
 
         if (!$thisstaff)
             Http::response(403, "Login required");
+        elseif (!$thisstaff->hasPerm(User::PERM_EDIT))
+            Http::response(403, 'Permission Denied');
         elseif (!($user = User::lookup($user_id)))
             Http::response(404, "No such user");
         elseif (!isset($_POST['forms']))
             Http::response(422, "Send updated forms list");
 
         // Add new forms
-        $forms = DynamicFormEntry::forUser($user_id);
+        $forms = DynamicFormEntry::forObject($user_id, 'U');
         foreach ($_POST['forms'] as $sort => $id) {
             $found = false;
             foreach ($forms as $e) {
diff --git a/include/api.tickets.php b/include/api.tickets.php
index d80b1582889f7bf3f944eea9808a3f675f6008f2..e2ee066aefff68921a26dc4bfbd28b62008b7a8e 100644
--- a/include/api.tickets.php
+++ b/include/api.tickets.php
@@ -20,9 +20,10 @@ class TicketApiController extends ApiController {
         # the names to the supported request structure
         if (isset($data['topicId'])
                 && ($topic = Topic::lookup($data['topicId']))
-                && ($form = $topic->getForm())) {
-            foreach ($form->getDynamicFields() as $field)
-                $supported[] = $field->get('name');
+                && ($forms = $topic->getForms())) {
+            foreach ($forms as $form)
+                foreach ($form->getDynamicFields() as $field)
+                    $supported[] = $field->get('name');
         }
 
         # Ticket form fields
@@ -40,7 +41,7 @@ class TicketApiController extends ApiController {
             $supported = array_merge($supported, array('header', 'mid',
                 'emailId', 'to-email-id', 'ticketId', 'reply-to', 'reply-to-name',
                 'in-reply-to', 'references', 'thread-type',
-                'flags' => array('bounce', 'auto-reply', 'spam', 'viral'),
+                'mailflags' => array('bounce', 'auto-reply', 'spam', 'viral'),
                 'recipients' => array('*' => array('name', 'email', 'source'))
                 ));
 
@@ -80,7 +81,8 @@ class TicketApiController extends ApiController {
                 }
                 // Validate and save immediately
                 try {
-                    $file['id'] = $fileField->uploadAttachment($file);
+                    $F = $fileField->uploadAttachment($file);
+                    $file['id'] = $F->getId();
                 }
                 catch (FileUploadError $ex) {
                     $file['error'] = $file['name'] . ': ' . $ex->getMessage();
@@ -150,14 +152,29 @@ class TicketApiController extends ApiController {
         if (!$data)
             $data = $this->getEmailRequest();
 
-        if (($thread = ThreadEntry::lookupByEmailHeaders($data))
-                && ($t=$thread->getTicket())
-                && ($data['staffId']
-                    || !$t->isClosed()
-                    || $t->isReopenable())
-                && $thread->postEmail($data)) {
-            return $thread->getTicket();
+        $seen = false;
+        if (($entry = ThreadEntry::lookupByEmailHeaders($data, $seen))
+            && ($message = $entry->postEmail($data))
+        ) {
+            if ($message instanceof ThreadEntry) {
+                return $message->getThread()->getObject();
+            }
+            else if ($seen) {
+                // Email has been processed previously
+                return $entry->getThread()->getObject();
+            }
+        }
+
+        // Allow continuation of thread without initial message or note
+        elseif (($thread = Thread::lookupByEmailHeaders($data))
+            && ($message = $thread->postEmail($data))
+        ) {
+            return $thread->getObject();
         }
+
+        // All emails which do not appear to be part of an existing thread
+        // will always create new "Tickets". All other objects will need to
+        // be created via the web interface or the API
         return $this->createTicket($data);
     }
 
diff --git a/include/class.ajax.php b/include/class.ajax.php
index 870d5ae88aaf3fd1cea9392a36c566704ceea67a..143dbac2a7d5f95cde71d620264660cfd75b2365 100644
--- a/include/class.ajax.php
+++ b/include/class.ajax.php
@@ -25,9 +25,6 @@ require_once (INCLUDE_DIR.'class.api.php');
  * consistency.
  */
 class AjaxController extends ApiController {
-    function AjaxController() {
-    
-    }
     function staffOnly() {
         global $thisstaff;
         if(!$thisstaff || !$thisstaff->isValid()) {
diff --git a/include/class.api.php b/include/class.api.php
index fef6ddb477f6536ebcdeb1b0c0200e6bfa76bb89..818b8826e8cf8526643c44d0020559b1564e47a2 100644
--- a/include/class.api.php
+++ b/include/class.api.php
@@ -19,7 +19,7 @@ class API {
 
     var $ht;
 
-    function API($id) {
+    function __construct($id) {
         $this->id = 0;
         $this->load($id);
     }
@@ -196,7 +196,7 @@ class ApiController {
     function getRequest($format) {
         global $ost;
 
-        $input = $ost->is_cli()?'php://stdin':'php://input';
+        $input = osTicket::is_cli()?'php://stdin':'php://input';
 
         if (!($stream = @fopen($input, 'r')))
             $this->exerr(400, __("Unable to read request body"));
@@ -353,12 +353,12 @@ class ApiXmlDataParser extends XmlDataParser {
                     $value['body'] = Charset::utf8($value['body'], $value['encoding']);
 
                 if (!strcasecmp($value['type'], 'text/html'))
-                    $value = new HtmlThreadBody($value['body']);
+                    $value = new HtmlThreadEntryBody($value['body']);
                 else
-                    $value = new TextThreadBody($value['body']);
+                    $value = new TextThreadEntryBody($value['body']);
 
             } else if ($key == "attachments") {
-                if(!isset($value['file'][':text']))
+                if(isset($value['file']) && !isset($value['file'][':text']))
                     $value = $value['file'];
 
                 if($value && is_array($value)) {
@@ -398,9 +398,9 @@ class ApiJsonDataParser extends JsonDataParser {
                 $data = Format::parseRfc2397($value, 'utf-8');
 
                 if (isset($data['type']) && $data['type'] == 'text/html')
-                    $value = new HtmlThreadBody($data['data']);
+                    $value = new HtmlThreadEntryBody($data['data']);
                 else
-                    $value = new TextThreadBody($data['data']);
+                    $value = new TextThreadEntryBody($data['data']);
 
             } else if ($key == "attachments") {
                 foreach ($value as &$info) {
diff --git a/include/class.attachment.php b/include/class.attachment.php
index 3f2101e18be4a18695f8f67a3eb5289006c8f8fc..15f3fd9b1dec6de619247e1158771a73434ba797 100644
--- a/include/class.attachment.php
+++ b/include/class.attachment.php
@@ -16,62 +16,48 @@
 require_once(INCLUDE_DIR.'class.ticket.php');
 require_once(INCLUDE_DIR.'class.file.php');
 
-class Attachment {
-    var $id;
-    var $file_id;
-    var $ticket_id;
-
-    var $info;
-
-    function Attachment($id,$tid=0) {
-
-        $sql='SELECT * FROM '.TICKET_ATTACHMENT_TABLE.' WHERE attach_id='.db_input($id);
-        if($tid)
-            $sql.=' AND ticket_id='.db_input($tid);
-
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
-
-        $this->ht=db_fetch_array($res);
-
-        $this->id=$this->ht['attach_id'];
-        $this->file_id=$this->ht['file_id'];
-        $this->ticket_id=$this->ht['ticket_id'];
-
-        $this->file=null;
-        $this->ticket=null;
-
-        return true;
-    }
+class Attachment extends VerySimpleModel {
+    static $meta = array(
+        'table' => ATTACHMENT_TABLE,
+        'pk' => array('id'),
+        'select_related' => array('file'),
+        'joins' => array(
+            'draft' => array(
+                'constraint' => array(
+                    'type' => "'D'",
+                    'object_id' => 'Draft.id',
+                ),
+            ),
+            'file' => array(
+                'constraint' => array(
+                    'file_id' => 'AttachmentFile.id',
+                ),
+            ),
+            'thread_entry' => array(
+                'constraint' => array(
+                    'type' => "'H'",
+                    'object_id' => 'ThreadEntry.id',
+                ),
+            ),
+        ),
+    );
+
+    var $object;
 
     function getId() {
         return $this->id;
     }
 
-    function getTicketId() {
-        return $this->ticket_id;
-    }
-
-    function getTicket() {
-        if(!$this->ticket && $this->getTicketId())
-            $this->ticket = Ticket::lookup($this->getTicketId());
-
-        return $this->ticket;
-    }
-
     function getFileId() {
         return $this->file_id;
     }
 
     function getFile() {
-        if(!$this->file && $this->getFileId())
-            $this->file = AttachmentFile::lookup($this->getFileId());
-
         return $this->file;
     }
 
-    function getCreateDate() {
-        return $this->ht['created'];
+    function getFilename() {
+        return $this->name ?: $this->file->name;
     }
 
     function getHashtable() {
@@ -82,140 +68,146 @@ class Attachment {
         return $this->getHashtable();
     }
 
-    /* Static functions */
-    function getIdByFileHash($hash, $tid=0) {
-        $sql='SELECT attach_id FROM '.TICKET_ATTACHMENT_TABLE.' a '
-            .' INNER JOIN '.FILE_TABLE.' f ON(f.id=a.file_id) '
-            .' WHERE f.`key`='.db_input($hash);
-        if($tid)
-            $sql.=' AND a.ticket_id='.db_input($tid);
+    function getObject() {
 
-        return db_result(db_query($sql));
-    }
-
-    function lookup($var,$tid=0) {
-        $id=is_numeric($var)?$var:self::getIdByFileHash($var,$tid);
+        if (!isset($this->object))
+            $this->object = ObjectModel::lookup(
+                    $this->ht['object_id'], $this->ht['type']);
 
-        return ($id && is_numeric($id)
-            && ($attach = new Attachment($id,$tid))
-            && $attach->getId()==$id)?$attach:null;
+        return $this->object;
     }
 
-}
+    static function lookupByFileHash($hash, $objectId=0) {
+        $file = static::objects()
+            ->filter(array('file__key' => $hash));
 
-class GenericAttachments {
+        if ($objectId)
+            $file->filter(array('object_id' => $objectId));
 
-    var $id;
-    var $type;
+        return $file->first();
+    }
 
-    function GenericAttachments($object_id, $type) {
-        $this->id = $object_id;
-        $this->type = $type;
+    static function lookup($var, $objectId=0) {
+        return (is_string($var))
+            ? static::lookupByFileHash($var, $objectId)
+            : parent::lookup($var);
     }
+}
 
-    function getId() { return $this->id; }
-    function getType() { return $this->type; }
+class GenericAttachments
+extends InstrumentedList {
+
+    var $lang;
+
+    function getId() { return $this->key['object_id']; }
+    function getType() { return $this->key['type']; }
+
+    /**
+     * Drop attachments whose file_id values are not in the included list,
+     * additionally, add new files whose IDs are in the list provided.
+     */
+    function keepOnlyFileIds($ids, $inline=false, $lang=false) {
+        if (!$ids) $ids = array();
+        $new = array_fill_keys($ids, 1);
+        foreach ($this as $A) {
+            if (!isset($new[$A->file_id]) && $A->lang == $lang && $A->inline == $inline)
+                // Not in the $ids list, delete
+                $this->remove($A);
+            unset($new[$A->file_id]);
+        }
+        // Everything remaining in $new is truly new
+        $this->upload(array_keys($new), $inline, $lang);
+    }
 
-    function upload($files, $inline=false) {
+    function upload($files, $inline=false, $lang=false) {
         $i=array();
-        if (!is_array($files)) $files=array($files);
+        if (!is_array($files))
+            $files = array($files);
         foreach ($files as $file) {
             if (is_numeric($file))
                 $fileId = $file;
             elseif (is_array($file) && isset($file['id']))
                 $fileId = $file['id'];
-            elseif (!($fileId = AttachmentFile::upload($file)))
+            elseif (isset($file['tmp_name']) && ($F = AttachmentFile::upload($file)))
+                $fileId = $F->getId();
+            elseif ($F = AttachmentFile::create($file))
+                $fileId = $F->getId();
+            else
                 continue;
 
             $_inline = isset($file['inline']) ? $file['inline'] : $inline;
 
-            $sql ='INSERT INTO '.ATTACHMENT_TABLE
-                .' SET `type`='.db_input($this->getType())
-                .',object_id='.db_input($this->getId())
-                .',file_id='.db_input($fileId)
-                .',inline='.db_input($_inline ? 1 : 0);
+            $att = $this->add(new Attachment(array(
+                'file_id' => $fileId,
+                'inline' => $_inline ? 1 : 0,
+            )));
+
+            // Record varying file names in the attachment record
+            if (is_array($file) && isset($file['name'])) {
+                $filename = $file['name'];
+            }
+            if ($filename) {
+                // This should be a noop since the ORM caches on PK
+                $file = $F ?: AttachmentFile::lookup($fileId);
+                // XXX: This is not Unicode safe
+                if ($file && 0 !== strcasecmp($file->name, $filename))
+                    $att->name = $filename;
+            }
+            if ($lang)
+                $att->lang = $lang;
+
             // File may already be associated with the draft (in the
             // event it was deleted and re-added)
-            if (db_query($sql, function($errno) { return $errno != 1062; })
-                    || db_errno() == 1062)
-                $i[] = $fileId;
+            $att->save();
+            $i[] = $fileId;
         }
-
         return $i;
     }
 
     function save($file, $inline=true) {
+        $ids = $this->upload($file, $inline);
+        return $ids[0];
+    }
 
-        if (is_numeric($file))
-            $fileId = $file;
-        elseif (is_array($file) && isset($file['id']))
-            $fileId = $file['id'];
-        elseif (!($fileId = AttachmentFile::save($file)))
-            return false;
-
-        $sql ='INSERT INTO '.ATTACHMENT_TABLE
-            .' SET `type`='.db_input($this->getType())
-            .',object_id='.db_input($this->getId())
-            .',file_id='.db_input($fileId)
-            .',inline='.db_input($inline ? 1 : 0);
-        if (!db_query($sql) || !db_affected_rows())
-            return false;
-
-        return $fileId;
-    }
-
-    function getInlines() { return $this->_getList(false, true); }
-    function getSeparates() { return $this->_getList(true, false); }
-    function getAll() { return $this->_getList(true, true); }
-
-    function _getList($separate=false, $inlines=false) {
-        if(!isset($this->attachments)) {
-            $this->attachments = array();
-            $sql='SELECT f.id, f.size, f.`key`, f.signature, f.name, a.inline '
-                .' FROM '.FILE_TABLE.' f '
-                .' INNER JOIN '.ATTACHMENT_TABLE.' a ON(f.id=a.file_id) '
-                .' WHERE a.`type`='.db_input($this->getType())
-                .' 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_url'] = AttachmentFile::generateDownloadUrl(
-                        $rec['id'], $rec['key'], $rec['signature']);
-                    $this->attachments[] = $rec;
-                }
-            }
-        }
-        $attachments = array();
-        foreach ($this->attachments as $a) {
-            if ($a['inline'] != $separate || $a['inline'] == $inlines) {
-                $a['file_id'] = $a['id'];
-                $a['hash'] = md5($a['file_id'].session_id().strtolower($a['key']));
-                $attachments[] = $a;
-            }
-        }
-        return $attachments;
+    function getInlines($lang=false) { return $this->_getList(false, true, $lang); }
+    function getSeparates($lang=false) { return $this->_getList(true, false, $lang); }
+    function getAll($lang=false) { return $this->_getList(true, true, $lang); }
+    function count($lang=false) { return count($this->getSeparates($lang)); }
+
+    function _getList($separates=false, $inlines=false, $lang=false) {
+        $base = $this;
+
+        if ($separates && !$inlines)
+            $base = $base->filter(array('inline' => 0));
+        elseif (!$separates && $inlines)
+            $base = $base->filter(array('inline' => 1));
+
+        if ($lang)
+            $base = $base->filter(array('lang' => $lang));
+
+        return $base;
     }
 
     function delete($file_id) {
-        $deleted = 0;
-        $sql='DELETE FROM '.ATTACHMENT_TABLE
-            .' WHERE object_id='.db_input($this->getId())
-            .'   AND `type`='.db_input($this->getType())
-            .'   AND file_id='.db_input($file_id);
-        return db_query($sql) && db_affected_rows() > 0;
+        return $this->objects()->filter(array('file_id'=>$file_id))->delete();
     }
 
     function deleteAll($inline_only=false){
-        $deleted=0;
-        $sql='DELETE FROM '.ATTACHMENT_TABLE
-            .' WHERE object_id='.db_input($this->getId())
-            .'   AND `type`='.db_input($this->getType());
         if ($inline_only)
-            $sql .= ' AND inline = 1';
-        return db_query($sql) && db_affected_rows() > 0;
+            return $this->objects()->filter(array('inline' => 1))->delete();
+
+        return parent::expunge();
     }
 
     function deleteInlines() {
         return $this->deleteAll(true);
     }
+
+    static function forIdAndType($id, $type) {
+        return new static(array(
+            'Attachment',
+            array('object_id' => $id, 'type' => $type)
+        ));
+    }
 }
 ?>
diff --git a/include/class.auth.php b/include/class.auth.php
index 4a97ab01adb25d60a6bfb74f69628de824772c4a..a924d9cd5a990bc507ba0028014490a5445e732b 100644
--- a/include/class.auth.php
+++ b/include/class.auth.php
@@ -1,16 +1,39 @@
 <?php
-require(INCLUDE_DIR.'class.ostsession.php');
-require(INCLUDE_DIR.'class.usersession.php');
 
+interface AuthenticatedUser {
+/* PHP 5.3 < 5.3.8 will crash with some abstract inheritance issue
+ * ------------------------------------------------------------
+    // Get basic information
+    function getId();
+    function getUsername();
+    function getUserType();
+
+    //Backend used to authenticate the user
+    function getAuthBackend();
+
+    //Authentication key
+    function setAuthKey($key);
 
-abstract class AuthenticatedUser {
+    function getAuthKey();
+
+    // logOut the user
+    function logOut();
+
+    // Signal method to allow performing extra things when a user is logged
+    // into the sysem
+    function onLogin($bk);
+ */
+}
+
+abstract class BaseAuthenticatedUser
+implements AuthenticatedUser {
     //Authorization key returned by the backend used to authorize the user
     private $authkey;
 
     // Get basic information
     abstract function getId();
     abstract function getUsername();
-    abstract function getRole();
+    abstract function getUserType();
 
     //Backend used to authenticate the user
     abstract function getAuthBackend();
@@ -32,8 +55,15 @@ abstract class AuthenticatedUser {
 
         return false;
     }
+
+    // Signal method to allow performing extra things when a user is logged
+    // into the sysem
+    function onLogin($bk) {}
 }
 
+require_once(INCLUDE_DIR.'class.ostsession.php');
+require_once(INCLUDE_DIR.'class.usersession.php');
+
 interface AuthDirectorySearch {
     /**
      * Indicates if the backend can be used to search for user information.
@@ -97,14 +127,13 @@ class ClientCreateRequest {
         $this_form = UserForm::getUserForm()->getForm($this->getInfo());
         $bk = $this->getBackend();
         $defaults = array(
-            'timezone_id' => $cfg->getDefaultTimezoneId(),
-            'dst' => $cfg->observeDaylightSaving(),
+            'timezone' => $cfg->getDefaultTimezone(),
             'username' => $this->getUsername(),
         );
         if ($bk->supportsInteractiveAuthentication())
             // User can only be authenticated against this backend
             $defaults['backend'] = $bk::$id;
-        if ($this_form->isValid(function($f) { return !$f->get('private'); })
+        if ($this_form->isValid(function($f) { return !$f->isVisibleToUsers(); })
                 && ($U = User::fromVars($this_form->getClean()))
                 && ($acct = ClientAccount::createForUser($U, $defaults))
                 // Confirm and save the account
@@ -186,7 +215,7 @@ abstract class AuthenticationBackend {
             $bk->audit($result, $credentials);
     }
 
-    static function process($username, $password=null, &$errors) {
+    static function process($username, $password=null, &$errors=array()) {
 
         if (!$username)
             return false;
@@ -321,6 +350,38 @@ abstract class AuthenticationBackend {
         return false;
     }
 
+    /**
+     * Request the backend to update the password for a user. This method is
+     * the main entry for password updates so that password policies can be
+     * applied to the new password before passing the new password to the
+     * backend for updating.
+     *
+     * Throws:
+     * BadPassword — if password does not meet policy requirement
+     * PasswordUpdateFailed — if backend failed to update the password
+     */
+    function setPassword($user, $password, $current=false) {
+        PasswordPolicy::checkPassword($password, $current);
+        $rv = $this->syncPassword($user, $password);
+        if ($rv) {
+            $info = array('password' => $password, 'current' => $current);
+            Signal::send('auth.pwchange', $user, $info);
+        }
+        return $rv;
+    }
+
+    /**
+     * Request the backend to update the user's password with the password
+     * given. This method should only be used if the backend advertises
+     * supported password updates with the supportsPasswordChange() method.
+     *
+     * Returns:
+     * true if the password was successfully updated and false otherwise.
+     */
+    protected function syncPassword($user, $password) {
+        return false;
+    }
+
     function supportsPasswordReset() {
         return false;
     }
@@ -427,14 +488,10 @@ abstract class StaffAuthenticationBackend  extends AuthenticationBackend {
             sprintf(_S("%s logged in [%s], via %s"), $staff->getUserName(),
                 $_SERVER['REMOTE_ADDR'], get_class($bk))); //Debug.
 
-        $sql='UPDATE '.STAFF_TABLE.' SET lastlogin=NOW() '
-            .' WHERE staff_id='.db_input($staff->getId());
-        db_query($sql);
-
-        //Tag the authkey.
+        // Tag the authkey.
         $authkey = $bk::$id.':'.$authkey;
 
-        //Now set session crap and lets roll baby!
+        // Now set session crap and lets roll baby!
         $authsession = &$_SESSION['_auth']['staff'];
 
         $authsession = array(); //clear.
@@ -444,14 +501,14 @@ abstract class StaffAuthenticationBackend  extends AuthenticationBackend {
         $staff->setAuthKey($authkey);
         $staff->refreshSession(true); //set the hash.
 
-        $_SESSION['TZ_OFFSET'] = $staff->getTZoffset();
-        $_SESSION['TZ_DST'] = $staff->observeDaylight();
-
         Signal::send('auth.login.succeeded', $staff);
 
         if ($bk->supportsInteractiveAuthentication())
             $staff->cancelResetTokens();
 
+        // Update last-used language, login time, etc
+        $staff->onLogin($bk);
+
         return true;
     }
 
@@ -510,7 +567,7 @@ abstract class StaffAuthenticationBackend  extends AuthenticationBackend {
 
     protected function validate($authkey) {
 
-        if (($staff = new StaffSession($authkey)) && $staff->getId())
+        if (($staff = StaffSession::lookup($authkey)) && $staff->getId())
             return $staff;
     }
 }
@@ -614,11 +671,6 @@ abstract class UserAuthenticationBackend  extends AuthenticationBackend {
 
         $user->refreshSession(true); //set the hash.
 
-        if (($acct = $user->getAccount()) && ($tid = $acct->get('timezone_id'))) {
-            $_SESSION['TZ_OFFSET'] = Timezone::getOffsetById($tid);
-            $_SESSION['TZ_DST'] = $acct->get('dst');
-        }
-
         //Log login info...
         $msg=sprintf(_S('%1$s (%2$s) logged in [%3$s]'
                 /* Tokens are <username>, <id>, and <ip> */),
@@ -628,6 +680,9 @@ abstract class UserAuthenticationBackend  extends AuthenticationBackend {
         if ($bk->supportsInteractiveAuthentication() && ($acct=$user->getAccount()))
             $acct->cancelResetTokens();
 
+        // Update last-used language, login time, etc
+        $user->onLogin($bk);
+
         return true;
     }
 
@@ -912,7 +967,7 @@ class osTicketAuthentication extends StaffAuthenticationBackend {
     static $id = "local";
 
     function authenticate($username, $password) {
-        if (($user = new StaffSession($username)) && $user->getId() &&
+        if (($user = StaffSession::lookup($username)) && $user->getId() &&
                 $user->check_passwd($password)) {
 
             //update last login && password reset stuff.
@@ -926,6 +981,14 @@ class osTicketAuthentication extends StaffAuthenticationBackend {
         }
     }
 
+    function supportsPasswordChange() {
+        return true;
+    }
+
+    function syncPassword($staff, $password) {
+        $staff->passwd = Passwd::hash($password);
+    }
+
 }
 StaffAuthenticationBackend::register('osTicketAuthentication');
 
@@ -943,8 +1006,9 @@ class PasswordResetTokenBackend extends StaffAuthenticationBackend {
             return false;
         elseif (!($_config = new Config('pwreset')))
             return false;
-        elseif (($staff = new StaffSession($_POST['userid'])) &&
-                !$staff->getId())
+
+        $staff = StaffSession::lookup($_POST['userid']);
+        if (!$staff || !$staff->getId())
             $errors['msg'] = __('Invalid user-id given');
         elseif (!($id = $_config->get($_POST['token']))
                 || $id != $staff->getId())
@@ -981,6 +1045,11 @@ class AuthTokenAuthentication extends UserAuthenticationBackend {
 
 
     function signOn() {
+        global $cfg;
+
+
+        if (!$cfg || !$cfg->isAuthTokenEnabled())
+            return null;
 
         $user = null;
         if ($_GET['auth']) {
@@ -1030,8 +1099,10 @@ class AuthTokenAuthentication extends UserAuthenticationBackend {
         $user = null;
         switch ($matches['type']) {
             case 'c': //Collaborator
-                $criteria = array( 'userId' => $matches['id'],
-                        'ticketId' => $matches['tid']);
+                $criteria = array(
+                    'user_id' => $matches['id'],
+                    'thread__ticket__ticket_id' => $matches['tid']
+                );
                 if (($c = Collaborator::lookup($criteria))
                         && ($c->getTicketId() == $matches['tid']))
                     $user = new ClientSession($c);
@@ -1054,6 +1125,7 @@ class AuthTokenAuthentication extends UserAuthenticationBackend {
     }
 
 }
+
 UserAuthenticationBackend::register('AuthTokenAuthentication');
 
 //Simple ticket lookup backend used to recover ticket access link.
@@ -1082,8 +1154,9 @@ class AccessLinkAuthentication extends UserAuthenticationBackend {
             $user = $ticket->getOwner();
         // Collaborator?
         elseif (!($user = Collaborator::lookup(array(
-                'userId' => $user->getId(),
-                'ticketId' => $ticket->getId()))))
+                'user_id' => $user->getId(),
+                'thread__ticket__ticket_id' => $ticket->getId())
+        )))
             return false; //Bro, we don't know you!
 
         return $user;
@@ -1160,7 +1233,7 @@ class ClientPasswordResetTokenBackend extends UserAuthenticationBackend {
                 || !($client = new ClientSession(new EndUser($acct->getUser()))))
             $errors['msg'] = __('Invalid user-id given');
         elseif (!($id = $_config->get($_POST['token']))
-                || $id != $client->getId())
+                || $id != 'c'.$client->getId())
             $errors['msg'] = __('Invalid reset token');
         elseif (!($ts = $_config->lastModified($_POST['token']))
                 && ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
@@ -1195,9 +1268,9 @@ class ClientAcctConfirmationTokenBackend extends UserAuthenticationBackend {
             return false;
         elseif (!($id = $_config->get($_GET['token'])))
             return false;
-        elseif (!($acct = ClientAccount::lookup(array('user_id'=>$id)))
+        elseif (!($acct = ClientAccount::lookup(array('user_id'=>substr($id,1))))
                 || !$acct->getId()
-                || $id != $acct->getUserId()
+                || $id != 'c'.$acct->getUserId()
                 || !($client = new ClientSession(new EndUser($acct->getUser()))))
             return false;
         else
@@ -1205,4 +1278,56 @@ class ClientAcctConfirmationTokenBackend extends UserAuthenticationBackend {
     }
 }
 UserAuthenticationBackend::register('ClientAcctConfirmationTokenBackend');
+
+// ----- Password Policy --------------------------------------
+
+class BadPassword extends Exception {}
+class PasswordUpdateFailed extends Exception {}
+
+abstract class PasswordPolicy {
+    static protected $registry = array();
+
+    /**
+     * Check a password and throw BadPassword with a meaningful message if
+     * the password cannot be accepted.
+     */
+    abstract function processPassword($new, $current);
+
+    static function checkPassword($new, $current) {
+        foreach (static::allActivePolicies() as $P) {
+            $P->processPassword($new, $current);
+        }
+    }
+
+    static function allActivePolicies() {
+        $policies = array();
+        foreach (static::$registry as $P) {
+            if (is_string($P) && class_exists($P))
+                $P = new $P();
+            if ($P instanceof PasswordPolicy)
+                $policies[] = $P;
+        }
+        return $policies;
+    }
+
+    static function register($policy) {
+        static::$registry[] = $policy;
+    }
+}
+
+class osTicketPasswordPolicy
+extends PasswordPolicy {
+    function processPassword($passwd, $current) {
+        if (strlen($passwd) < 6) {
+            throw new BadPassword(
+                __('Password must be at least 6 characters'));
+        }
+        // XXX: Changing case is technicall changing the password
+        if (0 === strcasecmp($passwd, $current)) {
+            throw new BadPassword(
+                __('New password MUST be different from the current password!'));
+        }
+    }
+}
+PasswordPolicy::register('osTicketPasswordPolicy');
 ?>
diff --git a/include/class.avatar.php b/include/class.avatar.php
new file mode 100644
index 0000000000000000000000000000000000000000..ee55147fc46442652bf1826386cf6f804c949ba4
--- /dev/null
+++ b/include/class.avatar.php
@@ -0,0 +1,239 @@
+<?php
+/*********************************************************************
+    class.avatar.php
+
+    Avatar sources for users and agents
+
+    Jared Hancock <jared@osticket.com>
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  2006-2015 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:
+**********************************************************************/
+
+abstract class Avatar {
+    var $user;
+
+    function __construct($user) {
+        $this->user = $user;
+    }
+
+    abstract function getUrl($size);
+
+    function getImageTag($size=null) {
+        $style = ($size)
+            ? sprintf('style="max-height:%spx"', $size)
+            : '';
+        return "<img {$style} class=\"avatar\" alt=\""
+            .__('Avatar').'" src="'.$this->getUrl($size).'" />';
+    }
+
+    function __toString() {
+        return $this->getImageTag();
+    }
+
+    function isChangeable() {
+        return false;
+    }
+    function toggle() {}
+}
+
+abstract class AvatarSource {
+    static $id;
+    static $name;
+    var $mode;
+
+    function __construct($mode=null) {
+        if (isset($mode))
+            $this->mode = $mode;
+    }
+
+    function getName() {
+        return __(static::$name);
+    }
+
+    abstract function getAvatar($user);
+
+    static $registry = array();
+    static function register($class) {
+        if (!class_exists($class))
+            throw new Exception($class.': Does not exist');
+        if (!isset($class::$id))
+            throw new Exception($class.': AvatarClass must specify $id');
+        static::$registry[$class::$id] = $class;
+    }
+
+    static function lookup($id, $mode=null) {
+        $class = static::$registry[$id];
+        if (!isset($class))
+            ; // TODO: Return built-in avatar source
+        if (is_string($class))
+            $class = static::$registry[$id] = new $class($mode);
+        return $class;
+    }
+
+    static function allSources() {
+        return static::$registry;
+    }
+
+    static function getModes() {
+        return null;
+    }
+}
+
+class LocalAvatarSource
+extends AvatarSource {
+    static $id = 'local';
+    static $name = /* @trans */ 'Built-In';
+    var $mode = 'ateam';
+
+    static function getModes() {
+        return array(
+            'ateam' => __("Oscar's A-Team"),
+        );
+    }
+
+    function getAvatar($user) {
+        return new LocalAvatar($user, $this->mode);
+    }
+}
+AvatarSource::register('LocalAvatarSource');
+
+class LocalAvatar
+extends Avatar {
+    var $mode;
+    var $code;
+
+    function __construct($user, $mode) {
+        parent::__construct($user);
+        $this->mode = $mode;
+    }
+
+    function getUrl($size) {
+        $code = $this->code;
+        if (!$code && method_exists($this->user, 'getExtraAttr'))
+            $code = $this->user->getExtraAttr('avatar');
+
+        if ($code)
+            $uid = md5($code);
+        else
+            // Generate a random string of 0-6 chars for the avatar signature
+            $uid = md5(strtolower($this->user->getEmail()));
+
+        return ROOT_PATH . 'avatar.php?'.Http::build_query(array('uid'=>$uid,
+            'mode' => $this->mode));
+    }
+
+    function toggle() {
+        $this->code = Misc::randCode(21);
+        return $this->code;
+    }
+
+    function isChangeable() {
+        return true;
+    }
+}
+
+class RandomAvatar {
+    var $mode;
+
+    static $sprites = array(
+        'ateam' => array(
+            'file' => 'images/avatar-sprite-ateam.png',
+            'grid' => 96,
+        ),
+    );
+
+    function __construct($mode) {
+        $this->mode = $mode;
+    }
+
+    function makeAvatar($uid) {
+        $sprite = self::$sprites[$this->mode];
+        if (!$sprite || !is_readable(ROOT_DIR . $sprite['file']) || !extension_loaded('gd'))
+            Http::redirect(ROOT_PATH.'images/mystery-oscar.png');
+
+        $source =  imagecreatefrompng(ROOT_DIR . $sprite['file']);
+        $grid = $sprite['grid'];
+        $avatar = imagecreatetruecolor($grid, $grid);
+        $width = imagesx($source) / $grid;
+        $height = imagesy($source) / $grid;
+
+        // Start with a white matte
+        $white = imagecolorallocate($avatar, 255, 255, 255);
+        imagefill($avatar, 0, 0, $white);
+
+        for ($i=0, $k=$height; $i<$k; $i++) {
+            $idx = hexdec($uid[$i]) % $width;
+            imagecopy($avatar, $source, 0, 0, $idx*$grid, $i*$grid, $grid, $grid);
+        }
+
+        return $avatar;
+    }
+}
+
+class AvatarsByGravatar
+extends AvatarSource {
+    static $name = 'Gravatar';
+    static $id = 'gravatar';
+    var $mode;
+
+    function __construct($mode=null) {
+        $this->mode = $mode ?: 'retro';
+    }
+
+    static function getModes() {
+        return array(
+            'mm' => __('Mystery Man'),
+            'identicon' => 'Identicon',
+            'monsterid' => 'Monster',
+            'wavatar' => 'Wavatar',
+            'retro' => 'Retro',
+        );
+    }
+
+    function getAvatar($user) {
+        return new Gravatar($user, $this->mode);
+    }
+}
+AvatarSource::register('AvatarsByGravatar');
+
+class Gravatar
+extends Avatar {
+    var $email;
+    var $d;
+    var $size;
+
+    function __construct($user, $imageset) {
+        $this->email = $user->getEmail();
+        $this->d = $imageset;
+    }
+
+    function setSize($size) {
+        $this->size = $size;
+    }
+
+    /**
+     * Get either a Gravatar URL or complete image tag for a specified email address.
+     *
+     * @param string $email The email address
+     * @param string $s Size in pixels, defaults to 80px [ 1 - 2048 ]
+     * @param string $d Default imageset to use [ 404 | mm | identicon | monsterid | wavatar ]
+     * @param string $r Maximum rating (inclusive) [ g | pg | r | x ]
+     * @param boole $img True to return a complete IMG tag False for just the URL
+     * @param array $atts Optional, additional key/value attributes to include in the IMG tag
+     * @return String containing either just a URL or a complete image tag
+     * @source http://gravatar.com/site/implement/images/php/
+     */
+    function getUrl($size=null) {
+        $size = $this->size ?: 80;
+        $url = '//www.gravatar.com/avatar/';
+        $url .= md5( strtolower( $this->email ) );
+        $url .= "?s=$size&d={$this->d}";
+        return $url;
+    }
+}
diff --git a/include/class.banlist.php b/include/class.banlist.php
index 939ae179b1cc142950daf65cd4c25f301c99b651..02c4ff2c79592cfc9660cd5d51b88f71ab769ff5 100644
--- a/include/class.banlist.php
+++ b/include/class.banlist.php
@@ -16,17 +16,40 @@
 
 require_once "class.filter.php";
 class Banlist {
-    
+
     function add($email,$submitter='') {
         return self::getSystemBanList()->addRule('email','equal',$email);
     }
-    
+
     function remove($email) {
         return self::getSystemBanList()->removeRule('email','equal',$email);
     }
-    
-    function isbanned($email) {
-        return TicketFilter::isBanned($email);
+
+    /**
+     * Quick function to determine if the received email-address is in the
+     * banlist. Returns the filter of the filter that has the address
+     * blacklisted and FALSE if the email is not blacklisted.
+     *
+     */
+    static function isBanned($addr) {
+
+        if (!($filter=self::getFilter()))
+            return false;
+
+        $sql='SELECT filter.id '
+            .' FROM '.FILTER_TABLE.' filter'
+            .' INNER JOIN '.FILTER_RULE_TABLE.' rule'
+            .'  ON (filter.id=rule.filter_id)'
+            .' WHERE filter.id='.db_input($filter->getId())
+            .'   AND filter.isactive'
+            .'   AND rule.isactive '
+            .'   AND rule.what="email"'
+            .'   AND rule.val='.db_input($addr);
+
+        if (($res=db_query($sql)) && db_num_rows($res))
+            return $filter;
+
+        return false;
     }
 
     function includes($email) {
@@ -49,7 +72,9 @@ class Banlist {
             'name'          => 'SYSTEM BAN LIST',
             'isactive'      => 1,
             'match_all_rules' => false,
-            'reject_ticket'  => true,
+            'actions'       => array(
+                'Nreject',
+            ),
             'rules'         => array(),
             'notes'         => __('Internal list for email banning. Do not remove')
         ), $errors);
@@ -59,7 +84,7 @@ class Banlist {
         return new Filter(self::ensureSystemBanList());
     }
 
-    function getFilter() {
+    static function getFilter() {
         return self::getSystemBanList();
     }
 }
diff --git a/include/class.canned.php b/include/class.canned.php
index 8cea1417b459d75d7dc5a53c5c2a2600a9833595..bb126d74088ab72b2677d3d0125c9bd0871a71bf 100644
--- a/include/class.canned.php
+++ b/include/class.canned.php
@@ -15,68 +15,89 @@
 **********************************************************************/
 include_once(INCLUDE_DIR.'class.file.php');
 
-class Canned {
-    var $id;
-    var $ht;
-
-    var $attachments;
-
-    function Canned($id){
-        $this->id=0;
-        $this->load($id);
-    }
-
-    function load($id=0) {
-
-        if(!$id && !($id=$this->getId()))
-            return false;
-
-        $sql='SELECT canned.*, count(attach.file_id) as attachments, '
-            .' count(filter.id) as filters '
-            .' FROM '.CANNED_TABLE.' canned '
-            .' LEFT JOIN '.ATTACHMENT_TABLE.' attach
-                    ON (attach.object_id=canned.canned_id AND attach.`type`=\'C\' AND NOT attach.inline) '
-            .' LEFT JOIN '.FILTER_TABLE.' filter ON (canned.canned_id = filter.canned_response_id) '
-            .' WHERE canned.canned_id='.db_input($id)
-            .' GROUP BY canned.canned_id';
-
-        if(!($res=db_query($sql)) ||  !db_num_rows($res))
-            return false;
-
-
-        $this->ht = db_fetch_array($res);
-        $this->id = $this->ht['canned_id'];
-        $this->attachments = new GenericAttachments($this->id, 'C');
-
-        return true;
-    }
-
-    function reload() {
-        return $this->load();
+class Canned
+extends VerySimpleModel {
+    static $meta = array(
+        'table' => CANNED_TABLE,
+        'pk' => array('canned_id'),
+        'joins' => array(
+            'attachments' => array(
+                'constraint' => array(
+                    "'C'" => 'Attachment.type',
+                    'canned_id' => 'Attachment.object_id',
+                ),
+                'list' => true,
+                'null' => true,
+                'broker' => 'GenericAttachments',
+            ),
+        ),
+    );
+
+    const PERM_MANAGE = 'canned.manage';
+
+    static protected $perms = array(
+            self::PERM_MANAGE => array(
+                'title' =>
+                /* @trans */ 'Premade',
+                'desc'  =>
+                /* @trans */ 'Ability to add/update/disable/delete canned responses')
+    );
+
+    static function getPermissions() {
+        return self::$perms;
     }
 
     function getId(){
-        return $this->id;
+        return $this->canned_id;
     }
 
     function isEnabled() {
-         return ($this->ht['isenabled']);
+         return $this->isenabled;
     }
 
     function isActive(){
         return $this->isEnabled();
     }
 
+    function getFilters() {
+
+        if (!isset($this->_filters)) {
+            $this->_filters = array();
+            $cid = sprintf('"canned_id":%d', $this->getId());
+            $sql='SELECT filter.id, filter.name '
+                .' FROM '.FILTER_TABLE.' filter'
+                .' INNER JOIN '.FILTER_ACTION_TABLE.' action'
+                .'  ON (filter.id=action.filter_id)'
+                .' WHERE action.type="canned"'
+                ."  AND action.configuration LIKE '%$cid%'";
+
+            if (($res=db_query($sql)) && db_num_rows($res))
+                while (list($id, $name) = db_fetch_row($res))
+                    $this->_filters[$id] = $name;
+        }
+
+        return $this->_filters;
+    }
+
+    function getAttachedFiles($inlines=false) {
+        return AttachmentFile::objects()
+            ->filter(array(
+                'attachments__type'=>'C',
+                'attachments__object_id'=>$this->getId(),
+                'attachments__inline' => $inlines,
+            ));
+    }
+
     function getNumFilters() {
-        return $this->ht['filters'];
+        return count($this->getFilters());
     }
 
     function getTitle() {
-        return $this->ht['title'];
+        return $this->title;
     }
 
     function getResponse() {
-        return $this->ht['response'];
+        return $this->response;
     }
     function getResponseWithImages() {
         return Format::viewableImages($this->getResponse());
@@ -111,16 +132,21 @@ class Canned {
                 if ($cb && is_callable($cb))
                     $resp = $cb($resp);
 
-                $resp['files'] = $this->attachments->getSeparates();
+                $resp['files'] = array();
+                foreach ($this->getAttachedFiles(!$html) as $file) {
+                    $_SESSION[':cannedFiles'][$file->id] = 1;
+                    $resp['files'][] = array(
+                        'id' => $file->id,
+                        'name' => $file->name,
+                        'size' => $file->size,
+                        'download_url' => $file->getDownloadUrl(),
+                    );
+                }
                 // strip html
                 if (!$html) {
                     $resp['response'] = Format::html2text($resp['response'], 90);
-                    $resp['files'] += $this->attachments->getInlines();
                 }
 
-                foreach ($resp['files'] as $f)
-                    $_SESSION[':cannedFiles'][$f['file_id']] = 1;
-
                 return Format::json_encode($resp);
                 break;
             case 'html':
@@ -153,84 +179,65 @@ class Canned {
     }
 
     function getHashtable() {
-        return $this->ht;
+        $base = $this->ht;
+        unset($base['attachments']);
+        return $base;
     }
 
     function getInfo() {
         return $this->getHashtable();
     }
 
-    function getFilters() {
-        if (!$this->_filters) {
-            $this->_filters = array();
-            $res = db_query(
-                  'SELECT name FROM '.FILTER_TABLE
-                .' WHERE canned_response_id = '.db_input($this->getId())
-                .' ORDER BY name');
-            while ($row = db_fetch_row($res))
-                $this->_filters[] = $row[0];
-        }
-        return $this->_filters;
+    function getNumAttachments() {
+        return $this->attachments->count();
     }
 
-    function update($vars, &$errors) {
+    function delete(){
+        if ($this->getNumFilters() > 0)
+            return false;
 
-        if(!$this->save($this->getId(),$vars,$errors))
+        if (!parent::delete())
             return false;
 
-        $this->reload();
+        $this->attachments->deleteAll();
 
         return true;
     }
 
-    function getNumAttachments() {
-        return $this->ht['attachments'];
-    }
-
-    function delete(){
-        if ($this->getNumFilters() > 0) return false;
-
-        $sql='DELETE FROM '.CANNED_TABLE.' WHERE canned_id='.db_input($this->getId()).' LIMIT 1';
-        if(db_query($sql) && ($num=db_affected_rows())) {
-            $this->attachments->deleteAll();
-        }
-
-        return $num;
-    }
-
     /*** Static functions ***/
-    function lookup($id){
-        return ($id && is_numeric($id) && ($c= new Canned($id)) && $c->getId()==$id)?$c:null;
-    }
 
-    function create($vars,&$errors) {
-        return self::save(0,$vars,$errors);
+    static function create($vars=false) {
+        $faq = new static($vars);
+        $faq->created = SqlFunction::NOW();
+        return $faq;
     }
 
-    function getIdByTitle($title) {
-        $sql='SELECT canned_id FROM '.CANNED_TABLE.' WHERE title='.db_input($title);
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id)=db_fetch_row($res);
+    static function getIdByTitle($title) {
+        $row = static::objects()
+            ->filter(array('title' => $title))
+            ->values_flat('canned_id')
+            ->first();
 
-        return $id;
+        return $row ? $row[0] : null;
     }
 
-    function getCannedResponses($deptId=0, $explicit=false) {
-
-        $sql='SELECT canned_id, title FROM '.CANNED_TABLE
-           .' WHERE isenabled';
-        if($deptId){
-            $sql.=' AND (dept_id='.db_input($deptId);
-            if(!$explicit)
-                $sql.=' OR dept_id=0';
-            $sql.=')';
+    static function getCannedResponses($deptId=0, $explicit=false) {
+        $canned = static::objects()
+            ->filter(array('isenabled' => true))
+            ->order_by('title')
+            ->values_flat('canned_id', 'title');
+
+        if ($deptId) {
+            $depts = array($deptId);
+            if (!$explicit)
+                $depts[] = 0;
+            $canned->filter(array('dept_id__in' => $depts));
         }
-        $sql.=' ORDER BY title';
 
         $responses = array();
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while(list($id,$title)=db_fetch_row($res))
-                $responses[$id]=$title;
+        foreach ($canned as $row) {
+            list($id, $title) = $row;
+            $responses[$id] = $title;
         }
 
         return $responses;
@@ -240,50 +247,53 @@ class Canned {
         return self::getCannedResponses($deptId, $explicit);
     }
 
-    function save($id,$vars,&$errors) {
+    function save($refetch=false) {
+        if ($this->dirty || $refetch)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($refetch || $this->dirty);
+    }
+
+    function update($vars,&$errors) {
         global $cfg;
 
-        $vars['title']=Format::striptags(trim($vars['title']));
+        $vars['title'] = Format::striptags(trim($vars['title']));
 
-        if($id && $id!=$vars['id'])
+        $id = isset($this->canned_id) ? $this->canned_id : null;
+        if ($id && $id != $vars['id'])
             $errors['err']=__('Internal error. Try again');
 
-        if(!$vars['title'])
-            $errors['title']=__('Title required');
-        elseif(strlen($vars['title'])<3)
-            $errors['title']=__('Title is too short. 3 chars minimum');
-        elseif(($cid=self::getIdByTitle($vars['title'])) && $cid!=$id)
-            $errors['title']=__('Title already exists');
+        if (!$vars['title'])
+            $errors['title'] = __('Title required');
+        elseif (strlen($vars['title']) < 3)
+            $errors['title'] = __('Title is too short. 3 chars minimum');
+        elseif (($cid=self::getIdByTitle($vars['title'])) && $cid!=$id)
+            $errors['title'] = __('Title already exists');
 
-        if(!$vars['response'])
-            $errors['response']=__('Response text is required');
+        if (!$vars['response'])
+            $errors['response'] = __('Response text is required');
 
-        if($errors) return false;
-
-        $sql=' updated=NOW() '.
-             ',dept_id='.db_input($vars['dept_id']?:0).
-             ',isenabled='.db_input($vars['isenabled']).
-             ',title='.db_input($vars['title']).
-             ',response='.db_input(Format::sanitize($vars['response'])).
-             ',notes='.db_input(Format::sanitize($vars['notes']));
-
-        if($id) {
-            $sql='UPDATE '.CANNED_TABLE.' SET '.$sql.' WHERE canned_id='.db_input($id);
-            if(db_query($sql))
-                return true;
+        if ($errors)
+            return false;
 
-            $errors['err']=sprintf(__('Unable to update %s.'), __('this canned response'));
+        $this->dept_id = $vars['dept_id'] ?: 0;
+        $this->isenabled = $vars['isenabled'];
+        $this->title = $vars['title'];
+        $this->response = Format::sanitize($vars['response']);
+        $this->notes = Format::sanitize($vars['notes']);
 
-        } else {
-            $sql='INSERT INTO '.CANNED_TABLE.' SET '.$sql.',created=NOW()';
-            if(db_query($sql) && ($id=db_insert_id()))
-                return $id;
+        $isnew = !isset($id);
+        if ($this->save())
+            return true;
 
+        if ($isnew)
+            $errors['err'] = sprintf(__('Unable to update %s.'), __('this canned response'));
+        else
             $errors['err']=sprintf(__('Unable to create %s.'), __('this canned response'))
                .' '.__('Internal error occurred');
-        }
 
-        return false;
+        return true;
     }
 }
+RolePermission::register( /* @trans */ 'Knowledgebase', Canned::getPermissions());
+
 ?>
diff --git a/include/class.captcha.php b/include/class.captcha.php
index 86f89d9792da363351dee17d151238f03872616e..c741172543b0c82297bd73e5653d75fba55b6086 100644
--- a/include/class.captcha.php
+++ b/include/class.captcha.php
@@ -18,7 +18,7 @@ class Captcha {
     var $bgimages=array('cottoncandy.png','grass.png','ripple.png','silk.png','whirlpool.png',
                         'bubbles.png','crackle.png','lines.png','sand.png','snakeskin.png');
     var $font = 10;
-    function Captcha($len=6,$font=7,$bg=''){
+    function __construct($len=6,$font=7,$bg=''){
 
         $this->hash = strtoupper(substr(md5(rand(0, 9999)),rand(0, 24),$len));
         $this->font = $font;
diff --git a/include/class.category.php b/include/class.category.php
index e890b079ee3532ddb53bf6851ac3dc9a7ba167fa..d8cbb72f27de63b4fe25e2256f49cb20f740ab59 100644
--- a/include/class.category.php
+++ b/include/class.category.php
@@ -12,157 +12,241 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
+require_once INCLUDE_DIR . 'class.faq.php';
 
-class Category {
-    var $id;
-    var $ht;
+class Category extends VerySimpleModel {
 
-    function Category($id) {
-        $this->id=0;
-        $this->load($id);
-    }
-
-    function load($id) {
+    static $meta = array(
+        'table' => FAQ_CATEGORY_TABLE,
+        'pk' => array('category_id'),
+        'ordering' => array('name'),
+        'joins' => array(
+            'faqs' => array(
+                'reverse' => 'FAQ.category'
+            ),
+        ),
+    );
 
-        $sql=' SELECT cat.*,count(faq.faq_id) as faqs '
-            .' FROM '.FAQ_CATEGORY_TABLE.' cat '
-            .' LEFT JOIN '.FAQ_TABLE.' faq ON(faq.category_id=cat.category_id) '
-            .' WHERE cat.category_id='.db_input($id)
-            .' GROUP BY cat.category_id';
-
-        if (!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
+    const VISIBILITY_FEATURED = 2;
+    const VISIBILITY_PUBLIC = 1;
+    const VISIBILITY_PRIVATE = 0;
 
-        $this->ht = db_fetch_array($res);
-        $this->id = $this->ht['category_id'];
+    var $_local;
 
-        return true;
+    /* ------------------> Getter methods <--------------------- */
+    function getId() { return $this->category_id; }
+    function getName() { return $this->name; }
+    function getNumFAQs() { return  $this->faqs->count(); }
+    function getDescription() { return $this->description; }
+    function getDescriptionWithImages() { return Format::viewableImages($this->description); }
+    function getNotes() { return $this->notes; }
+    function getCreateDate() { return $this->created; }
+    function getUpdateDate() { return $this->updated; }
+
+    function isPublic() {
+        return $this->ispublic != self::VISIBILITY_PRIVATE;
+    }
+    function getVisibilityDescription() {
+        switch ($this->ispublic) {
+        case self::VISIBILITY_PRIVATE:
+            return __('Private');
+        case self::VISIBILITY_PUBLIC:
+            return __('Public');
+        case self::VISIBILITY_FEATURED:
+            return __('Featured');
+        }
     }
+    function getHashtable() { return $this->ht; }
 
-    function reload() {
-        return $this->load($this->getId());
+    // Translation interface ----------------------------------
+    function getTranslateTag($subtag) {
+        return _H(sprintf('category.%s.%s', $subtag, $this->getId()));
+    }
+    function getLocal($subtag) {
+        $tag = $this->getTranslateTag($subtag);
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : $this->ht[$subtag];
+    }
+    function getLocalDescriptionWithImages($lang=false) {
+        return Format::viewableImages($this->_getLocal('description', $lang));
+    }
+    function getLocalName($lang=false) {
+        return $this->_getLocal('name', $lang);
+    }
+    function _getLocal($what, $lang=false) {
+        if (!$lang) {
+            $lang = $this->getDisplayLang();
+        }
+        $translations = $this->getAllTranslations();
+        foreach ($translations as $t) {
+            if (0 === strcasecmp($lang, $t->lang)) {
+                $data = $t->getComplex();
+                if (isset($data[$what]))
+                    return $data[$what];
+            }
+        }
+        return $this->ht[$what];
+    }
+    function getAllTranslations() {
+        if (!isset($this->_local)) {
+            $tag = $this->getTranslateTag('c:d');
+            $this->_local = CustomDataTranslation::allTranslations($tag, 'article');
+        }
+        return $this->_local;
+    }
+    function getDisplayLang() {
+        if (isset($_REQUEST['kblang']))
+            $lang = $_REQUEST['kblang'];
+        else
+            $lang = Internationalization::getCurrentLanguage();
+        return $lang;
     }
 
-    /* ------------------> Getter methods <--------------------- */
-    function getId() { return $this->id; }
-    function getName() { return $this->ht['name']; }
-    function getNumFAQs() { return  $this->ht['faqs']; }
-    function getDescription() { return $this->ht['description']; }
-    function getNotes() { return $this->ht['notes']; }
-    function getCreateDate() { return $this->ht['created']; }
-    function getUpdateDate() { return $this->ht['updated']; }
-
-    function isPublic() { return ($this->ht['ispublic']); }
-    function getHashtable() { return $this->ht; }
+    function getTopArticles() {
+        return $this->faqs
+            ->filter(Q::not(array('ispublished'=>0)))
+            ->order_by('-ispublished')
+            ->limit(5);
+    }
 
     /* ------------------> Setter methods <--------------------- */
-    function setName($name) { $this->ht['name']=$name; }
-    function setNotes($notes) { $this->ht['notes']=$notes; }
-    function setDescription($desc) { $this->ht['description']=$desc; }
+    function setName($name) { $this->name=$name; }
+    function setNotes($notes) { $this->notes=$notes; }
+    function setDescription($desc) { $this->description=$desc; }
 
     /* --------------> Database access methods <---------------- */
-    function update($vars, &$errors) {
+    function update($vars, &$errors, $validation=false) {
 
-        if(!$this->save($this->getId(), $vars, $errors))
-            return false;
+        // Cleanup.
+        $vars['name'] = Format::striptags(trim($vars['name']));
 
-        //TODO: move FAQs if requested.
+        // Validate
+        if ($vars['id'] && $this->getId() != $vars['id'])
+            $errors['err'] = __('Internal error occurred');
 
-        $this->reload();
+        if (!$vars['name'])
+            $errors['name'] = __('Category name is required');
+        elseif (strlen($vars['name']) < 3)
+            $errors['name'] = __('Name is too short. 3 chars minimum');
+        elseif (($cid=self::findIdByName($vars['name'])) && $cid != $vars['id'])
+            $errors['name'] = __('Category already exists');
 
-        return true;
-    }
+        if (!$vars['description'])
+            $errors['description'] = __('Category description is required');
 
-    function delete() {
-
-        $sql='DELETE FROM '.FAQ_CATEGORY_TABLE
-            .' WHERE category_id='.db_input($this->getId())
-            .' LIMIT 1';
-        if(db_query($sql) && ($num=db_affected_rows())) {
-            db_query('DELETE FROM '.FAQ_TABLE
-                    .' WHERE category_id='.db_input($this->getId()));
+        if ($errors)
+            return false;
 
-        }
+        /* validation only */
+        if ($validation)
+            return true;
 
-        return $num;
-    }
+        $this->ispublic = $vars['ispublic'];
+        $this->name = $vars['name'];
+        $this->description = Format::sanitize($vars['description']);
+        $this->notes = Format::sanitize($vars['notes']);
 
-    /* ------------------> Static methods <--------------------- */
+        if (!$this->save())
+            return false;
 
-    function lookup($id) {
-        return ($id && is_numeric($id) && ($c = new Category($id)))?$c:null;
-    }
+        if (isset($vars['trans']) && !$this->saveTranslations($vars))
+            return false;
 
-    function findIdByName($name) {
-        $sql='SELECT category_id FROM '.FAQ_CATEGORY_TABLE.' WHERE name='.db_input($name);
-        list($id) = db_fetch_row(db_query($sql));
+        // TODO: Move FAQs if requested.
 
-        return $id;
+        return true;
     }
 
-    function findByName($name) {
-        if(($id=self::findIdByName($name)))
-            return new Category($id);
-
-        return false;
+    function delete() {
+        try {
+            parent::delete();
+            $this->faqs->expunge();
+        }
+        catch (OrmException $e) {
+            return false;
+        }
+        return true;
     }
 
-    function validate($vars, &$errors) {
-         return self::save(0, $vars, $errors,true);
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($refetch || $this->dirty);
     }
 
-    function create($vars, &$errors) {
-        return self::save(0, $vars, $errors);
+    function saveTranslations($vars) {
+        global $thisstaff;
+
+        foreach ($this->getAllTranslations() as $t) {
+            $trans = @$vars['trans'][$t->lang];
+            if (!$trans || !array_filter($trans))
+                // Not updating translations
+                continue;
+
+            // Content is not new and shouldn't be added below
+            unset($vars['trans'][$t->lang]);
+            $content = array('name' => $trans['name'],
+                'description' => Format::sanitize($trans['description']));
+
+            // Don't update content which wasn't updated
+            if ($content == $t->getComplex())
+                continue;
+
+            $t->text = $content;
+            $t->agent_id = $thisstaff->getId();
+            $t->updated = SqlFunction::NOW();
+            if (!$t->save())
+                return false;
+        }
+        // New translations (?)
+        $tag = $this->getTranslateTag('c:d');
+        foreach ($vars['trans'] as $lang=>$parts) {
+            $content = array('name' => @$parts['name'],
+                'description' => Format::sanitize(@$parts['description']));
+            if (!array_filter($content))
+                continue;
+            $t = CustomDataTranslation::create(array(
+                'type'      => 'article',
+                'object_hash' => $tag,
+                'lang'      => $lang,
+                'text'      => $content,
+                'revision'  => 1,
+                'agent_id'  => $thisstaff->getId(),
+                'updated'   => SqlFunction::NOW(),
+            ));
+            if (!$t->save())
+                return false;
+        }
+        return true;
     }
 
-    function save($id, $vars, &$errors, $validation=false) {
-
-        //Cleanup.
-        $vars['name']=Format::striptags(trim($vars['name']));
-
-        //validate
-        if($id && $id!=$vars['id'])
-            $errors['err']=__('Internal error occurred');
-
-        if(!$vars['name'])
-            $errors['name']=__('Category name is required');
-        elseif(strlen($vars['name'])<3)
-            $errors['name']=__('Name is too short. 3 chars minimum');
-        elseif(($cid=self::findIdByName($vars['name'])) && $cid!=$id)
-            $errors['name']=__('Category already exists');
-
-        if(!$vars['description'])
-            $errors['description']=__('Category description is required');
-
-        if($errors) return false;
 
-        /* validation only */
-        if($validation) return true;
-
-        //save
-        $sql=' updated=NOW() '.
-             ',ispublic='.db_input(isset($vars['ispublic'])?$vars['ispublic']:0).
-             ',name='.db_input($vars['name']).
-             ',description='.db_input(Format::sanitize($vars['description'])).
-             ',notes='.db_input(Format::sanitize($vars['notes']));
+    /* ------------------> Static methods <--------------------- */
 
-        if($id) {
-            $sql='UPDATE '.FAQ_CATEGORY_TABLE.' SET '.$sql.' WHERE category_id='.db_input($id);
-            if(db_query($sql))
-                return true;
+    static function findIdByName($name) {
+        $row = self::objects()->filter(array(
+            'name'=>$name
+        ))->values_flat('category_id')->first();
 
-            $errors['err']=sprintf(__('Unable to update %s.'), __('this FAQ category'));
+        return ($row) ? $row[0] : null;
+    }
 
-        } else {
-            $sql='INSERT INTO '.FAQ_CATEGORY_TABLE.' SET '.$sql.',created=NOW()';
-            if(db_query($sql) && ($id=db_insert_id()))
-                return $id;
+    static function findByName($name) {
+        return self::objects()->filter(array(
+            'name'=>$name
+        ))->one();
+    }
 
-            $errors['err']=sprintf(__('Unable to create %s.'), __('this FAQ category'))
-               .' '.__('Internal error occurred');
-        }
+    static function getFeatured() {
+        return self::objects()->filter(array(
+            'ispublic'=>self::VISIBILITY_FEATURED
+        ));
+    }
 
-        return false;
+    static function create($vars=false) {
+        $category = new static($vars);
+        $category->created = SqlFunction::NOW();
+        return $category;
     }
 }
 ?>
diff --git a/setup/cli/modules/class.module.php b/include/class.cli.php
similarity index 94%
rename from setup/cli/modules/class.module.php
rename to include/class.cli.php
index de28f7b398bd25cff179cc3ff4baeeb6c3259568..f2f98ce2914a880603f36caf56162bc1bc6b2d62 100644
--- a/setup/cli/modules/class.module.php
+++ b/include/class.cli.php
@@ -35,8 +35,14 @@ class Option {
             $value = null;
         elseif ($value)
             $nargs = 1;
-        if ($this->type == 'int')
+        switch ($this->type) {
+        case 'int':
             $value = (int)$value;
+            break;
+        case 'bool':
+            $value = strcasecmp($value, 'true') === 0 || ((int) $value);
+            break;
+        }
         switch ($this->action) {
             case 'store_true':
                 $value = true;
@@ -147,7 +153,7 @@ class Module {
 
         if ($this->arguments) {
             echo "\nArguments:\n";
-            foreach ($this->arguments as $name=>$help)
+            foreach ($this->arguments as $name=>$help) {
                 $extra = '';
                 if (is_array($help)) {
                     if (isset($help['options']) && is_array($help['options'])) {
@@ -160,6 +166,7 @@ class Module {
                 echo $name . "\n    " . wordwrap(
                     preg_replace('/\s+/', ' ', $help), 76, "\n    ")
                         .$extra."\n";
+            }
         }
 
         if ($this->epilog) {
@@ -225,8 +232,10 @@ class Module {
         die();
     }
 
-    function _run($module_name) {
+    function _run($module_name, $bootstrap=true) {
         $this->module_name = $module_name;
+        if ($bootstrap)
+            $this->bootstrap();
         $this->parseOptions();
         return $this->run($this->_args, $this->_options);
     }
@@ -235,6 +244,12 @@ class Module {
     function run($args, $options) {
     }
 
+    function bootstrap() {
+        Bootstrap::loadConfig();
+        Bootstrap::defineTables(TABLE_PREFIX);
+        Bootstrap::loadCode();
+    }
+
     function fail($message) {
         $this->stderr->write($message . "\n");
         die();
diff --git a/include/class.client.php b/include/class.client.php
index 771f230b0b20b4118d6c6344193e95929ba79738..9a624e13c545934b7bd864551152ec05b5525c8e 100644
--- a/include/class.client.php
+++ b/include/class.client.php
@@ -16,7 +16,7 @@
 require_once INCLUDE_DIR.'class.user.php';
 
 abstract class TicketUser
-implements EmailContact {
+implements EmailContact, ITicketUser, TemplateVariable {
 
     static private $token_regex = '/^(?P<type>\w{1})(?P<algo>\d+)x(?P<hash>.*)$/i';
 
@@ -36,71 +36,43 @@ implements EmailContact {
                 ? call_user_func_array(array($this->user, $name), $args)
                 : call_user_func(array($this->user, $name));
 
-        if ($rv) return $rv;
-
-        $tag =  substr($name, 3);
-        switch (strtolower($tag)) {
-            case 'ticket_link':
-                return sprintf('%s/view.php?%s',
-                        $cfg->getBaseUrl(),
-                        Http::build_query(
-                            array('auth' => $this->getAuthToken()),
-                            false
-                            )
-                        );
-                break;
-        }
-
-        return false;
-
+        return $rv ?: false;
     }
 
-    function getId() { return ($this->user) ? $this->user->getId() : null; }
-    function getEmail() { return ($this->user) ? $this->user->getEmail() : null; }
-
-    function sendAccessLink() {
-        global $ost;
-
-        if (!($ticket = $this->getTicket())
-                || !($email = $ost->getConfig()->getDefaultEmail())
-                || !($content = Page::lookup(Page::getIdByType('access-link'))))
-            return;
-
-        $vars = array(
-            'url' => $ost->getConfig()->getBaseUrl(),
-            'ticket' => $this->getTicket(),
-            'user' => $this,
-            'recipient' => $this);
-
-        $msg = $ost->replaceTemplateVariables(array(
-            'subj' => $content->getName(),
-            'body' => $content->getBody(),
-        ), $vars);
-
-        $email->send($this->getEmail(), Format::striptags($msg['subj']),
-            $msg['body']);
+    // Required for Internationalization::getCurrentLanguage() in templates
+    function getLanguage() {
+        return $this->user->getLanguage();
     }
 
-    protected function getAuthToken($algo=1) {
+    static function getVarScope() {
+        return array(
+            'email' => __('Email address'),
+            'name' => array('class' => 'PersonsName', 'desc' => __('Full name')),
+            'ticket_link' => __('Link to view the ticket'),
+        );
+    }
 
-        //Format: // <user type><algo id used>x<pack of uid & tid><hash of the algo>
-        $authtoken = sprintf('%s%dx%s',
-                ($this->isOwner() ? 'o' : 'c'),
-                $algo,
-                Base32::encode(pack('VV',$this->getId(), $this->getTicketId())));
+    function getVar($tag) {
+        global $cfg;
 
-        switch($algo) {
-            case 1:
-                $authtoken .= substr(base64_encode(
-                            md5($this->getId().$this->getTicket()->getCreateDate().$this->getTicketId().SECRET_SALT, true)), 8);
-                break;
-            default:
-                return null;
+        switch (strtolower($tag)) {
+        case 'ticket_link':
+            $qstr = array();
+            if ($cfg && $cfg->isAuthTokenEnabled()
+                    && ($ticket=$this->getTicket()))
+                $qstr['auth'] = $ticket->getAuthToken($this);
+
+            return sprintf('%s/view.php?%s',
+                    $cfg->getBaseUrl(),
+                    Http::build_query($qstr, false)
+                    );
+            break;
         }
-
-        return $authtoken;
     }
 
+    function getId() { return ($this->user) ? $this->user->getId() : null; }
+    function getEmail() { return ($this->user) ? $this->user->getEmail() : null; }
+
     static function lookupByToken($token) {
 
         //Expecting well formatted token see getAuthToken routine for details.
@@ -113,6 +85,10 @@ implements EmailContact {
                 Base32::decode(strtolower(substr($matches['hash'], 0, 13))));
 
         $user = null;
+        if (!($ticket = Ticket::lookup($matches['tid'])))
+            // Require a ticket for now
+            return null;
+
         switch ($matches['type']) {
             case 'c': //Collaborator c
                 if (($user = Collaborator::lookup($matches['uid']))
@@ -120,17 +96,16 @@ implements EmailContact {
                     $user = null;
                 break;
             case 'o': //Ticket owner
-                if (($ticket = Ticket::lookup($matches['tid']))) {
-                    if (($user = $ticket->getOwner())
-                            && $user->getId() != $matches['uid'])
-                        $user = null;
+                if (($user = $ticket->getOwner())
+                        && $user->getId() != $matches['uid']) {
+                    $user = null;
                 }
                 break;
         }
 
         if (!$user
-                || !$user instanceof TicketUser
-                || strcasecmp($user->getAuthToken($matches['algo']), $token))
+                || !$user instanceof ITicketUser
+                || strcasecmp($ticket->getAuthToken($user, $matches['algo']), $token))
             return false;
 
         return $user;
@@ -187,10 +162,12 @@ class TicketOwner extends  TicketUser {
  *
  */
 
-class  EndUser extends AuthenticatedUser {
+class  EndUser extends BaseAuthenticatedUser {
 
     protected $user;
     protected $_account = false;
+    protected $_stats;
+    protected $topic_stats;
 
     function __construct($user) {
         $this->user = $user;
@@ -214,10 +191,13 @@ class  EndUser extends AuthenticatedUser {
         $u = $this;
         // Traverse the $user properties of all nested user objects to get
         // to the User instance with the custom data
-        while (isset($u->user))
+        while (isset($u->user)) {
             $u = $u->user;
-        if (method_exists($u, 'getVar'))
-            return $u->getVar($tag);
+            if (method_exists($u, 'getVar')) {
+                if ($rv = $u->getVar($tag))
+                    return $rv;
+            }
+        }
     }
 
     function getId() {
@@ -237,7 +217,7 @@ class  EndUser extends AuthenticatedUser {
         return $this->user->getEmail();
     }
 
-    function getRole() {
+    function getUserType() {
         return $this->isOwner() ? 'owner' : 'collaborator';
     }
 
@@ -247,26 +227,66 @@ class  EndUser extends AuthenticatedUser {
     }
 
     function getTicketStats() {
+        if (!isset($this->_stats))
+            $this->_stats = $this->getStats();
 
-        if (!isset($this->ht['stats']))
-            $this->ht['stats'] = $this->getStats();
+        return $this->_stats;
+    }
 
-        return $this->ht['stats'];
+    function getNumTickets($forMyOrg=false, $state=false) {
+        $stats = $this->getTicketStats();
+        $count = 0;
+        $section = $forMyOrg ? 'myorg' : 'mine';
+        foreach ($stats[$section] as $row) {
+            if ($state && $row['status__state'] != $state)
+                continue;
+            $count += $row['count'];
+        }
+        return $count;
     }
 
-    function getNumTickets() {
-        if (!($stats=$this->getTicketStats()))
-            return 0;
+    function getNumOpenTickets($forMyOrg=false) {
+        return $this->getNumTickets($forMyOrg, 'open') ?: 0;
+    }
 
-        return $stats['open']+$stats['closed'];
+    function getNumClosedTickets($forMyOrg=false) {
+        return $this->getNumTickets($forMyOrg, 'closed') ?: 0;
     }
 
-    function getNumOpenTickets() {
-        return ($stats=$this->getTicketStats())?$stats['open']:0;
+    function getNumTopicTickets($topic_id, $forMyOrg=false) {
+        $stats = $this->getTicketStats();
+        $section = $forMyOrg ? 'myorg' : 'mine';
+        if (!isset($this->topic_stats)) {
+            $this->topic_stats = array();
+            foreach ($stats[$section] as $row) {
+                $this->topic_stats[$row['topic_id']] += $row['count'];
+            }
+        }
+        return $this->topic_stats[$topic_id];
     }
 
-    function getNumClosedTickets() {
-        return ($stats=$this->getTicketStats())?$stats['closed']:0;
+    function getNumTopicTicketsInState($topic_id, $state=false, $forMyOrg=false) {
+        $stats = $this->getTicketStats();
+        $count = 0;
+        $section = $forMyOrg ? 'myorg' : 'mine';
+        foreach ($stats[$section] as $row) {
+            if ($topic_id != $row['topic_id'])
+                continue;
+            if ($state && $state != $row['status__state'])
+                continue;
+            $count += $row['count'];
+        }
+        return $count;
+    }
+
+    function getNumOrganizationTickets() {
+        return $this->getNumTickets(true);
+    }
+    function getNumOpenOrganizationTickets() {
+        return $this->getNumTickets(true, 'open');
+    }
+    function getNumClosedOrganizationTickets() {
+        return $this->getNumTickets(true, 'closed');
     }
 
     function getAccount() {
@@ -277,51 +297,55 @@ class  EndUser extends AuthenticatedUser {
         return $this->_account;
     }
 
-    function getLanguage() {
-        static $cached = false;
-        if (!$cached) $cached = &$_SESSION['client:lang'];
-
-        if (!$cached) {
-            if ($acct = $this->getAccount())
-                $cached = $acct->getLanguage();
-            if (!$cached)
-                $cached = Internationalization::getDefaultLanguage();
-        }
-        return $cached;
+    function getLanguage($flags=false) {
+        if ($acct = $this->getAccount())
+            return $acct->getLanguage($flags);
     }
 
     private function getStats() {
-
-        $where = ' WHERE ticket.user_id = '.db_input($this->getId())
-                .' OR collab.user_id = '.db_input($this->getId()).' ';
-
-        $join  =  'LEFT JOIN '.TICKET_COLLABORATOR_TABLE.' collab
-                    ON (collab.ticket_id=ticket.ticket_id
-                            AND collab.user_id = '.db_input($this->getId()).' ) ';
-
-        $sql =  'SELECT \'open\', count( ticket.ticket_id ) AS tickets '
-                .'FROM ' . TICKET_TABLE . ' ticket '
-                .'INNER JOIN '.TICKET_STATUS_TABLE. ' status
-                    ON (ticket.status_id=status.id
-                            AND status.state=\'open\') '
-                . $join
-                . $where
-
-                .'UNION SELECT \'closed\', count( ticket.ticket_id ) AS tickets '
-                .'FROM ' . TICKET_TABLE . ' ticket '
-                .'INNER JOIN '.TICKET_STATUS_TABLE. ' status
-                    ON (ticket.status_id=status.id
-                            AND status.state=\'closed\' ) '
-                . $join
-                . $where;
-
-        $res = db_query($sql);
-        $stats = array();
-        while($row = db_fetch_row($res)) {
-            $stats[$row[0]] = $row[1];
+        $basic = Ticket::objects()
+            ->annotate(array('count' => SqlAggregate::COUNT('ticket_id')))
+            ->values('status__state', 'topic_id')
+            ->distinct('status_id', 'topic_id');
+
+        // Share tickets among the organization for owners only
+        $mine = clone $basic;
+        $collab = clone $basic;
+        $mine->filter(array(
+            'user_id' => $this->getId(),
+        ));
+
+        // Also add collaborator tickets to the list. This may seem ugly;
+        // but the general rule for SQL is that a single query can only use
+        // one index. Therefore, to scan two indexes (by user_id and
+        // thread.collaborators.user_id), we need two queries. A union will
+        // help out with that.
+        $mine->union($collab->filter(array(
+            'thread__collaborators__user_id' => $this->getId(),
+            Q::not(array('user_id' => $this->getId()))
+        )));
+
+        if ($orgid = $this->getOrgId()) {
+            // Also generate a separate query for all the tickets owned by
+            // either my organization or ones that I'm collaborating on
+            // which are not part of the organization.
+            $myorg = clone $basic;
+            $myorg->values('user__org_id');
+            $collab = clone $myorg;
+
+            $myorg->filter(array('user__org_id' => $orgid));
+            $myorg->union($collab->filter(array(
+                'thread__collaborators__user_id' => $this->getId(),
+                Q::not(array('user__org_id' => $orgid))
+            )));
         }
 
-        return $stats;
+        return array('mine' => $mine, 'myorg' => $myorg);
+    }
+
+    function onLogin($bk) {
+        if ($account = $this->getAccount())
+            $account->onLogin($bk);
     }
 }
 
@@ -355,16 +379,27 @@ class ClientAccount extends UserAccount {
         // TODO: Drop password-reset tokens from the config table for
         //       this user id
         $sql = 'DELETE FROM '.CONFIG_TABLE.' WHERE `namespace`="pwreset"
-            AND `key`='.db_input($this->getUserId());
+            AND `value`='.db_input('c'.$this->getUserId());
         if (!db_query($sql, false))
             return false;
 
         unset($_SESSION['_client']['reset-token']);
     }
 
+    function onLogin($bk) {
+        $this->setExtraAttr('browser_lang',
+            Internationalization::getCurrentLanguage());
+        $this->save();
+    }
+
     function update($vars, &$errors) {
         global $cfg;
 
+        // FIXME: Updates by agents should go through UserAccount::update()
+        global $thisstaff;
+        if ($thisstaff)
+            return parent::update($vars, $errors);
+
         $rtoken = $_SESSION['_client']['reset-token'];
         if ($vars['passwd1'] || $vars['passwd2'] || $vars['cpasswd'] || $rtoken) {
 
@@ -377,7 +412,7 @@ class ClientAccount extends UserAccount {
 
             if ($rtoken) {
                 $_config = new Config('pwreset');
-                if ($_config->get($rtoken) != $this->getUserId())
+                if ($_config->get($rtoken) != 'c'.$this->getUserId())
                     $errors['err'] =
                         __('Invalid reset token. Logout and try again');
                 elseif (!($ts = $_config->lastModified($rtoken))
@@ -395,16 +430,16 @@ class ClientAccount extends UserAccount {
             }
         }
 
-        if (!$vars['timezone_id'])
-            $errors['timezone_id']=__('Time zone selection is required');
+        // Timezone selection is not required. System default is a valid
+        // fallback
 
         if ($errors) return false;
 
-        $this->set('timezone_id', $vars['timezone_id']);
+        $this->set('timezone', $vars['timezone']);
         $this->set('dst', isset($vars['dst']) ? 1 : 0);
         // Change language
         $this->set('lang', $vars['lang'] ?: null);
-        $_SESSION['client:lang'] = null;
+        Internationalization::setCurrentLanguage(null);
         TextDomain::configureForUser($this);
 
         if ($vars['backend']) {
@@ -427,5 +462,20 @@ class ClientAccount extends UserAccount {
 
 // Used by the email system
 interface EmailContact {
+    // function getId()
+    // function getName()
+    // function getEmail()
+}
+
+interface ITicketUser {
+/* PHP 5.3 < 5.3.8 will crash with some abstract inheritance issue
+ * ------------------------------------------------------------
+    function isOwner();
+    function flagGuest();
+    function isGuest();
+    function getUserId();
+    function getTicketId();
+    function getTicket();
+ */
 }
 ?>
diff --git a/include/class.collaborator.php b/include/class.collaborator.php
index b123644a0d85b3276f2a8e3c9403adaaaa9b60f4..2d73efbfd26aa211dd0e45adfe6b3510e0730e69 100644
--- a/include/class.collaborator.php
+++ b/include/class.collaborator.php
@@ -16,144 +16,141 @@
 require_once(INCLUDE_DIR . 'class.user.php');
 require_once(INCLUDE_DIR . 'class.client.php');
 
-class Collaborator extends TicketUser {
-
-    var $ht;
-
-    var $user;
-    var $ticket;
-
-    function __construct($id) {
-        $this->load($id);
-        parent::__construct($this->getUser());
-    }
-
-    function load($id) {
-
-        if(!$id && !($id=$this->getId()))
-            return;
-
-        $sql='SELECT * FROM '.TICKET_COLLABORATOR_TABLE
-            .' WHERE id='.db_input($id);
-
-        $this->ht = db_fetch_array(db_query($sql));
-        $this->ticket = null;
-    }
-
-    function reload() {
-        return $this->load();
-    }
+class Collaborator
+extends VerySimpleModel
+implements EmailContact, ITicketUser {
+
+    static $meta = array(
+        'table' => THREAD_COLLABORATOR_TABLE,
+        'pk' => array('id'),
+        'select_related' => array('user'),
+        'joins' => array(
+            'thread' => array(
+                'constraint' => array('thread_id' => 'Thread.id'),
+            ),
+            'user' => array(
+                'constraint' => array('user_id' => 'User.id'),
+            ),
+        ),
+    );
 
     function __toString() {
-        return Format::htmlchars(sprintf('%s <%s>', $this->getName(),
-                    $this->getEmail()));
+        return Format::htmlchars($this->toString());
+    }
+    function toString() {
+        return sprintf('%s <%s>', $this->getName(), $this->getEmail());
     }
 
     function getId() {
-        return $this->ht['id'];
+        return $this->id;
     }
 
     function isActive() {
-        return ($this->ht['isactive']);
+        return $this->isactive;
     }
 
     function getCreateDate() {
-        return $this->ht['created'];
+        return $this->created;
+    }
+
+    function getThreadId() {
+        return $this->thread_id;
     }
 
     function getTicketId() {
-        return $this->ht['ticket_id'];
+        if ($this->thread->object_type == ObjectModel::OBJECT_TYPE_TICKET)
+            return $this->thread->object_id;
     }
 
     function getTicket() {
-        if(!$this->ticket && $this->getTicketId())
-            $this->ticket = Ticket::lookup($this->getTicketId());
-
-        return $this->ticket;
+        // TODO: Change to $this->thread->ticket when Ticket goes to ORM
+        if ($id = $this->getTicketId())
+            return Ticket::lookup($id);
     }
 
-    function getUserId() {
-        return $this->ht['user_id'];
+    function getUser() {
+        return $this->user;
     }
 
-    function getUser() {
+    // EmailContact interface
+    function getEmail() {
+        return $this->user->getEmail();
+    }
+    function getName() {
+        return $this->user->getName();
+    }
 
-        if(!$this->user && $this->getUserId())
-            $this->user = User::lookup($this->getUserId());
+    // VariableReplacer interface
+    function getVar($what) {
+        global $cfg;
 
-        return $this->user;
+        switch (strtolower($what)) {
+        case 'ticket_link':
+            return sprintf('%s/view.php?%s',
+                $cfg->getBaseUrl(),
+                Http::build_query(
+                    // TODO: Chance to $this->getTicket when
+                    array('auth' => $this->getTicket()->getAuthToken($this)),
+                    false
+                )
+            );
+            break;
+        }
     }
 
-    function remove() {
+    // ITicketUser interface
+    var $_isguest;
+
+    function isOwner() {
+        return false;
+    }
+    function flagGuest() {
+        $this->_isguest = true;
+    }
+    function isGuest() {
+        return $this->_isguest;
+    }
+    function getUserId() {
+        return $this->user_id;
+    }
 
-        $sql='DELETE FROM '.TICKET_COLLABORATOR_TABLE
-            .' WHERE id='.db_input($this->getId())
-            .' LIMIT 1';
+    static function create($vars=false) {
+        $inst = new static($vars);
+        $inst->created = SqlFunction::NOW();
+        return $inst;
+    }
 
-        return  (db_query($sql) && db_affected_rows());
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($refetch || $this->dirty);
     }
 
     static function add($info, &$errors) {
 
-        if (!$info || !$info['ticketId'] || !$info['userId'])
+        if (!$info || !$info['threadId'] || !$info['userId'])
             $errors['err'] = __('Invalid or missing information');
-        elseif (($c=self::lookup($info)))
+        elseif ($c = static::lookup(array(
+            'thread_id' => $info['threadId'],
+            'user_id' => $info['userId'],
+        )))
             $errors['err'] = sprintf(__('%s is already a collaborator'),
                     $c->getName());
 
         if ($errors) return false;
 
-        $sql='INSERT INTO '.TICKET_COLLABORATOR_TABLE
-            .' SET updated=NOW() '
-            .' ,isactive='.db_input(isset($info['isactive']) ?  $info['isactive'] : 0)
-            .' ,ticket_id='.db_input($info['ticketId'])
-            .' ,user_id='.db_input($info['userId']);
-
-        if(db_query($sql) && ($id=db_insert_id()))
-            return self::lookup($id);
+        $collab = static::create(array(
+            'isactive' => isset($info['isactive']) ? $info['isactive'] : 0,
+            'thread_id' => $info['threadId'],
+            'user_id' => $info['userId'],
+        ));
+        if ($collab->save(true))
+            return $collab;
 
         $errors['err'] = __('Unable to add collaborator. Internal error');
 
         return false;
     }
 
-    static function forTicket($tid, $criteria=array()) {
-
-        $collaborators = array();
-
-        $sql='SELECT id FROM '.TICKET_COLLABORATOR_TABLE
-            .' WHERE ticket_id='.db_input($tid);
-
-        if(isset($criteria['isactive']))
-            $sql.=' AND isactive='.db_input($criteria['isactive']);
-
-        //TODO: sort by name of the user
-
-        if(($res=db_query($sql)) && db_num_rows($res))
-            while(list($id)=db_fetch_row($res))
-                $collaborators[] = self::lookup($id);
-
-        return $collaborators;
-    }
-
-    static function getIdByInfo($info) {
-
-        $sql='SELECT id FROM '.TICKET_COLLABORATOR_TABLE
-            .' WHERE ticket_id='.db_input($info['ticketId'])
-            .' AND user_id='.db_input($info['userId']);
-
-        return db_result(db_query($sql));
-    }
-
-    static function lookup($criteria) {
-
-        $id = is_numeric($criteria)
-            ? $criteria : self::getIdByInfo($criteria);
-
-        return ($id
-                && ($c = new Collaborator($id))
-                && $c->getId() == $id)
-            ? $c : null;
-    }
 }
 ?>
diff --git a/include/class.company.php b/include/class.company.php
index 049ecbb215a0abcbe1d6d25b0d5842c26fd97850..6773bd80e83ef59e724744fd9b33663c0f269b57 100644
--- a/include/class.company.php
+++ b/include/class.company.php
@@ -17,7 +17,8 @@
 require_once(INCLUDE_DIR.'class.forms.php');
 require_once(INCLUDE_DIR.'class.dynamic_forms.php');
 
-class Company {
+class Company
+implements TemplateVariable {
     var $form;
     var $entry;
 
@@ -48,7 +49,7 @@ class Company {
     }
 
     function getInfo() {
-        return $this->getForm()->getSaved();
+        return $this->getForm()->getClean();
     }
 
     function getName() {
@@ -59,6 +60,12 @@ class Company {
         return $this->getName();
     }
 
+    static function getVarScope() {
+        return VariableReplacer::compileFormScope(
+            DynamicForm::lookup(array('type'=>'C'))
+        );
+    }
+
     function __toString() {
         try {
             if ($name = $this->getForm()->getAnswer('name'))
diff --git a/include/class.config.php b/include/class.config.php
index 870f2f6dda13575706cf04c1d2cf8411e519a864..95c5c2ff15b4b02e45bed2d5854e4f9905a5b4a7 100644
--- a/include/class.config.php
+++ b/include/class.config.php
@@ -13,6 +13,7 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
+require_once INCLUDE_DIR . 'class.orm.php';
 
 class Config {
     var $config = array();
@@ -29,23 +30,25 @@ class Config {
     # new settings and the corresponding default values.
     var $defaults = array();                # List of default values
 
-    function Config($section=null) {
+    function __construct($section=null, $defaults=array()) {
         if ($section)
             $this->section = $section;
 
         if ($this->section === null)
             return false;
 
-        if (!isset($_SESSION['cfg:'.$this->section]))
-            $_SESSION['cfg:'.$this->section] = array();
-        $this->session = &$_SESSION['cfg:'.$this->section];
+        if ($defaults)
+            $this->defaults = $defaults;
 
-        $sql='SELECT id, `key`, value, `updated` FROM '.$this->table
-            .' WHERE `'.$this->section_column.'` = '.db_input($this->section);
+        if (isset($_SESSION['cfg:'.$this->section]))
+            $this->session = &$_SESSION['cfg:'.$this->section];
 
-        if(($res=db_query($sql)) && db_num_rows($res))
-            while ($row = db_fetch_array($res))
-                $this->config[$row['key']] = $row;
+        $this->load();
+    }
+
+    function load() {
+        foreach ($this->items() as $I)
+            $this->config[$I->key] = $I;
     }
 
     function getNamespace() {
@@ -54,16 +57,16 @@ class Config {
 
     function getInfo() {
         $info = $this->defaults;
-        foreach ($this->config as $key=>$setting)
-            $info[$key] = $setting['value'];
+        foreach ($this->config as $key=>$item)
+            $info[$key] = $item->value;
         return $info;
     }
 
     function get($key, $default=null) {
-        if (isset($this->session[$key]))
+        if (isset($this->session) && isset($this->session[$key]))
             return $this->session[$key];
         elseif (isset($this->config[$key]))
-            return $this->config[$key]['value'];
+            return $this->config[$key]->value;
         elseif (isset($this->defaults[$key]))
             return $this->defaults[$key];
 
@@ -79,26 +82,30 @@ class Config {
     }
 
     function persist($key, $value) {
+        if (!isset($this->session)) {
+            $this->session = &$_SESSION['cfg:'.$this->section];
+            $this->session = array();
+        }
         $this->session[$key] = $value;
         return true;
     }
 
     function lastModified($key) {
         if (isset($this->config[$key]))
-            return $this->config[$key]['updated'];
-        else
-            return false;
+            return $this->config[$key]->updated;
+
+        return false;
     }
 
     function create($key, $value) {
-        $sql = 'INSERT INTO '.$this->table
-            .' SET `'.$this->section_column.'`='.db_input($this->section)
-            .', `key`='.db_input($key)
-            .', value='.db_input($value);
-        if (!db_query($sql) || !($id=db_insert_id()))
+        $item = new ConfigItem([
+            $this->section_column => $this->section,
+            'key' => $key,
+            'value' => $value,
+        ]);
+        if (!$item->save())
             return false;
 
-        $this->config[$key] = array('key'=>$key, 'value'=>$value, 'id'=>$id);
         return true;
     }
 
@@ -108,16 +115,9 @@ class Config {
         elseif (!isset($this->config[$key]))
             return $this->create($key, $value);
 
-        $setting = &$this->config[$key];
-        if ($setting['value'] == $value)
-            return true;
-
-        if (!db_query('UPDATE '.$this->table.' SET updated=NOW(), value='
-                .db_input($value).' WHERE id='.db_input($setting['id'])))
-            return false;
-
-        $setting['value'] = $value;
-        return true;
+        $item = $this->config[$key];
+        $item->value = $value;
+        return $item->save();
     }
 
     function updateAll($updates) {
@@ -128,12 +128,47 @@ class Config {
     }
 
     function destroy() {
+        unset($this->session);
+        return $this->items()->delete();
+    }
 
-        $sql='DELETE FROM '.$this->table
-            .' WHERE `'.$this->section_column.'` = '.db_input($this->section);
+    function items() {
+        static $items;
 
-        db_query($sql);
-        unset($this->session);
+        if (!isset($items))
+            $items = ConfigItem::items($this->section, $this->section_column);
+
+        return $items;
+    }
+}
+
+class ConfigItem
+extends VerySimpleModel {
+    static $meta = array(
+        'table' => CONFIG_TABLE,
+        'pk' => array('id'),
+    );
+
+    static function items($namespace, $column='namespace') {
+
+        $items = static::objects()
+            ->filter([$column => $namespace]);
+
+        try {
+            count($items);
+        }
+        catch (InconsistentModelException $ex) {
+            // Pending upgrade ??
+            $items = array();
+        }
+
+        return $items;
+    }
+
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($this->dirty || $refetch);
     }
 }
 
@@ -150,13 +185,14 @@ class OsticketConfig extends Config {
     var $defaults = array(
         'allow_pw_reset' =>     true,
         'pw_reset_window' =>    30,
-        'enable_html_thread' => true,
+        'enable_richtext' =>    true,
+        'enable_avatars' =>     true,
         'allow_attachments' =>  true,
-        'name_format' =>        'full', # First Last
+        'agent_name_format' =>  'full', # First Last
+        'client_name_format' => 'original', # As entered
         'auto_claim_tickets'=>  true,
         'system_language' =>    'en_US',
         'default_storage_bk' => 'D',
-        'allow_client_updates' => false,
         'message_autoresponder_collabs' => true,
         'add_email_collabs' => true,
         'clients_only' => false,
@@ -165,31 +201,26 @@ class OsticketConfig extends Config {
         'default_help_topic' => 0,
         'help_topic_sort_mode' => 'a',
         'client_verify_email' => 1,
+        'allow_auth_tokens' => 1,
         'verify_email_addrs' => 1,
+        'client_avatar' => 'gravatar.mm',
+        'agent_avatar' => 'gravatar.mm',
+        'ticket_lock' => 2, // Lock on activity
+        'max_open_tickets' => 0,
     );
 
-    function OsticketConfig($section=null) {
-        parent::Config($section);
+    function __construct($section=null) {
+        parent::__construct($section);
 
         if (count($this->config) == 0) {
             // Fallback for osticket < 1.7@852ca89e
             $sql='SELECT * FROM '.$this->table.' WHERE id = 1';
+            $meta = ConfigItem::getMeta();
             if (($res=db_query($sql)) && db_num_rows($res))
                 foreach (db_fetch_array($res) as $key=>$value)
-                    $this->config[$key] = array('value'=>$value);
+                    $this->config[$key] = $meta->newInstance(array('value'=>$value));
         }
 
-        //Get the default time zone
-        // We can't JOIN timezone table above due to upgrade support.
-        if ($this->get('default_timezone_id')) {
-            if (!$this->exists('tz_offset'))
-                $this->persist('tz_offset',
-                    Timezone::getOffsetById($this->get('default_timezone_id')));
-        } else
-            // Previous osTicket versions saved the offset value instead of
-            // a timezone instance. This is compatibility for the upgrader
-            $this->persist('tz_offset', 0);
-
         return true;
     }
 
@@ -254,36 +285,95 @@ class OsticketConfig extends Config {
         return md5(self::getDBVersion());
     }
 
-    function getDBTZoffset() {
-        if (!$this->exists('db_tz_offset')) {
-            $sql='SELECT (TIME_TO_SEC(TIMEDIFF(NOW(), UTC_TIMESTAMP()))/3600) as db_tz_offset';
-            if(($res=db_query($sql)) && db_num_rows($res))
-                $this->persist('db_tz_offset', db_result($res));
+    function getDbTimezone() {
+        if (!$this->exists('db_timezone')) {
+            require_once INCLUDE_DIR . 'class.timezone.php';
+            $this->persist('db_timezone', DbTimezone::determine());
         }
-        return $this->get('db_tz_offset');
+        return $this->get('db_timezone');
     }
 
-    function getDefaultTimezoneId() {
-        return $this->get('default_timezone_id');
+    function getDefaultTimezone() {
+        return $this->get('default_timezone');
     }
 
-    /* Date & Time Formats */
-    function observeDaylightSaving() {
-        return ($this->get('enable_daylight_saving'));
+    function getTimezone($user=false) {
+        global $thisstaff, $thisclient;
+
+        $user = $user ?: $thisstaff;
+
+        if (!$user && $thisclient && is_callable(array($thisclient, 'getTimezone')))
+            $user = $thisclient;
+
+        if ($user)
+            $zone = $user->getTimezone();
+
+        if (!$zone)
+            $zone = $this->get('default_timezone');
+
+        if (!$zone)
+            $zone = ini_get('date.timezone');
+
+        return $zone;
     }
+
+    function getDefaultLocale() {
+        return $this->get('default_locale');
+    }
+
+    /* Date & Time Formats */
     function getTimeFormat() {
-        return $this->get('time_format');
+        if ($this->get('date_formats') == 'custom')
+            return $this->get('time_format');
+        return '';
+    }
+    function isForce24HourTime() {
+        return $this->get('date_formats') == '24';
     }
-    function getDateFormat() {
-        return $this->get('date_format');
+    /**
+     * getDateFormat
+     *
+     * Retrieve the current date format for the system, as a string, and in
+     * the intl (icu) format.
+     *
+     * Parameters:
+     * $propogate - (boolean:default=false), if set and the configuration
+     *      indicates default date and time formats (ie. not custom), then
+     *      the intl date formatter will be queried to find the pattern used
+     *      internally for the current locale settings.
+     */
+    function getDateFormat($propogate=false) {
+        if ($this->get('date_formats') == 'custom')
+            return $this->get('date_format');
+        if ($propogate) {
+            if (class_exists('IntlDateFormatter')) {
+                $formatter = new IntlDateFormatter(
+                    Internationalization::getCurrentLocale(),
+                    IntlDateFormatter::SHORT,
+                    IntlDateFormatter::NONE,
+                    $this->getTimezone(),
+                    IntlDateFormatter::GREGORIAN
+                );
+                return $formatter->getPattern();
+            }
+            else {
+                // Use a standard
+                return 'y-M-d';
+            }
+        }
+        return '';
     }
 
     function getDateTimeFormat() {
-        return $this->get('datetime_format');
+        if ($this->get('date_formats') == 'custom')
+            return $this->get('datetime_format');
+        return '';
     }
 
     function getDayDateTimeFormat() {
-        return $this->get('daydatetime_format');
+        if ($this->get('date_formats') == 'custom')
+            return $this->get('daydatetime_format');
+        return '';
     }
 
     function getConfigInfo() {
@@ -302,10 +392,6 @@ class OsticketConfig extends Config {
         return rtrim($this->getUrl(),'/');
     }
 
-    function getTZOffset() {
-        return $this->get('tz_offset');
-    }
-
     function getPageSize() {
         return $this->get('max_page_size');
     }
@@ -318,12 +404,12 @@ class OsticketConfig extends Config {
         return $this->get('passwd_reset_period');
     }
 
-    function isHtmlThreadEnabled() {
-        return $this->get('enable_html_thread');
+    function isRichTextEnabled() {
+        return $this->get('enable_richtext');
     }
 
-    function allowClientUpdates() {
-        return $this->get('allow_client_updates');
+    function isAvatarsEnabled() {
+        return $this->get('enable_avatars');
     }
 
     function getClientTimeout() {
@@ -358,12 +444,32 @@ class OsticketConfig extends Config {
         return $this->get('staff_max_logins');
     }
 
+    function getStaffAvatarSource() {
+        require_once INCLUDE_DIR . 'class.avatar.php';
+        list($source, $mode) = explode('.', $this->get('agent_avatar'), 2);
+        return AvatarSource::lookup($source, $mode);
+    }
+
+    function getClientAvatarSource() {
+        require_once INCLUDE_DIR . 'class.avatar.php';
+        list($source, $mode) = explode('.', $this->get('client_avatar'), 2);
+        return AvatarSource::lookup($source, $mode);
+    }
+
     function getLockTime() {
         return $this->get('autolock_minutes');
     }
 
-    function getDefaultNameFormat() {
-        return $this->get('name_format');
+    function getTicketLockMode() {
+        return $this->get('ticket_lock');
+    }
+
+    function getAgentNameFormat() {
+        return $this->get('agent_name_format');
+    }
+
+    function getClientNameFormat() {
+        return $this->get('client_name_format');
     }
 
     function getDefaultDeptId() {
@@ -465,8 +571,8 @@ class OsticketConfig extends Config {
 
     static function allTopicSortModes() {
         return array(
-            'a' => __('Alphabetically'),
-            'm' => __('Manually'),
+            Topic::SORT_ALPHA   => __('Alphabetically'),
+            Topic::SORT_MANUAL  => __('Manually'),
         );
     }
 
@@ -588,6 +694,10 @@ class OsticketConfig extends Config {
         return $this->get('client_verify_email');
     }
 
+    function isAuthTokenEnabled() {
+        return $this->get('allow_auth_tokens');
+    }
+
     function isCaptchaEnabled() {
         return (extension_loaded('gd') && function_exists('gd_info') && $this->get('enable_captcha'));
     }
@@ -632,22 +742,44 @@ class OsticketConfig extends Config {
         return true; //No longer an option...hint: big plans for headers coming!!
     }
 
-    function getDefaultSequence() {
-        if ($this->get('sequence_id'))
-            $sequence = Sequence::lookup($this->get('sequence_id'));
+    function getDefaultTicketSequence() {
+        if ($this->get('ticket_sequence_id'))
+            $sequence = Sequence::lookup($this->get('ticket_sequence_id'));
         if (!$sequence)
             $sequence = new RandomSequence();
         return $sequence;
     }
-    function getDefaultNumberFormat() {
-        return $this->get('number_format');
+
+    function getDefaultTicketNumberFormat() {
+        return $this->get('ticket_number_format');
     }
+
     function getNewTicketNumber() {
-        $s = $this->getDefaultSequence();
-        return $s->next($this->getDefaultNumberFormat(),
+        $s = $this->getDefaultTicketSequence();
+        return $s->next($this->getDefaultTicketNumberFormat(),
             array('Ticket', 'isTicketNumberUnique'));
     }
 
+    // Task sequence
+    function getDefaultTaskSequence() {
+        if ($this->get('task_sequence_id'))
+            $sequence = Sequence::lookup($this->get('task_sequence_id'));
+        if (!$sequence)
+            $sequence = new RandomSequence();
+
+        return $sequence;
+    }
+
+    function getDefaultTaskNumberFormat() {
+        return $this->get('task_number_format');
+    }
+
+    function getNewTaskNumber() {
+        $s = $this->getDefaultTaskSequence();
+        return $s->next($this->getDefaultTaskNumberFormat(),
+            array('Task', 'isNumberUnique'));
+    }
+
     /* autoresponders  & Alerts */
     function autoRespONNewTicket() {
         return ($this->get('ticket_autoresponder'));
@@ -791,6 +923,88 @@ class OsticketConfig extends Config {
         return ($this->get('overlimit_notice_active'));
     }
 
+    /* Tasks */
+
+    function alertONNewTask() {
+        return ($this->get('task_alert_active'));
+    }
+
+    function alertAdminONNewTask() {
+        return ($this->get('task_alert_admin'));
+    }
+
+    function alertDeptManagerONNewTask() {
+        return ($this->get('task_alert_dept_manager'));
+    }
+
+    function alertDeptMembersONNewTask() {
+        return ($this->get('task_alert_dept_members'));
+    }
+
+    function alertONTaskActivity() {
+        return ($this->get('task_activity_alert_active'));
+    }
+
+    function alertLastRespondentONTaskActivity() {
+        return ($this->get('task_activity_alert_laststaff'));
+    }
+
+    function alertAssignedONTaskActivity() {
+        return ($this->get('task_activity_alert_assigned'));
+    }
+
+    function alertDeptManagerONTaskActivity() {
+        return ($this->get('task_activity_alert_dept_manager'));
+    }
+
+    function alertONTaskTransfer() {
+        return ($this->get('task_transfer_alert_active'));
+    }
+
+    function alertAssignedONTaskTransfer() {
+        return ($this->get('task_transfer_alert_assigned'));
+    }
+
+    function alertDeptManagerONTaskTransfer() {
+        return ($this->get('task_transfer_alert_dept_manager'));
+    }
+
+    function alertDeptMembersONTaskTransfer() {
+        return ($this->get('task_transfer_alert_dept_members'));
+    }
+
+    function alertONTaskAssignment() {
+        return ($this->get('task_assignment_alert_active'));
+    }
+
+    function alertStaffONTaskAssignment() {
+        return ($this->get('task_assignment_alert_staff'));
+    }
+
+    function alertTeamLeadONTaskAssignment() {
+        return ($this->get('task_assignment_alert_team_lead'));
+    }
+
+    function alertTeamMembersONTaskAssignment() {
+        return ($this->get('task_assignment_alert_team_members'));
+    }
+
+    function alertONOverdueTask() {
+        return ($this->get('task_overdue_alert_active'));
+    }
+
+    function alertAssignedONOverdueTask() {
+        return ($this->get('task_overdue_alert_assigned'));
+    }
+
+    function alertDeptManagerONOverdueTask() {
+        return ($this->get('task_overdue_alert_dept_manager'));
+    }
+
+    function alertDeptMembersONOverdueTask() {
+        return ($this->get('task_overdue_alert_dept_members'));
+    }
+
     /* Error alerts sent to admin email when enabled */
     function alertONSQLError() {
         return ($this->get('send_sql_errors'));
@@ -814,10 +1028,16 @@ class OsticketConfig extends Config {
         return ($this->get('allow_attachments'));
     }
 
-    function getSystemLanguage() {
+    function getPrimaryLanguage() {
         return $this->get('system_language');
     }
 
+    function getSecondaryLanguages() {
+        $langs = $this->get('secondary_langs');
+        $langs = (is_string($langs)) ? explode(',', $langs) : array();
+        return array_filter($langs);
+    }
+
     /* Needed by upgrader on 1.6 and older releases upgrade - not not remove */
     function getUploadDir() {
         return $this->get('upload_dir');
@@ -843,20 +1063,20 @@ class OsticketConfig extends Config {
             case 'tickets':
                 return $this->updateTicketsSettings($vars, $errors);
                 break;
+            case 'tasks':
+                return $this->updateTasksSettings($vars, $errors);
+                break;
             case 'emails':
                 return $this->updateEmailsSettings($vars, $errors);
                 break;
             case 'pages':
                 return $this->updatePagesSettings($vars, $errors);
                 break;
-            case 'access':
-                return $this->updateAccessSettings($vars, $errors);
-                break;
-            case 'autoresp':
-                return $this->updateAutoresponderSettings($vars, $errors);
+            case 'agents':
+                return $this->updateAgentsSettings($vars, $errors);
                 break;
-            case 'alerts':
-                return $this->updateAlertsSettings($vars, $errors);
+            case 'users':
+                return $this->updateUsersSettings($vars, $errors);
                 break;
             case 'kb':
                 return $this->updateKBSettings($vars, $errors);
@@ -874,16 +1094,41 @@ class OsticketConfig extends Config {
         $f['helpdesk_url']=array('type'=>'string',   'required'=>1, 'error'=>__('Helpdesk URL is required'));
         $f['helpdesk_title']=array('type'=>'string',   'required'=>1, 'error'=>__('Helpdesk title is required'));
         $f['default_dept_id']=array('type'=>'int',   'required'=>1, 'error'=>__('Default Department is required'));
+        $f['autolock_minutes']=array('type'=>'int',   'required'=>1, 'error'=>__('Enter lock time in minutes'));
         //Date & Time Options
         $f['time_format']=array('type'=>'string',   'required'=>1, 'error'=>__('Time format is required'));
         $f['date_format']=array('type'=>'string',   'required'=>1, 'error'=>__('Date format is required'));
         $f['datetime_format']=array('type'=>'string',   'required'=>1, 'error'=>__('Datetime format is required'));
         $f['daydatetime_format']=array('type'=>'string',   'required'=>1, 'error'=>__('Day, Datetime format is required'));
-        $f['default_timezone_id']=array('type'=>'int',   'required'=>1, 'error'=>__('Default Timezone is required'));
+        $f['default_timezone']=array('type'=>'string',   'required'=>1, 'error'=>__('Default Timezone is required'));
+        $f['system_language']=array('type'=>'string',   'required'=>1, 'error'=>__('A primary system language is required'));
+
+        // Make sure the selected backend is valid
+        $storagebk = null;
+        if (isset($vars['default_storage_bk'])) {
+            try {
+                $storagebk = FileStorageBackend::lookup($vars['default_storage_bk']);
+
+            } catch (Exception $ex) {
+                $errors['default_storage_bk'] = $ex->getMessage();
+            }
+        }
 
         if(!Validator::process($f, $vars, $errors) || $errors)
             return false;
 
+        // Manage secondard languages
+        $vars['secondary_langs'][] = $vars['add_secondary_language'];
+        foreach ($vars['secondary_langs'] as $i=>$lang) {
+            if (!$lang || !Internationalization::isLanguageInstalled($lang))
+                unset($vars['secondary_langs'][$i]);
+        }
+        $secondary_langs = implode(',', $vars['secondary_langs']);
+
+        if ($storagebk)
+            $this->update('default_storage_bk', $storagebk->getBkChar());
+
+
         return $this->updateAll(array(
             'isonline'=>$vars['isonline'],
             'helpdesk_title'=>$vars['helpdesk_title'],
@@ -892,23 +1137,32 @@ class OsticketConfig extends Config {
             'max_page_size'=>$vars['max_page_size'],
             'log_level'=>$vars['log_level'],
             'log_graceperiod'=>$vars['log_graceperiod'],
-            'name_format'=>$vars['name_format'],
             'time_format'=>$vars['time_format'],
             'date_format'=>$vars['date_format'],
             'datetime_format'=>$vars['datetime_format'],
             'daydatetime_format'=>$vars['daydatetime_format'],
-            'default_timezone_id'=>$vars['default_timezone_id'],
-            'enable_daylight_saving'=>isset($vars['enable_daylight_saving'])?1:0,
+            'date_formats'=>$vars['date_formats'],
+            'default_timezone'=>$vars['default_timezone'],
+            'default_locale'=>$vars['default_locale'],
+            'system_language'=>$vars['system_language'],
+            'secondary_langs'=>$secondary_langs,
+            'max_file_size' => $vars['max_file_size'],
+            'autolock_minutes' => $vars['autolock_minutes'],
+            'enable_avatars' => isset($vars['enable_avatars']) ? 1 : 0,
+            'enable_richtext' => isset($vars['enable_richtext']) ? 1 : 0,
         ));
     }
 
-    function updateAccessSettings($vars, &$errors) {
+    function updateAgentsSettings($vars, &$errors) {
         $f=array();
         $f['staff_session_timeout']=array('type'=>'int',   'required'=>1, 'error'=>'Enter idle time in minutes');
-        $f['client_session_timeout']=array('type'=>'int',   'required'=>1, 'error'=>'Enter idle time in minutes');
         $f['pw_reset_window']=array('type'=>'int', 'required'=>1, 'min'=>1,
             'error'=>__('Valid password reset window required'));
 
+        require_once INCLUDE_DIR.'class.avatar.php';
+        list($avatar_source) = explode('.', $vars['agent_avatar']);
+        if (!AvatarSource::lookup($avatar_source))
+            $errors['agent_avatar'] = __('Select a value from the list');
 
         if(!Validator::process($f, $vars, $errors) || $errors)
             return false;
@@ -919,14 +1173,36 @@ class OsticketConfig extends Config {
             'staff_login_timeout'=>$vars['staff_login_timeout'],
             'staff_session_timeout'=>$vars['staff_session_timeout'],
             'staff_ip_binding'=>isset($vars['staff_ip_binding'])?1:0,
+            'allow_pw_reset'=>isset($vars['allow_pw_reset'])?1:0,
+            'pw_reset_window'=>$vars['pw_reset_window'],
+            'agent_name_format'=>$vars['agent_name_format'],
+            'hide_staff_name'=>isset($vars['hide_staff_name']) ? 1 : 0,
+            'agent_avatar'=>$vars['agent_avatar'],
+        ));
+    }
+
+    function updateUsersSettings($vars, &$errors) {
+        $f=array();
+        $f['client_session_timeout']=array('type'=>'int',   'required'=>1, 'error'=>'Enter idle time in minutes');
+
+        require_once INCLUDE_DIR.'class.avatar.php';
+        list($avatar_source) = explode('.', $vars['client_avatar']);
+        if (!AvatarSource::lookup($avatar_source))
+            $errors['client_avatar'] = __('Select a value from the list');
+
+        if(!Validator::process($f, $vars, $errors) || $errors)
+            return false;
+
+        return $this->updateAll(array(
             'client_max_logins'=>$vars['client_max_logins'],
             'client_login_timeout'=>$vars['client_login_timeout'],
             'client_session_timeout'=>$vars['client_session_timeout'],
-            'allow_pw_reset'=>isset($vars['allow_pw_reset'])?1:0,
-            'pw_reset_window'=>$vars['pw_reset_window'],
             'clients_only'=>isset($vars['clients_only'])?1:0,
             'client_registration'=>$vars['client_registration'],
             'client_verify_email'=>isset($vars['client_verify_email'])?1:0,
+            'allow_auth_tokens' => isset($vars['allow_auth_tokens']) ? 1 : 0,
+            'client_name_format'=>$vars['client_name_format'],
+            'client_avatar'=>$vars['client_avatar'],
         ));
     }
 
@@ -936,7 +1212,6 @@ class OsticketConfig extends Config {
         $f['default_ticket_status_id'] = array('type'=>'int', 'required'=>1, 'error'=>__('Selection required'));
         $f['default_priority_id']=array('type'=>'int',   'required'=>1, 'error'=>__('Selection required'));
         $f['max_open_tickets']=array('type'=>'int',   'required'=>1, 'error'=>__('Enter valid numeric value'));
-        $f['autolock_minutes']=array('type'=>'int',   'required'=>1, 'error'=>__('Enter lock time in minutes'));
 
 
         if($vars['enable_captcha']) {
@@ -952,36 +1227,108 @@ class OsticketConfig extends Config {
             $errors['default_help_topic'] = __('Default help topic must be set to active');
         }
 
-        if (!preg_match('`(?!<\\\)#`', $vars['number_format']))
-            $errors['number_format'] = 'Ticket number format requires at least one hash character (#)';
+        if (!preg_match('`(?!<\\\)#`', $vars['ticket_number_format']))
+            $errors['ticket_number_format'] = 'Ticket number format requires at least one hash character (#)';
+
+        $this->updateAutoresponderSettings($vars, $errors);
+        $this->updateAlertsSettings($vars, $errors);
 
         if(!Validator::process($f, $vars, $errors) || $errors)
             return false;
 
-        if (isset($vars['default_storage_bk']))
-            $this->update('default_storage_bk', $vars['default_storage_bk']);
-
         return $this->updateAll(array(
-            'number_format'=>$vars['number_format'] ?: '######',
-            'sequence_id'=>$vars['sequence_id'] ?: 0,
+            'ticket_number_format'=>$vars['ticket_number_format'] ?: '######',
+            'ticket_sequence_id'=>$vars['ticket_sequence_id'] ?: 0,
             'default_priority_id'=>$vars['default_priority_id'],
             'default_help_topic'=>$vars['default_help_topic'],
             'default_ticket_status_id'=>$vars['default_ticket_status_id'],
             'default_sla_id'=>$vars['default_sla_id'],
             'max_open_tickets'=>$vars['max_open_tickets'],
-            'autolock_minutes'=>$vars['autolock_minutes'],
             'enable_captcha'=>isset($vars['enable_captcha'])?1:0,
             'auto_claim_tickets'=>isset($vars['auto_claim_tickets'])?1:0,
             'show_assigned_tickets'=>isset($vars['show_assigned_tickets'])?0:1,
             'show_answered_tickets'=>isset($vars['show_answered_tickets'])?0:1,
             'show_related_tickets'=>isset($vars['show_related_tickets'])?1:0,
-            'hide_staff_name'=>isset($vars['hide_staff_name'])?1:0,
-            'enable_html_thread'=>isset($vars['enable_html_thread'])?1:0,
             'allow_client_updates'=>isset($vars['allow_client_updates'])?1:0,
-            'max_file_size'=>$vars['max_file_size'],
+            'ticket_lock' => $vars['ticket_lock'],
         ));
     }
 
+    function updateTasksSettings($vars, &$errors) {
+        $f=array();
+        $f['default_task_priority_id']=array('type'=>'int',   'required'=>1, 'error'=>__('Selection required'));
+
+        if (!preg_match('`(?!<\\\)#`', $vars['task_number_format']))
+            $errors['task_number_format'] = 'Task number format requires at least one hash character (#)';
+
+        Validator::process($f, $vars, $errors);
+
+        if ($vars['task_alert_active']
+                && (!isset($vars['task_alert_admin'])
+                    && !isset($vars['task_alert_dept_manager'])
+                    && !isset($vars['task_alert_dept_members'])
+                    && !isset($vars['task_alert_acct_manager']))) {
+            $errors['task_alert_active'] = __('Select recipient(s)');
+        }
+
+        if ($vars['task_activity_alert_active']
+                && (!isset($vars['task_activity_alert_laststaff'])
+                    && !isset($vars['task_activity_alert_assigned'])
+                    && !isset($vars['task_activity_alert_dept_manager']))) {
+            $errors['task_activity_alert_active'] = __('Select recipient(s)');
+        }
+
+        if ($vars['task_transfer_alert_active']
+                && (!isset($vars['task_transfer_alert_assigned'])
+                    && !isset($vars['task_transfer_alert_dept_manager'])
+                    && !isset($vars['task_transfer_alert_dept_members']))) {
+            $errors['task_transfer_alert_active'] = __('Select recipient(s)');
+        }
+
+        if ($vars['task_overdue_alert_active']
+                && (!isset($vars['task_overdue_alert_assigned'])
+                    && !isset($vars['task_overdue_alert_dept_manager'])
+                    && !isset($vars['task_overdue_alert_dept_members']))) {
+            $errors['task_overdue_alert_active'] = __('Select recipient(s)');
+        }
+
+        if ($vars['task_assignment_alert_active']
+                && (!isset($vars['task_assignment_alert_staff'])
+                    && !isset($vars['task_assignment_alert_team_lead'])
+                    && !isset($vars['task_assignment_alert_team_members']))) {
+            $errors['task_assignment_alert_active'] = __('Select recipient(s)');
+        }
+
+        if ($errors)
+            return false;
+
+        return $this->updateAll(array(
+            'task_number_format'=>$vars['task_number_format'] ?: '######',
+            'task_sequence_id'=>$vars['task_sequence_id'] ?: 0,
+            'default_task_priority_id'=>$vars['default_task_priority_id'],
+            'default_task_sla_id'=>$vars['default_task_sla_id'],
+            'task_alert_active'=>$vars['task_alert_active'],
+            'task_alert_admin'=>isset($vars['task_alert_admin']) ? 1 : 0,
+            'task_alert_dept_manager'=>isset($vars['task_alert_dept_manager']) ? 1 : 0,
+            'task_alert_dept_members'=>isset($vars['task_alert_dept_members']) ? 1 : 0,
+            'task_activity_alert_active'=>$vars['task_activity_alert_active'],
+            'task_activity_alert_laststaff'=>isset($vars['task_activity_alert_laststaff']) ? 1 : 0,
+            'task_activity_alert_assigned'=>isset($vars['task_activity_alert_assigned']) ? 1 : 0,
+            'task_activity_alert_dept_manager'=>isset($vars['task_activity_alert_dept_manager']) ? 1 : 0,
+            'task_assignment_alert_active'=>$vars['task_assignment_alert_active'],
+            'task_assignment_alert_staff'=>isset($vars['task_assignment_alert_staff']) ? 1 : 0,
+            'task_assignment_alert_team_lead'=>isset($vars['task_assignment_alert_team_lead']) ? 1 : 0,
+            'task_assignment_alert_team_members'=>isset($vars['task_assignment_alert_team_members']) ? 1 : 0,
+            'task_transfer_alert_active'=>$vars['task_transfer_alert_active'],
+            'task_transfer_alert_assigned'=>isset($vars['task_transfer_alert_assigned']) ? 1 : 0,
+            'task_transfer_alert_dept_manager'=>isset($vars['task_transfer_alert_dept_manager']) ? 1 : 0,
+            'task_transfer_alert_dept_members'=>isset($vars['task_transfer_alert_dept_members']) ? 1 : 0,
+            'task_overdue_alert_active'=>$vars['task_overdue_alert_active'],
+            'task_overdue_alert_assigned'=>isset($vars['task_overdue_alert_assigned']) ? 1 : 0,
+            'task_overdue_alert_dept_manager'=>isset($vars['task_overdue_alert_dept_manager']) ? 1 : 0,
+            'task_overdue_alert_dept_members'=>isset($vars['task_overdue_alert_dept_members']) ? 1 : 0,
+        ));
+    }
 
     function updateEmailsSettings($vars, &$errors) {
         $f=array();
@@ -1019,7 +1366,7 @@ class OsticketConfig extends Config {
 
     function getLogo($site) {
         $id = $this->get("{$site}_logo_id", false);
-        return ($id) ? AttachmentFile::lookup($id) : null;
+        return ($id) ? AttachmentFile::lookup((int) $id) : null;
     }
     function getClientLogo() {
         return $this->getLogo('client');
@@ -1038,6 +1385,14 @@ class OsticketConfig extends Config {
         return $this->getLogo('staff');
     }
 
+    function getStaffLoginBackdropId() {
+        return $this->get("staff_backdrop_id", false);
+    }
+    function getStaffLoginBackdrop() {
+        $id = $this->getStaffLoginBackdropId();
+        return ($id) ? AttachmentFile::lookup((int) $id) : null;
+    }
+
     function updatePagesSettings($vars, &$errors) {
         global $ost;
 
@@ -1053,10 +1408,21 @@ class OsticketConfig extends Config {
                 ; // Pass
             elseif ($logo['error'])
                 $errors['logo'] = $logo['error'];
-            elseif (!($id = AttachmentFile::uploadLogo($logo, $error)))
+            elseif (!AttachmentFile::uploadLogo($logo, $error))
                 $errors['logo'] = sprintf(__('Unable to upload logo image: %s'), $error);
         }
 
+        if ($_FILES['backdrop']) {
+            $error = false;
+            list($backdrop) = AttachmentFile::format($_FILES['backdrop']);
+            if (!$backdrop)
+                ; // Pass
+            elseif ($backdrop['error'])
+                $errors['backdrop'] = $backdrop['error'];
+            elseif (!AttachmentFile::uploadBackdrop($backdrop, $error))
+                $errors['backdrop'] = sprintf(__('Unable to upload backdrop image: %s'), $error);
+        }
+
         $company = $ost->company;
         $company_form = $company->getForm();
         $company_form->setSource($_POST);
@@ -1071,7 +1437,13 @@ class OsticketConfig extends Config {
         if (isset($vars['delete-logo']))
             foreach ($vars['delete-logo'] as $id)
                 if (($vars['selected-logo'] != $id)
-                        && ($f = AttachmentFile::lookup($id)))
+                        && ($f = AttachmentFile::lookup((int) $id)))
+                    $f->delete();
+
+        if (isset($vars['delete-backdrop']))
+            foreach ($vars['delete-backdrop'] as $id)
+                if (($vars['selected-logo'] != $id)
+                        && ($f = AttachmentFile::lookup((int) $id)))
                     $f->delete();
 
         return $this->updateAll(array(
@@ -1084,6 +1456,9 @@ class OsticketConfig extends Config {
             'staff_logo_id' => (
                 (is_numeric($vars['selected-logo-scp']) && $vars['selected-logo-scp'])
                 ? $vars['selected-logo-scp'] : false),
+            'staff_backdrop_id' => (
+                (is_numeric($vars['selected-backdrop']) && $vars['selected-backdrop'])
+                ? $vars['selected-backdrop'] : false),
            ));
     }
 
diff --git a/include/class.cron.php b/include/class.cron.php
index f231ac5b3db00695cfa0a0a1045ff62d0bec9745..94e1af314a5e97bb957c212ef40ec3765004a360 100644
--- a/include/class.cron.php
+++ b/include/class.cron.php
@@ -27,9 +27,11 @@ class Cron {
 
     function TicketMonitor() {
         require_once(INCLUDE_DIR.'class.ticket.php');
-        require_once(INCLUDE_DIR.'class.lock.php');
         Ticket::checkOverdue(); //Make stale tickets overdue
-        TicketLock::cleanup(); //Remove expired locks
+        // Cleanup any expired locks
+        require_once(INCLUDE_DIR.'class.lock.php');
+        Lock::cleanup();
+
     }
 
     function PurgeLogs() {
@@ -54,7 +56,7 @@ class Cron {
         $chance = rand(1,2000);
         switch ($chance) {
         case 42:
-            @db_query('OPTIMIZE TABLE '.TICKET_LOCK_TABLE);
+            @db_query('OPTIMIZE TABLE '.LOCK_TABLE);
             break;
         case 242:
             @db_query('OPTIMIZE TABLE '.SYSLOG_TABLE);
diff --git a/include/class.crypto.php b/include/class.crypto.php
index a9aebcd2f87b6e5e66b67a0c70ba0c6855ddd0cf..b169cbb29f4986fa40a989d73a8237e943ac0746 100644
--- a/include/class.crypto.php
+++ b/include/class.crypto.php
@@ -231,7 +231,7 @@ class CryptoAlgo {
 
     var $ciphers = null;
 
-    function  CryptoAlgo($tag) {
+    function __construct($tag) {
         $this->tag_number = $tag;
     }
 
@@ -332,8 +332,8 @@ Class CryptoMcrypt extends CryptoAlgo {
                 ),
             );
 
-    function getCipher($cid=null) {
-        return parent::getCipher($cid, array($this, '_checkCipher'));
+    function getCipher($cid=null, $callback=false) {
+        return parent::getCipher($cid, $callback ?: array($this, '_checkCipher'));
     }
 
    function _checkCipher($c) {
@@ -465,8 +465,8 @@ class CryptoOpenSSL extends CryptoAlgo {
             ? $cipher['method']: '';
     }
 
-    function getCipher($cid) {
-        return parent::getCipher($cid, array($this, '_checkCipher'));
+    function getCipher($cid=null, $callback=false) {
+        return parent::getCipher($cid, $callback ?: array($this, '_checkCipher'));
     }
 
     function _checkCipher($c) {
@@ -580,8 +580,8 @@ class CryptoPHPSecLib extends CryptoAlgo {
         return new $class($c['mode']);
     }
 
-    function getCipher($cid) {
-        return  parent::getCipher($cid, array($this, '_checkCipher'));
+    function getCipher($cid=null, $callback=false) {
+        return  parent::getCipher($cid, $callback ?: array($this, '_checkCipher'));
     }
 
     function _checkCipher($c) {
diff --git a/include/class.csrf.php b/include/class.csrf.php
index a1c3aed21392d5932b6b1edb31108cc6d3bedf8a..77a8bf833f3de3c0745e8c957cc1bda3a3fcc84b 100644
--- a/include/class.csrf.php
+++ b/include/class.csrf.php
@@ -34,7 +34,7 @@ Class CSRF {
 
     var $csrf;
 
-    function CSRF($name='__CSRFToken__', $timeout=0) {
+    function __construct($name='__CSRFToken__', $timeout=0) {
 
         $this->name = $name;
         $this->timeout = $timeout;
diff --git a/include/class.dept.php b/include/class.dept.php
index 9e6cf21460aae4ae065ce7fdae923d48d5dd16d1..397df89c480b7fafa20ba09a51cd6dfa41fca78c 100644
--- a/include/class.dept.php
+++ b/include/class.dept.php
@@ -14,82 +14,119 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-class Dept {
-    var $id;
-
-    var $email;
-    var $sla;
-    var $manager;
-    var $members;
-    var $groups;
-
-    var $ht;
+class Dept extends VerySimpleModel
+implements TemplateVariable {
+
+    static $meta = array(
+        'table' => DEPT_TABLE,
+        'pk' => array('id'),
+        'joins' => array(
+            'parent' => array(
+                'constraint' => array('pid' => 'Dept.id'),
+                'null' => true,
+            ),
+            'email' => array(
+                'constraint' => array('email_id' => 'Email.email_id'),
+                'null' => true,
+             ),
+            'sla' => array(
+                'constraint' => array('sla_id' => 'SLA.sla_id'),
+                'null' => true,
+            ),
+            'manager' => array(
+                'null' => true,
+                'constraint' => array('manager_id' => 'Staff.staff_id'),
+            ),
+            'members' => array(
+                'null' => true,
+                'list' => true,
+                'reverse' => 'Staff.dept',
+            ),
+            'extended' => array(
+                'null' => true,
+                'list' => true,
+                'reverse' => 'StaffDeptAccess.dept'
+            ),
+        ),
+    );
+
+    var $_members;
+    var $_groupids;
+    var $config;
+
+    var $template;
+    var $autorespEmail;
 
     const ALERTS_DISABLED = 2;
-    const ALERTS_DEPT_AND_GROUPS = 1;
+    const ALERTS_DEPT_AND_EXTENDED = 1;
     const ALERTS_DEPT_ONLY = 0;
 
-    function Dept($id) {
-        $this->id=0;
-        $this->load($id);
-    }
-
-    function load($id=0) {
-        global $cfg;
-
-        if(!$id && !($id=$this->getId()))
-            return false;
-
-        $sql='SELECT dept.*,dept.dept_id as id,dept.dept_name as name, dept.dept_signature as signature, count(staff.staff_id) as users '
-            .' FROM '.DEPT_TABLE.' dept '
-            .' LEFT JOIN '.STAFF_TABLE.' staff ON (dept.dept_id=staff.dept_id) '
-            .' WHERE dept.dept_id='.db_input($id)
-            .' GROUP BY dept.dept_id';
-
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
-
-
-
-        $this->ht=db_fetch_array($res);
-        $this->id=$this->ht['dept_id'];
-        $this->email=$this->sla=$this->manager=null;
-        $this->getEmail(); //Auto load email struct.
-        $this->config = new Config('dept.'.$this->id);
-        $this->members=$this->groups=array();
-
-        return true;
-    }
-
-    function reload() {
-        return $this->load();
-    }
+    const FLAG_ASSIGN_MEMBERS_ONLY = 0x0001;
+    const FLAG_DISABLE_AUTO_CLAIM  = 0x0002;
 
     function asVar() {
         return $this->getName();
     }
 
+    static function getVarScope() {
+        return array(
+            'name' => 'Department name',
+            'manager' => array(
+                'class' => 'Staff', 'desc' => 'Department manager',
+                'exclude' => 'dept',
+            ),
+            'members' => array(
+                'class' => 'UserList', 'desc' => 'Department members',
+            ),
+            'parent' => array(
+                'class' => 'Dept', 'desc' => 'Parent department',
+            ),
+            'sla' => array(
+                'class' => 'SLA', 'desc' => 'Service Level Agreement',
+            ),
+            'signature' => 'Department signature',
+        );
+    }
+
+    function getVar($tag) {
+        switch ($tag) {
+        case 'members':
+            return new UserList($this->getMembers()->all());
+        }
+    }
+
     function getId() {
         return $this->id;
     }
 
     function getName() {
-        return $this->ht['name'];
+        return $this->name;
     }
 
-
-    function getEmailId() {
-        return $this->ht['email_id'];
+    function getLocalName($locale=false) {
+        $tag = $this->getTranslateTag();
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : $this->name;
+    }
+    static function getLocalById($id, $subtag, $default) {
+        $tag = _H(sprintf('dept.%s.%s', $subtag, $id));
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : $default;
+    }
+    static function getLocalNameById($id, $default) {
+        return static::getLocalById($id, 'name', $default);
     }
 
-    function getEmail() {
-        global $cfg;
+    function getTranslateTag($subtag='name') {
+        return _H(sprintf('dept.%s.%s', $subtag, $this->getId()));
+    }
 
-        if(!$this->email)
-            if(!($this->email = Email::lookup($this->getEmailId())) && $cfg)
-                $this->email = $cfg->getDefaultEmail();
+    function getFullName() {
+        return self::getNameById($this->getId());
+    }
 
-        return $this->email;
+    function getEmailId() {
+        return $this->email_id;
     }
 
     /**
@@ -101,22 +138,19 @@ class Dept {
     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;
-    }
+        if ($this->email)
+            return $this->email;
 
-    function getNumStaff() {
-        return $this->ht['users'];
+        return $cfg ? $cfg->getDefaultEmail() : null;
     }
 
+    function getEmail() {
+        global $cfg;
+
+        if ($this->email)
+            return $this->email;
 
-    function getNumUsers() {
-        return $this->getNumStaff();
+        return $cfg? $cfg->getDefaultEmail() : null;
     }
 
     function getNumMembers() {
@@ -126,80 +160,94 @@ class Dept {
     function getMembers($criteria=null) {
         global $cfg;
 
-        if(!$this->members || $criteria) {
-            $members = array();
-            $sql='SELECT DISTINCT s.staff_id FROM '.STAFF_TABLE.' s '
-                .' LEFT JOIN '.GROUP_TABLE.' g ON (g.group_id=s.group_id) '
-                .' LEFT JOIN '.GROUP_DEPT_TABLE.' gd ON(s.group_id=gd.group_id) '
-                .' INNER JOIN '.DEPT_TABLE.' d
-                       ON ( d.dept_id=s.dept_id
-                            OR d.manager_id=s.staff_id
-                            OR (d.dept_id=gd.dept_id AND d.group_membership='.
-                                self::ALERTS_DEPT_AND_GROUPS.')
-                        ) '
-                .' WHERE d.dept_id='.db_input($this->getId());
-
-            if ($criteria && $criteria['available'])
-                $sql .= ' AND
-                        ( g.group_enabled=1
-                          AND s.isactive=1
-                          AND s.onvacation=0 ) ';
-
-            switch ($cfg->getDefaultNameFormat()) {
+        if (!$this->_members || $criteria) {
+            $members = Staff::objects()
+                ->distinct('staff_id')
+                ->constrain(array(
+                    // Ensure that joining through dept_access is only relevant
+                    // for this department, so that the `alerts` annotation
+                    // can work properly
+                    'dept_access' => new Q(array('dept_access__dept_id' => $this->getId()))
+                ))
+                ->filter(Q::any(array(
+                    'dept_id' => $this->getId(),
+                    'staff_id' => $this->manager_id,
+                    'dept_access__dept_id' => $this->getId(),
+                )));
+
+            // TODO: Consider moving this into ::getAvailableMembers
+            if ($criteria && $criteria['available']) {
+                $members->filter(array(
+                    'isactive' => 1,
+                    'onvacation' => 0,
+                ));
+            }
+            switch ($cfg->getAgentNameFormat()) {
             case 'last':
             case 'lastfirst':
             case 'legal':
-                $sql .= ' ORDER BY s.lastname, s.firstname';
+                $members->order_by('lastname', 'firstname');
                 break;
 
             default:
-                $sql .= ' ORDER BY s.firstname, s.lastname';
-            }
-
-            if(($res=db_query($sql)) && db_num_rows($res)) {
-                while(list($id)=db_fetch_row($res))
-                    $members[$id] = Staff::lookup($id);
+                $members->order_by('firstname', 'lastname');
             }
 
             if ($criteria)
                 return $members;
 
-            $this->members = $members;
-
+            $this->_members = $members;
         }
-
-        return $this->members;
+        return $this->_members;
     }
 
     function getAvailableMembers() {
         return $this->getMembers(array('available'=>1));
     }
 
+    // Get members  eligible members only
+    function getAssignees() {
+
+        $members = clone $this->getAvailableMembers();
+        // If restricted then filter to primary members ONLY!
+        if ($this->assignMembersOnly())
+            $members->filter(array('dept_id' => $this->getId()));
+
+        return $members;
+    }
+
     function getMembersForAlerts() {
         if ($this->isGroupMembershipEnabled() == self::ALERTS_DISABLED) {
             // Disabled for this department
             $rv = array();
         }
         else {
-            $rv = $this->getAvailableMembers();
+            $rv = clone $this->getAvailableMembers();
+            $rv->filter(Q::any(array(
+                // Ensure "Alerts" is enabled — must be a primary member or
+                // have alerts enabled on your membership and have alerts
+                // configured to extended to extended access members
+                'dept_id' => $this->getId(),
+                // NOTE: Manager is excluded here if not a member
+                Q::all(array(
+                    'dept_access__dept__group_membership' => self::ALERTS_DEPT_AND_EXTENDED,
+                    'dept_access__flags__hasbit' => StaffDeptAccess::FLAG_ALERTS,
+                )),
+            )));
         }
         return $rv;
     }
 
     function getSLAId() {
-        return $this->ht['sla_id'];
+        return $this->sla_id;
     }
 
     function getSLA() {
-
-        if(!$this->sla && $this->getSLAId())
-            $this->sla=SLA::lookup($this->getSLAId());
-
         return $this->sla;
     }
 
     function getTemplateId() {
-         return $this->ht['tpl_id'];
+         return $this->tpl_id;
     }
 
     function getTemplate() {
@@ -216,8 +264,8 @@ class Dept {
     function getAutoRespEmail() {
 
         if (!$this->autorespEmail) {
-            if (!$this->ht['autoresp_email_id']
-                    || !($this->autorespEmail = Email::lookup($this->ht['autoresp_email_id'])))
+            if (!$this->autoresp_email_id
+                    || !($this->autorespEmail = Email::lookup($this->autoresp_email_id)))
                 $this->autorespEmail = $this->getEmail();
         }
 
@@ -230,7 +278,7 @@ class Dept {
     }
 
     function getSignature() {
-        return $this->ht['signature'];
+        return $this->signature;
     }
 
     function canAppendSignature() {
@@ -238,14 +286,10 @@ class Dept {
     }
 
     function getManagerId() {
-        return $this->ht['manager_id'];
+        return $this->manager_id;
     }
 
     function getManager() {
-
-        if(!$this->manager && $this->getManagerId())
-            $this->manager=Staff::lookup($this->getManagerId());
-
         return $this->manager;
     }
 
@@ -268,244 +312,427 @@ class Dept {
     }
 
     function isPublic() {
-         return ($this->ht['ispublic']);
+         return $this->ispublic;
     }
 
     function autoRespONNewTicket() {
-        return ($this->ht['ticket_auto_response']);
+        return $this->ticket_auto_response;
     }
 
     function autoRespONNewMessage() {
-        return ($this->ht['message_auto_response']);
+        return $this->message_auto_response;
     }
 
     function noreplyAutoResp() {
-         return ($this->ht['noreply_autoresp']);
+         return $this->noreply_autoresp;
     }
 
     function assignMembersOnly() {
-        return ($this->config->get('assign_members_only', 0));
-    }
-
-    function isGroupMembershipEnabled() {
-        return ($this->ht['group_membership']);
+        return $this->flags & self::FLAG_ASSIGN_MEMBERS_ONLY;
     }
 
-    function getHashtable() {
-        return $this->ht;
-    }
-
-    function getInfo() {
-        return $this->config->getInfo() + $this->getHashtable();
+    function disableAutoClaim() {
+        return $this->flags & self::FLAG_DISABLE_AUTO_CLAIM;
     }
 
-    function getAllowedGroups() {
-
-        if($this->groups) return $this->groups;
-
-        $sql='SELECT group_id FROM '.GROUP_DEPT_TABLE
-            .' WHERE dept_id='.db_input($this->getId());
-
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while(list($id)=db_fetch_row($res))
-                $this->groups[] = $id;
-        }
-
-        return $this->groups;
+    function isGroupMembershipEnabled() {
+        return $this->group_membership;
     }
 
-    function updateSettings($vars) {
-
-        // Groups allowes to access department
-        if($vars['groups'] && is_array($vars['groups'])) {
-            foreach($vars['groups'] as $k=>$id) {
-                $sql='INSERT IGNORE INTO '.GROUP_DEPT_TABLE
-                    .' SET dept_id='.db_input($this->getId()).', group_id='.db_input($id);
-                db_query($sql);
-            }
-        }
-        $sql='DELETE FROM '.GROUP_DEPT_TABLE.' WHERE dept_id='.db_input($this->getId());
-        if($vars['groups'] && is_array($vars['groups']))
-            $sql.=' AND group_id NOT IN ('.implode(',', db_input($vars['groups'])).')';
-
-        db_query($sql);
-
-        // Misc. config settings
-        $this->config->set('assign_members_only', $vars['assign_members_only']);
+    function getHashtable() {
+        $ht = $this->ht;
+        if (static::$meta['joins'])
+            foreach (static::$meta['joins'] as $k => $v)
+                unset($ht[$k]);
 
-        return true;
+        $ht['assign_members_only'] = $this->assignMembersOnly();
+        $ht['disable_auto_claim'] =  $this->disableAutoClaim();
+        return $ht;
     }
 
-    function update($vars, &$errors) {
-
-        if(!$this->save($this->getId(), $vars, $errors))
-            return false;
-
-        $this->updateSettings($vars);
-        $this->reload();
-
-        return true;
+    function getInfo() {
+        return $this->getHashtable();
     }
 
     function delete() {
         global $cfg;
 
-        if(!$cfg
-                // Default department cannot be deleted
-                || $this->getId()==$cfg->getDefaultDeptId()
-                // Department  with users cannot be deleted
-                || $this->getNumUsers())
+        if (!$cfg
+            // Default department cannot be deleted
+            || $this->getId()==$cfg->getDefaultDeptId()
+            // Department  with users cannot be deleted
+            || $this->members->count()
+        ) {
             return 0;
+        }
 
-        $id=$this->getId();
-        $sql='DELETE FROM '.DEPT_TABLE.' WHERE dept_id='.db_input($id).' LIMIT 1';
-        if(db_query($sql) && ($num=db_affected_rows())) {
+        $id = $this->getId();
+        if (parent::delete()) {
             // DO SOME HOUSE CLEANING
             //Move tickets to default Dept. TODO: Move one ticket at a time and send alerts + log notes.
-            db_query('UPDATE '.TICKET_TABLE.' SET dept_id='.db_input($cfg->getDefaultDeptId()).' WHERE dept_id='.db_input($id));
+            Ticket::objects()
+                ->filter(array('dept_id' => $id))
+                ->update(array('dept_id' => $cfg->getDefaultDeptId()));
+
             //Move Dept members: This should never happen..since delete should be issued only to empty Depts...but check it anyways
-            db_query('UPDATE '.STAFF_TABLE.' SET dept_id='.db_input($cfg->getDefaultDeptId()).' WHERE dept_id='.db_input($id));
+            Staff::objects()
+                ->filter(array('dept_id' => $id))
+                ->update(array('dept_id' => $cfg->getDefaultDeptId()));
 
             // Clear any settings using dept to default back to system default
-            db_query('UPDATE '.TOPIC_TABLE.' SET dept_id=0 WHERE dept_id='.db_input($id));
-            db_query('UPDATE '.EMAIL_TABLE.' SET dept_id=0 WHERE dept_id='.db_input($id));
-            db_query('UPDATE '.FILTER_TABLE.' SET dept_id=0 WHERE dept_id='.db_input($id));
-
-            //Delete group access
-            db_query('DELETE FROM '.GROUP_DEPT_TABLE.' WHERE dept_id='.db_input($id));
+            Topic::objects()
+                ->filter(array('dept_id' => $id))
+                ->delete();
+            Email::objects()
+                ->filter(array('dept_id' => $id))
+                ->delete();
+
+            foreach(FilterAction::objects()
+                ->filter(array('type' => FA_RouteDepartment::$type)) as $fa
+            ) {
+                $config = $fa->getConfiguration();
+                if ($config && $config['dept_id'] == $id) {
+                    $config['dept_id'] = 0;
+                    // FIXME: Move this code into FilterAction class
+                    $fa->set('configuration', JsonDataEncoder::encode($config));
+                    $fa->save();
+                }
+            }
 
-            // Destrory config settings
-            $this->config->destroy();
+            // Delete extended access entries
+            StaffDeptAccess::objects()
+                ->filter(array('dept_id' => $id))
+                ->delete();
         }
-
-        return $num;
+        return true;
     }
 
     function __toString() {
         return $this->getName();
     }
 
-    /*----Static functions-------*/
-	function getIdByName($name) {
-        $id=0;
-        $sql ='SELECT dept_id FROM '.DEPT_TABLE.' WHERE dept_name='.db_input($name);
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id)=db_fetch_row($res);
+    function getParent() {
+        return static::lookup($this->ht['pid']);
+    }
 
-        return $id;
+    /**
+     * getFullPath
+     *
+     * Utility function to retrieve a '/' separated list of department IDs
+     * in the ancestry of this department. This is used to populate the
+     * `path` field in the database and is used for access control rather
+     * than the ID field since nesting of departments is necessary and
+     * department access can be cascaded.
+     *
+     * Returns:
+     * Slash-separated string of ID ancestry of this department. The string
+     * always starts and ends with a slash, and will always contain the ID
+     * of this department last.
+     */
+    function getFullPath() {
+        $path = '';
+        if ($p = $this->getParent())
+            $path .= $p->getFullPath();
+        else
+            $path .= '/';
+        $path .= $this->getId() . '/';
+        return $path;
     }
 
-    function lookup($id) {
-        return ($id && is_numeric($id) && ($dept = new Dept($id)) && $dept->getId()==$id)?$dept:null;
+    /**
+     * setFlag
+     *
+     * Utility method to set/unset flag bits
+     *
+     */
+
+    private function setFlag($flag, $val) {
+
+        if ($val)
+            $this->flags |= $flag;
+        else
+            $this->flags &= ~$flag;
     }
 
-    function getNameById($id) {
+    /*----Static functions-------*/
+	static function getIdByName($name, $pid=null) {
+        $row = static::objects()
+            ->filter(array(
+                        'name' => $name,
+                        'pid'  => $pid ?: null))
+            ->values_flat('id')
+            ->first();
 
-        if($id && ($dept=Dept::lookup($id)))
-            $name= $dept->getName();
+        return $row ? $row[0] : 0;
+    }
 
-        return $name;
+    function getNameById($id) {
+        $names = static::getDepartments();
+        return $names[$id];
     }
 
     function getDefaultDeptName() {
         global $cfg;
-        return ($cfg && $cfg->getDefaultDeptId() && ($name=Dept::getNameById($cfg->getDefaultDeptId())))?$name:null;
-    }
 
-    function getDepartments( $criteria=null) {
+        return ($cfg
+            && ($did = $cfg->getDefaultDeptId())
+            && ($names = self::getDepartments()))
+            ? $names[$did]
+            : null;
+    }
+
+    static function getDepartments( $criteria=null, $localize=true) {
+        static $depts = null;
+
+        if (!isset($depts) || $criteria) {
+            // XXX: This will upset the static $depts array
+            $depts = array();
+            $query = self::objects();
+            if (isset($criteria['publiconly']))
+                $query->filter(array(
+                            'ispublic' => ($criteria['publiconly'] ? 1 : 0)));
+
+            if ($manager=$criteria['manager'])
+                $query->filter(array(
+                            'manager_id' => is_object($manager)?$manager->getId():$manager));
+
+            if (isset($criteria['nonempty'])) {
+                $query->annotate(array(
+                    'user_count' => SqlAggregate::COUNT('members')
+                ))->filter(array(
+                    'user_count__gt' => 0
+                ));
+            }
 
-        $depts=array();
-        $sql='SELECT dept_id, dept_name FROM '.DEPT_TABLE.' WHERE 1';
-        if($criteria['publiconly'])
-            $sql.=' AND  ispublic=1';
+            $query->order_by('name')
+                ->values('id', 'pid', 'name', 'parent');
+
+            foreach ($query as $row)
+                $depts[$row['id']] = $row;
+
+            $localize_this = function($id, $default) use ($localize) {
+                if (!$localize)
+                    return $default;
+
+                $tag = _H("dept.name.{$id}");
+                $T = CustomDataTranslation::translate($tag);
+                return $T != $tag ? $T : $default;
+            };
+
+            // Resolve parent names
+            $names = array();
+            foreach ($depts as $id=>$info) {
+                $name = $info['name'];
+                $loop = array($id=>true);
+                $parent = false;
+                while ($info['pid'] && ($info = $depts[$info['pid']])) {
+                    $name = sprintf('%s / %s', $info['name'], $name);
+                    if (isset($loop[$info['pid']]))
+                        break;
+                    $loop[$info['pid']] = true;
+                    $parent = $info;
+                }
+                // Fetch local names
+                $names[$id] = $localize_this($id, $name);
+            }
+            asort($names);
 
-        if(($manager=$criteria['manager']))
-            $sql.=' AND manager_id='.db_input(is_object($manager)?$manager->getId():$manager);
+            // TODO: Use locale-aware sorting mechanism
 
-        $sql.=' ORDER BY dept_name';
+            if ($criteria)
+                return $names;
 
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while(list($id, $name)=db_fetch_row($res))
-                $depts[$id] = $name;
+            $depts = $names;
         }
 
         return $depts;
     }
 
-    function getPublicDepartments() {
-        return self::getDepartments(array('publiconly'=>true));
+    static function getPublicDepartments() {
+        static $depts =null;
+
+        if (!$depts)
+            $depts = self::getDepartments(array('publiconly'=>true));
+
+        return $depts;
+    }
+
+    static function create($vars=false, &$errors=array()) {
+        $dept = new static($vars);
+        $dept->created = SqlFunction::NOW();
+        return $dept;
     }
 
-    function create($vars, &$errors) {
+    static function __create($vars, &$errors) {
+        $dept = self::create($vars);
+        $dept->update($vars, $errors);
 
-        if(!($id=self::save(0, $vars, $errors)))
-            return null;
+        return isset($dept->id) ? $dept : null;
+    }
 
-        if (($dept=self::lookup($id)))
-            $dept->updateSettings($vars);
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
 
-        return $id;
+        return parent::save($refetch || $this->dirty);
     }
 
-    function save($id, $vars, &$errors) {
+    function update($vars, &$errors) {
         global $cfg;
 
-        if($id && $id!=$vars['id'])
+        $id = $this->id;
+        if ($id && $id != $vars['id'])
             $errors['err']=__('Missing or invalid Dept ID (internal error).');
 
-        if(!$vars['name']) {
+        if (!$vars['name']) {
             $errors['name']=__('Name required');
-        } elseif(strlen($vars['name'])<4) {
-            $errors['name']=__('Name is too short.');
-        } elseif(($did=Dept::getIdByName($vars['name'])) && $did!=$id) {
+        } elseif (($did = static::getIdByName($vars['name'], $vars['pid']))
+                && $did != $id) {
             $errors['name']=__('Department already exists');
         }
 
-        if(!$vars['ispublic'] && $cfg && ($vars['id']==$cfg->getDefaultDeptId()))
+        if (!$vars['ispublic'] && $cfg && ($vars['id']==$cfg->getDefaultDeptId()))
             $errors['ispublic']=__('System default department cannot be private');
 
-        if($errors) return false;
+        if ($vars['pid'] && !($p = static::lookup($vars['pid'])))
+            $errors['pid'] = __('Department selection is required');
 
+        // Format access update as [array(dept_id, role_id, alerts?)]
+        $access = array();
+        if (isset($vars['members'])) {
+            foreach (@$vars['members'] as $staff_id) {
+                $access[] = array($staff_id, $vars['member_role'][$staff_id],
+                    @$vars['member_alerts'][$staff_id]);
+            }
+        }
+        $this->updateAccess($access, $errors);
 
-        $sql='SET updated=NOW() '
-            .' ,ispublic='.db_input(isset($vars['ispublic'])?$vars['ispublic']:0)
-            .' ,email_id='.db_input(isset($vars['email_id'])?$vars['email_id']:0)
-            .' ,tpl_id='.db_input(isset($vars['tpl_id'])?$vars['tpl_id']:0)
-            .' ,sla_id='.db_input(isset($vars['sla_id'])?$vars['sla_id']:0)
-            .' ,autoresp_email_id='.db_input(isset($vars['autoresp_email_id'])?$vars['autoresp_email_id']:0)
-            .' ,manager_id='.db_input($vars['manager_id']?$vars['manager_id']:0)
-            .' ,dept_name='.db_input(Format::striptags($vars['name']))
-            .' ,dept_signature='.db_input(Format::sanitize($vars['signature']))
-            .' ,group_membership='.db_input($vars['group_membership'])
-            .' ,ticket_auto_response='.db_input(isset($vars['ticket_auto_response'])?$vars['ticket_auto_response']:1)
-            .' ,message_auto_response='.db_input(isset($vars['message_auto_response'])?$vars['message_auto_response']:1);
-
+        if ($errors)
+            return false;
 
-        if($id) {
-            $sql='UPDATE '.DEPT_TABLE.' '.$sql.' WHERE dept_id='.db_input($id);
-            if(db_query($sql) && db_affected_rows())
-                return true;
+        $this->pid = $vars['pid'] ?: null;
+        $this->ispublic = isset($vars['ispublic'])?$vars['ispublic']:0;
+        $this->email_id = isset($vars['email_id'])?$vars['email_id']:0;
+        $this->tpl_id = isset($vars['tpl_id'])?$vars['tpl_id']:0;
+        $this->sla_id = isset($vars['sla_id'])?$vars['sla_id']:0;
+        $this->autoresp_email_id = isset($vars['autoresp_email_id'])?$vars['autoresp_email_id']:0;
+        $this->manager_id = $vars['manager_id'] ?: 0;
+        $this->name = Format::striptags($vars['name']);
+        $this->signature = Format::sanitize($vars['signature']);
+        $this->group_membership = $vars['group_membership'];
+        $this->ticket_auto_response = isset($vars['ticket_auto_response'])?$vars['ticket_auto_response']:1;
+        $this->message_auto_response = isset($vars['message_auto_response'])?$vars['message_auto_response']:1;
+        $this->flags = 0;
+        $this->setFlag(self::FLAG_ASSIGN_MEMBERS_ONLY, isset($vars['assign_members_only']));
+        $this->setFlag(self::FLAG_DISABLE_AUTO_CLAIM, isset($vars['disable_auto_claim']));
+
+        $this->path = $this->getFullPath();
+
+        $wasnew = $this->__new__;
+        if ($this->save() && $this->extended->saveAll()) {
+            if ($wasnew) {
+                // The ID wasn't available until after the commit
+                $this->path = $this->getFullPath();
+                $this->save();
+            }
+            return true;
+        }
 
+        if (isset($this->id))
             $errors['err']=sprintf(__('Unable to update %s.'), __('this department'))
                .' '.__('Internal error occurred');
-
-        } else {
-            if (isset($vars['id']))
-                $sql .= ', dept_id='.db_input($vars['id']);
-
-            $sql='INSERT INTO '.DEPT_TABLE.' '.$sql.',created=NOW()';
-            if(db_query($sql) && ($id=db_insert_id()))
-                return $id;
-
-
+        else
             $errors['err']=sprintf(__('Unable to create %s.'), __('this department'))
                .' '.__('Internal error occurred');
 
-        }
-
-
         return false;
     }
 
+    function updateAccess($access, &$errors) {
+      reset($access);
+      $dropped = array();
+      foreach ($this->extended as $DA)
+          $dropped[$DA->staff_id] = 1;
+      while (list(, list($staff_id, $role_id, $alerts)) = each($access)) {
+          unset($dropped[$staff_id]);
+          if (!$role_id || !Role::lookup($role_id))
+              $errors['members'][$staff_id] = __('Select a valid role');
+          if (!$staff_id || !Staff::lookup($staff_id))
+              $errors['members'][$staff_id] = __('No such agent');
+          $da = $this->extended->findFirst(array('staff_id' => $staff_id));
+          if (!isset($da)) {
+              $da = StaffDeptAccess::create(array(
+                  'staff_id' => $staff_id, 'role_id' => $role_id
+              ));
+              $this->extended->add($da);
+          }
+          else {
+              $da->role_id = $role_id;
+          }
+          $da->setAlerts($alerts);
+      }
+      if (!$errors && $dropped) {
+          $this->extended
+              ->filter(array('staff_id__in' => array_keys($dropped)))
+              ->delete();
+          $this->extended->reset();
+      }
+      return !$errors;
+    }
+}
+
+class DepartmentQuickAddForm
+extends Form {
+    function getFields() {
+        if ($this->fields)
+            return $this->fields;
+
+        return $this->fields = array(
+            'pid' => new ChoiceField(array(
+                'label' => '',
+                'default' => 0,
+                'choices' =>
+                    array(0 => '— '.__('Top-Level Department').' —')
+                    + Dept::getDepartments()
+            )),
+            'name' => new TextboxField(array(
+                'required' => true,
+                'configuration' => array(
+                    'placeholder' => __('Name'),
+                    'classes' => 'span12',
+                    'autofocus' => true,
+                    'length' => 128,
+                ),
+            )),
+            'email_id' => new ChoiceField(array(
+                'label' => __('Email Mailbox'),
+                'default' => 0,
+                'choices' =>
+                    array(0 => '— '.__('System Default').' —')
+                    + Email::getAddresses(),
+                'configuration' => array(
+                    'classes' => 'span12',
+                ),
+            )),
+            'private' => new BooleanField(array(
+                'configuration' => array(
+                    'classes' => 'form footer',
+                    'desc' => __('This department is for internal use'),
+                ),
+            )),
+        );
+    }
+
+    function getClean() {
+        $clean = parent::getClean();
+
+        $clean['ispublic'] = !$clean['private'];
+        unset($clean['private']);
+
+        return $clean;
+    }
+
+    function render($staff=true, $title=false, $options=array()) {
+        return parent::render($staff, $title, $options + array('template' => 'dynamic-form-simple.tmpl.php'));
+    }
 }
-?>
diff --git a/include/class.dispatcher.php b/include/class.dispatcher.php
index 3490e78826d62287530f2fc23ba1a1bf4a15b462..00cec8700a644dcf33d1c352ac36304e2069c209 100644
--- a/include/class.dispatcher.php
+++ b/include/class.dispatcher.php
@@ -21,7 +21,7 @@
  * functions aren't separated
  */
 class Dispatcher {
-    function Dispatcher($file=false) {
+    function __construct($file=false) {
         $this->urls = array();
         $this->file = $file;
     }
@@ -81,7 +81,7 @@ class Dispatcher {
 }
 
 class UrlMatcher {
-    function UrlMatcher($regex, $func, $args=false, $method=false) {
+    function __construct($regex, $func, $args=false, $method=false) {
         # Add the slashes for the Perl syntax
         $this->regex = "@" . $regex . "@";
         $this->func = $func;
diff --git a/include/class.draft.php b/include/class.draft.php
index 3235a9ff92a5e7f967a73d78811568951975ebec..d123ff6935abd7d21c96890d23bb2084d656a57b 100644
--- a/include/class.draft.php
+++ b/include/class.draft.php
@@ -1,28 +1,75 @@
 <?php
 
-class Draft {
+/**
+ * Class: Draft
+ *
+ * Defines a simple draft-saving mechanism for osTicket which supports draft
+ * fetch and update via an ajax mechanism (include/ajax.draft.php).
+ *
+ * Fields:
+ * id - (int:auto:pk) Draft ID number
+ * body - (text) Body of the draft
+ * namespace - (string) Identifier of draft grouping — useful for multiple
+ *      drafts on the same document by different users
+ * staff_id - (int:null) Staff owner of the draft
+ * extra - (text:json) Extra attributes of the draft
+ * created - (date) Date draft was initially created
+ * updated - (date:null) Date draft was last updated
+ */
+class Draft extends VerySimpleModel {
+
+    static $meta = array(
+        'table' => DRAFT_TABLE,
+        'pk' => array('id'),
+        'joins' => array(
+            'attachments' => array(
+                'constraint' => array(
+                    "'D'" => 'Attachment.type',
+                    'id' => 'Attachment.object_id',
+                ),
+                'list' => true,
+                'null' => true,
+                'broker' => 'GenericAttachments',
+            ),
+        ),
+    );
 
-    var $id;
-    var $ht;
-
-    var $_attachments;
-
-    function Draft($id) {
-        $this->id = $id;
-        $this->load();
+    function getId() { return $this->id; }
+    function getBody() { return $this->body; }
+    function getStaffId() { return $this->staff_id; }
+    function getNamespace() { return $this->namespace; }
+
+    static protected function getCurrentUserId() {
+        global $thisstaff, $thisclient;
+        
+        $user = $thisstaff ?: $thisclient;
+        if ($user)
+            return $user->getId();
+
+        return 1 << 31;
     }
 
-    function load() {
-        $this->attachments = new GenericAttachments($this->id, 'D');
-        $sql = 'SELECT * FROM '.DRAFT_TABLE.' WHERE id='.db_input($this->id);
-        return (($res = db_query($sql))
-            && ($this->ht = db_fetch_array($res)));
-    }
+    static function getDraftAndDataAttrs($namespace, $id=0, $original='') {
+        $draft_body = null;
+        $attrs = array(sprintf('data-draft-namespace="%s"', Format::htmlchars($namespace)));
+        $criteria = array(
+            'namespace' => $namespace,
+            'staff_id' => self::getCurrentUserId(),
+        );
+        if ($id) {
+            $attrs[] = sprintf('data-draft-object-id="%s"', Format::htmlchars($id));
+            $criteria['namespace'] .= '.' . $id;
+        }
+        if ($draft = static::objects()->filter($criteria)->first()) {
+            $attrs[] = sprintf('data-draft-id="%s"', $draft->getId());
+            $draft_body = $draft->getBody();
+        }
+        $attrs[] = sprintf('data-draft-original="%s"',
+            Format::htmlchars(Format::viewableImages($original)));
 
-    function getId() { return $this->id; }
-    function getBody() { return $this->ht['body']; }
-    function getStaffId() { return $this->ht['staff_id']; }
-    function getNamespace() { return $this->ht['namespace']; }
+        return array(Format::htmlchars(Format::viewableImages($draft_body)),
+            implode(' ', $attrs));
+    }
 
     function getAttachmentIds($body=false) {
         $attachments = array();
@@ -31,11 +78,13 @@ class Draft {
         $body = Format::localizeInlineImages($body);
         $matches = array();
         if (preg_match_all('/"cid:([\\w.-]{32})"/', $body, $matches)) {
-            foreach ($matches[1] as $hash) {
-                if ($file_id = AttachmentFile::getIdByHash($hash))
-                    $attachments[] = array(
-                            'id' => $file_id,
-                            'inline' => true);
+            $files = AttachmentFile::objects()
+                ->filter(array('key__in' => $matches[1]));
+            foreach ($files as $F) {
+                $attachments[] = array(
+                    'id' => $F->getId(),
+                    'inline' => true
+                );
             }
         }
         return $attachments;
@@ -55,77 +104,63 @@ class Draft {
 
         // Purge current attachments
         $this->attachments->deleteInlines();
-        foreach ($matches[1] as $hash)
-            if ($file = AttachmentFile::getIdByHash($hash))
-                $this->attachments->upload($file, true);
+        foreach (AttachmentFile::objects()
+            ->filter(array('key__in' => $matches[1]))
+            as $F
+        ) {
+            $this->attachments->upload($F->getId(), true);
+        }
     }
 
     function setBody($body) {
         // Change file.php urls back to content-id's
-        $body = Format::sanitize($body, false);
-        $this->ht['body'] = $body;
+        $body = Format::sanitize($body, false,
+            // Preserve annotation information, if any
+            'img=data-annotations,data-orig-annotated-image-src');
 
-        $sql='UPDATE '.DRAFT_TABLE.' SET updated=NOW()'
-            .',body='.db_input($body)
-            .' WHERE id='.db_input($this->getId());
-        return db_query($sql) && db_affected_rows() == 1;
+        $this->body = $body ?: ' ';
+        $this->updated = SqlFunction::NOW();
+        return $this->save();
     }
 
     function delete() {
         $this->attachments->deleteAll();
-        $sql = 'DELETE FROM '.DRAFT_TABLE
-            .' WHERE id='.db_input($this->getId());
-        return (db_query($sql) && db_affected_rows() == 1);
+        return parent::delete();
     }
 
-    function save($id, $vars, &$errors) {
+    function isValid() {
         // Required fields
-        if (!$vars['namespace'] || !isset($vars['body']) || !isset($vars['staff_id']))
-            return false;
+        return $this->namespace && isset($this->staff_id);
+    }
 
-        $sql = ' SET `namespace`='.db_input($vars['namespace'])
-            .' ,body='.db_input(Format::sanitize($vars['body'], false))
-            .' ,staff_id='.db_input($vars['staff_id']);
+    function save($refetch=false) {
+        if (!$this->isValid())
+            return false;
 
-        if (!$id) {
-            $sql = 'INSERT INTO '.DRAFT_TABLE.$sql
-                .' ,created=NOW()';
-            if(!db_query($sql) || !($draft=self::lookup(db_insert_id())))
-                return false;
+        return parent::save($refetch);
+    }
 
-            // Cloned attachments...
-            if($vars['attachments'] && is_array($vars['attachments']))
-                $draft->attachments->upload($vars['attachments'], true);
+    static function create($vars=false) {
+        $attachments = @$vars['attachments'];
+        unset($vars['attachments']);
 
-            return $draft;
-        }
-        else {
-            $sql = 'UPDATE '.DRAFT_TABLE.$sql
-                .' WHERE id='.db_input($id);
-            if (db_query($sql) && db_affected_rows() == 1)
-                return $this;
-        }
-    }
+        $vars['created'] = SqlFunction::NOW();
+        $vars['staff_id'] = self::getCurrentUserId();
+        $draft = new static($vars);
 
-    function create($vars, &$errors) {
-        return self::save(0, $vars, $errors);
-    }
+        // Cloned attachments ...
+        if (false && $attachments && is_array($attachments))
+            // XXX: This won't work until the draft is saved
+            $draft->attachments->upload($attachments, true);
 
-    function lookup($id) {
-        return ($id && is_numeric($id)
-                && ($d = new Draft($id))
-                && $d->getId()==$id
-                ) ? $d : null;
+        return $draft;
     }
 
-    function findByNamespaceAndStaff($namespace, $staff_id) {
-        $sql = 'SELECT id FROM '.DRAFT_TABLE
-            .' WHERE `namespace`='.db_input($namespace)
-            .' AND staff_id='.db_input($staff_id);
-        if (($res = db_query($sql)) && (list($id) = db_fetch_row($res)))
-            return $id;
-        else
-            return false;
+    static function lookupByNamespaceAndStaff($namespace, $staff_id) {
+        return static::lookup(array(
+            'namespace'=>$namespace,
+            'staff_id'=>$staff_id
+        ));
     }
 
     /**
@@ -134,30 +169,25 @@ class Draft {
      * closing a ticket, the staff_id should be left null so that all drafts
      * are cleaned up.
      */
-    /* static */
-    function deleteForNamespace($namespace, $staff_id=false) {
-        $sql = 'DELETE attach FROM '.ATTACHMENT_TABLE.' attach
-                INNER JOIN '.DRAFT_TABLE.' draft
-                ON (attach.object_id = draft.id AND attach.`type`=\'D\')
-                WHERE draft.`namespace` LIKE '.db_input($namespace);
+    static function deleteForNamespace($namespace, $staff_id=false) {
+        $attachments = Attachment::objects()
+            ->filter(array('draft__namespace__startswith' => $namespace));
         if ($staff_id)
-            $sql .= ' AND draft.staff_id='.db_input($staff_id);
-        if (!db_query($sql))
-            return false;
+            $attachments->filter(array('draft__staff_id' => $staff_id));
 
-        $sql = 'DELETE FROM '.DRAFT_TABLE
-             .' WHERE `namespace` LIKE '.db_input($namespace);
+        $attachments->delete();
+
+        $criteria = array('namespace__like'=>$namespace);
         if ($staff_id)
-            $sql .= ' AND staff_id='.db_input($staff_id);
-        return (!db_query($sql) || !db_affected_rows());
+            $criteria['staff_id'] = $staff_id;
+        return static::objects()->filter($criteria)->delete();
     }
 
     static function cleanup() {
-        // Keep client drafts for two weeks (14 days)
+        // Keep drafts for two weeks (14 days)
         $sql = 'DELETE FROM '.DRAFT_TABLE
-            ." WHERE `namespace` LIKE 'ticket.client.%'
-            AND ((updated IS NULL AND datediff(now(), created) > 14)
-                OR datediff(now(), updated) > 14)";
+            ." WHERE (updated IS NULL AND datediff(now(), created) > 14)
+                OR datediff(now(), updated) > 14";
         return db_query($sql);
     }
 }
diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php
index 84080172d5a1ac82ca985256e76eeb9058b37aeb..4ad787f73007bc6a1d9ab225a6d085dca7e4f932 100644
--- a/include/class.dynamic_forms.php
+++ b/include/class.dynamic_forms.php
@@ -32,6 +32,11 @@ class DynamicForm extends VerySimpleModel {
         'table' => FORM_SEC_TABLE,
         'ordering' => array('title'),
         'pk' => array('id'),
+        'joins' => array(
+            'fields' => array(
+                'reverse' => 'DynamicFormField.form',
+            ),
+        ),
     );
 
     // Registered form types
@@ -41,39 +46,49 @@ class DynamicForm extends VerySimpleModel {
         'O' => 'Organization Information',
     );
 
+    const FLAG_DELETABLE    = 0x0001;
+    const FLAG_DELETED      = 0x0002;
+
     var $_form;
     var $_fields;
     var $_has_data = false;
     var $_dfields;
 
-    function getFields($cache=true) {
-        if (!$cache) {
-            $this->_fields = null;
-        }
+    function getInfo() {
+        $base = $this->ht;
+        unset($base['fields']);
+        return $base;
+    }
 
+    function getId() {
+        return $this->id;
+    }
+
+    /**
+     * Fetch a list of field implementations for the fields defined in this
+     * form. This method should *always* be preferred over
+     * ::getDynamicFields() to avoid caching confusion
+     */
+    function getFields() {
         if (!$this->_fields) {
             $this->_fields = new ListObject();
             foreach ($this->getDynamicFields() as $f)
                 $this->_fields->append($f->getImpl($f));
         }
-
         return $this->_fields;
     }
 
+    /**
+     * Fetch the dynamic fields associated with this dynamic form. Do not
+     * use this list for data processing or validation. Use ::getFields()
+     * for that.
+     */
     function getDynamicFields() {
-        if (!isset($this->id))
-            return array();
-        elseif (!$this->_dfields) {
-            $this->_dfields = DynamicFormField::objects()
-                ->filter(array('form_id'=>$this->id))
-                ->all();
-            foreach ($this->_dfields as $f)
-                $f->setForm($this);
-        }
-        return $this->_dfields;
+        return $this->fields;
     }
 
-    // Multiple inheritance -- delegate to Form
+    // Multiple inheritance -- delegate methods not defined to a forms API
+    // Form
     function __call($what, $args) {
         $delegate = array($this->getForm(), $what);
         if (!is_callable($delegate))
@@ -81,23 +96,14 @@ class DynamicForm extends VerySimpleModel {
         return call_user_func_array($delegate, $args);
     }
 
-    function getField($name, $cache=true) {
-        foreach ($this->getFields($cache) as $f) {
-            if (!strcasecmp($f->get('name'), $name))
-                return $f;
-        }
-        if ($cache)
-            return $this->getField($name, false);
+    function getTitle() {
+        return $this->getLocal('title');
     }
 
-    function hasField($name) {
-        return ($this->getField($name));
+    function getInstructions() {
+        return $this->getLocal('instructions');
     }
 
-
-    function getTitle() { return $this->get('title'); }
-    function getInstructions() { return $this->get('instructions'); }
-
     /**
      * Drop field errors clean info etc. Useful when replacing the source
      * content of the form. This is necessary because the field listing is
@@ -113,90 +119,250 @@ class DynamicForm extends VerySimpleModel {
         if ($source)
             $this->reset();
         $fields = $this->getFields();
-        $form = new Form($fields, $source, array(
-            'title'=>$this->title, 'instructions'=>$this->instructions));
+        $form = new SimpleForm($fields, $source, array(
+            'title' => $this->getLocal('title'),
+            'instructions' => $this->getLocal('instructions'))
+        );
         return $form;
     }
 
-    function addErrors(array $formErrors, $replace=false) {
-        $fields = array();
-        foreach ($this->getFields() as $f)
-            $fields[$f->get('id')] = $f;
-        foreach ($formErrors as $id => $fieldErrors) {
-            if (isset($fields[$id])) {
-                if ($replace)
-                    $fields[$id]->_errors = $fieldErrors;
-                else
-                    foreach ($fieldErrors as $E)
-                        $fields[$id]->addError($E);
-            }
-        }
+    function isDeletable() {
+        return $this->flags & self::FLAG_DELETABLE;
     }
 
-    function isDeletable() {
-        return $this->get('deletable');
+    function setFlag($flag) {
+        $this->flags |= $flag;
+    }
+
+    function hasAnyVisibleFields($user=false) {
+        global $thisstaff, $thisclient;
+        $user = $user ?: $thisstaff ?: $thisclient;
+        $visible = 0;
+        $isstaff = $user instanceof Staff;
+        foreach ($this->getFields() as $F) {
+            if ($isstaff) {
+                if ($F->isVisibleToStaff())
+                    $visible++;
+            }
+            elseif ($F->isVisibleToUsers()) {
+                $visible++;
+            }
+        }
+        return $visible > 0;
     }
 
     function instanciate($sort=1, $data=null) {
-        return DynamicFormEntry::create(
-            array('form_id'=>$this->get('id'), 'sort'=>$sort),
-            $data);
+        $inst = DynamicFormEntry::create(
+            array('form_id'=>$this->get('id'), 'sort'=>$sort)
+        );
+        if ($data)
+            $inst->setSource($data);
+        return $inst;
     }
 
-    function data($data) {
-        if ($data instanceof DynamicFormEntry) {
-            $this->_fields = $data->getFields();
-            $this->_has_data = true;
+    function disableFields(array $ids) {
+        foreach ($this->getFields() as $F) {
+            if (in_array($F->get('id'), $ids)) {
+                $F->disable();
+            }
         }
     }
 
+    function getTranslateTag($subtag) {
+        return _H(sprintf('form.%s.%s', $subtag, $this->id));
+    }
+    function getLocal($subtag) {
+        $tag = $this->getTranslateTag($subtag);
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : $this->get($subtag);
+    }
+
     function save($refetch=false) {
         if (count($this->dirty))
             $this->set('updated', new SqlFunction('NOW'));
-        return parent::save($refetch);
+        if ($rv = parent::save($refetch | $this->dirty))
+            return $this->saveTranslations();
+        return $rv;
     }
 
     function delete() {
+
         if (!$this->isDeletable())
             return false;
-        else
-            return parent::delete();
-    }
 
+        // Soft Delete: Mark the form as deleted.
+        $this->setFlag(self::FLAG_DELETED);
+        return $this->save();
+    }
 
-    function getExportableFields($exclude=array()) {
-
+    function getExportableFields($exclude=array(), $prefix='__') {
         $fields = array();
         foreach ($this->getFields() as $f) {
             // Ignore core fields
             if ($exclude && in_array($f->get('name'), $exclude))
                 continue;
             // Ignore non-data fields
+            // FIXME: Consider ::isStorable() too
             elseif (!$f->hasData() || $f->isPresentationOnly())
                 continue;
 
-            $fields['__field_'.$f->get('id')] = $f;
+            $name = $f->get('name') ?: ('field_'.$f->get('id'));
+            $fields[$prefix.$name] = $f;
         }
-
         return $fields;
     }
 
-
     static function create($ht=false) {
-        $inst = parent::create($ht);
+        $inst = new static($ht);
         $inst->set('created', new SqlFunction('NOW'));
         if (isset($ht['fields'])) {
             $inst->save();
             foreach ($ht['fields'] as $f) {
-                $f = DynamicFormField::create($f);
-                $f->form = $inst;
-                $f->save();
+                $field = DynamicFormField::create(array('form' => $inst) + $f);
+                $field->save();
             }
         }
         return $inst;
     }
 
+    function saveTranslations($vars=false) {
+        global $thisstaff;
+
+        $vars = $vars ?: $_POST;
+        $tags = array(
+            'title' => $this->getTranslateTag('title'),
+            'instructions' => $this->getTranslateTag('instructions'),
+        );
+        $rtags = array_flip($tags);
+        $translations = CustomDataTranslation::allTranslations($tags, 'phrase');
+        foreach ($translations as $t) {
+            $T = $rtags[$t->object_hash];
+            $content = @$vars['trans'][$t->lang][$T];
+            if (!isset($content))
+                continue;
+
+            // Content is not new and shouldn't be added below
+            unset($vars['trans'][$t->lang][$T]);
+
+            $t->text = $content;
+            $t->agent_id = $thisstaff->getId();
+            $t->updated = SqlFunction::NOW();
+            if (!$t->save())
+                return false;
+        }
+        // New translations (?)
+        if ($vars['trans'] && is_array($vars['trans'])) {
+            foreach ($vars['trans'] as $lang=>$parts) {
+                if (!Internationalization::isLanguageEnabled($lang))
+                    continue;
+                foreach ($parts as $T => $content) {
+                    $content = trim($content);
+                    if (!$content)
+                        continue;
+                    $t = CustomDataTranslation::create(array(
+                        'type'      => 'phrase',
+                        'object_hash' => $tags[$T],
+                        'lang'      => $lang,
+                        'text'      => $content,
+                        'agent_id'  => $thisstaff->getId(),
+                        'updated'   => SqlFunction::NOW(),
+                    ));
+                    if (!$t->save())
+                        return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    static function ensureDynamicDataView() {
+
+        if (!($cdata=static::$cdata) || !$cdata['table'])
+            return false;
+
+        $sql = 'SHOW TABLES LIKE \''.$cdata['table'].'\'';
+        if (!db_num_rows(db_query($sql)))
+            return static::buildDynamicDataView($cdata);
+    }
+
+    static function buildDynamicDataView($cdata) {
+        $sql = 'CREATE TABLE IF NOT EXISTS `'.$cdata['table'].'` (PRIMARY KEY
+                ('.$cdata['object_id'].')) DEFAULT CHARSET=utf8 AS '
+             .  static::getCrossTabQuery( $cdata['object_type'], $cdata['object_id']);
+        db_query($sql);
+    }
+
+    static function dropDynamicDataView($table) {
+        db_query('DROP TABLE IF EXISTS `'.$table.'`');
+    }
+
+    static function updateDynamicDataView($answer, $data) {
+        // TODO: Detect $data['dirty'] for value and value_id
+        // We're chiefly concerned with Ticket form answers
+
+        $cdata = static::$cdata;
+        if (!$cdata
+                || !$cdata['table']
+                || !($e = $answer->getEntry())
+                || $e->form->get('type') != $cdata['object_type'])
+            return;
+
+        // $record = array();
+        // $record[$f] = $answer->value'
+        // TicketFormData::objects()->filter(array('ticket_id'=>$a))
+        //      ->merge($record);
+        $sql = 'SHOW TABLES LIKE \''.$cdata['table'].'\'';
+        if (!db_num_rows(db_query($sql)))
+            return;
+
+        $f = $answer->getField();
+        $name = $f->get('name') ? $f->get('name')
+            : 'field_'.$f->get('id');
+        $fields = sprintf('`%s`=', $name) . db_input($answer->getSearchKeys());
+        $sql = 'INSERT INTO `'.$cdata['table'].'` SET '.$fields
+            . sprintf(', `%s`= %s',
+                    $cdata['object_id'],
+                    db_input($answer->getEntry()->get('object_id')))
+            .' ON DUPLICATE KEY UPDATE '.$fields;
+        if (!db_query($sql))
+            return self::dropDynamicDataView($cdata['table']);
+    }
+
+    static function updateDynamicFormEntryAnswer($answer, $data) {
+        if (!$answer
+                || !($e = $answer->getEntry())
+                || !$e->form)
+            return;
+
+        switch ($e->form->get('type')) {
+        case 'T':
+            return TicketForm::updateDynamicDataView($answer, $data);
+        case 'A':
+            return TaskForm::updateDynamicDataView($answer, $data);
+        case 'U':
+            return UserForm::updateDynamicDataView($answer, $data);
+        case 'O':
+            return OrganizationForm::updateDynamicDataView($answer, $data);
+        }
+
+    }
+
+    static function updateDynamicFormField($field, $data) {
+        if (!$field || !$field->form)
+            return;
+
+        switch ($field->form->get('type')) {
+        case 'T':
+            return TicketForm::dropDynamicDataView(TicketForm::$cdata['table']);
+        case 'A':
+            return TaskForm::dropDynamicDataView(TaskForm::$cdata['table']);
+        case 'U':
+            return UserForm::dropDynamicDataView(UserForm::$cdata['table']);
+        case 'O':
+            return OrganizationForm::dropDynamicDataView(OrganizationForm::$cdata['table']);
+        }
 
+    }
 
     static function getCrossTabQuery($object_type, $object_id='object_id', $exclude=array()) {
         $fields = static::getDynamicDataViewFields($exclude);
@@ -207,8 +373,7 @@ class DynamicForm extends VerySimpleModel {
             WHERE entry.object_type='$object_type' GROUP BY entry.object_id";
     }
 
-    // Materialized View for Ticket custom data (MySQL FlexViews would be
-    // nice)
+    // Materialized View for custom data (MySQL FlexViews would be nice)
     //
     // @see http://code.google.com/p/flexviews/
     static function getDynamicDataViewFields($exclude) {
@@ -217,7 +382,7 @@ class DynamicForm extends VerySimpleModel {
             if ($exclude && in_array($f->get('name'), $exclude))
                 continue;
 
-            $impl = $f->getImpl();
+            $impl = $f->getImpl($f);
             if (!$impl->hasData() || $impl->isPresentationOnly())
                 continue;
 
@@ -247,6 +412,12 @@ class UserForm extends DynamicForm {
     static $instance;
     static $form;
 
+    static $cdata = array(
+            'table' => USER_CDATA_TABLE,
+            'object_id' => 'user_id',
+            'object_type' => ObjectModel::OBJECT_TYPE_USER,
+        );
+
     static function objects() {
         $os = parent::objects();
         return $os->filter(array('type'=>'U'));
@@ -290,6 +461,12 @@ Filter::addSupportedMatches(/* @trans */ 'User Data', function() {
 class TicketForm extends DynamicForm {
     static $instance;
 
+    static $cdata = array(
+            'table' => TICKET_CDATA_TABLE,
+            'object_id' => 'ticket_id',
+            'object_type' => 'T',
+        );
+
     static function objects() {
         $os = parent::objects();
         return $os->filter(array('type'=>'T'));
@@ -307,58 +484,6 @@ class TicketForm extends DynamicForm {
         return static::$instance;
     }
 
-    static function ensureDynamicDataView() {
-        $sql = 'SHOW TABLES LIKE \''.TABLE_PREFIX.'ticket__cdata\'';
-        if (!db_num_rows(db_query($sql)))
-            return static::buildDynamicDataView();
-    }
-
-    static function buildDynamicDataView() {
-        // create  table __cdata (primary key (ticket_id)) as select
-        // entry.object_id as ticket_id, MAX(IF(field.name = 'subject',
-        // ans.value, NULL)) as `subject`,MAX(IF(field.name = 'priority',
-        // ans.value, NULL)) as `priority_desc`,MAX(IF(field.name =
-        // 'priority', ans.value_id, NULL)) as `priority_id`
-        // FROM ost_form_entry entry LEFT JOIN ost_form_entry_values ans ON
-        // ans.entry_id = entry.id LEFT JOIN ost_form_field field ON
-        // field.id=ans.field_id
-        // where entry.object_type='T' group by entry.object_id;
-        $sql = 'CREATE TABLE `'.TABLE_PREFIX.'ticket__cdata` (PRIMARY KEY
-                (ticket_id)) AS ' . static::getCrossTabQuery('T', 'ticket_id');
-        db_query($sql);
-    }
-
-    static function dropDynamicDataView() {
-        db_query('DROP TABLE IF EXISTS `'.TABLE_PREFIX.'ticket__cdata`');
-    }
-
-    static function updateDynamicDataView($answer, $data) {
-        // TODO: Detect $data['dirty'] for value and value_id
-        // We're chiefly concerned with Ticket form answers
-        if (!($e = $answer->getEntry()) || $e->getForm()->get('type') != 'T')
-            return;
-
-        // $record = array();
-        // $record[$f] = $answer->value'
-        // TicketFormData::objects()->filter(array('ticket_id'=>$a))
-        //      ->merge($record);
-        $sql = 'SHOW TABLES LIKE \''.TABLE_PREFIX.'ticket__cdata\'';
-        if (!db_num_rows(db_query($sql)))
-            return;
-
-        $f = $answer->getField();
-        if (!$f->getFormId())
-            return;
-
-        $name = $f->get('name') ?: ('field_'.$f->get('id'));
-        $fields = sprintf('`%s`=', $name) . db_input(
-            implode(',', $answer->getSearchKeys()));
-        $sql = 'INSERT INTO `'.TABLE_PREFIX.'ticket__cdata` SET '.$fields
-            .', `ticket_id`='.db_input($answer->getEntry()->get('object_id'))
-            .' ON DUPLICATE KEY UPDATE '.$fields;
-        if (!db_query($sql) || !db_affected_rows())
-            return self::dropDynamicDataView();
-    }
 }
 // Add fields from the standard ticket form to the ticket filterable fields
 Filter::addSupportedMatches(/* @trans */ 'Ticket Data', function() {
@@ -378,27 +503,24 @@ Filter::addSupportedMatches(/* @trans */ 'Ticket Data', function() {
 }, 30);
 // Manage materialized view on custom data updates
 Signal::connect('model.created',
-    array('TicketForm', 'updateDynamicDataView'),
+    array('DynamicForm', 'updateDynamicFormEntryAnswer'),
     'DynamicFormEntryAnswer');
 Signal::connect('model.updated',
-    array('TicketForm', 'updateDynamicDataView'),
+    array('DynamicForm', 'updateDynamicFormEntryAnswer'),
     'DynamicFormEntryAnswer');
 // Recreate the dynamic view after new or removed fields to the ticket
 // details form
 Signal::connect('model.created',
-    array('TicketForm', 'dropDynamicDataView'),
-    'DynamicFormField',
-    function($o) { return $o->form->get('type') == 'T'; });
+    array('DynamicForm', 'updateDynamicFormField'),
+    'DynamicFormField');
 Signal::connect('model.deleted',
-    array('TicketForm', 'dropDynamicDataView'),
-    'DynamicFormField',
-    function($o) { return $o->form->get('type') == 'T'; });
+    array('DynamicForm', 'updateDynamicFormField'),
+    'DynamicFormField');
 // If the `name` column is in the dirty list, we would be renaming a
 // column. Delete the view instead.
 Signal::connect('model.updated',
-    array('TicketForm', 'dropDynamicDataView'),
+    array('DynamicForm', 'updateDynamicFormField'),
     'DynamicFormField',
-    // TODO: Lookup the dynamic form to verify {type == 'T'}
     function($o, $d) { return isset($d['dirty'])
         && (isset($d['dirty']['name']) || isset($d['dirty']['type'])); });
 
@@ -428,6 +550,7 @@ class DynamicFormField extends VerySimpleModel {
         'table' => FORM_FIELD_TABLE,
         'ordering' => array('sort'),
         'pk' => array('id'),
+        'select_related' => array('form'),
         'joins' => array(
             'form' => array(
                 'null' => true,
@@ -437,28 +560,49 @@ class DynamicFormField extends VerySimpleModel {
     );
 
     var $_field;
+    var $_disabled = false;
+
+    const FLAG_ENABLED          = 0x00001;
+    const FLAG_EXT_STORED       = 0x00002; // Value stored outside of form_entry_value
+    const FLAG_CLOSE_REQUIRED   = 0x00004;
 
-    const REQUIRE_NOBODY = 0;
-    const REQUIRE_EVERYONE = 1;
-    const REQUIRE_ENDUSER = 2;
-    const REQUIRE_AGENT = 3;
+    const FLAG_MASK_CHANGE      = 0x00010;
+    const FLAG_MASK_DELETE      = 0x00020;
+    const FLAG_MASK_EDIT        = 0x00040;
+    const FLAG_MASK_DISABLE     = 0x00080;
+    const FLAG_MASK_REQUIRE     = 0x10000;
+    const FLAG_MASK_VIEW        = 0x20000;
+    const FLAG_MASK_NAME        = 0x40000;
 
-    const VISIBLE_EVERYONE = 0;
-    const VISIBLE_AGENTONLY = 1;
-    const VISIBLE_ENDUSERONLY = 2;
+    const MASK_MASK_INTERNAL    = 0x400B2;  # !change, !delete, !disable, !edit-name
+    const MASK_MASK_ALL         = 0x700F2;
 
-    // Multiple inheritance -- delegate to FormField
+    const FLAG_CLIENT_VIEW      = 0x00100;
+    const FLAG_CLIENT_EDIT      = 0x00200;
+    const FLAG_CLIENT_REQUIRED  = 0x00400;
+
+    const MASK_CLIENT_FULL      = 0x00700;
+
+    const FLAG_AGENT_VIEW       = 0x01000;
+    const FLAG_AGENT_EDIT       = 0x02000;
+    const FLAG_AGENT_REQUIRED   = 0x04000;
+
+    const MASK_AGENT_FULL       = 0x7000;
+
+    // Multiple inheritance -- delegate methods not defined here to the
+    // forms API FormField instance
     function __call($what, $args) {
         return call_user_func_array(
             array($this->getField(), $what), $args);
     }
 
-    function getField($cache=true) {
+    /**
+     * Fetch a forms API FormField instance which represents this designable
+     * DynamicFormField.
+     */
+    function getField() {
         global $thisstaff;
 
-        if (!$cache)
-            return new FormField($this->ht);
-
         // Finagle the `required` flag for the FormField instance
         $ht = $this->ht;
         $ht['required'] = ($thisstaff) ? $this->isRequiredForStaff()
@@ -469,8 +613,6 @@ class DynamicFormField extends VerySimpleModel {
         return $this->_field;
     }
 
-    function getAnswer() { return $this->answer; }
-
     function getForm() { return $this->form; }
     function getFormId() { return $this->form_id; }
 
@@ -485,6 +627,7 @@ class DynamicFormField extends VerySimpleModel {
      * configuration of this field
      *
      * Parameters:
+     * vars - POST request / data
      * errors - (OUT array) receives validation errors of the parsed
      *      configuration form
      *
@@ -493,64 +636,157 @@ class DynamicFormField extends VerySimpleModel {
      * errors. If false, the errors were written into the received errors
      * array.
      */
-    function setConfiguration(&$errors=array()) {
+    function setConfiguration($vars, &$errors=array()) {
         $config = array();
-        foreach ($this->getConfigurationForm($_POST)->getFields() as $name=>$field) {
+        foreach ($this->getConfigurationForm($vars)->getFields() as $name=>$field) {
             $config[$name] = $field->to_php($field->getClean());
             $errors = array_merge($errors, $field->errors());
         }
-        if (count($errors) === 0)
-            $this->set('configuration', JsonDataEncoder::encode($config));
-        $this->set('hint', $_POST['hint']);
-        return count($errors) === 0;
+
+        if (count($errors))
+            return false;
+
+        // See if field impl. need to save or override anything
+        $config = $this->getImpl()->to_config($config);
+        $this->set('configuration', JsonDataEncoder::encode($config));
+        $this->set('hint', Format::sanitize($vars['hint']));
+
+        return true;
     }
 
     function isDeletable() {
-        return (($this->get('edit_mask') & 1) == 0);
+        return !$this->hasFlag(self::FLAG_MASK_DELETE);
     }
     function isNameForced() {
-        return $this->get('edit_mask') & 2;
+        return $this->hasFlag(self::FLAG_MASK_NAME);
     }
     function isPrivacyForced() {
-        return $this->get('edit_mask') & 4;
+        return $this->hasFlag(self::FLAG_MASK_VIEW);
     }
     function isRequirementForced() {
-        return $this->get('edit_mask') & 8;
+        return $this->hasFlag(self::FLAG_MASK_REQUIRE);
     }
 
     function  isChangeable() {
-        return (($this->get('edit_mask') & 16) == 0);
+        return !$this->hasFlag(self::FLAG_MASK_CHANGE);
     }
 
     function  isEditable() {
-        return (($this->get('edit_mask') & 32) == 0);
+        return $this->hasFlag(self::FLAG_MASK_EDIT);
     }
+    function disable() {
+        $this->_disabled = true;
+    }
+    function isEnabled() {
+        return !$this->_disabled && $this->hasFlag(self::FLAG_ENABLED);
+    }
+
+    function hasFlag($flag) {
+        return (isset($this->flags) && ($this->flags & $flag) != 0);
+    }
+
+    /**
+     * Describes the current visibility settings for this field. Returns a
+     * comma-separated, localized list of flag descriptions.
+     */
+    function getVisibilityDescription() {
+        $F = $this->flags;
+
+        if (!$this->hasFlag(self::FLAG_ENABLED))
+            return __('Disabled');
 
+        $impl = $this->getImpl();
+
+        $hints = array();
+        $VIEW = self::FLAG_CLIENT_VIEW | self::FLAG_AGENT_VIEW;
+        if (($F & $VIEW) == 0) {
+            $hints[] = __('Hidden');
+        }
+        elseif (~$F & self::FLAG_CLIENT_VIEW) {
+            $hints[] = __('Internal');
+        }
+        elseif (~$F & self::FLAG_AGENT_VIEW) {
+            $hints[] = __('For EndUsers Only');
+        }
+        if ($impl->hasData()) {
+            if ($F & (self::FLAG_CLIENT_REQUIRED | self::FLAG_AGENT_REQUIRED)) {
+                $hints[] = __('Required');
+            }
+            else {
+                $hints[] = __('Optional');
+            }
+            if (!($F & (self::FLAG_CLIENT_EDIT | self::FLAG_AGENT_EDIT))) {
+                $hints[] = __('Immutable');
+            }
+        }
+        return implode(', ', $hints);
+    }
+    function getTranslateTag($subtag) {
+        return _H(sprintf('field.%s.%s', $subtag, $this->id));
+    }
+    function getLocal($subtag, $default=false) {
+        $tag = $this->getTranslateTag($subtag);
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : ($default ?: $this->get($subtag));
+    }
+
+    /**
+     * Fetch a list of names to flag settings to make configuring new fields
+     * a bit easier.
+     *
+     * Returns:
+     * <Array['desc', 'flags']>, where the 'desc' key is a localized
+     * description of the flag set, and the 'flags' key is a bit mask of
+     * flags which should be set on the new field to implement the
+     * requirement / visibility mode.
+     */
     function allRequirementModes() {
         return array(
             'a' => array('desc' => __('Optional'),
-                'private' => self::VISIBLE_EVERYONE, 'required' => self::REQUIRE_NOBODY),
+                'flags' => self::FLAG_CLIENT_VIEW | self::FLAG_AGENT_VIEW
+                    | self::FLAG_CLIENT_EDIT | self::FLAG_AGENT_EDIT),
             'b' => array('desc' => __('Required'),
-                'private' => self::VISIBLE_EVERYONE, 'required' => self::REQUIRE_EVERYONE),
+                'flags' => self::FLAG_CLIENT_VIEW | self::FLAG_AGENT_VIEW
+                    | self::FLAG_CLIENT_EDIT | self::FLAG_AGENT_EDIT
+                    | self::FLAG_CLIENT_REQUIRED | self::FLAG_AGENT_REQUIRED),
             'c' => array('desc' => __('Required for EndUsers'),
-                'private' => self::VISIBLE_EVERYONE, 'required' => self::REQUIRE_ENDUSER),
+                'flags' => self::FLAG_CLIENT_VIEW | self::FLAG_AGENT_VIEW
+                    | self::FLAG_CLIENT_EDIT | self::FLAG_AGENT_EDIT
+                    | self::FLAG_CLIENT_REQUIRED),
             'd' => array('desc' => __('Required for Agents'),
-                'private' => self::VISIBLE_EVERYONE, 'required' => self::REQUIRE_AGENT),
+                'flags' => self::FLAG_CLIENT_VIEW | self::FLAG_AGENT_VIEW
+                    | self::FLAG_CLIENT_EDIT | self::FLAG_AGENT_EDIT
+                    | self::FLAG_AGENT_REQUIRED),
             'e' => array('desc' => __('Internal, Optional'),
-                'private' => self::VISIBLE_AGENTONLY, 'required' => self::REQUIRE_NOBODY),
+                'flags' => self::FLAG_AGENT_VIEW | self::FLAG_AGENT_EDIT),
             'f' => array('desc' => __('Internal, Required'),
-                'private' => self::VISIBLE_AGENTONLY, 'required' => self::REQUIRE_EVERYONE),
+                'flags' => self::FLAG_AGENT_VIEW | self::FLAG_AGENT_EDIT
+                    | self::FLAG_AGENT_REQUIRED),
             'g' => array('desc' => __('For EndUsers Only'),
-                'private' => self::VISIBLE_ENDUSERONLY, 'required' => self::REQUIRE_ENDUSER),
+                'flags' => self::FLAG_CLIENT_VIEW | self::FLAG_CLIENT_EDIT
+                    | self::FLAG_CLIENT_REQUIRED),
         );
     }
 
+    /**
+     * Fetch a list of valid requirement modes for this field. This list
+     * will be filtered based on flags which are not supported or not
+     * allowed for this field.
+     *
+     * Deprecated:
+     * This was used in previous versions when a drop-down list was
+     * presented for editing a field's visibility. The current software
+     * version presents the drop-down list for new fields only.
+     *
+     * Returns:
+     * <Array['desc', 'flags']> Filtered list from ::allRequirementModes
+     */
     function getAllRequirementModes() {
         $modes = static::allRequirementModes();
         if ($this->isPrivacyForced()) {
             // Required to be internal
             foreach ($modes as $m=>$info) {
-                if ($info['private'] != $this->get('private'))
+                if ($info['flags'] & (self::FLAG_CLIENT_VIEW | self::FLAG_AGENT_VIEW))
                     unset($modes[$m]);
             }
         }
@@ -558,47 +794,46 @@ class DynamicFormField extends VerySimpleModel {
         if ($this->isRequirementForced()) {
             // Required to be required
             foreach ($modes as $m=>$info) {
-                if ($info['required'] != $this->get('required'))
+                if ($info['flags'] & (self::FLAG_CLIENT_REQUIRED | self::FLAG_AGENT_REQUIRED))
                     unset($modes[$m]);
             }
         }
         return $modes;
     }
 
-    function getRequirementMode() {
-        foreach ($this->getAllRequirementModes() as $m=>$info) {
-            if ($this->get('private') == $info['private']
-                    && $this->get('required') == $info['required'])
-                return $m;
-        }
-        return false;
-    }
-
     function setRequirementMode($mode) {
         $modes = $this->getAllRequirementModes();
         if (!isset($modes[$mode]))
             return false;
 
         $info = $modes[$mode];
-        $this->set('required', $info['required']);
-        $this->set('private', $info['private']);
+        $this->set('flags', $info['flags'] | self::FLAG_ENABLED);
     }
 
     function isRequiredForStaff() {
-        return in_array($this->get('required'),
-            array(self::REQUIRE_EVERYONE, self::REQUIRE_AGENT));
+        return $this->hasFlag(self::FLAG_AGENT_REQUIRED);
     }
     function isRequiredForUsers() {
-        return in_array($this->get('required'),
-            array(self::REQUIRE_EVERYONE, self::REQUIRE_ENDUSER));
+        return $this->hasFlag(self::FLAG_CLIENT_REQUIRED);
+    }
+    function isRequiredForClose() {
+        return $this->hasFlag(self::FLAG_CLOSE_REQUIRED);
+    }
+    function isEditableToStaff() {
+        return $this->isEnabled()
+            && $this->hasFlag(self::FLAG_AGENT_EDIT);
     }
     function isVisibleToStaff() {
-        return in_array($this->get('private'),
-            array(self::VISIBLE_EVERYONE, self::VISIBLE_AGENTONLY));
+        return $this->isEnabled()
+            && $this->hasFlag(self::FLAG_AGENT_VIEW);
+    }
+    function isEditableToUsers() {
+        return $this->isEnabled()
+            && $this->hasFlag(self::FLAG_CLIENT_EDIT);
     }
     function isVisibleToUsers() {
-        return in_array($this->get('private'),
-            array(self::VISIBLE_EVERYONE, self::VISIBLE_ENDUSERONLY));
+        return $this->isEnabled()
+            && $this->hasFlag(self::FLAG_CLIENT_VIEW);
     }
 
     /**
@@ -612,12 +847,15 @@ class DynamicFormField extends VerySimpleModel {
         if (!$this->get('label'))
             $this->addError(
                 __("Label is required for custom form fields"), "label");
-        if ($this->get('required') && !$this->get('name'))
+        if (($this->isRequiredForStaff() || $this->isRequiredForUsers())
+            && !$this->get('name')
+        ) {
             $this->addError(
                 __("Variable name is required for required fields"
                 /* `required` is a visibility setting fields */
                 /* `variable` is used for automation. Internally it's called `name` */
                 ), "name");
+        }
         if (preg_match('/[.{}\'"`; ]/u', $this->get('name')))
             $this->addError(__(
                 'Invalid character in variable name. Please use letters and numbers only.'
@@ -626,24 +864,36 @@ class DynamicFormField extends VerySimpleModel {
     }
 
     function delete() {
-        // Don't really delete form fields as that will screw up the data
+        // Don't really delete form fields with data as that will screw up the data
         // model. Instead, just drop the association with the form which
         // will give the appearance of deletion. Not deleting means that
         // the field will continue to exist on form entries it may already
         // have answers on, but since it isn't associated with the form, it
         // won't be available for new form submittals.
         $this->set('form_id', 0);
-        $this->save();
+
+        $impl = $this->getImpl();
+
+        // Trigger db_clean so the field can do house cleaning
+        $impl->db_cleanup(true);
+
+        // Short-circuit deletion if the field has data.
+        if ($impl->hasData())
+            return $this->save();
+
+        // Delete the field for realz
+        parent::delete();
+
     }
 
-    function save() {
+    function save($refetch=false) {
         if (count($this->dirty))
             $this->set('updated', new SqlFunction('NOW'));
-        return parent::save();
+        return parent::save($this->dirty || $refetch);
     }
 
     static function create($ht=false) {
-        $inst = parent::create($ht);
+        $inst = new static($ht);
         $inst->set('created', new SqlFunction('NOW'));
         if (isset($ht['configuration']))
             $inst->configuration = JsonDataEncoder::encode($ht['configuration']);
@@ -667,6 +917,7 @@ class DynamicFormEntry extends VerySimpleModel {
         'table' => FORM_ENTRY_TABLE,
         'ordering' => array('sort'),
         'pk' => array('id'),
+        'select_related' => array('form'),
         'joins' => array(
             'form' => array(
                 'null' => true,
@@ -678,7 +929,6 @@ class DynamicFormEntry extends VerySimpleModel {
         ),
     );
 
-    var $_values;
     var $_fields;
     var $_form;
     var $_errors = false;
@@ -690,14 +940,7 @@ class DynamicFormEntry extends VerySimpleModel {
     }
 
     function getAnswers() {
-        if (!isset($this->_values)) {
-            $this->_values = DynamicFormEntryAnswer::objects()
-                ->filter(array('entry_id'=>$this->get('id')))
-                ->all();
-            foreach ($this->_values as $v)
-                $v->entry = $this;
-        }
-        return $this->_values;
+        return $this->answers;
     }
 
     function getAnswer($name) {
@@ -706,10 +949,12 @@ class DynamicFormEntry extends VerySimpleModel {
                 return $ans;
         return null;
     }
+
     function setAnswer($name, $value, $id=false) {
         foreach ($this->getAnswers() as $ans) {
-            if ($ans->getField()->get('name') == $name) {
-                $ans->getField()->reset();
+            $f = $ans->getField();
+            if ($f->isStorable() && $f->get('name') == $name) {
+                $f->reset();
                 $ans->set('value', $value);
                 if ($id !== false)
                     $ans->set('value_id', $id);
@@ -722,19 +967,45 @@ class DynamicFormEntry extends VerySimpleModel {
         return $this->_errors;
     }
 
-    function getTitle() { return $this->getForm()->getTitle(); }
-    function getInstructions() { return $this->getForm()->getInstructions(); }
+    function getTitle() {
+        return $this->form->getTitle();
+    }
 
-    function getForm() {
-        $form = DynamicForm::lookup($this->get('form_id'));
-        if ($form) {
-            if (isset($this->id))
-                $form->data($this);
-            if ($this->errors())
-                $form->addErrors($this->errors(), true);
+    function getInstructions() {
+        return $this->form->getInstructions();
+    }
+
+    function getDynamicForm() {
+        return $this->form;
+    }
+
+    function getForm($source=false, $options=array()) {
+        if (!isset($this->_form)) {
+
+            $fields = $this->getFields();
+            if (isset($this->extra)) {
+                $x = JsonDataParser::decode($this->extra) ?: array();
+                foreach ($x['disable'] ?: array() as $id) {
+                    unset($fields[$id]);
+                }
+            }
+
+            $source = $source ?: $this->getSource();
+            $options += array(
+                'title' => $this->getTitle(),
+                'instructions' => $this->getInstructions()
+                );
+            $this->_form = new CustomForm($fields, $source, $options);
         }
-        return $form;
+
+
+        return $this->_form;
+    }
+
+    function getDynamicFields() {
+        return $this->form->fields;
     }
+
     function getMedia() {
         return $this->getForm()->getMedia();
     }
@@ -743,34 +1014,49 @@ class DynamicFormEntry extends VerySimpleModel {
         if (!isset($this->_fields)) {
             $this->_fields = array();
             // Get all dynamic fields associated with the form
-            //  even when stored elsewhere -- important during validation
-            foreach ($this->getForm()->getDynamicFields() as $field) {
-                $field = $field->getImpl($field);
-                if ($field instanceof ThreadEntryField)
-                    continue;
-                $this->_fields[$field->get('id')] = $field;
+            // even when stored elsewhere -- important during validation
+            foreach ($this->getDynamicFields() as $f) {
+                $f = $f->getImpl($f);
+                $this->_fields[$f->get('id')] = $f;
+                $f->isnew = true;
             }
-            // Get answers to entries
+            // Include any other answers included in this entry, which may
+            // be for fields which have since been deleted
             foreach ($this->getAnswers() as $a) {
-                if (!($f = $a->getField())) continue;
-                $this->_fields[$f->get('id')] = $f;
+                $f = $a->getField();
+                $id = $f->get('id');
+                if (!isset($this->_fields[$id])) {
+                    // This field is not currently on the associated form
+                    $a->deleted = true;
+                }
+                $this->_fields[$id] = $f;
+                // This field has an answer, so it isn't new (to this entry)
+                $f->isnew = false;
             }
         }
-        foreach ($this->_fields as $F)
-            $F->setForm($this);
-
         return $this->_fields;
     }
 
+    function filterFields($filter) {
+        $this->getFields();
+        foreach ($this->_fields as $i=>$f) {
+            if ($filter($f))
+                unset($this->_fields[$i]);
+        }
+    }
+
     function getSource() {
         return $this->_source ?: (isset($this->id) ? false : $_POST);
     }
     function setSource($source) {
         $this->_source = $source;
+        // Ensure the field is connected to this data source
+        foreach ($this->getFields() as $F)
+            if (!$F->getForm())
+                $F->setForm($this);
     }
 
     function getField($name) {
-
         foreach ($this->getFields() as $field)
             if (!strcasecmp($field->get('name'), $name))
                 return $field;
@@ -784,16 +1070,17 @@ class DynamicFormEntry extends VerySimpleModel {
      * Parameters:
      * $filter - (callback) function to receive each field and return
      *      boolean true if the field's errors are significant
+     * $options - options to pass to form and fields.
+     *
      */
-    function isValid($filter=false) {
+    function isValid($filter=false, $options=array()) {
+
         if (!is_array($this->_errors)) {
-            $this->_errors = array();
-            $this->getClean();
-            foreach ($this->getFields() as $field) {
-                if ($field->errors() && (!$filter || $filter($field)))
-                    $this->_errors[$field->get('id')] = $field->errors();
-            }
+            $form = $this->getForm(false, $options);
+            $form->isValid($filter);
+            $this->_errors = $form->errors();
         }
+
         return !$this->_errors;
     }
 
@@ -812,13 +1099,7 @@ class DynamicFormEntry extends VerySimpleModel {
     }
 
     function getClean() {
-        if (!$this->_clean) {
-            $this->_clean = array();
-            foreach ($this->getFields() as $field)
-                $this->_clean[$field->get('id')]
-                    = $this->_clean[$field->get('name')] = $field->getClean();
-        }
-        return $this->_clean;
+        return $this->getForm()->getClean();
     }
 
     /**
@@ -854,16 +1135,6 @@ class DynamicFormEntry extends VerySimpleModel {
         return $vars;
     }
 
-    function getSaved() {
-        $info = array();
-        foreach ($this->getAnswers() as $a) {
-            $field = $a->getField();
-            $info[$field->get('id')]
-                = $info[$field->get('name')] = $a->getValue();
-        }
-        return $info;
-    }
-
     function forTicket($ticket_id, $force=false) {
         static $entries = array();
         if (!isset($entries[$ticket_id]) || $force) {
@@ -881,11 +1152,6 @@ class DynamicFormEntry extends VerySimpleModel {
         $this->object_id = $ticket_id;
     }
 
-    function forClient($user_id) {
-        return DynamicFormEntry::objects()
-            ->filter(array('object_id'=>$user_id, 'object_type'=>'U'));
-    }
-
     function setClientId($user_id) {
         $this->object_type = 'U';
         $this->object_id = $user_id;
@@ -895,20 +1161,30 @@ class DynamicFormEntry extends VerySimpleModel {
         $this->object_id = $object_id;
     }
 
-    function forUser($user_id) {
-        return DynamicFormEntry::objects()
-            ->filter(array('object_id'=>$user_id, 'object_type'=>'U'));
-    }
-
-    function forOrganization($org_id) {
+    function forObject($object_id, $object_type) {
         return DynamicFormEntry::objects()
-            ->filter(array('object_id'=>$org_id, 'object_type'=>'O'));
+            ->filter(array('object_id'=>$object_id, 'object_type'=>$object_type));
     }
 
     function render($staff=true, $title=false, $options=array()) {
         return $this->getForm()->render($staff, $title, $options);
     }
 
+    function getChanges() {
+        $fields = array();
+        foreach ($this->getAnswers() as $a) {
+            $field = $a->getField();
+            if (!$field->hasData() || $field->isPresentationOnly())
+                continue;
+            $after = $field->to_database($field->getClean());
+            $before = $field->to_database($a->getValue());
+            if ($before == $after)
+                continue;
+            $fields[$field->get('id')] = array($before, $after);
+        }
+        return $fields;
+    }
+
     /**
      * addMissingFields
      *
@@ -918,41 +1194,20 @@ class DynamicFormEntry extends VerySimpleModel {
      * entry.
      */
     function addMissingFields() {
-        // Track deletions
-        foreach ($this->getAnswers() as $answer)
-            $answer->deleted = true;
-
-        foreach ($this->getForm()->getDynamicFields() as $field) {
-            $found = false;
-            foreach ($this->getAnswers() as $answer) {
-                if ($answer->get('field_id') == $field->get('id')) {
-                    $answer->deleted = false; $found = true; break;
-                }
-            }
-            if (!$found && ($field = $field->getImpl($field))
-                    && !$field->isPresentationOnly()) {
+        foreach ($this->getFields() as $field) {
+            if ($field->isnew && $field->isEnabled()
+                && !$field->isPresentationOnly()
+                && $field->hasData()
+                && $field->isStorable()
+            ) {
                 $a = DynamicFormEntryAnswer::create(
-                    array('field_id'=>$field->get('id'), 'entry_id'=>$this->id));
-                $a->field = $field;
-                $a->entry = $this;
-                $a->deleted = false;
-                // Add to list of answers
-                $this->_values[] = $a;
-                $this->_fields[$field->get('id')] = $field;
-                $this->_form = null;
-
-                // Omit fields without data
-                // For user entries, the name and email fields should not be
-                // saved with the rest of the data
-                if ($this->object_type == 'U'
-                        && in_array($field->get('name'), array('name','email')))
-                    continue;
+                    array('field_id'=>$field->get('id'), 'entry'=>$this));
 
-                if ($this->object_type == 'O'
-                        && in_array($field->get('name'), array('name')))
-                    continue;
+                // Add to list of answers
+                $this->answers->add($a);
 
-                if (!$field->hasData())
+                // Omit fields without data and non-storable fields.
+                if (!$field->hasData() || !$field->isStorable())
                     continue;
 
                 $a->save();
@@ -978,6 +1233,7 @@ class DynamicFormEntry extends VerySimpleModel {
     function save($refetch=false) {
         if (count($this->dirty))
             $this->set('updated', new SqlFunction('NOW'));
+
         if (!parent::save($refetch || count($this->dirty)))
             return false;
 
@@ -985,17 +1241,17 @@ class DynamicFormEntry extends VerySimpleModel {
         foreach ($this->getAnswers() as $a) {
             $field = $a->getField();
 
-            if ($this->object_type == 'U'
-                    && in_array($field->get('name'), array('name','email')))
+            // Don't save answers for presentation-only fields or fields
+            // which are stored elsewhere
+            if (!$field->hasData() || !$field->isStorable()
+                || $field->isPresentationOnly()
+            ) {
                 continue;
-
-            if ($this->object_type == 'O'
-                    && in_array($field->get('name'), array('name')))
-                continue;
-
-            // Set the entry ID here so that $field->getClean() can use the
+            }
+            // Set the entry here so that $field->getClean() can use the
             // entry-id if necessary
-            $a->set('entry_id', $this->get('id'));
+            $a->entry = $this;
+
             try {
                 $field->setForm($this);
                 $val = $field->to_database($field->getClean());
@@ -1008,39 +1264,40 @@ class DynamicFormEntry extends VerySimpleModel {
                 $a->set('value', $val[0]);
                 $a->set('value_id', $val[1]);
             }
-            else
+            else {
                 $a->set('value', $val);
-            // Don't save answers for presentation-only fields
-            if ($field->hasData() && !$field->isPresentationOnly()) {
-                if ($a->dirty)
-                    $dirty++;
-                $a->save();
             }
+            if ($a->dirty)
+                $dirty++;
+            $a->save();
         }
-        $this->_values = null;
         return $dirty;
     }
 
     function delete() {
+        if (!parent::delete())
+            return false;
+
         foreach ($this->getAnswers() as $a)
             $a->delete();
-        return parent::delete();
+
+        return true;
     }
 
     static function create($ht=false, $data=null) {
-        $inst = parent::create($ht);
+        $inst = new static($ht);
         $inst->set('created', new SqlFunction('NOW'));
         if ($data)
             $inst->setSource($data);
-        $form = $inst->getForm();
-        foreach ($form->getFields() as $f) {
-            if (!$f->hasData()) continue;
-            $a = DynamicFormEntryAnswer::create(
-                array('field_id'=>$f->get('id')));
-            $a->field = $f;
+        foreach ($inst->getDynamicFields() as $field) {
+            if (!($impl = $field->getImpl($field)))
+                continue;
+            if (!$impl->hasData() || !$impl->isStorable())
+                continue;
+            $a = new DynamicFormEntryAnswer(
+                array('field'=>$field, 'entry'=>$inst));
             $a->field->setAnswer($a);
-            $a->entry = $inst;
-            $inst->_values[] = $a;
+            $inst->answers->add($a);
         }
         return $inst;
     }
@@ -1057,6 +1314,8 @@ class DynamicFormEntryAnswer extends VerySimpleModel {
         'table' => FORM_ANSWER_TABLE,
         'ordering' => array('field__sort'),
         'pk' => array('entry_id', 'field_id'),
+        'select_related' => array('field'),
+        'fields' => array('entry_id', 'field_id', 'value', 'value_id'),
         'joins' => array(
             'field' => array(
                 'constraint' => array('field_id' => 'DynamicFormField.id'),
@@ -1067,9 +1326,7 @@ class DynamicFormEntryAnswer extends VerySimpleModel {
         ),
     );
 
-    var $field;
-    var $form;
-    var $entry;
+    var $_field;
     var $deleted = false;
     var $_value;
 
@@ -1078,18 +1335,15 @@ class DynamicFormEntryAnswer extends VerySimpleModel {
     }
 
     function getForm() {
-        if (!$this->form)
-            $this->form = $this->getEntry()->getForm();
-        return $this->form;
+        return $this->getEntry()->getForm();
     }
 
     function getField() {
-        if (!isset($this->field)) {
-            $f = DynamicFormField::lookup($this->get('field_id'));
-            $this->field = $f->getImpl($f);
-            $this->field->setAnswer($this);
+        if (!isset($this->_field)) {
+            $this->_field = $this->field->getImpl($this->field);
+            $this->_field->setAnswer($this);
         }
-        return $this->field;
+        return $this->_field;
     }
 
     function getValue() {
@@ -1103,6 +1357,10 @@ class DynamicFormEntryAnswer extends VerySimpleModel {
         return $this->_value;
     }
 
+    function getLocal($tag) {
+        return $this->field->getLocal($tag);
+    }
+
     function getIdValue() {
         return $this->get('value_id');
     }
@@ -1128,24 +1386,18 @@ class DynamicFormEntryAnswer extends VerySimpleModel {
     }
 
     function getSearchKeys() {
-        $val = $this->getField()->to_php(
-            $this->get('value'), $this->get('value_id'));
-        if (is_array($val))
-            return array_keys($val);
-        elseif (is_object($val) && method_exists($val, 'getId'))
-            return array($val->getId());
-
-        return array($val);
+        return implode(',', (array) $this->getField()->getKeys($this->getValue()));
     }
 
     function asVar() {
-        return (is_object($this->getValue()))
-            ? $this->getValue() : $this->toString();
+        return $this->getField()->asVar(
+            $this->get('value'), $this->get('value_id')
+        );
     }
 
     function getVar($tag) {
-        if (is_object($this->getValue()) && method_exists($this->getValue(), 'getVar'))
-            return $this->getValue()->getVar($tag);
+        if (is_object($var = $this->asVar()) && method_exists($var, 'getVar'))
+            return $var->getVar($tag);
     }
 
     function __toString() {
@@ -1178,11 +1430,13 @@ class SelectionField extends FormField {
         return $this->_list;
     }
 
-    function getWidget() {
+    function getWidget($widgetClass=false) {
         $config = $this->getConfiguration();
-        $widgetClass = false;
-        if ($config['widget'] == 'typeahead')
+        if ($config['widget'] == 'typeahead' && $config['multiselect'] == false)
             $widgetClass = 'TypeaheadSelectionWidget';
+        elseif ($config['widget'] == 'textbox')
+            $widgetClass = 'TextboxSelectionWidget';
+
         return parent::getWidget($widgetClass);
     }
 
@@ -1194,16 +1448,28 @@ class SelectionField extends FormField {
         $config = $this->getConfiguration();
         $choices = $this->getChoices();
         $selection = array();
+
+        if ($value && !is_array($value))
+            $value = array($value);
+
         if ($value && is_array($value)) {
             foreach ($value as $k=>$v) {
-                if (($i=$list->getItem((int) $k)))
+                if ($k && ($i=$list->getItem((int) $k)))
                     $selection[$i->getId()] = $i->getValue();
+                elseif (isset($choices[$k]))
+                    $selection[$k] = $choices[$k];
                 elseif (isset($choices[$v]))
                     $selection[$v] = $choices[$v];
+                elseif (($i=$list->getItem($v, true)))
+                    $selection[$i->getId()] = $i->getValue();
             }
+        } elseif($value) {
+            //Assume invalid textbox input to be validated
+            $selection[] = $value;
         }
 
-        return $selection;
+        // Don't return an empty array
+        return $selection ?: null;
     }
 
     function to_database($value) {
@@ -1239,6 +1505,55 @@ class SelectionField extends FormField {
         return $value;
     }
 
+    function getKeys($value) {
+        if (!is_array($value))
+            $value = $this->getChoice($value);
+        if (is_array($value))
+            return implode(', ', array_keys($value));
+        return (string) $value;
+    }
+
+    // PHP 5.4 Move this to a trait
+    function whatChanged($before, $after) {
+        $before = (array) $before;
+        $after = (array) $after;
+        $added = array_diff($after, $before);
+        $deleted = array_diff($before, $after);
+        $added = array_map(array($this, 'display'), $added);
+        $deleted = array_map(array($this, 'display'), $deleted);
+
+        if ($added && $deleted) {
+            $desc = sprintf(
+                __('added <strong>%1$s</strong> and removed <strong>%2$s</strong>'),
+                implode(', ', $added), implode(', ', $deleted));
+        }
+        elseif ($added) {
+            $desc = sprintf(
+                __('added <strong>%1$s</strong>'),
+                implode(', ', $added));
+        }
+        elseif ($deleted) {
+            $desc = sprintf(
+                __('removed <strong>%1$s</strong>'),
+                implode(', ', $deleted));
+        }
+        else {
+            $desc = sprintf(
+                __('changed to <strong>%1$s</strong>'),
+                $this->display($after));
+        }
+        return $desc;
+    }
+
+    function asVar($value, $id=false) {
+        $values = $this->to_php($value, $id);
+        if (is_array($values)) {
+            return new PlaceholderList($this->getList()->getAllItems()
+                ->filter(array('id__in' => array_keys($values)))
+            );
+        }
+    }
+
     function hasSubFields() {
         return $this->getList()->getForm();
     }
@@ -1265,7 +1580,16 @@ class SelectionField extends FormField {
         parent::validateEntry($entry);
         if (!$this->errors()) {
             $config = $this->getConfiguration();
-            if ($config['typeahead']
+            if ($config['widget'] == 'textbox') {
+                if ($entry && (
+                        !($k=key($entry))
+                     || !($i=$this->getList()->getItem((int) $k))
+                 )) {
+                    $config = $this->getConfiguration();
+                    $this->_errors[] = $this->getLocal('validator-error', $config['validator-error'])
+                        ?: __('Unknown or invalid input');
+                }
+            } elseif ($config['typeahead']
                     && ($entered = $this->getWidget()->getEnteredValue())
                     && !in_array($entered, $entry))
                 $this->_errors[] = __('Select a value from the list');
@@ -1274,35 +1598,49 @@ class SelectionField extends FormField {
 
     function getConfigurationOptions() {
         return array(
+            'multiselect' => new BooleanField(array(
+                'id'=>2,
+                'label'=>__(/* Type of widget allowing multiple selections */ 'Multiselect'),
+                'required'=>false, 'default'=>false,
+                'configuration'=>array(
+                    'desc'=>__('Allow multiple selections')),
+            )),
             'widget' => new ChoiceField(array(
                 'id'=>1,
                 'label'=>__('Widget'),
                 'required'=>false, 'default' => 'dropdown',
                 'choices'=>array(
                     'dropdown' => __('Drop Down'),
-                    'typeahead' =>__('Typeahead'),
+                    'typeahead' => __('Typeahead'),
+                    'textbox' => __('Text Input'),
                 ),
                 'configuration'=>array(
                     'multiselect' => false,
                 ),
+                'visibility' => new VisibilityConstraint(
+                    new Q(array('multiselect__eq'=>false)),
+                    VisibilityConstraint::HIDDEN
+                ),
                 'hint'=>__('Typeahead will work better for large lists')
             )),
-            'multiselect' => new BooleanField(array(
-                'id'=>2,
-                'label'=>__(/* Type of widget allowing multiple selections */ 'Multiselect'),
-                'required'=>false, 'default'=>false,
-                'configuration'=>array(
-                    'desc'=>__('Allow multiple selections')),
+            'validator-error' => new TextboxField(array(
+                'id'=>5, 'label'=>__('Validation Error'), 'default'=>'',
+                'configuration'=>array('size'=>40, 'length'=>80,
+                    'translatable'=>$this->getTranslateTag('validator-error')
+                ),
                 'visibility' => new VisibilityConstraint(
-                    new Q(array('widget__eq'=>'dropdown')),
+                    new Q(array('widget__eq'=>'textbox')),
                     VisibilityConstraint::HIDDEN
                 ),
+                'hint'=>__('Message shown to user if the item entered is not in the list')
             )),
             'prompt' => new TextboxField(array(
                 'id'=>3,
                 'label'=>__('Prompt'), 'required'=>false, 'default'=>'',
                 'hint'=>__('Leading text shown before a value is selected'),
-                'configuration'=>array('size'=>40, 'length'=>40),
+                'configuration'=>array('size'=>40, 'length'=>40,
+                    'translatable'=>$this->getTranslateTag('prompt'),
+                ),
             )),
             'default' => new SelectionField(array(
                 'id'=>4, 'label'=>__('Default'), 'required'=>false, 'default'=>'',
@@ -1318,7 +1656,7 @@ class SelectionField extends FormField {
         if ($config['widget'])
             $config['typeahead'] = $config['widget'] == 'typeahead';
 
-        //Typeahed doesn't support multiselect for now  TODO: Add!
+        // Drop down list does not support multiple selections
         if ($config['typeahead'])
             $config['multiselect'] = false;
 
@@ -1327,21 +1665,27 @@ class SelectionField extends FormField {
 
     function getChoices($verbose=false) {
         if (!$this->_choices || $verbose) {
-            $this->_choices = array();
+            $choices = array();
             foreach ($this->getList()->getItems() as $i)
-                $this->_choices[$i->getId()] = $i->getValue();
+                $choices[$i->getId()] = $i->getValue();
 
             // Retired old selections
             $values = ($a=$this->getAnswer()) ? $a->getValue() : array();
             if ($values && is_array($values)) {
                 foreach ($values as $k => $v) {
-                    if (!isset($this->_choices[$k])) {
+                    if (!isset($choices[$k])) {
                         if ($verbose) $v .= ' '.__('(retired)');
-                        $this->_choices[$k] = $v;
+                        $choices[$k] = $v;
                     }
                 }
             }
+
+            if ($verbose) // Don't cache
+                return $choices;
+
+            $this->_choices = $choices;
         }
+
         return $this->_choices;
     }
 
@@ -1357,6 +1701,27 @@ class SelectionField extends FormField {
         return $selection;
     }
 
+    function lookupChoice($value) {
+
+        // See if it's in the choices.
+        $choices = $this->getChoices();
+        if ($choices && ($i=array_search($value, $choices)))
+            return array($i=>$choices[$i]);
+
+        // Query the store by value or extra (abbrv.)
+        if (!($list=$this->getList()))
+            return null;
+
+        if ($i = $list->getItem($value))
+            return array($i->getId() => $i->getValue());
+
+        if ($i = $list->getItem($value, true))
+            return array($i->getId() => $i->getValue());
+
+        return null;
+    }
+
+
     function getFilterData() {
         // Start with the filter data for the list item as the [0] index
         $data = array(parent::getFilterData());
@@ -1376,12 +1741,49 @@ class SelectionField extends FormField {
         }
         return $data;
     }
+
+    function getSearchMethods() {
+        return array(
+            'set' =>        __('has a value'),
+            'notset' =>     __('does not have a value'),
+            'includes' =>   __('includes'),
+            '!includes' =>  __('does not include'),
+        );
+    }
+
+    function getSearchMethodWidgets() {
+        return array(
+            'set' => null,
+            'notset' => null,
+            'includes' => array('ChoiceField', array(
+                'choices' => $this->getChoices(),
+                'configuration' => array('multiselect' => true),
+            )),
+            '!includes' => array('ChoiceField', array(
+                'choices' => $this->getChoices(),
+                'configuration' => array('multiselect' => true),
+            )),
+        );
+    }
+
+    function getSearchQ($method, $value, $name=false) {
+        $name = $name ?: $this->get('name');
+        switch ($method) {
+        case '!includes':
+            return Q::not(array("{$name}__intersect" => array_keys($value)));
+        case 'includes':
+            return new Q(array("{$name}__intersect" => array_keys($value)));
+        default:
+            return parent::getSearchQ($method, $value, $name);
+        }
+    }
 }
 
 class TypeaheadSelectionWidget extends ChoicesWidget {
-    function render($how) {
-        if ($how == 'search')
-            return parent::render($how);
+    function render($options=array()) {
+
+        if ($options['mode'] == 'search')
+            return parent::render($options);
 
         $name = $this->getEnteredValue();
         $config = $this->field->getConfiguration();
@@ -1413,8 +1815,8 @@ class TypeaheadSelectionWidget extends ChoicesWidget {
             placeholder="<?php echo $config['prompt'];
             ?>" autocomplete="off" />
         <input type="hidden" name="<?php echo $this->name;
-            ?>[<?php echo $value; ?>]" id="<?php echo $this->name;
-            ?>_id" value="<?php echo Format::htmlchars($name); ?>"/>
+            ?>_id" id="<?php echo $this->name;
+            ?>_id" value="<?php echo Format::htmlchars($value); ?>"/>
         <script type="text/javascript">
         $(function() {
             $('input#<?php echo $this->name; ?>').typeahead({
@@ -1434,10 +1836,24 @@ class TypeaheadSelectionWidget extends ChoicesWidget {
         <?php
     }
 
+    function parsedValue() {
+        return array($this->getValue() => $this->getEnteredValue());
+    }
+
     function getValue() {
         $data = $this->field->getSource();
-        if (isset($data[$this->name]))
-            return $data[$this->name];
+        $name = $this->field->get('name');
+        if (isset($data["{$this->name}_id"]) && is_numeric($data["{$this->name}_id"])) {
+            return array($data["{$this->name}_id"] => $data["{$this->name}_name"]);
+        }
+        elseif (isset($data[$name])) {
+            return $data[$name];
+        }
+        // Attempt to lookup typed value (usually from a default)
+        elseif ($val = $this->getEnteredValue()) {
+            return $this->field->lookupChoice($val);
+        }
+
         return parent::getValue();
     }
 
@@ -1447,7 +1863,10 @@ class TypeaheadSelectionWidget extends ChoicesWidget {
         if (isset($data[$this->name.'_name'])) {
             // Drop the extra part, if any
             $v = $data[$this->name.'_name'];
-            $v = substr($v, 0, strrpos($v, ' — '));
+            $pos = strrpos($v, ' — ');
+            if ($pos !== false)
+                $v = substr($v, 0, $pos);
+
             return trim($v);
         }
         return parent::getValue();
diff --git a/include/class.email.php b/include/class.email.php
index 26cd68648b799c1cd76ae3fb6ef13fdd3a2d99ae..e0bdb64bd38fe3ffb82a15139d222cebffed18df 100644
--- a/include/class.email.php
+++ b/include/class.email.php
@@ -11,55 +11,67 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
-
+include_once INCLUDE_DIR.'class.role.php';
 include_once(INCLUDE_DIR.'class.dept.php');
 include_once(INCLUDE_DIR.'class.mailfetch.php');
 
-class Email {
-    var $id;
-    var $address;
-
-    var $dept;
-    var $ht;
-
-    function Email($id) {
-        $this->id=0;
-        $this->load($id);
-    }
-
-    function load($id=0) {
-
-        if(!$id && !($id=$this->getId()))
-            return false;
-
-        $sql='SELECT * FROM '.EMAIL_TABLE.' WHERE email_id='.db_input($id);
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
+class Email extends VerySimpleModel {
+    static $meta = array(
+        'table' => EMAIL_TABLE,
+        'pk' => array('email_id'),
+        'joins' => array(
+            'priority' => array(
+                'constraint' => array('priority_id' => 'Priority.priority_id'),
+                'null' => true,
+            ),
+            'dept' => array(
+                'constraint' => array('dept_id' => 'Dept.id'),
+                'null' => true,
+            ),
+            'topic' => array(
+                'constraint' => array('topic_id' => 'Topic.topic_id'),
+                'null' => true,
+            ),
+        )
+    );
+
+    const PERM_BANLIST = 'emails.banlist';
+
+    static protected $perms = array(
+            self::PERM_BANLIST => array(
+                'title' =>
+                /* @trans */ 'Banlist',
+                'desc'  =>
+                /* @trans */ 'Ability to add/remove emails from banlist via ticket interface',
+                'primary' => true,
+            ));
 
 
-        $this->ht=db_fetch_array($res);
-        $this->ht['mail_proto'] = $this->ht['mail_protocol'];
-        if ($this->ht['mail_encryption'] == 'SSL')
-            $this->ht['mail_proto'] .= "/".$this->ht['mail_encryption'];
+    var $address;
+    var $mail_proto;
 
-        $this->id=$this->ht['email_id'];
-        $this->address=$this->ht['name']?($this->ht['name'].'<'.$this->ht['email'].'>'):$this->ht['email'];
+    function getId() {
+        return $this->email_id;
+    }
 
-        $this->dept = null;
+    function __toString() {
+        if ($this->name)
+            return sprintf('%s <%s>', $this->name, $this->email);
 
-        return true;
+        return $this->email;
     }
 
-    function reload() {
-        return $this->load();
-    }
 
-    function getId() {
-        return $this->id;
+    function __onload() {
+        $this->mail_proto = $this->get('mail_protocol');
+        if ($this->mail_encryption == 'SSL')
+            $this->mail_proto .= "/".$this->mail_encryption;
+
+        $this->address=$this->name?($this->name.'<'.$this->email.'>'):$this->email;
     }
 
     function getEmail() {
-        return $this->ht['email'];
+        return $this->email;
     }
 
     function getAddress() {
@@ -67,41 +79,37 @@ class Email {
     }
 
     function getName() {
-        return $this->ht['name'];
+        return $this->name;
     }
 
     function getPriorityId() {
-        return $this->ht['priority_id'];
+        return $this->priority_id;
     }
 
     function getDeptId() {
-        return $this->ht['dept_id'];
+        return $this->dept_id;
     }
 
     function getDept() {
-
-        if(!$this->dept && $this->getDeptId())
-            $this->dept=Dept::lookup($this->getDeptId());
-
         return $this->dept;
     }
 
     function getTopicId() {
-        return $this->ht['topic_id'];
+        return $this->topic_id;
     }
 
     function getTopic() {
-        // Topic::lookup will do validation on the ID, no need to duplicate
-        // code here
-        return Topic::lookup($this->getTopicId());
+        return $this->topic;
     }
 
     function autoRespond() {
-        return (!$this->ht['noautoresp']);
+        return !$this->noautoresp;
     }
 
     function getPasswd() {
-        return $this->ht['userpass']?Crypto::decrypt($this->ht['userpass'], SECRET_SALT, $this->ht['userid']):'';
+        if (!$this->userpass)
+            return '';
+        return Crypto::decrypt($this->userpass, SECRET_SALT, $this->userid);
     }
 
     function getHashtable() {
@@ -109,7 +117,9 @@ class Email {
     }
 
     function getInfo() {
-        return $this->getHashtable();
+        $base = $this->getHashtable();
+        $base['mail_proto'] = $this->mail_proto;
+        return $base;
     }
 
     function getMailAccountInfo() {
@@ -117,18 +127,18 @@ class Email {
         /*NOTE: Do not change any of the tags - otherwise mail fetching will fail */
         $info = array(
                 //Mail server info
-                'host'  => $this->ht['mail_host'],
-                'port'  => $this->ht['mail_port'],
-                'protocol'  => $this->ht['mail_protocol'],
-                'encryption' => $this->ht['mail_encryption'],
-                'username'  => $this->ht['userid'],
-                'password' => Crypto::decrypt($this->ht['userpass'], SECRET_SALT, $this->ht['userid']),
+                'host'  => $this->mail_host,
+                'port'  => $this->mail_port,
+                'protocol'  => $this->mail_protocol,
+                'encryption' => $this->mail_encryption,
+                'username'  => $this->userid,
+                'password' => Crypto::decrypt($this->userpass, SECRET_SALT, $this->userid),
                 //osTicket specific
                 'email_id'  => $this->getId(), //Required for email routing to work.
-                'max_fetch' => $this->ht['mail_fetchmax'],
-                'delete_mail' => $this->ht['mail_delete'],
-                'archive_folder' => $this->ht['mail_archivefolder']
-                );
+                'max_fetch' => $this->mail_fetchmax,
+                'delete_mail' => $this->mail_delete,
+                'archive_folder' => $this->mail_archivefolder
+        );
 
         return $info;
     }
@@ -136,24 +146,24 @@ class Email {
     function isSMTPEnabled() {
 
         return (
-                $this->ht['smtp_active']
+                $this->smtp_active
                     && ($info=$this->getSMTPInfo())
                     && (!$info['auth'] || $info['password'])
                 );
     }
 
     function allowSpoofing() {
-        return ($this->ht['smtp_spoofing']);
+        return ($this->smtp_spoofing);
     }
 
     function getSMTPInfo() {
 
         $info = array (
-                'host' => $this->ht['smtp_host'],
-                'port' => $this->ht['smtp_port'],
-                'auth' => (bool) $this->ht['smtp_auth'],
-                'username' => $this->ht['userid'],
-                'password' => Crypto::decrypt($this->ht['userpass'], SECRET_SALT, $this->ht['userid'])
+                'host' => $this->smtp_host,
+                'port' => $this->smtp_port,
+                'auth' => (bool) $this->smtp_auth,
+                'username' => $this->userid,
+                'password' => Crypto::decrypt($this->userpass, SECRET_SALT, $this->userid)
                 );
 
         return $info;
@@ -178,75 +188,62 @@ class Email {
         return $this->send($to, $subject, $message, $attachments, $options);
     }
 
-    function update($vars,&$errors) {
-        $vars=$vars;
-        $vars['cpasswd']=$this->getPasswd(); //Current decrypted password.
-
-        if(!$this->save($this->getId(), $vars, $errors))
-            return false;
-
-        $this->reload();
-
-        return true;
-    }
-
-
    function delete() {
         global $cfg;
         //Make sure we are not trying to delete default emails.
         if(!$cfg || $this->getId()==$cfg->getDefaultEmailId() || $this->getId()==$cfg->getAlertEmailId()) //double...double check.
             return 0;
 
-        $sql='DELETE FROM '.EMAIL_TABLE.' WHERE email_id='.db_input($this->getId()).' LIMIT 1';
-        if(db_query($sql) && ($num=db_affected_rows())) {
-            $sql='UPDATE '.DEPT_TABLE.' SET autoresp_email_id=0 '.
-                 ',email_id='.db_input($cfg->getDefaultEmailId()).
-                 ' WHERE email_id='.db_input($this->getId());
-            db_query($sql);
-        }
-
-        return $num;
-    }
+        if (!parent::delete())
+            return false;
 
+        Dept::objects()
+            ->filter(array('email_id' => $this->getId()))
+            ->update(array(
+                'email_id' => $cfg->getDefaultEmailId()
+            ));
 
-   function __toString() {
+        Dept::objects()
+            ->filter(array('autoresp_email_id' => $this->getId()))
+            ->update(array(
+                'autoresp_email_id' => 0,
+            ));
 
-       $email = $this->getEmail();
-       if ($this->getName())
-           $email = sprintf('%s <%s>', $this->getName(), $this->getEmail());
+        return true;
+    }
 
-       return $email;
-   }
 
     /******* Static functions ************/
 
-   function getIdByEmail($email) {
-
-        $sql='SELECT email_id FROM '.EMAIL_TABLE.' WHERE email='.db_input($email);
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
+   static function getIdByEmail($email) {
+        $qs = static::objects()->filter(array('email' => $email))
+            ->values_flat('email_id');
 
-        return db_result($res);
+        $row = $qs->first();
+        return $row ? $row[0] : false;
     }
 
-    function lookup($var) {
-        $id=is_numeric($var)?$var:Email::getIdByEmail($var);
-        return ($id && is_numeric($id) && ($email=new Email($id)) && $email->getId())?$email:null;
+    static function create($vars=false) {
+        $inst = new static($vars);
+        $inst->created = SqlFunction::NOW();
+        return $inst;
     }
 
-    function create($vars,&$errors) {
-        return Email::save(0,$vars,$errors);
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($refetch || $this->dirty);
     }
 
-
-    function save($id,$vars,&$errors) {
+    function update($vars, &$errors=false) {
         global $cfg;
 
-        //very basic checks
-
+        // very basic checks
+        $vars['cpasswd']=$this->getPasswd(); //Current decrypted password.
         $vars['name']=Format::striptags(trim($vars['name']));
         $vars['email']=trim($vars['email']);
 
+        $id = isset($this->email_id) ? $this->getId() : 0;
         if($id && $id!=$vars['id'])
             $errors['err']=__('Internal error. Get technical help.');
 
@@ -312,19 +309,24 @@ class Email {
         }
 
         //abort on errors
-        if($errors) return false;
+        if ($errors)
+            return false;
 
         if(!$errors && ($vars['mail_host'] && $vars['userid'])) {
-            $sql='SELECT email_id FROM '.EMAIL_TABLE
-                .' WHERE mail_host='.db_input($vars['mail_host']).' AND userid='.db_input($vars['userid']);
-            if($id)
-                $sql.=' AND email_id!='.db_input($id);
+            $existing = static::objects()
+                ->filter(array(
+                    'mail_host' => $vars['mail_host'],
+                    'userid' => $vars['userid']
+                ));
+
+            if ($id)
+                $existing->exclude(array('email_id' => $id));
 
-            if(db_num_rows(db_query($sql)))
+            if ($existing->exists())
                 $errors['userid']=$errors['host']=__('Host/userid combination already in use.');
         }
 
-        $passwd=$vars['passwd']?$vars['passwd']:$vars['cpasswd'];
+        $passwd = $vars['passwd'] ?: $vars['cpasswd'];
         if(!$errors && $vars['mail_active']) {
             //note: password is unencrypted at this point...MailFetcher expect plain text.
             $fetcher = new MailFetcher(
@@ -370,56 +372,77 @@ class Email {
 
         if($errors) return false;
 
-        $sql='updated=NOW(),mail_errors=0, mail_lastfetch=NULL'.
-             ',email='.db_input($vars['email']).
-             ',name='.db_input(Format::striptags($vars['name'])).
-             ',dept_id='.db_input($vars['dept_id']).
-             ',priority_id='.db_input($vars['priority_id']).
-             ',topic_id='.db_input($vars['topic_id']).
-             ',noautoresp='.db_input(isset($vars['noautoresp'])?1:0).
-             ',userid='.db_input($vars['userid']).
-             ',mail_active='.db_input($vars['mail_active']).
-             ',mail_host='.db_input($vars['mail_host']).
-             ',mail_protocol='.db_input($vars['mail_protocol']?$vars['mail_protocol']:'POP').
-             ',mail_encryption='.db_input($vars['mail_encryption']).
-             ',mail_port='.db_input($vars['mail_port']?$vars['mail_port']:0).
-             ',mail_fetchfreq='.db_input($vars['mail_fetchfreq']?$vars['mail_fetchfreq']:0).
-             ',mail_fetchmax='.db_input($vars['mail_fetchmax']?$vars['mail_fetchmax']:0).
-             ',smtp_active='.db_input($vars['smtp_active']).
-             ',smtp_host='.db_input($vars['smtp_host']).
-             ',smtp_port='.db_input($vars['smtp_port']?$vars['smtp_port']:0).
-             ',smtp_auth='.db_input($vars['smtp_auth']).
-             ',smtp_spoofing='.db_input(isset($vars['smtp_spoofing'])?1:0).
-             ',notes='.db_input(Format::sanitize($vars['notes']));
+        $this->mail_errors = 0;
+        $this->mail_lastfetch = null;
+        $this->email = $vars['email'];
+        $this->name = Format::striptags($vars['name']);
+        $this->dept_id = $vars['dept_id'];
+        $this->priority_id = $vars['priority_id'];
+        $this->topic_id = $vars['topic_id'];
+        $this->noautoresp = isset($vars['noautoresp'])?1:0;
+        $this->userid = $vars['userid'];
+        $this->mail_active = $vars['mail_active'];
+        $this->mail_host = $vars['mail_host'];
+        $this->mail_protocol = $vars['mail_protocol']?$vars['mail_protocol']:'POP';
+        $this->mail_encryption = $vars['mail_encryption'];
+        $this->mail_port = $vars['mail_port']?$vars['mail_port']:0;
+        $this->mail_fetchfreq = $vars['mail_fetchfreq']?$vars['mail_fetchfreq']:0;
+        $this->mail_fetchmax = $vars['mail_fetchmax']?$vars['mail_fetchmax']:0;
+        $this->smtp_active = $vars['smtp_active'];
+        $this->smtp_host = $vars['smtp_host'];
+        $this->smtp_port = $vars['smtp_port']?$vars['smtp_port']:0;
+        $this->smtp_auth = $vars['smtp_auth'];
+        $this->smtp_spoofing = isset($vars['smtp_spoofing'])?1:0;
+        $this->notes = Format::sanitize($vars['notes']);
 
         //Post fetch email handling...
-        if($vars['postfetch'] && !strcasecmp($vars['postfetch'],'delete'))
-            $sql.=',mail_delete=1,mail_archivefolder=NULL';
-        elseif($vars['postfetch'] && !strcasecmp($vars['postfetch'],'archive') && $vars['mail_archivefolder'])
-            $sql.=',mail_delete=0,mail_archivefolder='.db_input($vars['mail_archivefolder']);
-        else
-            $sql.=',mail_delete=0,mail_archivefolder=NULL';
+        if ($vars['postfetch'] && !strcasecmp($vars['postfetch'],'delete')) {
+            $this->mail_delete = 1;
+            $this->mail_archivefolder = null;
+        }
+        elseif($vars['postfetch'] && !strcasecmp($vars['postfetch'],'archive') && $vars['mail_archivefolder']) {
+            $this->mail_delete = 0;
+            $this->mail_archivefolder = $vars['mail_archivefolder'];
+        }
+        else {
+            $this->mail_delete = 0;
+            $this->mail_archivefolder = null;
+        }
 
-        if($vars['passwd']) //New password - encrypt.
-            $sql.=',userpass='.db_input(Crypto::encrypt($vars['passwd'],SECRET_SALT, $vars['userid']));
+        if ($vars['passwd']) //New password - encrypt.
+            $this->userpass = Crypto::encrypt($vars['passwd'],SECRET_SALT, $vars['userid']);
 
-        if($id) { //update
-            $sql='UPDATE '.EMAIL_TABLE.' SET '.$sql.' WHERE email_id='.db_input($id);
-            if(db_query($sql) && db_affected_rows())
-                return true;
+        if ($this->save())
+            return true;
 
+        if ($id) { //update
             $errors['err']=sprintf(__('Unable to update %s.'), __('this email'))
                .' '.__('Internal error occurred');
-        }else {
-            $sql='INSERT INTO '.EMAIL_TABLE.' SET '.$sql.',created=NOW()';
-            if(db_query($sql) && ($id=db_insert_id()))
-                return $id;
-
+        }
+        else {
             $errors['err']=sprintf(__('Unable to add %s.'), __('this email'))
                .' '.__('Internal error occurred');
         }
 
         return false;
     }
+
+    static function getPermissions() {
+        return self::$perms;
+    }
+
+    static function getAddresses($options=array()) {
+        $objects = static::objects();
+        if ($options['smtp'])
+            $objects = $objects->filter(array('smtp_active'=>true));
+
+        $addresses = array();
+        foreach ($objects->values_flat('email_id', 'email') as $row) {
+            list($id, $email) = $row;
+            $addresses[$id] = $email;
+        }
+        return $addresses;
+    }
 }
+RolePermission::register(/* @trans */ 'Miscellaneous', Email::getPermissions());
 ?>
diff --git a/include/class.error.php b/include/class.error.php
index 72909a615e2a61cde374ccdcba9213a80f4e2515..c083437d879e0719582d65d6ce5807e89a424219 100644
--- a/include/class.error.php
+++ b/include/class.error.php
@@ -17,7 +17,7 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-class Error extends Exception {
+class BaseError extends Exception {
     static $title = '';
     static $sendAlert = true;
 
@@ -42,7 +42,7 @@ class Error extends Exception {
     }
 }
 
-class InitialDataError extends Error {
+class InitialDataError extends BaseError {
     static $title = 'Problem with install initial data';
 }
 
@@ -52,7 +52,7 @@ function raise_error($message, $class=false) {
 }
 
 // File storage backend exceptions
-class IOException extends Error {
+class IOException extends BaseError {
     static $title = 'Unable to read resource content';
 }
 
diff --git a/include/class.export.php b/include/class.export.php
index 5f2d729e2c373906ed6a6e90fba2a5903393f945..9235a18bc93164c737c824ad59b01db4b0f9ee35 100644
--- a/include/class.export.php
+++ b/include/class.export.php
@@ -42,44 +42,58 @@ class Export {
     #      attachments associated with each, ...
     static function dumpTickets($sql, $how='csv') {
         // Add custom fields to the $sql statement
-        $cdata = $fields = $select = array();
+        $cdata = $fields = array();
         foreach (TicketForm::getInstance()->getFields() as $f) {
             // Ignore core fields
-            if (in_array($f->get('name'), array('subject','priority')))
+            if (in_array($f->get('name'), array('priority')))
                 continue;
             // Ignore non-data fields
             elseif (!$f->hasData() || $f->isPresentationOnly())
                 continue;
 
-            $name = $f->get('name') ? $f->get('name') : 'field_'.$f->get('id');
-            $key = '__field_'.$f->get('id');
-            $cdata[$key] = $f->get('label');
+            $name = $f->get('name') ?: 'field_'.$f->get('id');
+            $key = 'cdata.'.$name;
             $fields[$key] = $f;
-            $select[] = "cdata.`$name` AS __field_".$f->get('id');
+            $cdata[$key] = $f->getLocal('label');
         }
-        if ($select)
-            $sql = str_replace(' FROM ', ',' . implode(',', $select) . ' FROM ', $sql);
-        return self::dumpQuery($sql,
+        // Reset the $sql query
+        $tickets = $sql->models()
+            ->select_related('user', 'user__default_email', 'dept', 'staff',
+                'team', 'staff', 'cdata', 'topic', 'status', 'cdata__:priority')
+            ->annotate(array(
+                'collab_count' => TicketThread::objects()
+                    ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
+                    ->aggregate(array('count' => SqlAggregate::COUNT('collaborators__id'))),
+                'attachment_count' => TicketThread::objects()
+                    ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
+                    ->filter(array('entries__attachments__inline' => 0))
+                    ->aggregate(array('count' => SqlAggregate::COUNT('entries__attachments__id'))),
+                'thread_count' => TicketThread::objects()
+                    ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
+                    ->exclude(array('entries__flags__hasbit' => ThreadEntry::FLAG_HIDDEN))
+                    ->aggregate(array('count' => SqlAggregate::COUNT('entries__id'))),
+            ));
+
+        return self::dumpQuery($tickets,
             array(
                 'number' =>         __('Ticket Number'),
-                'ticket_created' => __('Date'),
-                'subject' =>        __('Subject'),
-                'name' =>           __('From'),
-                'email' =>          __('From Email'),
-                'priority_desc' =>  __('Priority'),
-                'dept_name' =>      __('Department'),
-                'helptopic' =>      __('Help Topic'),
+                'created' =>        __('Date Created'),
+                'cdata.subject' =>  __('Subject'),
+                'user.name' =>      __('From'),
+                'user.default_email.address' => __('From Email'),
+                'cdata.:priority.priority_desc' => __('Priority'),
+                'dept::getLocalName' => __('Department'),
+                'topic::getName' => __('Help Topic'),
                 'source' =>         __('Source'),
-                'status' =>         __('Current Status'),
-                'effective_date' => __('Last Updated'),
-                'duedate' =>        __('Due Date'),
+                'status::getName' =>__('Current Status'),
+                'lastupdate' =>     __('Last Updated'),
+                'est_duedate' =>    __('Due Date'),
                 'isoverdue' =>      __('Overdue'),
                 'isanswered' =>     __('Answered'),
-                'assigned' =>       __('Assigned To'),
-                'staff' =>          __('Agent Assigned'),
-                'team' =>           __('Team Assigned'),
+                'staff::getName' => __('Agent Assigned'),
+                'team::getName' =>  __('Team Assigned'),
                 'thread_count' =>   __('Thread Count'),
-                'attachments' =>    __('Attachment Count'),
+                'attachment_count' => __('Attachment Count'),
             ) + $cdata,
             $how,
             array('modify' => function(&$record, $keys) use ($fields) {
@@ -93,9 +107,65 @@ class Export {
             );
     }
 
-    /* static */ function saveTickets($sql, $filename, $how='csv') {
-        ob_start();
+    static  function saveTickets($sql, $filename, $how='csv') {
+        Http::download($filename, "text/$how");
         self::dumpTickets($sql, $how);
+        exit;
+    }
+
+
+    static function dumpTasks($sql, $how='csv') {
+        // Add custom fields to the $sql statement
+        $cdata = $fields = array();
+        foreach (TaskForm::getInstance()->getFields() as $f) {
+            // Ignore non-data fields
+            if (!$f->hasData() || $f->isPresentationOnly())
+                continue;
+
+            $name = $f->get('name') ?: 'field_'.$f->get('id');
+            $key = 'cdata.'.$name;
+            $fields[$key] = $f;
+            $cdata[$key] = $f->getLocal('label');
+        }
+        // Reset the $sql query
+        $tasks = $sql->models()
+            ->select_related('dept', 'staff', 'team', 'cdata')
+            ->annotate(array(
+            'collab_count' => SqlAggregate::COUNT('thread__collaborators'),
+            'attachment_count' => SqlAggregate::COUNT('thread__entries__attachments'),
+            'thread_count' => SqlAggregate::COUNT('thread__entries'),
+        ));
+
+        return self::dumpQuery($tasks,
+            array(
+                'number' =>         __('Task Number'),
+                'created' =>        __('Date Created'),
+                'cdata.title' =>    __('Title'),
+                'dept::getLocalName' => __('Department'),
+                '::getStatus' =>    __('Current Status'),
+                'duedate' =>        __('Due Date'),
+                'staff::getName' => __('Agent Assigned'),
+                'team::getName' =>  __('Team Assigned'),
+                'thread_count' =>   __('Thread Count'),
+                'attachment_count' => __('Attachment Count'),
+            ) + $cdata,
+            $how,
+            array('modify' => function(&$record, $keys) use ($fields) {
+                foreach ($fields as $k=>$f) {
+                    if (($i = array_search($k, $keys)) !== false) {
+                        $record[$i] = $f->export($f->to_php($record[$i]));
+                    }
+                }
+                return $record;
+            })
+            );
+    }
+
+
+    static function saveTasks($sql, $filename, $how='csv') {
+
+        ob_start();
+        self::dumpTasks($sql, $how);
         $stuff = ob_get_contents();
         ob_end_clean();
         if ($stuff)
@@ -108,32 +178,21 @@ class Export {
 
         $exclude = array('name', 'email');
         $form = UserForm::getUserForm();
-        $fields = $form->getExportableFields($exclude);
-
-        // Field selection callback
-        $fname = function ($f) {
-            return 'cdata.`'.$f->getSelectName().'` AS __field_'.$f->get('id');
-        };
-
-        $sql = substr_replace($sql,
-                ','.implode(',', array_map($fname, $fields)).' ',
-                strpos($sql, 'FROM '), 0);
-
-        $sql = substr_replace($sql,
-                'LEFT JOIN ('.$form->getCrossTabQuery($form->type, 'user_id', $exclude).') cdata
-                    ON (cdata.user_id = user.id) ',
-                strpos($sql, 'WHERE '), 0);
+        $fields = $form->getExportableFields($exclude, 'cdata.');
 
         $cdata = array_combine(array_keys($fields),
                 array_values(array_map(
-                        function ($f) { return $f->get('label'); }, $fields)));
+                        function ($f) { return $f->getLocal('label'); }, $fields)));
+
+        $users = $sql->models()
+            ->select_related('org', 'cdata');
 
         ob_start();
-        echo self::dumpQuery($sql,
+        echo self::dumpQuery($users,
                 array(
                     'name'  =>          __('Name'),
-                    'organization' =>   __('Organization'),
-                    'email' =>          __('Email'),
+                    'org' =>   __('Organization'),
+                    '::getEmail' =>          __('Email'),
                     ) + $cdata,
                 $how,
                 array('modify' => function(&$record, $keys) use ($fields) {
@@ -158,30 +217,19 @@ class Export {
 
         $exclude = array('name');
         $form = OrganizationForm::getDefaultForm();
-        $fields = $form->getExportableFields($exclude);
-
-        // Field selection callback
-        $fname = function ($f) {
-            return 'cdata.`'.$f->getSelectName().'` AS __field_'.$f->get('id');
-        };
-
-        $sql = substr_replace($sql,
-                ','.implode(',', array_map($fname, $fields)).' ',
-                strpos($sql, 'FROM '), 0);
-
-        $sql = substr_replace($sql,
-                'LEFT JOIN ('.$form->getCrossTabQuery($form->type, '_org_id', $exclude).') cdata
-                    ON (cdata._org_id = org.id) ',
-                strpos($sql, 'WHERE '), 0);
-
+        $fields = $form->getExportableFields($exclude, 'cdata.');
         $cdata = array_combine(array_keys($fields),
                 array_values(array_map(
-                        function ($f) { return $f->get('label'); }, $fields)));
+                        function ($f) { return $f->getLocal('label'); }, $fields)));
 
-        $cdata += array('account_manager' => 'Account Manager', 'users' => 'Users');
+        $cdata += array(
+                '::getNumUsers' => 'Users',
+                '::getAccountManager' => 'Account Manager',
+                );
 
+        $orgs = $sql->models();
         ob_start();
-        echo self::dumpQuery($sql,
+        echo self::dumpQuery($orgs,
                 array(
                     'name'  =>  'Name',
                     ) + $cdata,
@@ -209,32 +257,22 @@ class Export {
 class ResultSetExporter {
     var $output;
 
-    function ResultSetExporter($sql, $headers, $options=array()) {
+    function __construct($sql, $headers, $options=array()) {
         $this->headers = array_values($headers);
-        if ($s = strpos(strtoupper($sql), ' LIMIT '))
-            $sql = substr($sql, 0, $s);
+        // Remove limit and offset
+        $sql->limit(null)->offset(null);
         # TODO: If $filter, add different LIMIT clause to query
         $this->options = $options;
         $this->output = $options['output'] ?: fopen('php://output', 'w');
 
-        $this->_res = db_query($sql, true, true);
-        if ($row = db_fetch_array($this->_res)) {
-            $query_fields = array_keys($row);
-            $this->headers = array();
-            $this->keys = array();
-            $this->lookups = array();
-            foreach ($headers as $field=>$name) {
-                if (array_key_exists($field, $row)) {
-                    $this->headers[] = $name;
-                    $this->keys[] = $field;
-                    # Remember the location of this header in the query results
-                    # (column-wise) so we don't have to do hashtable lookups for every
-                    # column of every row.
-                    $this->lookups[] = array_search($field, $query_fields);
-                }
-            }
-            db_data_reset($this->_res);
+        $this->headers = array();
+        $this->keys = array();
+        foreach ($headers as $field=>$name) {
+            $this->headers[] = $name;
+            $this->keys[] = $field;
         }
+        $this->_res = $sql->getIterator();
+        $this->_res->rewind();
     }
 
     function getHeaders() {
@@ -242,12 +280,30 @@ class ResultSetExporter {
     }
 
     function next() {
-        if (!($row = db_fetch_row($this->_res)))
+        if (!$this->_res->valid())
             return false;
 
+        $object = $this->_res->current();
+        $this->_res->next();
+
         $record = array();
-        foreach ($this->lookups as $idx)
-            $record[] = $row[$idx];
+
+        foreach ($this->keys as $field) {
+            list($field, $func) = explode('::', $field);
+            $path = explode('.', $field);
+            $current = $object;
+            // Evaluate dotted ORM path
+            if ($field) {
+                foreach ($path as $P) {
+                    $current = $current->{$P};
+                }
+            }
+            // Evalutate :: function call on target current
+            if ($func && (method_exists($current, $func) || method_exists($current, '__call'))) {
+                $current = $current->{$func}();
+            }
+            $record[] = (string) $current;
+        }
 
         if (isset($this->options['modify']) && is_callable($this->options['modify']))
             $record = $this->options['modify']($record, $this->keys);
@@ -276,10 +332,23 @@ class CsvResultsExporter extends ResultSetExporter {
         if (!$this->output)
              $this->output = fopen('php://output', 'w');
 
+        // Detect delimeter from the current locale settings. For locales
+        // which use comma (,) as the decimal separator, the semicolon (;)
+        // should be used as the field separator
+        $delimiter = ',';
+        if (class_exists('NumberFormatter')) {
+            $nf = NumberFormatter::create(Internationalization::getCurrentLocale(),
+                NumberFormatter::DECIMAL);
+            $s = $nf->getSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);
+            if ($s == ',')
+                $delimiter = ';';
+        }
+
+        // Output a UTF-8 BOM (byte order mark)
         fputs($this->output, chr(0xEF) . chr(0xBB) . chr(0xBF));
-        fputcsv($this->output, $this->getHeaders());
+        fputcsv($this->output, $this->getHeaders(), $delimiter);
         while ($row=$this->next())
-            fputcsv($this->output, $row);
+            fputcsv($this->output, $row, $delimiter);
 
         fclose($this->output);
     }
@@ -310,21 +379,21 @@ class DatabaseExporter {
     var $options;
     var $tables = array(CONFIG_TABLE, SYSLOG_TABLE, FILE_TABLE,
         FILE_CHUNK_TABLE, STAFF_TABLE, DEPT_TABLE, TOPIC_TABLE, GROUP_TABLE,
-        GROUP_DEPT_TABLE, TEAM_TABLE, TEAM_MEMBER_TABLE, FAQ_TABLE,
+        STAFF_DEPT_TABLE, TEAM_TABLE, TEAM_MEMBER_TABLE, FAQ_TABLE,
         FAQ_TOPIC_TABLE, FAQ_CATEGORY_TABLE, DRAFT_TABLE,
         CANNED_TABLE, TICKET_TABLE, ATTACHMENT_TABLE,
-        TICKET_THREAD_TABLE, TICKET_ATTACHMENT_TABLE, TICKET_PRIORITY_TABLE,
-        TICKET_LOCK_TABLE, TICKET_EVENT_TABLE, TICKET_EMAIL_INFO_TABLE,
+        THREAD_TABLE, THREAD_ENTRY_TABLE, THREAD_ENTRY_EMAIL_TABLE,
+        LOCK_TABLE, THREAD_EVENT_TABLE, TICKET_PRIORITY_TABLE,
         EMAIL_TABLE, EMAIL_TEMPLATE_TABLE, EMAIL_TEMPLATE_GRP_TABLE,
         FILTER_TABLE, FILTER_RULE_TABLE, SLA_TABLE, API_KEY_TABLE,
         TIMEZONE_TABLE, SESSION_TABLE, PAGE_TABLE,
         FORM_SEC_TABLE, FORM_FIELD_TABLE, LIST_TABLE, LIST_ITEM_TABLE,
         FORM_ENTRY_TABLE, FORM_ANSWER_TABLE, USER_TABLE, USER_EMAIL_TABLE,
-        PLUGIN_TABLE, TICKET_COLLABORATOR_TABLE,
+        PLUGIN_TABLE, THREAD_COLLABORATOR_TABLE, TRANSLATION_TABLE,
         USER_ACCOUNT_TABLE, ORGANIZATION_TABLE, NOTE_TABLE
     );
 
-    function DatabaseExporter($stream, $options=array()) {
+    function __construct($stream, $options=array()) {
         $this->stream = $stream;
         $this->options = $options;
     }
diff --git a/include/class.faq.php b/include/class.faq.php
index 5df6655d54f90e7eb50a5f5fb22648dedc92ed1e..027b25ef09815a24ab775ba55ed9e639393f3eb1 100644
--- a/include/class.faq.php
+++ b/include/class.faq.php
@@ -14,130 +14,215 @@
 **********************************************************************/
 require_once('class.file.php');
 require_once('class.category.php');
-
-class FAQ {
-
-    var $id;
-    var $ht;
-
-    var $category;
-    var $attachments;
-
-    function FAQ($id) {
-        $this->id=0;
-        $this->ht = array();
-        $this->load($id);
-    }
-
-    function load($id) {
-
-        $sql='SELECT faq.*,cat.ispublic, count(attach.file_id) as attachments '
-            .' FROM '.FAQ_TABLE.' faq '
-            .' LEFT JOIN '.FAQ_CATEGORY_TABLE.' cat ON(cat.category_id=faq.category_id) '
-            .' LEFT JOIN '.ATTACHMENT_TABLE.' attach
-                 ON(attach.object_id=faq.faq_id AND attach.`type`=\'F\' AND attach.inline=0) '
-            .' WHERE faq.faq_id='.db_input($id)
-            .' GROUP BY faq.faq_id';
-
-        if (!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
-
-        $this->ht = db_fetch_array($res);
-        $this->ht['id'] = $this->id = $this->ht['faq_id'];
-        $this->category = null;
-        $this->attachments = new GenericAttachments($this->id, 'F');
-
-        return true;
-    }
-
-    function reload() {
-        return $this->load($this->getId());
-    }
+require_once('class.thread.php');
+
+class FAQ extends VerySimpleModel {
+
+    static $meta = array(
+        'table' => FAQ_TABLE,
+        'pk' => array('faq_id'),
+        'ordering' => array('question'),
+        'defer' => array('answer'),
+        'select_related'=> array('category'),
+        'joins' => array(
+            'category' => array(
+                'constraint' => array(
+                    'category_id' => 'Category.category_id'
+                ),
+            ),
+            'attachments' => array(
+                'constraint' => array(
+                    "'F'" => 'Attachment.type',
+                    'faq_id' => 'Attachment.object_id',
+                ),
+                'list' => true,
+                'null' => true,
+                'broker' => 'GenericAttachments',
+            ),
+            'topics' => array(
+                'reverse' => 'FaqTopic.faq',
+            ),
+        ),
+    );
+
+    const PERM_MANAGE  = 'faq.manage';
+    static protected $perms = array(
+            self::PERM_MANAGE => array(
+                'title' =>
+                /* @trans */ 'FAQ',
+                'desc'  =>
+                /* @trans */ 'Ability to add/update/disable/delete knowledgebase categories and FAQs',
+                'primary' => true,
+            ));
+
+    var $_local;
+    var $_attachments;
+
+    const VISIBILITY_PRIVATE = 0;
+    const VISIBILITY_PUBLIC = 1;
+    const VISIBILITY_FEATURED = 2;
 
     /* ------------------> Getter methods <--------------------- */
-    function getId() { return $this->id; }
-    function getHashtable() { return $this->ht; }
-    function getKeywords() { return $this->ht['keywords']; }
-    function getQuestion() { return $this->ht['question']; }
-    function getAnswer() { return $this->ht['answer']; }
+    function getId() { return $this->faq_id; }
+    function getHashtable() {
+        $base = $this->ht;
+        unset($base['category']);
+        unset($base['attachments']);
+        return $base;
+    }
+    function getKeywords() { return $this->keywords; }
+    function getQuestion() { return $this->question; }
+    function getAnswer() { return $this->answer; }
     function getAnswerWithImages() {
-        return Format::viewableImages($this->ht['answer']);
+        return Format::viewableImages($this->answer);
+    }
+    function getTeaser() {
+        return Format::truncate(Format::striptags($this->answer), 150);
     }
     function getSearchableAnswer() {
-        return ThreadBody::fromFormattedText($this->ht['answer'], 'html')
+        return ThreadEntryBody::fromFormattedText($this->answer, 'html')
             ->getSearchable();
     }
-    function getNotes() { return $this->ht['notes']; }
-    function getNumAttachments() { return $this->ht['attachments']; }
+    function getNotes() { return $this->notes; }
+    function getNumAttachments() { return $this->attachments->count(); }
 
-    function isPublished() { return (!!$this->ht['ispublished'] && !!$this->ht['ispublic']); }
-
-    function getCreateDate() { return $this->ht['created']; }
-    function getUpdateDate() { return $this->ht['updated']; }
+    function isPublished() {
+        return $this->ispublished != self::VISIBILITY_PRIVATE
+            && $this->category->isPublic();
+    }
+    function getVisibilityDescription() {
+        switch ($this->ispublished) {
+        case self::VISIBILITY_PRIVATE:
+            return __('Internal');
+        case self::VISIBILITY_PUBLIC:
+            return __('Public');
+        case self::VISIBILITY_FEATURED:
+            return __('Featured');
+        }
+    }
 
-    function getCategoryId() { return $this->ht['category_id']; }
-    function getCategory() {
-        if(!$this->category && $this->getCategoryId())
-            $this->category = Category::lookup($this->getCategoryId());
+    function getCreateDate() { return $this->created; }
+    function getUpdateDate() { return $this->updated; }
 
-        return $this->category;
-    }
+    function getCategoryId() { return $this->category_id; }
+    function getCategory() { return $this->category; }
 
     function getHelpTopicsIds() {
+        $ids = array();
+        foreach ($this->getHelpTopics() as $T)
+            $ids[] = $T->topic->getId();
+        return $ids;
+    }
 
-        if (!isset($this->ht['topics']) && ($topics=$this->getHelpTopics())) {
-            $this->ht['topics'] = array_keys($topics);
-        }
-
-        return $this->ht['topics'];
+    function getHelpTopicNames() {
+        $names = array();
+        foreach ($this->getHelpTopics() as $T)
+            $names[] = $T->topic->getFullName();
+        return $names;
     }
 
     function getHelpTopics() {
-        //XXX: change it to obj (when needed)!
-
-        if (!isset($this->topics)) {
-            $this->topics = array();
-            $sql='SELECT t.topic_id, CONCAT_WS(" / ", pt.topic, t.topic) as name  FROM '.TOPIC_TABLE.' t '
-                .' INNER JOIN '.FAQ_TOPIC_TABLE.' ft ON(ft.topic_id=t.topic_id AND ft.faq_id='.db_input($this->id).') '
-                .' LEFT JOIN '.TOPIC_TABLE.' pt ON(pt.topic_id=t.topic_pid) '
-                .' ORDER BY t.topic';
-            if (($res=db_query($sql)) && db_num_rows($res)) {
-                while(list($id,$name) = db_fetch_row($res))
-                    $this->topics[$id]=$name;
-            }
-        }
-
         return $this->topics;
     }
 
     /* ------------------> Setter methods <--------------------- */
-    function setPublished($val) { $this->ht['ispublished'] = !!$val; }
-    function setQuestion($question) { $this->ht['question'] = Format::striptags(trim($question)); }
-    function setAnswer($text) { $this->ht['answer'] = $text; }
-    function setKeywords($words) { $this->ht['keywords'] = $words; }
-    function setNotes($text) { $this->ht['notes'] = $text; }
-
-    /* For ->attach() and ->detach(), use $this->attachments() (nolint) */
-    function attach($file) { return $this->_attachments->add($file); }
-    function detach($file) { return $this->_attachments->remove($file); }
+    function setPublished($val) { $this->ispublished = !!$val; }
+    function setQuestion($question) { $this->question = Format::striptags(trim($question)); }
+    function setAnswer($text) { $this->answer = $text; }
+    function setKeywords($words) { $this->keywords = $words; }
+    function setNotes($text) { $this->notes = $text; }
 
     function publish() {
         $this->setPublished(1);
-
-        return $this->apply();
+        return $this->save();
     }
 
     function unpublish() {
         $this->setPublished(0);
+        return $this->save();
+    }
 
-        return $this->apply();
+    function printPdf() {
+        global $thisstaff;
+        require_once(INCLUDE_DIR.'class.pdf.php');
+
+        $paper = 'Letter';
+        if ($thisstaff)
+            $paper = $thisstaff->getDefaultPaperSize();
+
+        ob_start();
+        $faq = $this;
+        include STAFFINC_DIR . 'templates/faq-print.tmpl.php';
+        $html = ob_get_clean();
+
+        $pdf = new mPDFWithLocalImages('', $paper);
+        // Setup HTML writing and load default thread stylesheet
+        $pdf->WriteHtml(
+            '<style>
+            .bleed { margin: 0; padding: 0; }
+            .faded { color: #666; }
+            .faq-title { font-size: 170%; font-weight: bold; }
+            .thread-body { font-family: serif; }'
+            .file_get_contents(ROOT_DIR.'css/thread.css')
+            .'</style>'
+            .'<div>'.$html.'</div>', 0, true, true);
+
+        $pdf->Output(Format::slugify($faq->getQuestion()) . '.pdf', 'I');
     }
 
-    /* Same as update - but mainly called after one or more setters are changed. */
-    function apply() {
-        $errors = array();
-        //XXX: set errors and add ->getErrors() & ->getError()
-        return $this->update($this->ht, $errors);
+    // Internationalization of the knowledge base
+
+    function getTranslateTag($subtag) {
+        return _H(sprintf('faq.%s.%s', $subtag, $this->getId()));
+    }
+    function getLocal($subtag) {
+        $tag = $this->getTranslateTag($subtag);
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : $this->ht[$subtag];
+    }
+    function getAllTranslations() {
+        if (!isset($this->_local)) {
+            $tag = $this->getTranslateTag('q:a');
+            $this->_local = CustomDataTranslation::allTranslations($tag, 'article');
+        }
+        return $this->_local;
+    }
+    function getLocalQuestion($lang=false) {
+        return $this->_getLocal('question', $lang);
+    }
+    function getLocalAnswer($lang=false) {
+        return $this->_getLocal('answer', $lang);
+    }
+    function getLocalAnswerWithImages($lang=false) {
+        return Format::viewableImages($this->getLocalAnswer($lang));
+    }
+    function _getLocal($what, $lang=false) {
+        if (!$lang) {
+            $lang = $this->getDisplayLang();
+        }
+        $translations = $this->getAllTranslations();
+        foreach ($translations as $t) {
+            if (0 === strcasecmp($lang, $t->lang)) {
+                $data = $t->getComplex();
+                if (isset($data[$what]))
+                    return $data[$what];
+            }
+        }
+        return $this->ht[$what];
+    }
+    function getDisplayLang() {
+        if (isset($_REQUEST['kblang']))
+            $lang = $_REQUEST['kblang'];
+        else
+            $lang = Internationalization::getCurrentLanguage();
+        return $lang;
+    }
+
+    function getLocalAttachments($lang=false) {
+        return $this->attachments->getSeparates()->filter(Q::any(array(
+            'lang__isnull' => true,
+            'lang' => $lang ?: $this->getDisplayLang(),
+        )));
     }
 
     function updateTopics($ids){
@@ -153,49 +238,69 @@ class FAQ {
             }
         }
 
-        $sql='DELETE FROM '.FAQ_TOPIC_TABLE.' WHERE faq_id='.db_input($this->getId());
-        if($ids)
-            $sql.=' AND topic_id NOT IN('.implode(',', db_input($ids)).')';
-
-        if (!db_query($sql))
-            return false;
-
-        Signal::send('model.updated', $this);
+        if ($ids)
+            $this->topics->filter(Q::not(array('topic_id__in' => $ids)))->delete();
+        else
+            $this->topics->delete();
     }
 
-    function update($vars, &$errors) {
-
-        if(!$this->save($this->getId(), $vars, $errors))
-            return false;
-
-        $this->updateTopics($vars['topics']);
-
-        //Delete removed attachments.
-        $keepers = $vars['files'];
-        if(($attachments = $this->attachments->getSeparates())) {
-            foreach($attachments as $file) {
-                if($file['id'] && !in_array($file['id'], $keepers))
-                    $this->attachments->delete($file['id']);
-            }
+    function saveTranslations($vars) {
+        global $thisstaff;
+
+        foreach ($this->getAllTranslations() as $t) {
+            $trans = @$vars['trans'][$t->lang];
+            if (!$trans || !array_filter($trans))
+                // Not updating translations
+                continue;
+
+            // Content is not new and shouldn't be added below
+            unset($vars['trans'][$t->lang]);
+            $content = array('question' => $trans['question'],
+                'answer' => Format::sanitize($trans['answer']));
+
+            // Don't update content which wasn't updated
+            if ($content == $t->getComplex())
+                continue;
+
+            $t->text = $content;
+            $t->agent_id = $thisstaff->getId();
+            $t->updated = SqlFunction::NOW();
+            if (!$t->save())
+                return false;
+        }
+        // New translations (?)
+        $tag = $this->getTranslateTag('q:a');
+        foreach ($vars['trans'] as $lang=>$parts) {
+            $content = array('question' => @$parts['question'],
+                'answer' => Format::sanitize(@$parts['answer']));
+            if (!array_filter($content))
+                continue;
+            $t = CustomDataTranslation::create(array(
+                'type'      => 'article',
+                'object_hash' => $tag,
+                'lang'      => $lang,
+                'text'      => $content,
+                'revision'  => 1,
+                'agent_id'  => $thisstaff->getId(),
+                'updated'   => SqlFunction::NOW(),
+            ));
+            if (!$t->save())
+                return false;
         }
-
-        // Upload new attachments IF any.
-        $this->attachments->upload($keepers);
-
-        // Inline images (attached to the draft)
-        $this->attachments->deleteInlines();
-        $this->attachments->upload(Draft::getAttachmentIds($vars['answer']));
-
-        $this->reload();
-
-        Signal::send('model.updated', $this);
         return true;
     }
 
+    function getAttachments($lang=null) {
+        $att = $this->attachments;
+        if ($lang)
+            $att = $att->window(array('lang' => $lang));
+        return $att;
+    }
+
     function getAttachmentsLinks($separator=' ',$target='') {
 
         $str='';
-        if(($attachments=$this->attachments->getSeparates())) {
+        if ($attachments = $this->getLocalAttachments()->all()) {
             foreach($attachments as $attachment ) {
             /* The h key must match validation in file.php */
             if($attachment['size'])
@@ -210,127 +315,186 @@ class FAQ {
     }
 
     function delete() {
-
-        $sql='DELETE FROM '.FAQ_TABLE
-            .' WHERE faq_id='.db_input($this->getId())
-            .' LIMIT 1';
-        if(!db_query($sql) || !db_affected_rows())
+        try {
+            parent::delete();
+            // Cleanup help topics.
+            $this->topics->delete();
+            // Cleanup attachments.
+            $this->attachments->deleteAll();
+        }
+        catch (OrmException $ex) {
             return false;
-
-        //Cleanup help topics.
-        db_query('DELETE FROM '.FAQ_TOPIC_TABLE.' WHERE faq_id='.db_input($this->id));
-        //Cleanup attachments.
-        $this->attachments->deleteAll();
-
+        }
         return true;
     }
 
     /* ------------------> Static methods <--------------------- */
 
-    function add($vars, &$errors) {
-        if(!($id=self::create($vars, $errors)))
+    static function add($vars, &$errors) {
+        if(!($faq = self::create($vars)))
             return false;
 
-        if(($faq=self::lookup($id))) {
-            $faq->updateTopics($vars['topics']);
-
-            if($_FILES['attachments'] && ($files=AttachmentFile::format($_FILES['attachments'])))
-                $faq->attachments->upload($files);
-
-            // Inline images (attached to the draft)
-            if (isset($vars['draft_id']) && $vars['draft_id'])
-                if ($draft = Draft::lookup($vars['draft_id']))
-                    $faq->attachments->upload($draft->getAttachmentIds(), true);
-
-            $faq->reload();
-        }
-
         return $faq;
     }
 
-    function create($vars, &$errors) {
-        return self::save(0, $vars, $errors);
+    static function create($vars=false) {
+        $faq = new static($vars);
+        $faq->created = SqlFunction::NOW();
+        return $faq;
     }
 
-    function lookup($id) {
-        return ($id && is_numeric($id) && ($obj= new FAQ($id)) && $obj->getId()==$id)? $obj : null;
+    static function allPublic() {
+        return static::objects()->exclude(Q::any(array(
+            'ispublished'=>self::VISIBILITY_PRIVATE,
+            'category__ispublic'=>Category::VISIBILITY_PRIVATE,
+        )));
     }
 
-    function countPublishedFAQs() {
-        $sql='SELECT count(faq.faq_id) '
-            .' FROM '.FAQ_TABLE.' faq '
-            .' INNER JOIN '.FAQ_CATEGORY_TABLE.' cat ON(cat.category_id=faq.category_id AND cat.ispublic=1) '
-            .' WHERE faq.ispublished=1';
-
-        return db_result(db_query($sql));
+    static function countPublishedFAQs() {
+        static $count;
+        if (!isset($count)) {
+            $count = self::allPublic()->count();
+        }
+        return $count;
     }
 
-    function findIdByQuestion($question) {
-        $sql='SELECT faq_id FROM '.FAQ_TABLE
-            .' WHERE question='.db_input($question);
-
-        list($id) =db_fetch_row(db_query($sql));
-
-        return $id;
+    static function getFeatured() {
+        return self::objects()
+            ->filter(array('ispublished__in'=>array(1,2), 'category__ispublic'=>1))
+            ->order_by('-ispublished');
     }
 
-    function findByQuestion($question) {
+    static function findIdByQuestion($question) {
+        $row = self::objects()->filter(array(
+            'question'=>$question
+        ))->values_flat('faq_id')->first();
 
-        if(($id=self::findIdByQuestion($question)))
-            return self::lookup($id);
-
-        return false;
+        return ($row) ? $row[0] : null;
     }
 
-    function save($id, $vars, &$errors, $validation=false) {
+    static function findByQuestion($question) {
+        return self::objects()->filter(array(
+            'question'=>$question
+        ))->one();
+    }
 
-        //Cleanup.
-        $vars['question']=Format::striptags(trim($vars['question']));
+    function update($vars, &$errors) {
+        global $cfg;
 
-        //validate
-        if($id && $id!=$vars['id'])
-            $errors['err'] = __('Internal error. Try again');
+        // Cleanup.
+        $vars['question'] = Format::striptags(trim($vars['question']));
 
-        if(!$vars['question'])
+        // Validate
+        if ($vars['id'] && $this->getId() != $vars['id'])
+            $errors['err'] = __('Internal error occurred');
+        elseif (!$vars['question'])
             $errors['question'] = __('Question required');
-        elseif(($qid=self::findIdByQuestion($vars['question'])) && $qid!=$id)
+        elseif (($qid=self::findIdByQuestion($vars['question'])) && $qid != $vars['id'])
             $errors['question'] = __('Question already exists');
 
-        if(!$vars['category_id'] || !($category=Category::lookup($vars['category_id'])))
+        if (!$vars['category_id'] || !($category=Category::lookup($vars['category_id'])))
             $errors['category_id'] = __('Category is required');
 
-        if(!$vars['answer'])
+        if (!$vars['answer'])
             $errors['answer'] = __('FAQ answer is required');
 
-        if($errors || $validation) return (!$errors);
+        if ($errors)
+            return false;
 
-        //save
-        $sql=' updated=NOW() '
-            .', question='.db_input($vars['question'])
-            .', answer='.db_input(Format::sanitize($vars['answer'], false))
-            .', category_id='.db_input($vars['category_id'])
-            .', ispublished='.db_input(isset($vars['ispublished'])?$vars['ispublished']:0)
-            .', notes='.db_input(Format::sanitize($vars['notes']));
+        $this->question = $vars['question'];
+        $this->answer = Format::sanitize($vars['answer']);
+        $this->category = $category;
+        $this->ispublished = $vars['ispublished'];
+        $this->notes = Format::sanitize($vars['notes']);
+        $this->keywords = ' ';
 
-        if($id) {
-            $sql='UPDATE '.FAQ_TABLE.' SET '.$sql.' WHERE faq_id='.db_input($id);
-            if(db_query($sql))
-                return true;
+        $this->updateTopics($vars['topics']);
 
-            $errors['err']=sprintf(__('Unable to update %s.'), __('this FAQ article'));
+        if (!$this->save())
+            return false;
 
-        } else {
-            $sql='INSERT INTO '.FAQ_TABLE.' SET '.$sql.',created=NOW()';
-            if (db_query($sql) && ($id=db_insert_id())) {
-                Signal::send('model.created', FAQ::lookup($id));
-                return $id;
-            }
+        // General attachments (for all languages)
+        // ---------------------
+        // Delete removed attachments.
+        if (isset($vars['files'])) {
+            $this->getAttachments()->keepOnlyFileIds($vars['files'], false);
+        }
+
+        $images = Draft::getAttachmentIds($vars['answer']);
+        $images = array_map(function($i) { return $i['id']; }, $images);
+        $this->getAttachments()->keepOnlyFileIds($images, true);
+
+        // Handle language-specific attachments
+        // ----------------------
+        $langs = $cfg ? $cfg->getSecondaryLanguages() : false;
+        if ($langs) {
+            $langs[] = $cfg->getPrimaryLanguage();
+            foreach ($langs as $lang) {
+                if (!isset($vars['files_'.$lang]))
+                    // Not updating the FAQ
+                    continue;
 
-            $errors['err']=sprintf(__('Unable to create %s.'), __('this FAQ article'))
-               .' '.__('Internal error occurred');
+                $keepers = $vars['files_'.$lang];
+
+                // FIXME: Include inline images in translated content
+
+                $this->getAttachments($lang)->keepOnlyFileIds($keepers, false, $lang);
+            }
         }
 
-        return false;
+        if (isset($vars['trans']) && !$this->saveTranslations($vars))
+            return false;
+
+        return true;
+    }
+
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($refetch || $this->dirty);
+    }
+
+    static function getPermissions() {
+        return self::$perms;
+    }
+}
+
+RolePermission::register( /* @trans */ 'Knowledgebase',
+        FAQ::getPermissions());
+
+class FaqTopic extends VerySimpleModel {
+
+    static $meta = array(
+        'table' => FAQ_TOPIC_TABLE,
+        'pk' => array('faq_id', 'topic_id'),
+        'select_related' => 'topic',
+        'joins' => array(
+            'faq' => array(
+                'constraint' => array(
+                    'faq_id' => 'FAQ.faq_id',
+                ),
+            ),
+            'topic' => array(
+                'constraint' => array(
+                    'topic_id' => 'Topic.topic_id',
+                ),
+            ),
+        ),
+    );
+}
+
+class FaqAccessMgmtForm
+extends AbstractForm {
+    function buildFields() {
+        return array(
+            'ispublished' => new ChoiceField(array(
+                'label' => __('Listing Type'),
+                'choices' => array(
+                    FAQ::VISIBILITY_PRIVATE => __('Internal'),
+                    FAQ::VISIBILITY_PUBLIC => __('Public'),
+                    FAQ::VISIBILITY_FEATURED => __('Featured'),
+                ),
+            )),
+        );
     }
 }
-?>
diff --git a/include/class.file.php b/include/class.file.php
index 8b65e36a755f971060c33e504140aeecd84b4b70..99aad650aa051e7676fa7e5e905a49a8ff3251d5 100644
--- a/include/class.file.php
+++ b/include/class.file.php
@@ -14,39 +14,22 @@
 require_once(INCLUDE_DIR.'class.signal.php');
 require_once(INCLUDE_DIR.'class.error.php');
 
-class AttachmentFile {
+class AttachmentFile extends VerySimpleModel {
 
-    var $id;
-    var $ht;
+    static $meta = array(
+        'table' => FILE_TABLE,
+        'pk' => array('id'),
+        'joins' => array(
+            'attachments' => array(
+                'reverse' => 'Attachment.file'
+            ),
+        ),
+    );
+    static $keyCache = array();
 
-    function AttachmentFile($id) {
-        $this->id =0;
-        return ($this->load($id));
-    }
-
-    function load($id=0) {
-
-        if(!$id && !($id=$this->getId()))
-            return false;
-
-        $sql='SELECT id, f.type, size, name, `key`, signature, ft, bk, f.created, '
-            .' count(DISTINCT a.object_id) as canned, count(DISTINCT t.ticket_id) as tickets '
-            .' FROM '.FILE_TABLE.' f '
-            .' LEFT JOIN '.ATTACHMENT_TABLE.' a ON(a.file_id=f.id) '
-            .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' t ON(t.file_id=f.id) '
-            .' WHERE f.id='.db_input($id)
-            .' GROUP BY f.id';
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
-
-        $this->ht=db_fetch_array($res);
-        $this->id =$this->ht['id'];
-
-        return true;
-    }
-
-    function reload() {
-        return $this->load();
+    function __onload() {
+        // Cache for lookup in the ::lookupByHash method below
+        static::$keyCache[$this->key] = $this;
     }
 
     function getHashtable() {
@@ -57,16 +40,16 @@ class AttachmentFile {
         return $this->getHashtable();
     }
 
-    function getNumTickets() {
-        return $this->ht['tickets'];
+    function getNumEntries() {
+        return $this->attachments->count();
     }
 
     function isCanned() {
-        return ($this->ht['canned']);
+        return $this->getNumEntries();
     }
 
     function isInUse() {
-        return ($this->getNumTickets() || $this->isCanned());
+        return $this->getNumEntries();
     }
 
     function getId() {
@@ -74,11 +57,11 @@ class AttachmentFile {
     }
 
     function getType() {
-        return $this->ht['type'];
+        return $this->type;
     }
 
     function getBackend() {
-        return $this->ht['bk'];
+        return $this->bk;
     }
 
     function getMime() {
@@ -86,25 +69,25 @@ class AttachmentFile {
     }
 
     function getSize() {
-        return $this->ht['size'];
+        return $this->size;
     }
 
     function getName() {
-        return $this->ht['name'];
+        return $this->name;
     }
 
     function getKey() {
-        return $this->ht['key'];
+        return $this->key;
     }
 
     function getSignature($cascade=false) {
-        $sig = $this->ht['signature'];
+        $sig = $this->signature;
         if (!$sig && $cascade) return $this->getKey();
         return $sig;
     }
 
     function lastModified() {
-        return $this->ht['created'];
+        return $this->created;
     }
 
     function open() {
@@ -142,8 +125,7 @@ class AttachmentFile {
 
     function delete() {
 
-        $sql='DELETE FROM '.FILE_TABLE.' WHERE id='.db_input($this->getId()).' LIMIT 1';
-        if(!db_query($sql) || !db_affected_rows())
+        if (!parent::delete())
             return false;
 
         if ($bk = $this->open())
@@ -249,8 +231,10 @@ class AttachmentFile {
             return;
         $ttl = ($expires) ? $expires - Misc::gmtime() : false;
         $this->makeCacheable($ttl);
-        Http::download($this->getName(), $this->getType() ?: 'application/octet-stream',
-            null, $disposition);
+        $type = $this->getType() ?: 'application/octet-stream';
+        if (isset($_REQUEST['overridetype']))
+            $type = $_REQUEST['overridetype'];
+        Http::download($this->getName(), $type, null, 'inline');
         header('Content-Length: '.$this->getSize());
         $this->sendData(false);
         exit();
@@ -293,7 +277,7 @@ class AttachmentFile {
     }
 
     /* Function assumes the files types have been validated */
-    function upload($file, $ft='T', $deduplicate=true) {
+    static function upload($file, $ft='T', $deduplicate=true) {
 
         if(!$file['name'] || $file['error'] || !is_uploaded_file($file['tmp_name']))
             return false;
@@ -309,42 +293,62 @@ class AttachmentFile {
                     'tmp_name'=>$file['tmp_name'],
                     );
 
-        return AttachmentFile::save($info, $ft, $deduplicate);
+        return static::create($info, $ft, $deduplicate);
+    }
+
+    static function uploadBackdrop(array $file, &$error) {
+        if (extension_loaded('gd')) {
+            $source_path = $file['tmp_name'];
+            list($source_width, $source_height, $source_type) = getimagesize($source_path);
+
+            switch ($source_type) {
+                case IMAGETYPE_GIF:
+                case IMAGETYPE_JPEG:
+                case IMAGETYPE_PNG:
+                    break;
+                default:
+                    $error = __('Invalid image file type');
+                    return false;
+            }
+        }
+        return self::upload($file, 'B', false);
     }
 
-    function uploadLogo($file, &$error, $aspect_ratio=2) {
+    static function uploadLogo($file, &$error, $aspect_ratio=2) {
         /* Borrowed in part from
          * http://salman-w.blogspot.com/2009/04/crop-to-fit-image-using-aspphp.html
          */
-        if (!extension_loaded('gd'))
-            return self::upload($file, 'L');
-
-        $source_path = $file['tmp_name'];
+        if (extension_loaded('gd')) {
+            $source_path = $file['tmp_name'];
+            list($source_width, $source_height, $source_type) = getimagesize($source_path);
+
+            switch ($source_type) {
+                case IMAGETYPE_GIF:
+                case IMAGETYPE_JPEG:
+                case IMAGETYPE_PNG:
+                    break;
+                default:
+                    $error = __('Invalid image file type');
+                    return false;
+            }
 
-        list($source_width, $source_height, $source_type) = getimagesize($source_path);
+            $source_aspect_ratio = $source_width / $source_height;
 
-        switch ($source_type) {
-            case IMAGETYPE_GIF:
-            case IMAGETYPE_JPEG:
-            case IMAGETYPE_PNG:
-                break;
-            default:
-                // TODO: Return an error
-                $error = __('Invalid image file type');
+            if ($source_aspect_ratio < $aspect_ratio) {
+                $error = __('Image is too square. Upload a wider image');
                 return false;
+            }
         }
-
-        $source_aspect_ratio = $source_width / $source_height;
-
-        if ($source_aspect_ratio >= $aspect_ratio)
-            return self::upload($file, 'L', false);
-
-        $error = __('Image is too square. Upload a wider image');
-        return false;
+        return self::upload($file, 'L', false);
     }
 
-    function save(&$file, $ft='T', $deduplicate=true) {
-
+    static function create(&$file, $ft='T', $deduplicate=true) {
+        if (isset($file['encoding'])) {
+            switch ($file['encoding']) {
+            case 'base64':
+                $file['data'] = base64_decode($file['data']);
+            }
+        }
         if (isset($file['data'])) {
             // Allow a callback function to delay or avoid reading or
             // fetching ihe file contents
@@ -359,15 +363,17 @@ class AttachmentFile {
 
         if (isset($file['size']) && $file['size'] > 0) {
             // Check and see if the file is already on record
-            $sql = 'SELECT id, `key` FROM '.FILE_TABLE
-                .' WHERE signature='.db_input($file['signature'])
-                .' AND size='.db_input($file['size']);
-
-            // If the record exists in the database already, a file with the
-            // same hash and size is already on file -- just return its ID
-            if ($deduplicate && (list($id, $key) = db_fetch_row(db_query($sql, false)))) {
-                $file['key'] = $key;
-                return $id;
+            $existing = static::objects()->filter(array(
+                'signature' => $file['signature'],
+                'size' => $file['size']
+            ))->first();
+
+            // If the record exists in the database already, a file with
+            // the same hash and size is already on file -- just return
+            // the file
+            if ($deduplicate && $existing) {
+                $file['key'] = $existing->key;
+                return $existing;
             }
         }
         elseif (!isset($file['data'])) {
@@ -388,20 +394,20 @@ class AttachmentFile {
         if (!$file['type'])
             $file['type'] = 'application/octet-stream';
 
-        $sql='INSERT INTO '.FILE_TABLE.' SET created=NOW() '
-            .',type='.db_input(strtolower($file['type']))
-            .',name='.db_input($file['name'])
-            .',`key`='.db_input($file['key'])
-            .',ft='.db_input($ft ?: 'T')
-            .',signature='.db_input($file['signature']);
 
-        if (isset($file['size']))
-            $sql .= ',size='.db_input($file['size']);
+        $f = new static(array(
+            'type' => strtolower($file['type']),
+            'name' => $file['name'],
+            'key' => $file['key'],
+            'ft' => $ft ?: 'T',
+            'signature' => $file['signature'],
+            'created' => SqlFunction::NOW(),
+        ));
 
-        if (!(db_query($sql) && ($id = db_insert_id())))
-            return false;
+        if (isset($file['size']))
+            $f->size = $file['size'];
 
-        if (!($f = AttachmentFile::lookup($id)))
+        if (!$f->save())
             return false;
 
         // Note that this is preferred over $f->open() because the file does
@@ -435,26 +441,26 @@ class AttachmentFile {
             return false;
         }
 
-        $sql = 'UPDATE '.FILE_TABLE.' SET bk='.db_input($bk->getBkChar());
+        $f->bk = $bk->getBkChar();
 
         if (!isset($file['size'])) {
             if ($size = $bk->getSize())
-                $file['size'] = $size;
+                $f->size = $size;
             // Prefer mb_strlen, because mbstring.func_overload will
             // automatically prefer it if configured.
             elseif (extension_loaded('mbstring'))
-                $file['size'] = mb_strlen($file['data'], '8bit');
+                $f->size = mb_strlen($file['data'], '8bit');
             // bootstrap.php include a compat version of mb_strlen
             else
-                $file['size'] = strlen($file['data']);
-
-            $sql .= ', `size`='.db_input($file['size']);
+                $f->size = strlen($file['data']);
         }
 
-        $sql .= ' WHERE id='.db_input($f->getId());
-        db_query($sql);
+        $f->save();
+        return $f;
+    }
 
-        return $f->getId();
+    static function __create($file, &$errors) {
+        return static::create($file);
     }
 
     /**
@@ -518,10 +524,8 @@ class AttachmentFile {
             return false;
         }
 
-        $sql = 'UPDATE '.FILE_TABLE.' SET bk='
-            .db_input($target->getBkChar())
-            .' WHERE id='.db_input($this->getId());
-        if (!db_query($sql) || db_affected_rows()!=1)
+        $this->bk = $target->getBkChar();
+        if (!$this->save())
             return false;
 
         return $source->unlink();
@@ -542,38 +546,30 @@ class AttachmentFile {
     static function getBackendForFile($file) {
         global $cfg;
 
-        if (!$cfg)
+        $char = null;
+        if ($cfg) {
+            $char = $cfg->getDefaultStorageBackendChar();
+        }
+        try {
+            return FileStorageBackend::lookup($char ?: 'D', $file);
+        }
+        catch (Exception $x) {
             return new AttachmentChunkedData($file);
-
-        $char = $cfg->getDefaultStorageBackendChar();
-        return FileStorageBackend::lookup($char, $file);
-    }
-
-    /* Static functions */
-    function getIdByHash($hash) {
-
-        $sql='SELECT id FROM '.FILE_TABLE.' WHERE `key`='.db_input($hash);
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id)=db_fetch_row($res);
-
-        return $id;
+        }
     }
 
-    function lookup($id) {
+    static function lookupByHash($hash) {
+        if (isset(static::$keyCache[$hash]))
+            return static::$keyCache[$hash];
 
-        $id = is_numeric($id)?$id:AttachmentFile::getIdByHash($id);
-
-        return ($id && ($file = new AttachmentFile($id)) && $file->getId()==$id)?$file:null;
+        // Cache a negative lookup if no such file exists
+        return parent::lookup(array('key' => $hash));
     }
 
-    static function create($info, &$errors) {
-        if (isset($info['encoding'])) {
-            switch ($info['encoding']) {
-                case 'base64':
-                    $info['data'] = base64_decode($info['data']);
-            }
-        }
-        return self::save($info);
+    static function lookup($id) {
+        return is_string($id)
+            ? static::lookupByHash($id)
+            : parent::lookup($id);
     }
 
     /*
@@ -615,36 +611,36 @@ class AttachmentFile {
      * Removes files and associated meta-data for files which no ticket,
      * canned-response, or faq point to any more.
      */
-    /* static */ function deleteOrphans() {
+    static function deleteOrphans() {
 
         // XXX: Allow plugins to define filetypes which do not represent
         //      files attached to tickets or other things in the attachment
         //      table and are not logos
-        $sql = 'SELECT id FROM '.FILE_TABLE.' WHERE id NOT IN ('
-                .'SELECT file_id FROM '.TICKET_ATTACHMENT_TABLE
-                .' UNION '
-                .'SELECT file_id FROM '.ATTACHMENT_TABLE
-            .") AND `ft` = 'T' AND TIMESTAMPDIFF(DAY, `created`, CURRENT_TIMESTAMP) > 1";
-
-        if (!($res = db_query($sql)))
-            return false;
-
-        while (list($id) = db_fetch_row($res))
-            if (($file = self::lookup($id)) && !$file->delete())
+        $files = static::objects()
+            ->filter(array(
+                'attachments__object_id__isnull' => true,
+                'ft' => 'T',
+                'created__gt' => new DateTime('now -1 day'),
+            ));
+
+        foreach ($files as $f) {
+            if (!$f->delete())
                 break;
+        }
 
         return true;
     }
 
-    /* static */
-    function allLogos() {
-        $sql = 'SELECT id FROM '.FILE_TABLE.' WHERE ft="L"
-            ORDER BY created';
-        $logos = array();
-        $res = db_query($sql);
-        while (list($id) = db_fetch_row($res))
-            $logos[] = AttachmentFile::lookup($id);
-        return $logos;
+    static function allLogos() {
+        return static::objects()
+            ->filter(array('ft' => 'L'))
+            ->order_by('created');
+    }
+
+    static function allBackdrops() {
+        return static::objects()
+            ->filter(array('ft' => 'B'))
+            ->order_by('created');
     }
 }
 
@@ -653,6 +649,7 @@ class FileStorageBackend {
     static $desc = false;
     static $registry;
     static $blocksize = 131072;
+    static $private = false;
 
     /**
      * All storage backends should call this function during the request
@@ -662,8 +659,15 @@ class FileStorageBackend {
         self::$registry[$typechar] = $class;
     }
 
-    static function allRegistered() {
-        return self::$registry;
+    static function allRegistered($private=false) {
+        $R = self::$registry;
+        if (!$private) {
+            foreach ($R as $i=>$bk) {
+                if ($bk::$private)
+                    unset($R[$i]);
+            }
+        }
+        return $R;
     }
 
     /**
@@ -822,31 +826,58 @@ class FileStorageBackend {
  * LOB fields in the MySQL database
  */
 define('CHUNK_SIZE', 500*1024); # Beware if you change this...
+class AttachmentFileChunk extends VerySimpleModel {
+    static $meta = array(
+        'table' => FILE_CHUNK_TABLE,
+        'pk' => array('file_id', 'chunk_id'),
+        'joins' => array(
+            'file' => array(
+                'constraint' => array('file_id' => 'AttachmentFile.id'),
+            ),
+        ),
+    );
+}
 class AttachmentChunkedData extends FileStorageBackend {
-    static $desc = "In the database";
+    static $desc = /* @trans */ "In the database";
     static $blocksize = CHUNK_SIZE;
 
+    const FILE_TYPES = array(
+        'T' => 'Attachment',
+        'L' => 'Logo',
+        'B' => 'Backdrop',
+    );
+
     function __construct($file) {
         $this->file = $file;
         $this->_chunk = 0;
         $this->_buffer = false;
+        $this->eof = false;
     }
 
     function getSize() {
-        list($length) = db_fetch_row(db_query(
-             'SELECT SUM(LENGTH(filedata)) FROM '.FILE_CHUNK_TABLE
-            .' WHERE file_id='.db_input($this->file->getId())));
-        return $length;
+        $row = AttachmentFileChunk::objects()
+            ->filter(array('file' => $this->file))
+            ->aggregate(array('length' => SqlAggregate::SUM(SqlFunction::LENGTH(new SqlField('filedata')))))
+            ->one();
+        return $row['length'];
     }
 
     function read($amount=CHUNK_SIZE, $offset=0) {
         # Read requested length of data from attachment chunks
+        if ($this->eof)
+            return false;
+
         while (strlen($this->_buffer) < $amount + $offset) {
-            list($buf) = @db_fetch_row(db_query(
-                'SELECT filedata FROM '.FILE_CHUNK_TABLE.' WHERE file_id='
-                .db_input($this->file->getId()).' AND chunk_id='.$this->_chunk++));
-            if (!$buf)
+            try {
+                list($buf) = AttachmentFileChunk::objects()
+                    ->filter(array('file' => $this->file, 'chunk_id' => $this->_chunk++))
+                    ->values_flat('filedata')
+                    ->one();
+            }
+            catch (DoesNotExist $e) {
+                $this->eof = true;
                 break;
+            }
             $this->_buffer .= $buf;
         }
         $chunk = substr($this->_buffer, $offset, $amount);
@@ -856,25 +887,79 @@ class AttachmentChunkedData extends FileStorageBackend {
 
     function write($what, $chunk_size=CHUNK_SIZE) {
         $offset=0;
-        for (;;) {
-            $block = bin2hex(substr($what, $offset, $chunk_size));
-            if (!$block) break;
-            if (!db_query('REPLACE INTO '.FILE_CHUNK_TABLE
-                    .' SET filedata=0x'.$block.', file_id='
-                    .db_input($this->file->getId()).', chunk_id='.db_input($this->_chunk++)))
+        while ($block = substr($what, $offset, $chunk_size)) {
+            // Chunks are considered immutable. Importing chunks should
+            // forceable remove the contents of a file before write()ing new
+            // chunks. Therefore, inserts should be safe.
+            $chunk = AttachmentFileChunk::create(array(
+                'file' => $this->file,
+                'chunk_id' => $this->_chunk++,
+                'filedata' => $block
+            ));
+            if (!$chunk->save())
                 return false;
-            $offset += strlen($block)/2;
+            $offset += strlen($block);
         }
 
         return $this->_chunk;
     }
 
     function unlink() {
-        db_query('DELETE FROM '.FILE_CHUNK_TABLE
-            .' WHERE file_id='.db_input($this->file->getId()));
-        return db_affected_rows() > 0;
+        return AttachmentFileChunk::objects()
+            ->filter(array('file' => $this->file))
+            ->delete();
     }
 }
 FileStorageBackend::register('D', 'AttachmentChunkedData');
 
+/**
+ * This class provides an interface for files attached on the filesystem in
+ * versions previous to v1.7. The upgrader will keep the attachments on the
+ * disk where they were and write the path into the `attrs` field of the
+ * %file table. This module will continue to serve those files until they
+ * are migrated with the `file` cli app
+ */
+class OneSixAttachments extends FileStorageBackend {
+    static $desc = "upload_dir folder (from osTicket v1.6)";
+    static $private = true;
+
+    function read($bytes=32768, $offset=false) {
+        $filename = $this->meta->attrs;
+        if (!$this->fp)
+            $this->fp = @fopen($filename, 'rb');
+        if (!$this->fp)
+            throw new IOException($filename.': Unable to open for reading');
+        if ($offset)
+            fseek($this->fp, $offset);
+        if (($status = @fread($this->fp, $bytes)) === false)
+            throw new IOException($filename.': Unable to read from file');
+        return $status;
+    }
+
+    function passthru() {
+        $filename = $this->meta->attrs;
+        if (($status = @readfile($filename)) === false)
+            throw new IOException($filename.': Unable to read from file');
+        return $status;
+    }
+
+    function write($data) {
+        throw new IOException('This backend does not support new files');
+    }
+
+    function upload($filepath) {
+        throw new IOException('This backend does not support new files');
+    }
+
+    function unlink() {
+        $filename = $this->meta->attrs;
+        if (!@unlink($filename))
+            throw new IOException($filename.': Unable to delete file');
+        // Drop usage of the `attrs` field
+        $this->meta->attrs = null;
+        $this->meta->save();
+        return true;
+    }
+}
+FileStorageBackend::register('6', 'OneSixAttachments');
 ?>
diff --git a/include/class.filter.php b/include/class.filter.php
index 7cfca2905c61cc22555c1a51fd8b9cfe7f77976e..20bac5ce53b856d13d439a0afedd8e8abe1ab97a 100644
--- a/include/class.filter.php
+++ b/include/class.filter.php
@@ -14,6 +14,8 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
+require_once INCLUDE_DIR . 'class.filter_action.php';
+
 class Filter {
 
     var $id;
@@ -35,7 +37,7 @@ class Filter {
         ),
     );
 
-    function Filter($id) {
+    function __construct($id) {
         $this->id=0;
         $this->load($id);
     }
@@ -174,20 +176,6 @@ class Filter {
         return $this->ht['rules'];
     }
 
-    function getFlatRules() { //Format used on html... I'm ashamed
-
-        $info=array();
-        if(($rules=$this->getRules())) {
-            foreach($rules as $k=>$rule) {
-                $i=$k+1;
-                $info["rule_w$i"]=$rule['w'];
-                $info["rule_h$i"]=$rule['h'];
-                $info["rule_v$i"]=$rule['v'];
-            }
-        }
-        return $info;
-    }
-
     function addRule($what, $how, $val,$extra=array()) {
         $errors = array();
 
@@ -293,48 +281,28 @@ class Filter {
 
         return $match;
     }
+
+    function getActions() {
+        return FilterAction::objects()->filter(array(
+            'filter_id'=>$this->getId()
+        ));
+    }
     /**
      * If the matches() method returns TRUE, send the initial ticket to this
      * method to apply the filter actions defined
      */
-    function apply(&$ticket, $info=null) {
-        # TODO: Disable alerting
-        # XXX: Does this imply turning it on as well? (via ->sendAlerts())
-        if ($this->disableAlerts()) $ticket['autorespond']=false;
-        #       Set owning department (?)
-        if ($this->getDeptId())     $ticket['deptId']=$this->getDeptId();
-        #       Set ticket priority (?)
-        if ($this->getPriorityId()) $ticket['priorityId']=$this->getPriorityId();
-        #       Set SLA plan (?)
-        if ($this->getSLAId())      $ticket['slaId']=$this->getSLAId();
-        #       Set status
-        if ($this->getStatusId())   $ticket['statusId']=$this->getStatusId();
-        #       Auto-assign to (?)
-        #       XXX: Unset the other (of staffId or teamId) (?)
-        if ($this->getStaffId())    $ticket['staffId']=$this->getStaffId();
-        elseif ($this->getTeamId()) $ticket['teamId']=$this->getTeamId();
-        #       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($ticket);
-
+    function apply(&$ticket, $vars) {
+        foreach ($this->getActions() as $a) {
+            $a->setFilter($this);
+            $a->apply($ticket, $vars);
         }
+    }
 
-        # Use canned response.
-        if ($this->getCannedResponse())
-            $ticket['cannedResponseId'] = $this->getCannedResponse();
-
-        # Apply help topic
-        if ($this->getHelpTopic())
-            $ticket['topicId'] = $this->getHelpTopic();
+    function getVars() {
+        return $this->vars;
     }
-     static function getSupportedMatches() {
+
+    static function getSupportedMatches() {
         foreach (static::$match_types as $k=>&$v) {
             if (is_callable($v[0]))
                 $v[0] = $v[0]();
@@ -428,36 +396,36 @@ class Filter {
         $types = array_keys(self::getSupportedMatchTypes());
 
         $rules=array();
-        for($i=1; $i<=25; $i++) { //Expecting no more than 25 rules...
-            if($vars["rule_w$i"] || $vars["rule_h$i"]) {
+        foreach ($vars['rules'] as $i=>$rule) {
+            if($rule["w"] || $rule["h"]) {
                 // Check for REGEX compile errors
-                if (in_array($vars["rule_h$i"], array('match','not_match'))) {
-                    $wrapped = "/".$vars["rule_v$i"]."/iu";
-                    if (false === @preg_match($vars["rule_v$i"], ' ')
+                if (in_array($rule["h"], array('match','not_match'))) {
+                    $wrapped = "/".$rule["v"]."/iu";
+                    if (false === @preg_match($rule["v"], ' ')
                             && (false !== @preg_match($wrapped, ' ')))
-                        $vars["rule_v$i"] = $wrapped;
+                        $rule["v"] = $wrapped;
                 }
 
-                if(!$vars["rule_w$i"] || !in_array($vars["rule_w$i"],$matches))
+                if(!$rule["w"] || !in_array($rule["w"],$matches))
                     $errors["rule_$i"]=__('Invalid match selection');
-                elseif(!$vars["rule_h$i"] || !in_array($vars["rule_h$i"],$types))
+                elseif(!$rule["h"] || !in_array($rule["h"],$types))
                     $errors["rule_$i"]=__('Invalid match type selection');
-                elseif(!$vars["rule_v$i"])
+                elseif(!$rule["v"])
                     $errors["rule_$i"]=__('Value required');
-                elseif($vars["rule_w$i"]=='email'
-                        && $vars["rule_h$i"]=='equal'
-                        && !Validator::is_email($vars["rule_v$i"]))
+                elseif($rule["w"]=='email'
+                        && $rule["h"]=='equal'
+                        && !Validator::is_email($rule["v"]))
                     $errors["rule_$i"]=__('Valid email required for the match type');
-                elseif (in_array($vars["rule_h$i"], array('match','not_match'))
-                        && (false === @preg_match($vars["rule_v$i"], ' ')))
+                elseif (in_array($rule["h"], array('match','not_match'))
+                        && (false === @preg_match($rule["v"], ' ')))
                     $errors["rule_$i"] = sprintf(__('Regex compile error: (#%s)'),
                         preg_last_error());
 
 
                 else //for everything-else...we assume it's valid.
-                    $rules[]=array('what'=>$vars["rule_w$i"],
-                        'how'=>$vars["rule_h$i"],'val'=>trim($vars["rule_v$i"]));
-            }elseif($vars["rule_v$i"]) {
+                    $rules[]=array('what'=>$rule["w"],
+                        'how'=>$rule["h"],'val'=>trim($rule["v"]));
+            }elseif($rule["v"]) {
                 $errors["rule_$i"]=__('Incomplete selection');
             }
         }
@@ -519,28 +487,10 @@ class Filter {
             .',name='.db_input($vars['name'])
             .',execorder='.db_input($vars['execorder'])
             .',email_id='.db_input($emailId)
-            .',dept_id='.db_input($vars['dept_id'])
-            .',status_id='.db_input($vars['status_id'])
-            .',priority_id='.db_input($vars['priority_id'])
-            .',sla_id='.db_input($vars['sla_id'])
-            .',topic_id='.db_input($vars['topic_id'])
             .',match_all_rules='.db_input($vars['match_all_rules'])
             .',stop_onmatch='.db_input(isset($vars['stop_onmatch'])?1:0)
-            .',reject_ticket='.db_input(isset($vars['reject_ticket'])?1:0)
-            .',use_replyto_email='.db_input(isset($vars['use_replyto_email'])?1:0)
-            .',disable_autoresponder='.db_input(isset($vars['disable_autoresponder'])?1:0)
-            .',canned_response_id='.db_input($vars['canned_response_id'])
             .',notes='.db_input(Format::sanitize($vars['notes']));
 
-
-        //Auto assign ID is overloaded...
-        if($vars['assign'] && $vars['assign'][0]=='s')
-             $sql.=',team_id=0,staff_id='.db_input(preg_replace("/[^0-9]/", "",$vars['assign']));
-        elseif($vars['assign'] && $vars['assign'][0]=='t')
-            $sql.=',staff_id=0,team_id='.db_input(preg_replace("/[^0-9]/", "",$vars['assign']));
-        else
-            $sql.=',staff_id=0,team_id=0 '; //no auto-assignment!
-
         if($id) {
             $sql='UPDATE '.FILTER_TABLE.' SET '.$sql.' WHERE id='.db_input($id);
             if(!db_query($sql))
@@ -559,8 +509,48 @@ class Filter {
         # Don't care about errors stashed in $xerrors
         $xerrors = array();
         self::save_rules($id,$vars,$xerrors);
+        self::save_actions($id, $vars, $errors);
 
-        return true;
+        return count($errors) == 0;
+    }
+
+    function save_actions($id, $vars, &$errors) {
+        if (!is_array(@$vars['actions']))
+            return;
+
+        foreach ($vars['actions'] as $sort=>$v) {
+            if (is_array($v)) {
+                $info = $v['type'];
+                $sort = $v['sort'] ?: $sort;
+                $action = 'N';
+            } else {
+                $action = $v[0];
+                $info = substr($v, 1);
+            }
+
+            switch ($action) {
+            case 'N': # new filter action
+                $I = new FilterAction(array(
+                    'type'=>$info,
+                    'filter_id'=>$id,
+                    'sort' => (int) $sort,
+                ));
+                $I->setConfiguration($errors, $vars);
+                $I->save();
+                break;
+            case 'I': # exiting filter action
+                if ($I = FilterAction::lookup($info)) {
+                    $I->setConfiguration($errors, $vars);
+                    $I->sort = (int) $sort;
+                    $I->save();
+                }
+                break;
+            case 'D': # deleted filter action
+                if ($I = FilterAction::lookup($info))
+                    $I->delete();
+                break;
+            }
+        }
     }
 }
 
@@ -571,7 +561,7 @@ class FilterRule {
 
     var $filter;
 
-    function FilterRule($id,$filterId=0) {
+    function __construct($id,$filterId=0) {
         $this->id=0;
         $this->load($id,$filterId);
     }
@@ -707,7 +697,7 @@ class TicketFilter {
      *  ---------------
      *  @see Filter::matches() for a complete list of supported keys
      */
-    function TicketFilter($origin, $vars=array()) {
+    function __construct($origin, $vars=array()) {
 
         //Normalize the target based on ticket's origin.
         $this->target = self::origin2target($origin);
@@ -780,8 +770,6 @@ class TicketFilter {
      */
     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;
         }
@@ -802,51 +790,6 @@ class TicketFilter {
         return db_query($sql);
     }
 
-    /**
-     * Quick function to determine if the received email-address is
-     * indicated by an active email filter to be banned. Returns the id of
-     * the filter that has the address blacklisted and FALSE if the email is
-     * not blacklisted.
-     *
-     * XXX: If more detailed matching is to be supported, perhaps this
-     *      should receive an array like the constructor and
-     *      Filter::matches() method.
-     *      Peter - Let's keep it as a quick scan for obviously banned emails.
-     */
-    /* static */
-    function isBanned($addr) {
-
-        $sql='SELECT filter.id, what, how, UPPER(val) '
-            .' FROM '.FILTER_TABLE.' filter'
-            .' INNER JOIN '.FILTER_RULE_TABLE.' rule'
-            .' ON (filter.id=rule.filter_id)'
-            .' WHERE filter.reject_ticket'
-            .'   AND filter.match_all_rules=0'
-            .'   AND filter.email_id=0'
-            .'   AND filter.isactive'
-            .'   AND rule.isactive '
-            .'   AND rule.what="email"'
-            .'   AND LOCATE(rule.val,'.db_input($addr).')';
-
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
-
-        # XXX: Use MB_xxx function for proper unicode support
-        $addr = strtoupper($addr);
-        $how=array('equal'      => array('strcmp', 0),
-                   'contains'   => array('strpos', null, false));
-
-        while ($row=db_fetch_array($res)) {
-            list($func, $pos, $neg) = $how[$row['how']];
-            if (!$func) continue;
-            $result = call_user_func($func, $addr, $row['val']);
-            if (($neg === null && $result === $pos) || $result !== $neg)
-                return $row['id'];
-        }
-
-        return false;
-    }
-
     /**
      * Simple true/false if the headers of the email indicate that the email
      * is an automatic response.
diff --git a/include/class.filter_action.php b/include/class.filter_action.php
new file mode 100644
index 0000000000000000000000000000000000000000..6c687cc6faee238d1adb0f007560eb6941404d12
--- /dev/null
+++ b/include/class.filter_action.php
@@ -0,0 +1,575 @@
+<?php
+
+require_once INCLUDE_DIR . 'class.orm.php';
+
+class FilterAction extends VerySimpleModel {
+    static $meta = array(
+        'table' => FILTER_ACTION_TABLE,
+        'pk' => array('id'),
+        'ordering' => array('sort'),
+    );
+
+    static $registry = array();
+    static $registry_group = array();
+
+    var $_impl;
+    var $_config;
+    var $_filter;
+
+    function getId() {
+        return $this->id;
+    }
+
+    function setFilter($filter) {
+        $this->_filter = $filter;
+    }
+    function getFilter() {
+        return $this->_filter;
+    }
+
+    function getConfiguration() {
+        if (!$this->_config) {
+            $this->_config = $this->get('configuration');
+            if (is_string($this->_config))
+                $this->_config = JsonDataParser::parse($this->_config);
+            elseif (!$this->_config)
+                $this->_config = array();
+            foreach ($this->getImpl()->getConfigurationOptions() as $name=>$field)
+                if (!isset($this->_config[$name]))
+                    $this->_config[$name] = $field->get('default');
+        }
+        return $this->_config;
+    }
+
+    function setConfiguration(&$errors=array(), $source=false) {
+        $config = array();
+        foreach ($this->getImpl()->getConfigurationForm($source ?: $_POST)
+                ->getFields() as $name=>$field) {
+            if (!$field->hasData())
+                continue;
+            $config[$name] = $field->to_php($field->getClean());
+            $errors = array_merge($errors, $field->errors());
+        }
+        if (count($errors) === 0)
+            $this->set('configuration', JsonDataEncoder::encode($config));
+        return count($errors) === 0;
+    }
+
+    function getImpl() {
+        if (!isset($this->_impl)) {
+            if (!($I = self::lookupByType($this->type, $this)))
+                throw new Exception(sprintf(
+                    '%s: No such filter action registered', $this->type));
+            $this->_impl = $I;
+        }
+        return $this->_impl;
+    }
+
+    function apply(&$ticket, array $info) {
+        return $this->getImpl()->apply($ticket, $info);
+    }
+
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($refetch || $this->dirty);
+    }
+
+    static function register($class, $group=false) {
+        if (!$class::$type)
+            throw new Exception('Filter actions must specify ::$type');
+        elseif (!is_subclass_of($class, 'TriggerAction'))
+            throw new Exception('Filter actions must extend from TriggerAction');
+
+        self::$registry[$class::$type] = $class;
+        self::$registry_group[$group ?: ''][$class::$type] = $class;
+    }
+
+    static function lookupByType($type, $thisObj=false) {
+        if (!isset(self::$registry[$type]))
+            return null;
+
+        $class = self::$registry[$type];
+        return new $class($thisObj);
+    }
+
+    static function allRegistered($group=false) {
+        $types = array();
+        foreach (self::$registry_group as $group=>$actions) {
+            $G = $group ? __($group) : '';
+            foreach ($actions as $type=>$class) {
+                $types[$G][$type] = __($class::getName());
+            }
+        }
+        return $types;
+    }
+}
+
+abstract class TriggerAction {
+    static $type = false;
+    static $flags = 0;
+
+    const FLAG_MULTI_USE    = 0x0001;   // Action can be used multiple times
+
+    var $action;
+
+    function __construct($action=false) {
+        $this->action = $action;
+    }
+
+    function getConfiguration() {
+        if ($this->action)
+            return $this->action->getConfiguration();
+        return array();
+    }
+
+    function getConfigurationForm($source=false) {
+        if (!$this->_cform) {
+            $config = $this->getConfiguration();
+            $options = $this->getConfigurationOptions();
+            // Find a uid offset for this guy
+            $uid = 1000;
+            foreach (FilterAction::$registry as $type=>$class) {
+                $uid += 100;
+                if ($type == $this->getType())
+                    break;
+            }
+            // Ensure IDs are unique
+            foreach ($options as $f) {
+                $f->set('id', $uid++);
+            }
+            $this->_cform = new SimpleForm($options, $source);
+            if (!$source) {
+                foreach ($this->_cform->getFields() as $name=>$f) {
+                    if ($config && isset($config[$name]))
+                        $f->value = $config[$name];
+                    elseif ($f->get('default'))
+                        $f->value = $f->get('default');
+                }
+            }
+        }
+        return $this->_cform;
+    }
+
+    function hasFlag($flag) {
+        return static::$flags & $flag > 0;
+    }
+
+    static function getType() { return static::$type; }
+    static function getName() { return __(static::$name); }
+
+    abstract function apply(&$ticket, array $info);
+    abstract function getConfigurationOptions();
+}
+
+class FA_RejectTicket extends TriggerAction {
+    static $type = 'reject';
+    static $name = /* @trans */ 'Reject Ticket';
+
+    function apply(&$ticket, array $info) {
+        throw new RejectedException($this->action->getFilter(), $ticket);
+    }
+
+    function getConfigurationOptions() {
+        return array(
+            '' => new FreeTextField(array(
+                'configuration' => array(
+                    'content' => sprintf('<span style="color:red"><b>%s</b></span>',
+                        __('Reject Ticket')),
+                )
+            )),
+        );
+    }
+}
+FilterAction::register('FA_RejectTicket', /* @trans */ 'Ticket');
+
+class FA_UseReplyTo extends TriggerAction {
+    static $type = 'replyto';
+    static $name = /* @trans */ 'Use Reply-To Email';
+
+    function apply(&$ticket, array $info) {
+        if (!$info['reply-to'])
+            // Nothing to do
+            return;
+        $changed = $info['reply-to'] != $ticket['email']
+            || ($info['reply-to-name'] && $ticket['name'] != $info['reply-to-name']);
+        if ($changed) {
+            $ticket['email'] = $info['reply-to'];
+            if ($info['reply-to-name'])
+                $ticket['name'] = $info['reply-to-name'];
+            throw new FilterDataChanged($ticket);
+        }
+    }
+
+    function getConfigurationOptions() {
+        return array(
+            '' => new FreeTextField(array(
+                'configuration' => array(
+                    'content' => __('<strong>Use</strong> the Reply-To email header')
+                )
+            )),
+        );
+    }
+}
+FilterAction::register('FA_UseReplyTo', /* @trans */ 'Communication');
+
+class FA_DisableAutoResponse extends TriggerAction {
+    static $type = 'noresp';
+    static $name = /* @trans */ "Disable autoresponse";
+
+    function apply(&$ticket, array $info) {
+        # TODO: Disable alerting
+        # XXX: Does this imply turning it on as well? (via ->sendAlerts())
+        $ticket['autorespond']=false;
+    }
+
+    function getConfigurationOptions() {
+        return array(
+            '' => new FreeTextField(array(
+                'configuration' => array(
+                    'content' => __('<strong>Disable</strong> new ticket auto-response')
+                ),
+            )),
+        );
+    }
+}
+FilterAction::register('FA_DisableAutoResponse', /* @trans */ 'Communication');
+
+class FA_AutoCannedResponse extends TriggerAction {
+    static $type = 'canned';
+    static $name = /* @trans */ "Attach Canned Response";
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['canned_id']) {
+            $ticket['cannedResponseId'] = $config['canned_id'];
+        }
+    }
+
+    function getConfigurationOptions() {
+        $sql='SELECT canned_id, title, isenabled FROM '.CANNED_TABLE .' ORDER by title';
+        $choices = array(false => '— '.__('None').' —');
+        if ($res=db_query($sql)) {
+            while (list($id, $title, $isenabled)=db_fetch_row($res)) {
+                if (!$isenabled)
+                    $title .= ' ' . __('(disabled)');
+                $choices[$id] = $title;
+            }
+        }
+        return array(
+            'canned_id' => new ChoiceField(array(
+                'default' => false,
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_AutoCannedResponse', /* @trans */ 'Communication');
+
+class FA_RouteDepartment extends TriggerAction {
+    static $type = 'dept';
+    static $name = /* @trans */ 'Set Department';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['dept_id'])
+            $ticket['deptId'] = $config['dept_id'];
+    }
+
+    function getConfigurationOptions() {
+        return array(
+            'dept_id' => new ChoiceField(array(
+                'configuration' => array(
+                    'prompt' => __('Unchanged'),
+                    'data' => array('quick-add' => 'department'),
+                ),
+                'choices' =>
+                    Dept::getDepartments() +
+                    array(':new:' => '— '.__('Add New').' —'),
+                'validators' => function($self, $clean) {
+                    if ($clean === ':new:')
+                        $self->addError(__('Select a department'));
+                }
+            )),
+        );
+    }
+}
+FilterAction::register('FA_RouteDepartment', /* @trans */ 'Ticket');
+
+class FA_AssignPriority extends TriggerAction {
+    static $type = 'pri';
+    static $name = /* @trans */ "Set Priority";
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['priority'])
+            $ticket['priorityId'] = $config['priority'];
+    }
+
+    function getConfigurationOptions() {
+        $sql = 'SELECT priority_id, priority_desc FROM '.PRIORITY_TABLE
+              .' ORDER BY priority_urgency DESC';
+        $choices = array();
+        if ($res = db_query($sql)) {
+            while ($row = db_fetch_row($res))
+                $choices[$row[0]] = $row[1];
+        }
+        return array(
+            'priority' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_AssignPriority', /* @trans */ 'Ticket');
+
+class FA_AssignSLA extends TriggerAction {
+    static $type = 'sla';
+    static $name = /* @trans */ 'Set SLA Plan';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['sla_id'])
+            $ticket['slaId'] = $config['sla_id'];
+    }
+
+    function getConfigurationOptions() {
+        $choices = SLA::getSLAs();
+        return array(
+            'sla_id' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_AssignSLA', /* @trans */ 'Ticket');
+
+class FA_AssignTeam extends TriggerAction {
+    static $type = 'team';
+    static $name = /* @trans */ 'Assign Team';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['team_id'])
+            $ticket['teamId'] = $config['team_id'];
+    }
+
+    function getConfigurationOptions() {
+        $choices = Team::getTeams();
+        return array(
+            'team_id' => new ChoiceField(array(
+                'configuration' => array(
+                    'prompt' => __('Unchanged'),
+                    'data' => array('quick-add' => 'team'),
+                ),
+                'choices' =>
+                    Team::getTeams() +
+                    array(':new:' => '— '.__('Add New').' —'),
+                'validators' => function($self, $clean) {
+                    if ($clean === ':new:')
+                        $self->addError(__('Select a team'));
+                }
+            )),
+        );
+    }
+}
+FilterAction::register('FA_AssignTeam', /* @trans */ 'Ticket');
+
+class FA_AssignAgent extends TriggerAction {
+    static $type = 'agent';
+    static $name = /* @trans */ 'Assign Agent';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['staff_id'])
+            $ticket['staffId'] = $config['staff_id'];
+    }
+
+    function getConfigurationOptions() {
+        $choices = Staff::getStaffMembers();
+        return array(
+            'staff_id' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_AssignAgent', /* @trans */ 'Ticket');
+
+class FA_AssignTopic extends TriggerAction {
+    static $type = 'topic';
+    static $name = /* @trans */ 'Set Help Topic';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['topic_id'])
+            $ticket['topicId'] = $config['topic_id'];
+    }
+
+    function getConfigurationOptions() {
+        $choices = Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED);
+        return array(
+            'topic_id' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_AssignTopic', /* @trans */ 'Ticket');
+
+class FA_SetStatus extends TriggerAction {
+    static $type = 'status';
+    static $name = /* @trans */ 'Set Ticket Status';
+
+    function apply(&$ticket, array $info) {
+        $config = $this->getConfiguration();
+        if ($config['status_id'])
+            $ticket['statusId'] = $config['status_id'];
+    }
+
+    function getConfigurationOptions() {
+        $choices = array();
+        foreach (TicketStatusList::getStatuses(array(
+            'states' => array('open', 'closed')
+        ))
+        as $S) {
+            // TODO: Move this to TicketStatus::getName
+            $name = $S->getName();
+            if (!($isenabled = $S->isEnabled()))
+                $name.=' '.__('(disabled)');
+            $choices[$S->getId()] = $name;
+        }
+        return array(
+            'status_id' => new ChoiceField(array(
+                'configuration' => array('prompt' => __('Unchanged')),
+                'choices' => $choices,
+            )),
+        );
+    }
+}
+FilterAction::register('FA_SetStatus', /* @trans */ 'Ticket');
+
+class FA_SendEmail extends TriggerAction {
+    static $type = 'email';
+    static $name = /* @trans */ 'Send an Email';
+    static $flags = TriggerAction::FLAG_MULTI_USE;
+
+    function apply(&$ticket, array $info) {
+        global $ost;
+
+        $config = $this->getConfiguration();
+        $info = array('subject' => $config['subject'],
+            'message' => $config['message']);
+        $info = $ost->replaceTemplateVariables(
+            $info, array('ticket' => $ticket)
+        );
+
+        // Honor FROM address settings
+        if (!$config['from'] || !($mailer = Email::lookup($config['from'])))
+            $mailer = new Mailer();
+
+        // Allow %{user} in the To: line
+        $replacer = new VariableReplacer();
+        $replacer->assign(array(
+            'user' => sprintf('"%s" <%s>', $ticket['name'], $ticket['email'])
+        ));
+        $to = $replacer->replaceVars($config['recipients']);
+
+        require_once PEAR_DIR . 'Mail/RFC822.php';
+        require_once PEAR_DIR . 'PEAR.php';
+
+        if (!($mails = Mail_RFC822::parseAddressList($to)) || PEAR::isError($mails))
+            return false;
+
+        // Allow %{recipient} in the body
+        foreach ($mails as $R) {
+            $recipient = sprintf('%s <%s@%s>', $R->personal, $R->mailbox, $R->host);
+            $replacer->assign(array(
+                'recipient' => new EmailAddress($recipient),
+            ));
+            $I = $replacer->replaceVars($info);
+            $mailer->send($recipient, $I['subject'], $I['message']);
+        }
+
+    }
+
+    static function getVarScope() {
+        $context = array(
+            'ticket' => array(
+                'class' => 'FA_SendEmail_TicketInfo', 'desc' => __('Ticket'),
+            ),
+            'user' => __('Ticket Submitter'),
+            'recipient' => array(
+                'class' => 'EmailAddress', 'desc' => __('Recipient'),
+            ),
+        ) + osTicket::getVarScope();
+        return VariableReplacer::compileScope($context);
+    }
+
+    function getConfigurationOptions() {
+        $choices = array('' => __('Default System Email'));
+        $choices += Email::getAddresses();
+
+        return array(
+            'recipients' => new TextboxField(array(
+                'label' => __('Recipients'), 'required' => true,
+                'configuration' => array(
+                    'size' => 80, 'length' => 1000,
+                ),
+                'validators' => function($self, $value) {
+                    if (!($mails = Mail_RFC822::parseAddressList($value)) || PEAR::isError($mails))
+                        $self->addError('Unable to parse address list. '
+                            .'Use commas to separate addresses.');
+
+                    $valid = array('user',);
+                    foreach ($mails as $M) {
+                        // Check placeholders like '%{user}'
+                        $P = array();
+                        if (preg_match('`%\{([^}]+)\}`', $M->mailbox, $P)) {
+                            if (!in_array($P[1], $valid))
+                                $self->addError(sprintf('%s: Not a valid variable', $P[0]));
+                        }
+                        elseif ($M->host == 'localhost' || !$M->mailbox) {
+                            $self->addError(sprintf(__('%s: Not a valid email address'),
+                                $M->mailbox . '@' . $M->host));
+                        }
+                    }
+                }
+            )),
+            'subject' => new TextboxField(array(
+                'required' => true,
+                'configuration' => array(
+                    'size' => 80,
+                    'placeholder' => __('Subject')
+                ),
+            )),
+            'message' => new TextareaField(array(
+                'required' => true,
+                'configuration' => array(
+                    'placeholder' => __('Message'),
+                    'html' => true,
+                    'context' => 'fa:send_email',
+                ),
+            )),
+            'from' => new ChoiceField(array(
+                'label' => __('From Email'),
+                'choices' => $choices,
+                'default' => '',
+            )),
+        );
+    }
+}
+FilterAction::register('FA_SendEmail', /* @trans */ 'Communication');
+
+class FA_SendEmail_TicketInfo {
+    static function getVarScope() {
+        return array(
+            'message' => __('Message from the EndUser'),
+            'source' => __('Source'),
+        );
+    }
+}
diff --git a/include/class.format.php b/include/class.format.php
index c1acf4e03879058ab654fbb671c1ca1f03d796f0..d25579051f6c8b2f376ce35688bb382c925466b9 100644
--- a/include/class.format.php
+++ b/include/class.format.php
@@ -15,6 +15,7 @@
 **********************************************************************/
 
 include_once INCLUDE_DIR.'class.charset.php';
+require_once INCLUDE_DIR.'class.variable.php';
 
 class Format {
 
@@ -194,6 +195,9 @@ class Format {
 
     function html2text($html, $width=74, $tidy=true) {
 
+        if (!$html)
+            return $html;
+
 
         # Tidy html: decode, balance, sanitize tags
         if($tidy)
@@ -201,8 +205,9 @@ class Format {
 
         # See if advanced html2text is available (requires xml extension)
         if (function_exists('convert_html_to_text')
-                && extension_loaded('dom'))
-            return convert_html_to_text($html, $width);
+                && extension_loaded('dom')
+                && ($text = convert_html_to_text($html, $width)))
+                return $text;
 
         # Try simple html2text  - insert line breaks after new line tags.
         $html = preg_replace(
@@ -314,7 +319,8 @@ class Format {
             'schemes' => 'href: aim, feed, file, ftp, gopher, http, https, irc, mailto, news, nntp, sftp, ssh, telnet; *:file, http, https; src: cid, http, https, data',
             'hook_tag' => function($e, $a=0) { return Format::__html_cleanup($e, $a); },
             'elements' => '*+iframe',
-            'spec' => 'iframe=-*,height,width,type,src(match="`^(https?:)?//(www\.)?(youtube|dailymotion|vimeo)\.com/`i"),frameborder',
+            'spec' =>
+            'iframe=-*,height,width,type,style,src(match="`^(https?:)?//(www\.)?(youtube|dailymotion|vimeo)\.com/`i"),frameborder'.($options['spec'] ? '; '.$options['spec'] : ''),
         );
 
         return Format::html($html, $config);
@@ -327,10 +333,10 @@ class Format {
             'src="cid:$1', $text);
     }
 
-    function sanitize($text, $striptags=false) {
+    function sanitize($text, $striptags=false, $spec=false) {
 
         //balance and neutralize unsafe tags.
-        $text = Format::safe_html($text);
+        $text = Format::safe_html($text, array('spec' => $spec));
 
         $text = self::localizeInlineImages($text);
 
@@ -378,7 +384,7 @@ class Format {
     }
 
     //Format text for display..
-    function display($text, $inline_images=true) {
+    function display($text, $inline_images=true, $balance=true) {
         // Make showing offsite images optional
         $text = preg_replace_callback('/<img ([^>]*)(src="http[^"]+")([^>]*)\/>/',
             function($match) {
@@ -389,8 +395,10 @@ class Format {
             },
             $text);
 
-        //make urls clickable.
-        $text = self::html_balance($text, false);
+        if ($balance)
+            $text = self::html_balance($text, false);
+
+        // make urls clickable.
         $text = Format::clickableurls($text);
 
         if ($inline_images)
@@ -412,7 +420,7 @@ class Format {
         global $ost;
 
         // Find all text between tags
-        $text = preg_replace_callback(':^[^<]+|>[^<]+:',
+        return preg_replace_callback(':^[^<]+|>[^<]+:',
             function($match) {
                 // Scan for things that look like URLs
                 return preg_replace_callback(
@@ -439,33 +447,6 @@ class Format {
                     $match[0]);
             },
             $text);
-
-        // Now change @href and @src attributes to come back through our
-        // system as well
-        $config = array(
-            '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) {
-                    $a['target'] = $target;
-                    $a['class'] = 'no-pjax';
-                }
-
-                $at = '';
-                if (is_array($a)) {
-                    foreach ($a as $k=>$v)
-                        $at .= " $k=\"$v\"";
-                    return "<{$e}{$at}".(isset($eE[$e])?" /":"").">";
-                } else {
-                    return "</{$e}>";
-                }
-            },
-            'schemes' => 'href: aim, feed, file, ftp, gopher, http, https, irc, mailto, news, nntp, sftp, ssh, telnet; *:file, http, https; src: cid, http, https, data',
-            'elements' => '*+iframe',
-            'balance' => 0,
-            'spec' => 'span=data-src,width,height',
-        );
-        return Format::html($text, $config);
     }
 
     function stripEmptyLines($string) {
@@ -474,10 +455,10 @@ class Format {
 
 
     function viewableImages($html, $script=false) {
+        $cids = $images = array();
         return preg_replace_callback('/"cid:([\w._-]{32})"/',
-        function($match) use ($script) {
-            $hash = $match[1];
-            if (!($file = AttachmentFile::lookup($hash)))
+        function($match) use ($script, $images) {
+            if (!($file = AttachmentFile::lookup($match[1])))
                 return $match[0];
             return sprintf('"%s" data-cid="%s"',
                 $file->getDownloadUrl(false, 'inline', $script), $match[1]);
@@ -525,34 +506,177 @@ class Format {
         return $tstring;
     }
 
-    /* Dates helpers...most of this crap will change once we move to PHP 5*/
-    function db_date($time) {
+    function __formatDate($timestamp, $format, $fromDb, $dayType, $timeType,
+            $strftimeFallback, $timezone, $user=false) {
         global $cfg;
-        return Format::userdate($cfg->getDateFormat(), Misc::db2gmtime($time));
+        static $cache;
+
+        if (!$timestamp)
+            return '';
+
+        if ($fromDb)
+            $timestamp = Misc::db2gmtime($timestamp);
+
+        if (class_exists('IntlDateFormatter')) {
+            $locale = Internationalization::getCurrentLocale($user);
+            $key = "{$locale}:{$dayType}:{$timeType}:{$timezone}:{$format}";
+            if (!isset($cache[$key])) {
+                // Setting up the IntlDateFormatter is pretty expensive, so
+                // cache it since there aren't many variations of the
+                // arguments passed to the constructor
+                $cache[$key] = $formatter = new IntlDateFormatter(
+                    $locale,
+                    $dayType,
+                    $timeType,
+                    $timezone,
+                    IntlDateFormatter::GREGORIAN,
+                    $format ?: null
+                );
+                if ($cfg->isForce24HourTime()) {
+                    $format = str_replace(array('a', 'h'), array('', 'H'),
+                        $formatter->getPattern());
+                    $formatter->setPattern($format);
+                }
+            }
+            else {
+                $formatter = $cache[$key];
+            }
+            return $formatter->format($timestamp);
+        }
+        // Fallback using strftime
+        static $user_timezone;
+        if (!isset($user_timezone))
+            $user_timezone = new DateTimeZone($cfg->getTimezone() ?: date_default_timezone_get());
+
+        $format = self::getStrftimeFormat($format);
+        // Properly convert to user local time
+        if (!($time = DateTime::createFromFormat('U', $timestamp, new DateTimeZone('UTC'))))
+           return '';
+
+        $offset = $user_timezone->getOffset($time);
+        $timestamp = $time->getTimestamp() + $offset;
+        return strftime($format ?: $strftimeFallback, $timestamp);
+    }
+
+    function parseDate($date, $format=false) {
+        global $cfg;
+
+        if (class_exists('IntlDateFormatter')) {
+            $formatter = new IntlDateFormatter(
+                Internationalization::getCurrentLocale(),
+                null,
+                null,
+                null,
+                IntlDateFormatter::GREGORIAN,
+                $format ?: null
+            );
+            if ($cfg->isForce24HourTime()) {
+                $format = str_replace(array('a', 'h'), array('', 'H'),
+                    $formatter->getPattern());
+                $formatter->setPattern($format);
+            }
+            return $formatter->parse($date);
+        }
+        // Fallback using strtotime
+        return strtotime($date);
     }
 
-    function db_datetime($time) {
+    function time($timestamp, $fromDb=true, $format=false, $timezone=false, $user=false) {
         global $cfg;
-        return Format::userdate($cfg->getDateTimeFormat(), Misc::db2gmtime($time));
+
+        return self::__formatDate($timestamp,
+            $format ?: $cfg->getTimeFormat(), $fromDb,
+            IDF_NONE, IDF_SHORT,
+            '%X', $timezone ?: $cfg->getTimezone(), $user);
     }
 
-    function db_daydatetime($time) {
+    function date($timestamp, $fromDb=true, $format=false, $timezone=false, $user=false) {
         global $cfg;
-        return Format::userdate($cfg->getDayDateTimeFormat(), Misc::db2gmtime($time));
+
+        return self::__formatDate($timestamp,
+            $format ?: $cfg->getDateFormat(), $fromDb,
+            IDF_SHORT, IDF_NONE,
+            '%x', $timezone ?: $cfg->getTimezone(), $user);
     }
 
-    function userdate($format, $gmtime) {
-        return Format::date($format, $gmtime, $_SESSION['TZ_OFFSET'], $_SESSION['TZ_DST']);
+    function datetime($timestamp, $fromDb=true, $timezone=false, $user=false) {
+        global $cfg;
+
+        return self::__formatDate($timestamp,
+                $cfg->getDateTimeFormat(), $fromDb,
+                IDF_SHORT, IDF_SHORT,
+                '%x %X', $timezone ?: $cfg->getTimezone(), $user);
     }
 
-    function date($format, $gmtimestamp, $offset=0, $daylight=false){
+    function daydatetime($timestamp, $fromDb=true, $timezone=false, $user=false) {
+        global $cfg;
+
+        return self::__formatDate($timestamp,
+                $cfg->getDayDateTimeFormat(), $fromDb,
+                IDF_FULL, IDF_SHORT,
+                '%x %X', $timezone ?: $cfg->getTimezone(), $user);
+    }
+
+    function getStrftimeFormat($format) {
+        static $codes, $ids;
+
+        if (!isset($codes)) {
+            // This array is flipped because of duplicated formats on the
+            // intl side due to slight differences in the libraries
+            $codes = array(
+            '%d' => 'dd',
+            '%a' => 'EEE',
+            '%e' => 'd',
+            '%A' => 'EEEE',
+            '%w' => 'e',
+            '%w' => 'c',
+            '%z' => 'D',
+
+            '%V' => 'w',
+
+            '%B' => 'MMMM',
+            '%m' => 'MM',
+            '%b' => 'MMM',
+
+            '%g' => 'Y',
+            '%G' => 'Y',
+            '%Y' => 'y',
+            '%y' => 'yy',
+
+            '%P' => 'a',
+            '%l' => 'h',
+            '%k' => 'H',
+            '%I' => 'hh',
+            '%H' => 'HH',
+            '%M' => 'mm',
+            '%S' => 'ss',
+
+            '%z' => 'ZZZ',
+            '%Z' => 'z',
+            );
 
-        if(!$gmtimestamp || !is_numeric($gmtimestamp))
-            return "";
+            $flipped = array_flip($codes);
+            krsort($flipped);
 
-        $offset+=$daylight?date('I', $gmtimestamp):0; //Daylight savings crap.
+            // Also establish a list of ids, so we can do a creative replacement
+            // without clobbering the common letters in the formats
+            $keys = array_keys($flipped);
+            $ids = array_combine($keys, array_map('chr', array_flip($keys)));
 
-        return date($format, ($gmtimestamp+ ($offset*3600)));
+            // Now create an array from the id codes back to strftime codes
+            $codes = array_combine($ids, $flipped);
+        }
+        // $ids => array(intl => #id)
+        // $codes => array(#id => strftime)
+        $format = str_replace(array_keys($ids), $ids, $format);
+        $format = str_replace($ids, $codes, $format);
+
+        return preg_replace_callback('`[\x00-\x1f]`',
+            function($m) use ($ids) {
+                return $ids[ord($m[0])];
+            },
+            $format
+        );
     }
 
     // Thanks, http://stackoverflow.com/a/2955878/1025836
@@ -636,41 +760,11 @@ class Format {
             // Normalize text input :: remove diacritics and such
             $text = normalizer_normalize($text, Normalizer::FORM_C);
         }
-        else {
-            // As a lightweight compatiblity, use a lightweight C
-            // normalizer with diacritic removal, thanks
-            // http://ahinea.com/en/tech/accented-translate.html
-            $tr = array(
-                "ä" => "a", "ñ" => "n", "ö" => "o", "ü" => "u", "ÿ" => "y"
-            );
-            $text = strtr($text, $tr);
-        }
-        // Decompose compatible versions of characters (ä => ae)
-        $tr = array(
-            "ß" => "ss", "Æ" => "AE", "æ" => "ae", "IJ" => "IJ",
-            "ij" => "ij", "Œ" => "OE", "œ" => "oe", "Ð" => "D",
-            "Đ" => "D", "ð" => "d", "đ" => "d", "Ħ" => "H", "ħ" => "h",
-            "ı" => "i", "ĸ" => "k", "Ŀ" => "L", "Ł" => "L", "ŀ" => "l",
-            "ł" => "l", "Ŋ" => "N", "ʼn" => "n", "ŋ" => "n", "Ø" => "O",
-            "ø" => "o", "ſ" => "s", "Þ" => "T", "Ŧ" => "T", "þ" => "t",
-            "ŧ" => "t", "ä" => "ae", "ö" => "oe", "ü" => "ue",
-            "Ä" => "AE", "Ö" => "OE", "Ü" => "UE",
-        );
-        $text = strtr($text, $tr);
-
-        // Drop separated diacritics
-        $text = preg_replace('/\p{M}/u', '', $text);
-
-        // Drop extraneous whitespace
-        $text = preg_replace('/(\s)\s+/u', '$1', $text);
-
-        // Drop leading and trailing whitespace
-        $text = trim($text);
 
         if (false && class_exists('IntlBreakIterator')) {
             // Split by word boundaries
             if ($tokenizer = IntlBreakIterator::createWordInstance(
-                    $lang ?: ($cfg ? $cfg->getSystemLanguage() : 'en_US'))
+                    $lang ?: ($cfg ? $cfg->getPrimaryLanguage() : 'en_US'))
             ) {
                 $tokenizer->setText($text);
                 $tokens = array();
@@ -684,8 +778,200 @@ class Format {
             // http://www.unicode.org/reports/tr29/#Word_Boundaries
 
             // Punt for now
+
+            // Drop extraneous whitespace
+            $text = preg_replace('/(\s)\s+/u', '$1', $text);
+
+            // Drop leading and trailing whitespace
+            $text = trim($text);
         }
         return $text;
     }
+
+    function relativeTime($to, $from=false, $granularity=1) {
+        if (!$to)
+            return false;
+        $timestamp = $to;
+        if (gettype($timestamp) === 'string')
+            $timestamp = strtotime($timestamp);
+        $from = $from ?: Misc::gmtime();
+        if (gettype($timestamp) === 'string')
+            $from = strtotime($from);
+        $timeDiff = $from - $timestamp;
+        $absTimeDiff = abs($timeDiff);
+
+        // Roll back to the nearest multiple of $granularity
+        $absTimeDiff -= $absTimeDiff % $granularity;
+
+        // within 2 seconds
+        if ($absTimeDiff <= 2) {
+          return $timeDiff >= 0 ? __('just now') : __('now');
+        }
+
+        // within a minute
+        if ($absTimeDiff < 60) {
+          return sprintf($timeDiff >= 0 ? __('%d seconds ago') : __('in %d seconds'), $absTimeDiff);
+        }
+
+        // within 2 minutes
+        if ($absTimeDiff < 120) {
+          return sprintf($timeDiff >= 0 ? __('about a minute ago') : __('in about a minute'));
+        }
+
+        // within an hour
+        if ($absTimeDiff < 3600) {
+          return sprintf($timeDiff >= 0 ? __('%d minutes ago') : __('in %d minutes'), $absTimeDiff / 60);
+        }
+
+        // within 2 hours
+        if ($absTimeDiff < 7200) {
+          return ($timeDiff >= 0 ? __('about an hour ago') : __('in about an hour'));
+        }
+
+        // within 24 hours
+        if ($absTimeDiff < 86400) {
+          return sprintf($timeDiff >= 0 ? __('%d hours ago') : __('in %d hours'), $absTimeDiff / 3600);
+        }
+
+        // within 2 days
+        $days2 = 2 * 86400;
+        if ($absTimeDiff < $days2) {
+            // XXX: yesterday / tomorrow?
+          return $absTimeDiff >= 0 ? __('yesterday') : __('tomorrow');
+        }
+
+        // within 29 days
+        $days29 = 29 * 86400;
+        if ($absTimeDiff < $days29) {
+          return sprintf($timeDiff >= 0 ? __('%d days ago') : __('in %d days'), $absTimeDiff / 86400);
+        }
+
+        // within 60 days
+        $days60 = 60 * 86400;
+        if ($absTimeDiff < $days60) {
+          return ($timeDiff >= 0 ? __('about a month ago') : __('in about a month'));
+        }
+
+        $currTimeYears = date('Y', $from);
+        $timestampYears = date('Y', $timestamp);
+        $currTimeMonths = $currTimeYears * 12 + date('n', $from);
+        $timestampMonths = $timestampYears * 12 + date('n', $timestamp);
+
+        // within a year
+        $monthDiff = $currTimeMonths - $timestampMonths;
+        if ($monthDiff < 12 && $monthDiff > -12) {
+          return sprintf($monthDiff >= 0 ? __('%d months ago') : __('in %d months'), abs($monthDiff));
+        }
+
+        $yearDiff = $currTimeYears - $timestampYears;
+        if ($yearDiff < 2 && $yearDiff > -2) {
+          return $yearDiff >= 0 ? __('a year ago') : __('in a year');
+        }
+
+        return sprintf($yearDiff >= 0 ? __('%d years ago') : __('in %d years'), abs($yearDiff));
+    }
+}
+
+if (!class_exists('IntlDateFormatter')) {
+    define('IDF_NONE', 0);
+    define('IDF_SHORT', 1);
+    define('IDF_FULL', 2);
+}
+else {
+    define('IDF_NONE', IntlDateFormatter::NONE);
+    define('IDF_SHORT', IntlDateFormatter::SHORT);
+    define('IDF_FULL', IntlDateFormatter::FULL);
+}
+
+class FormattedLocalDate
+implements TemplateVariable {
+    var $date;
+    var $timezone;
+    var $fromdb;
+
+    function __construct($date, $timezone=false, $user=false, $fromdb=true) {
+        $this->date = $date;
+        $this->timezone = $timezone;
+        $this->user = $user;
+        $this->fromdb = $fromdb;
+    }
+
+    function asVar() {
+        return $this->getVar('long');
+    }
+
+    function __toString() {
+        return $this->asVar();
+    }
+
+    function getVar($what) {
+        // TODO: Rebase date format so that locale is discovered HERE.
+
+        switch ($what) {
+        case 'short':
+            return Format::date($this->date, $this->fromdb, false, $this->timezone, $this->user);
+        case 'long':
+            return Format::datetime($this->date, $this->fromdb, $this->timezone, $this->user);
+        case 'time':
+            return Format::time($this->date, $this->fromdb, false, $this->timezone, $this->user);
+        case 'full':
+            return Format::daydatetime($this->date, $this->fromdb, $this->timezone, $this->user);
+        }
+    }
+
+    static function getVarScope() {
+        return array(
+            'full' => 'Expanded date, e.g. day, month dd, yyyy',
+            'long' => 'Date and time, e.g. d/m/yyyy hh:mm',
+            'short' => 'Date only, e.g. d/m/yyyy',
+            'time' => 'Time only, e.g. hh:mm',
+        );
+    }
+}
+
+class FormattedDate
+extends FormattedLocalDate {
+    function asVar() {
+        return $this->getVar('system')->asVar();
+    }
+
+    function __toString() {
+        global $cfg;
+        return (string) new FormattedLocalDate($this->date, $cfg->getTimezone(), false, $this->fromdb);
+    }
+
+    function getVar($what, $context=null) {
+        global $cfg;
+
+        if ($rv = parent::getVar($what, $context))
+            return $rv;
+
+        switch ($what) {
+        case 'user':
+            // Fetch $recipient from the context and find that user's time zone
+            if ($context && ($recipient = $context->getObj('recipient'))) {
+                $tz = $recipient->getTimezone() ?: $cfg->getDefaultTimezone();
+                return new FormattedLocalDate($this->date, $tz, $recipient);
+            }
+            // Don't resolve the variable until correspondance is sent out
+            return false;
+        case 'system':
+            return new FormattedLocalDate($this->date, $cfg->getDefaultTimezone());
+        }
+    }
+
+    function getHumanize() {
+        return Format::relativeTime(Misc::db2gmtime($this->date));
+    }
+
+    static function getVarScope() {
+        return parent::getVarScope() + array(
+            'humanize' => 'Humanized time, e.g. about an hour ago',
+            'user' => array(
+                'class' => 'FormattedLocalDate', 'desc' => "Localize to recipient's time zone and locale"),
+            'system' => array(
+                'class' => 'FormattedLocalDate', 'desc' => 'Localize to system default time zone'),
+        );
+    }
 }
 ?>
diff --git a/include/class.forms.php b/include/class.forms.php
index 6d5e9cf63dbca1d9cbe6680732c597c7bb6f094c..15b32c3063d2fece589f19765ad208d6cda50f56 100644
--- a/include/class.forms.php
+++ b/include/class.forms.php
@@ -19,33 +19,56 @@
  * data for a ticket
  */
 class Form {
+    static $renderer = 'GridFluidLayout';
+    static $id = 0;
+
+    var $options = array();
     var $fields = array();
     var $title = '';
     var $instructions = '';
 
+    var $validators = array();
+
     var $_errors = null;
     var $_source = false;
 
-    function __construct($fields=array(), $source=null, $options=array()) {
-        $this->fields = $fields;
-        foreach ($fields as $k=>$f) {
-            $f->setForm($this);
-            if (!$f->get('name') && $k && !is_numeric($k))
-                $f->set('name', $k);
-        }
+    function __construct($source=null, $options=array()) {
+
+        $this->options = $options;
         if (isset($options['title']))
             $this->title = $options['title'];
         if (isset($options['instructions']))
             $this->instructions = $options['instructions'];
+        if (isset($options['id']))
+            $this->id = $options['id'];
+
         // Use POST data if source was not specified
         $this->_source = ($source) ? $source : $_POST;
     }
+
+    function getId() {
+        return static::$id;
+    }
+
     function data($source) {
         foreach ($this->fields as $name=>$f)
             if (isset($source[$name]))
                 $f->value = $source[$name];
     }
 
+    function setFields($fields) {
+
+        if (!is_array($fields) && !$fields instanceof Traversable)
+            return;
+
+        $this->fields = $fields;
+        foreach ($fields as $k=>$f) {
+            $f->setForm($this);
+            if (!$f->get('name') && $k && !is_numeric($k))
+                $f->set('name', $k);
+        }
+    }
+
     function getFields() {
         return $this->fields;
     }
@@ -59,6 +82,10 @@ class Form {
             return $fields[$name];
     }
 
+    function hasField($name) {
+        return $this->getField($name);
+    }
+
     function getTitle() { return $this->title; }
     function getInstructions() { return $this->instructions; }
     function getSource() { return $this->_source; }
@@ -74,7 +101,7 @@ class Form {
     function isValid($include=false) {
         if (!isset($this->_errors)) {
             $this->_errors = array();
-            $this->getClean();
+            $this->validate($this->getClean());
             foreach ($this->getFields() as $field)
                 if ($field->errors() && (!$include || $include($field)))
                     $this->_errors[$field->get('id')] = $field->errors();
@@ -82,12 +109,24 @@ class Form {
         return !$this->_errors;
     }
 
+    function validate($clean_data) {
+        // Validate the whole form so that errors can be added to the
+        // individual fields and collected below.
+        foreach ($this->validators as $V) {
+            $V($this);
+        }
+    }
+
     function getClean() {
         if (!$this->_clean) {
             $this->_clean = array();
             foreach ($this->getFields() as $key=>$field) {
-                if ($field->isPresentationOnly())
+                if (!$field->hasData())
                     continue;
+
+                // Prefer indexing by field.id if indexing numerically
+                if (is_int($key) && $field->get('id'))
+                    $key = $field->get('id');
                 $this->_clean[$key] = $this->_clean[$field->get('name')]
                     = $field->getClean();
             }
@@ -96,8 +135,31 @@ class Form {
         return $this->_clean;
     }
 
-    function errors() {
-        return $this->_errors;
+    function errors($formOnly=false) {
+        return ($formOnly) ? $this->_errors['form'] : $this->_errors;
+    }
+
+    function addError($message, $index=false) {
+
+        if ($index)
+            $this->_errors[$index] = $message;
+        else
+            $this->_errors['form'][] = $message;
+    }
+
+    function addErrors($errors=array()) {
+        foreach ($errors as $k => $v) {
+            if (($f=$this->getField($k)))
+                $f->addError($v);
+            else
+                $this->addError($v, $k);
+        }
+    }
+
+    function addValidator($function) {
+        if (!is_callable($function))
+            throw new Exception('Form validator must be callable');
+        $this->validators[] = $function;
     }
 
     function render($staff=true, $title=false, $options=array()) {
@@ -106,10 +168,22 @@ class Form {
         if (isset($options['instructions']))
             $this->instructions = $options['instructions'];
         $form = $this;
+        $template = $options['template'] ?: 'dynamic-form.tmpl.php';
         if ($staff)
-            include(STAFFINC_DIR . 'templates/dynamic-form.tmpl.php');
+            include(STAFFINC_DIR . 'templates/' . $template);
         else
-            include(CLIENTINC_DIR . 'templates/dynamic-form.tmpl.php');
+            include(CLIENTINC_DIR . 'templates/' . $template);
+        echo $this->getMedia();
+    }
+
+    function getLayout($title=false, $options=array()) {
+        $rc = @$options['renderer'] ?: static::$renderer;
+        return new $rc($title, $options);
+    }
+
+    function asTable($title=false, $options=array()) {
+        return $this->getLayout($title, $options)->asTable($this);
+        // XXX: Media can't go in a table
         echo $this->getMedia();
     }
 
@@ -133,6 +207,42 @@ class Form {
         }
     }
 
+    function emitJavascript($options=array()) {
+
+        // Check if we need to emit javascript
+        if (!($fid=$this->getId()))
+            return;
+        ?>
+        <script type="text/javascript">
+          $(function() {
+            <?php
+            //XXX: We ONLY want to watch field on this form. We'll only
+            // watch form inputs if form_id is specified. Current FORM API
+            // doesn't generate the entire form  (just fields)
+            if ($fid) {
+                ?>
+                $(document).off('change.<?php echo $fid; ?>');
+                $(document).on('change.<?php echo $fid; ?>',
+                    'form#<?php echo $fid; ?> :input',
+                    function() {
+                        //Clear any current errors...
+                        var errors = $('#field'+$(this).attr('id')+'_error');
+                        if (errors.length)
+                            errors.slideUp('fast', function (){
+                                $(this).remove();
+                                });
+                        //TODO: Validation input inplace or via ajax call
+                        // and set any new errors AND visibilty changes
+                    }
+                   );
+            <?php
+            }
+            ?>
+            });
+        </script>
+        <?php
+    }
+
     static function emitMedia($url, $type) {
         if ($url[0] == '/')
             $url = ROOT_PATH . substr($url, 1);
@@ -146,6 +256,250 @@ class Form {
             break;
         }
     }
+
+    /**
+     * getState
+     *
+     * Retrieves an array of information which can be passed to the
+     * ::loadState method later to recreate the current state of the form
+     * fields and values.
+     */
+    function getState() {
+        $info = array();
+        foreach ($this->getFields() as $f) {
+            // Skip invisible fields
+            if (!$f->isVisible())
+                continue;
+
+            // Skip fields set to default values
+            $v = $f->getClean();
+            $d = $f->get('default');
+            if ($v == $d)
+                continue;
+
+            // Skip empty values
+            if (!$v)
+                continue;
+
+            $info[$f->get('name') ?: $f->get('id')] = $f->to_database($v);
+        }
+        return $info;
+    }
+
+    /**
+     * loadState
+     *
+     * Reset this form to the state previously recorded by the ::getState()
+     * method
+     */
+    function loadState($state) {
+        foreach ($this->getFields() as $f) {
+            $name = $f->get('name');
+            $f->reset();
+            if (isset($state[$name])) {
+                $f->value = $f->to_php($state[$name]);
+            }
+        }
+    }
+
+    /*
+     * Initialize a generic static form
+     */
+    static function instantiate() {
+        $r = new ReflectionClass(get_called_class());
+        return $r->newInstanceArgs(func_get_args());
+    }
+}
+
+/**
+ * SimpleForm
+ * Wrapper for inline/static forms.
+ *
+ */
+class SimpleForm extends Form {
+    function __construct($fields=array(), $source=null, $options=array()) {
+        parent::__construct($source, $options);
+        $this->setFields($fields);
+    }
+}
+
+class CustomForm extends SimpleForm {
+
+    function getFields() {
+        global $thisstaff, $thisclient;
+
+        $options = $this->options;
+        $user = $options['user'] ?: $thisstaff ?: $thisclient;
+        $isedit = ($options['mode'] == 'edit');
+        $fields = array();
+        foreach (parent::getFields() as $field) {
+            if ($isedit && !$field->isEditable($user))
+                continue;
+
+            $fields[] = $field;
+        }
+
+        return $fields;
+    }
+}
+
+abstract class AbstractForm extends Form {
+    function __construct($source=null, $options=array()) {
+        parent::__construct($source, $options);
+        $this->setFields($this->buildFields());
+    }
+    /**
+     * Fetch the fields defined for this form. This method is only called
+     * once.
+     */
+    abstract function buildFields();
+}
+
+/**
+ * Container class to represent the connection between the form fields and the
+ * rendered state of the form.
+ */
+interface FormRenderer {
+    // Render the form fields into a table
+    function asTable($form);
+    // Render the form fields into divs
+    function asBlock($form);
+}
+
+abstract class FormLayout {
+    static $default_cell_layout = 'Cell';
+
+    var $title;
+    var $options;
+
+    function __construct($title=false, $options=array()) {
+        $this->title = $title;
+        $this->options = $options;
+    }
+
+    function getLayout($field) {
+        $layout = $field->get('layout') ?: static::$default_cell_layout;
+        if (is_string($layout))
+            $layout = new $layout();
+        return $layout;
+    }
+}
+
+class GridFluidLayout
+extends FormLayout
+implements FormRenderer {
+    function asTable($form) {
+      ob_start();
+?>
+      <table class="<?php echo 'grid form' ?>">
+          <caption><?php echo Format::htmlchars($this->title ?: $form->getTitle()); ?>
+                  <div><small><?php echo Format::viewableImages($form->getInstructions()); ?></small></div>
+          </caption>
+          <tbody><tr><?php for ($i=0; $i<12; $i++) echo '<td style="width:8.3333%"/>'; ?></tr></tbody>
+<?php
+      $row_size = 12;
+      $cols = $row = 0;
+
+      //Layout and rendering options
+      $options = $this->options;
+
+      foreach ($form->getFields() as $f) {
+          $layout = $this->getLayout($f);
+          $size = $layout->getWidth() ?: 12;
+          if ($offs = $layout->getOffset()) {
+              $size += $offs;
+          }
+          if ($cols < $size || $layout->isBreakForced()) {
+              if ($row) echo '</tr>';
+              echo '<tr>';
+              $cols = $row_size;
+              $row++;
+          }
+          // Render the cell
+          $cols -= $size;
+          $attrs = array('colspan' => $size, 'rowspan' => $layout->getHeight(),
+              'style' => '"'.$layout->getOption('style').'"');
+          if ($offs) { ?>
+              <td colspan="<?php echo $offs; ?>"></td> <?php
+          }
+          ?>
+          <td class="cell" <?php echo Format::array_implode('=', ' ', array_filter($attrs)); ?>
+              data-field-id="<?php echo $f->get('id'); ?>">
+              <fieldset class="field <?php if (!$f->isVisible()) echo 'hidden'; ?>"
+                id="field<?php echo $f->getWidget()->id; ?>"
+                data-field-id="<?php echo $f->get('id'); ?>">
+<?php         if ($label = $f->get('label')) { ?>
+              <label class="<?php if ($f->isRequired()) echo 'required'; ?>"
+                  for="<?php echo $f->getWidget()->id; ?>">
+                  <?php echo Format::htmlchars($label); ?>:
+                <?php if ($f->isRequired()) { ?>
+                <span class="error">*</span>
+                <?php
+                }?>
+              </label>
+<?php         }
+              if ($f->get('hint')) { ?>
+                  <div class="field-hint-text">
+                      <?php echo Format::htmlchars($f->get('hint')); ?>
+                  </div>
+<?php         }
+              $f->render($options);
+              if ($f->errors())
+                  foreach ($f->errors() as $e)
+                      echo sprintf('<div class="error">%s</div>', Format::htmlchars($e));
+?>
+              </fieldset>
+          </td>
+      <?php
+      }
+      if ($row)
+        echo  '</tr>';
+
+      echo '</tbody></table>';
+
+      return ob_get_clean();
+    }
+
+    function asBlock($form) {}
+}
+
+/**
+ * Basic container for field and form layouts. By default every cell takes
+ * a whole output row and does not imply any sort of width.
+ */
+class Cell {
+    function isBreakForced()  { return true; }
+    function getWidth()       { return false; }
+    function getHeight()      { return 1; }
+    function getOffset()      { return 0; }
+    function getOption($prop) { return false; }
+}
+
+/**
+ * Fluid grid layout, meaning each cell renders to the right of the previous
+ * cell (for left-to-right layouts). A width in columns can be specified for
+ * each cell along with an offset from the previous cell. A height of columns
+ * along with an optional break is supported.
+ */
+class GridFluidCell
+extends Cell {
+    var $span;
+    var $options;
+
+    function __construct($span, $options=array()) {
+        $this->span = $span;
+        $this->options = $options + array(
+            'rows' => 1,        # rowspan
+            'offset' => 0,      # skip some columns
+            'break' => false,   # start on a new row
+        );
+    }
+
+    function isBreakForced()  { return $this->options['break']; }
+    function getWidth()       { return $this->span; }
+    function getHeight()      { return $this->options['rows']; }
+    function getOffset()      { return $this->options['offset']; }
+    function getOption($prop) { return $this->options[$prop]; }
 }
 
 require_once(INCLUDE_DIR . "class.json.php");
@@ -154,7 +508,7 @@ class FormField {
     static $widget = false;
 
     var $ht = array(
-        'label' => 'Unlabeled',
+        'label' => false,
         'required' => false,
         'default' => false,
         'configuration' => array(),
@@ -184,17 +538,21 @@ class FormField {
         ),
     );
     static $more_types = array();
-    static $uid = 100;
+    static $uid = null;
+
+    function _uid() {
+        return ++self::$uid;
+    }
 
     function __construct($options=array()) {
         $this->ht = array_merge($this->ht, $options);
         if (!isset($this->ht['id']))
-            $this->ht['id'] = self::$uid++;
+            $this->ht['id'] = self::_uid();
     }
 
     function __clone() {
         $this->_widget = null;
-        $this->ht['id'] = self::$uid++;
+        $this->ht['id'] = self::_uid();
     }
 
     static function addFieldTypes($group, $callable) {
@@ -219,13 +577,19 @@ class FormField {
                 return $types[$type];
     }
 
-    function get($what) {
-        return $this->ht[$what];
+    function get($what, $default=null) {
+        return array_key_exists($what, $this->ht)
+            ? $this->ht[$what]
+            : $default;
     }
     function set($field, $value) {
         $this->ht[$field] = $value;
     }
 
+    function getId() {
+        return $this->ht['id'];
+    }
+
     /**
      * getClean
      *
@@ -237,6 +601,8 @@ class FormField {
     function getClean() {
         if (!isset($this->_clean)) {
             $this->_clean = (isset($this->value))
+                // XXX: The widget value may be parsed already if this is
+                //      linked to dynamic data via ::getAnswer()
                 ? $this->value : $this->parse($this->getWidget()->value);
 
             if ($vs = $this->get('cleaners')) {
@@ -251,23 +617,34 @@ class FormField {
                             $vs, array($this, $this->_clean));
             }
 
+            if (!isset($this->_clean) && ($d = $this->get('default')))
+                $this->_clean = $d;
+
             if ($this->isVisible())
                 $this->validateEntry($this->_clean);
         }
         return $this->_clean;
     }
     function reset() {
-        $this->_clean = $this->_widget = null;
+        $this->value = $this->_clean = $this->_widget = null;
+    }
+
+    function getValue() {
+        return $this->getWidget()->getValue();
     }
 
     function errors() {
         return $this->_errors;
     }
-    function addError($message, $field=false) {
-        if ($field)
-            $this->_errors[$field] = $message;
+    function addError($message, $index=false) {
+        if ($index)
+            $this->_errors[$index] = $message;
         else
             $this->_errors[] = $message;
+
+        // Update parent form errors for the field
+        if ($this->_form)
+            $this->_form->addError($this->errors(), $this->get('id'));
     }
 
     function isValidEntry() {
@@ -292,8 +669,9 @@ class FormField {
         # Validates a user-input into an instance of this field on a dynamic
         # form
         if ($this->get('required') && !$value && $this->hasData())
-            $this->_errors[] = sprintf(__('%s is a required field'),
-                $this->getLabel());
+            $this->_errors[] = $this->getLabel()
+                ? sprintf(__('%s is a required field'), $this->getLabel())
+                : __('This is a required field');
 
         # Perform declared validators for the field
         if ($vs = $this->get('validators')) {
@@ -315,7 +693,6 @@ class FormField {
      * field is visible and should be considered for validation
      */
     function isVisible() {
-        $config = $this->getConfiguration();
         if ($this->get('visibility') instanceof VisibilityConstraint) {
             return $this->get('visibility')->isVisible($this);
         }
@@ -323,14 +700,41 @@ class FormField {
     }
 
     /**
-     * FIXME: Temp
+     * Check if the user has edit rights
+     *
+     */
+
+    function isEditable($user=null) {
+
+        // Internal editable flag used by internal forms e.g internal lists
+        if (!$user && isset($this->ht['editable']))
+            return $this->ht['editable'];
+
+        if ($user instanceof Staff)
+            $flag = DynamicFormField::FLAG_AGENT_EDIT;
+        else
+            $flag = DynamicFormField::FLAG_CLIENT_EDIT;
+
+        return (($this->get('flags') & $flag) != 0);
+    }
+
+
+    /**
+     * isStorable
+     *
+     * Indicate if this field data is storable locally (default).Some field's data
+     * might beed to be stored elsewhere for optimization reasons at the
+     * application level.
      *
      */
 
-    function isEditable() {
-        return (($this->get('edit_mask') & 32) == 0);
+    function isStorable() {
+        return (($this->get('flags') & DynamicFormField::FLAG_EXT_STORED) == 0);
     }
 
+    function isRequired() {
+        return $this->get('required');
+    }
 
     /**
      * parse
@@ -361,6 +765,20 @@ class FormField {
         return $value;
     }
 
+    /**
+     * to_config
+     *
+     * Transform the data from the value to config form (as determined by
+     * field). to_php is used for each field returned from
+     * ::getConfigurationOptions(), and when the whole configuration is
+     * built, to_config() is called and receives the config array. The array
+     * should be returned, perhaps with modifications, and will be JSON
+     * encoded and stashed in the database.
+     */
+    function to_config($value) {
+        return $value;
+    }
+
     /**
      * to_database
      *
@@ -403,7 +821,7 @@ class FormField {
      * cleaned as well. This hook allows fields to participate when the data
      * for a field is cleaned up.
      */
-    function db_cleanup() {
+    function db_cleanup($field=false) {
     }
 
     /**
@@ -422,6 +840,37 @@ class FormField {
         return $this->toString($value);
     }
 
+    /**
+     * Fetch a value suitable for embedding the value of this field in an
+     * email template. Reference implementation uses ::to_php();
+     */
+    function asVar($value, $id=false) {
+        return $this->to_php($value, $id);
+    }
+
+    /**
+     * Fetch the var type used with the email templating system's typeahead
+     * feature. This helps with variable expansion if supported by this
+     * field's ::asVar() method. This method should return a valid classname
+     * which implements the `TemplateVariable` interface.
+     */
+    function asVarType() {
+        return false;
+    }
+
+    /**
+     * Describe the difference between the to two values. Note that the
+     * values should be passed through ::parse() or to_php() before
+     * utilizing this method.
+     */
+    function whatChanged($before, $after) {
+        if ($before)
+            $desc = __('changed from <strong>%2$s</strong> to <strong>%1$s</strong>');
+        else
+            $desc = __('set to <strong>%1$s</strong>');
+        return sprintf($desc, $this->display($after), $this->display($before));
+    }
+
     /**
      * Convert the field data to something matchable by filtering. The
      * primary use of this is for ticket filtering.
@@ -430,10 +879,124 @@ class FormField {
         return $this->toString($this->getClean());
     }
 
+    /**
+     * Fetches a value that represents this content in a consistent,
+     * searchable format. This is used by the search engine system and
+     * backend.
+     */
     function searchable($value) {
         return Format::searchable($this->toString($value));
     }
 
+    function getKeys($value) {
+        return $this->to_database($value);
+    }
+
+    /**
+     * Fetches a list of options for searching. The values returned from
+     * this method are passed to the widget's `::render()` method so that
+     * the widget can be affected by this setting. For instance, date fields
+     * might have a 'between' search option which should trigger rendering
+     * of two date widgets for search results.
+     */
+    function getSearchMethods() {
+        return array(
+            'set' =>        __('has a value'),
+            'nset' =>       __('does not have a value'),
+            'equal' =>      __('is'),
+            'nequal' =>     __('is not'),
+            'contains' =>   __('contains'),
+            'match' =>      __('matches'),
+        );
+    }
+
+    function getSearchMethodWidgets() {
+        return array(
+            'set' => null,
+            'nset' => null,
+            'equal' => array('TextboxField', array()),
+            'nequal' => array('TextboxField', array()),
+            'contains' => array('TextboxField', array()),
+            'match' => array('TextboxField', array(
+                'placeholder' => __('Valid regular expression'),
+                'configuration' => array('size'=>30),
+                'validators' => function($self, $v) {
+                    if (false === @preg_match($v, ' ')
+                        && false === @preg_match("/$v/", ' '))
+                        $self->addError(__('Cannot compile this regular expression'));
+                })),
+        );
+    }
+
+    /**
+     * This is used by the searching system to build a query for the search
+     * engine. The function should return a criteria listing to match
+     * content saved by the field by the `::to_database()` function.
+     */
+    function getSearchQ($method, $value, $name=false) {
+        $criteria = array();
+        $Q = new Q();
+        $name = $name ?: $this->get('name');
+        switch ($method) {
+            case 'nset':
+                $Q->negate();
+            case 'set':
+                $criteria[$name . '__isnull'] = false;
+                break;
+
+            case 'nequal':
+                $Q->negate();
+            case 'equal':
+                $criteria[$name] = $value;
+                break;
+
+            case 'contains':
+                $criteria[$name . '__contains'] = $value;
+                break;
+
+            case 'match':
+                $criteria[$name . '__regex'] = $value;
+                break;
+        }
+        return $Q->add($criteria);
+    }
+
+    function getSearchWidget($method) {
+        $methods = $this->getSearchMethodWidgets();
+        $info = $methods[$method];
+        if (is_array($info)) {
+            $class = $info[0];
+            return new $class($info[1]);
+        }
+        return $info;
+    }
+
+    function describeSearchMethod($method) {
+        switch ($method) {
+        case 'set':
+            return __('%s has a value');
+        case 'nset':
+            return __('%s does not have a value');
+        case 'equal':
+            return __('%s is %s' /* describes an equality */);
+        case 'nequal':
+            return __('%s is not %s' /* describes an inequality */);
+        case 'contains':
+            return __('%s contains "%s"');
+        case 'match':
+            return __('%s matches pattern %s');
+        case 'includes':
+            return __('%s in (%s)');
+        case '!includes':
+            return __('%s not in (%s)');
+        }
+    }
+    function describeSearch($method, $value, $name=false) {
+        $desc = $this->describeSearchMethod($method);
+        $value = $this->toString($value);
+        return sprintf($desc, $name, $value);
+    }
+
     function getLabel() { return $this->get('label'); }
 
     /**
@@ -473,6 +1036,11 @@ class FormField {
     function getAnswer() { return $this->answer; }
     function setAnswer($ans) { $this->answer = $ans; }
 
+    function setValue($value) {
+        $this->reset();
+        $this->getWidget()->value = $value;
+    }
+
     function getFormName() {
         if (is_numeric($this->get('id')))
             return substr(md5(
@@ -501,15 +1069,15 @@ class FormField {
             return array();
     }
 
-    function render($mode=null) {
-        $rv = $this->getWidget()->render($mode);
+    function render($options=array()) {
+        $rv = $this->getWidget()->render($options);
         if ($v = $this->get('visibility')) {
             $v->emitJavascript($this);
         }
         return $rv;
     }
 
-    function renderExtras($mode=null) {
+    function renderExtras($options=array()) {
         return;
     }
 
@@ -619,9 +1187,9 @@ class FormField {
         if (!$this->_cform) {
             $type = static::getFieldType($this->get('type'));
             $clazz = $type[1];
-            $T = new $clazz(array('type'=>$this->get('type')));
+            $T = new $clazz($this->ht);
             $config = $this->getConfiguration();
-            $this->_cform = new Form($T->getConfigurationOptions(), $source);
+            $this->_cform = new SimpleForm($T->getConfigurationOptions(), $source);
             if (!$source) {
                 foreach ($this->_cform->getFields() as $name=>$f) {
                     if ($config && isset($config[$name]))
@@ -657,6 +1225,16 @@ class FormField {
 
         return $name;
     }
+
+    function getTranslateTag($subtag) {
+        return _H(sprintf('field.%s.%s%s', $subtag, $this->get('id'),
+            $this->get('form_id') ? '' : '*internal*'));
+    }
+    function getLocal($subtag, $default=false) {
+        $tag = $this->getTranslateTag($subtag);
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : ($default ?: $this->get($subtag));
+    }
 }
 
 class TextboxField extends FormField {
@@ -699,12 +1277,16 @@ class TextboxField extends FormField {
                 })),
             'validator-error' => new TextboxField(array(
                 'id'=>4, 'label'=>__('Validation Error'), 'default'=>'',
-                'configuration'=>array('size'=>40, 'length'=>60),
+                'configuration'=>array('size'=>40, 'length'=>60,
+                    'translatable'=>$this->getTranslateTag('validator-error')
+                ),
                 'hint'=>__('Message shown to user if the input does not match the validator'))),
             'placeholder' => new TextboxField(array(
                 'id'=>5, 'label'=>__('Placeholder'), 'required'=>false, 'default'=>'',
                 'hint'=>__('Text shown in before any input from the user'),
-                'configuration'=>array('size'=>40, 'length'=>40),
+                'configuration'=>array('size'=>40, 'length'=>40,
+                    'translatable'=>$this->getTranslateTag('placeholder')
+                ),
             )),
         );
     }
@@ -742,7 +1324,7 @@ class TextboxField extends FormField {
         $func = $validators[$valid];
         $error = $func[1];
         if ($config['validator-error'])
-            $error = $config['validator-error'];
+            $error = $this->getLocal('validator-error', $config['validator-error']);
         if (is_array($func) && is_callable($func[0]))
             if (!call_user_func($func[0], $value))
                 $this->_errors[] = $error;
@@ -790,7 +1372,8 @@ class TextareaField extends FormField {
             'placeholder' => new TextboxField(array(
                 'id'=>5, 'label'=>__('Placeholder'), 'required'=>false, 'default'=>'',
                 'hint'=>__('Text shown in before any input from the user'),
-                'configuration'=>array('size'=>40, 'length'=>40),
+                'configuration'=>array('size'=>40, 'length'=>40,
+                    'translatable'=>$this->getTranslateTag('placeholder')),
             )),
         );
     }
@@ -808,9 +1391,8 @@ class TextareaField extends FormField {
     }
 
     function searchable($value) {
-        $value = preg_replace(array('`<br(\s*)?/?>`i', '`</div>`i'), "\n", $value);
-        $value = Format::htmldecode(Format::striptags($value));
-        return Format::searchable($value);
+        $body = new HtmlThreadEntryBody($value);
+        return $body->getSearchable();
     }
 
     function export($value) {
@@ -920,10 +1502,37 @@ class BooleanField extends FormField {
     function toString($value) {
         return ($value) ? __('Yes') : __('No');
     }
+
+    function getSearchMethods() {
+        return array(
+            'set' =>        __('checked'),
+            'nset' =>    __('unchecked'),
+        );
+    }
+
+    function getSearchMethodWidgets() {
+        return array(
+            'set' => null,
+            'nset' => null,
+        );
+    }
+
+    function getSearchQ($method, $value, $name=false) {
+        $name = $name ?: $this->get('name');
+        switch ($method) {
+        case 'set':
+            return new Q(array($name => '1'));
+        case 'nset':
+            return new Q(array($name => '0'));
+        default:
+            return parent::getSearchQ($method, $value, $name);
+        }
+    }
 }
 
 class ChoiceField extends FormField {
     static $widget = 'ChoicesWidget';
+    var $_choices;
 
     function getConfigurationOptions() {
         return array(
@@ -940,7 +1549,9 @@ class ChoiceField extends FormField {
             'prompt' => new TextboxField(array(
                 'id'=>2, 'label'=>__('Prompt'), 'required'=>false, 'default'=>'',
                 'hint'=>__('Leading text shown before a value is selected'),
-                'configuration'=>array('size'=>40, 'length'=>40),
+                'configuration'=>array('size'=>40, 'length'=>40,
+                    'translatable'=>$this->getTranslateTag('prompt'),
+                ),
             )),
             'multiselect' => new BooleanField(array(
                 'id'=>1, 'label'=>'Multiselect', 'required'=>false, 'default'=>false,
@@ -997,6 +1608,57 @@ class ChoiceField extends FormField {
         return (string) $value;
     }
 
+    function getKeys($value) {
+        if (!is_array($value))
+            $value = $this->getChoice($value);
+        if (is_array($value))
+            return implode(', ', array_keys($value));
+        return (string) $value;
+    }
+
+    function whatChanged($before, $after) {
+        $B = (array) $before;
+        $A = (array) $after;
+        $added = array_diff($A, $B);
+        $deleted = array_diff($B, $A);
+        $added = array_map(array($this, 'display'), $added);
+        $deleted = array_map(array($this, 'display'), $deleted);
+
+        if ($added && $deleted) {
+            $desc = sprintf(
+                __('added <strong>%1$s</strong> and removed <strong>%2$s</strong>'),
+                implode(', ', $added), implode(', ', $deleted));
+        }
+        elseif ($added) {
+            $desc = sprintf(
+                __('added <strong>%1$s</strong>'),
+                implode(', ', $added));
+        }
+        elseif ($deleted) {
+            $desc = sprintf(
+                __('removed <strong>%1$s</strong>'),
+                implode(', ', $deleted));
+        }
+        else {
+            $desc = sprintf(
+                __('changed from <strong>%1$s</strong> to <strong>%2$s</strong>'),
+                $this->display($before), $this->display($after));
+        }
+        return $desc;
+    }
+
+    /*
+     Return criteria to which the choice should be filtered by
+     */
+    function getCriteria() {
+        $config = $this->getConfiguration();
+        $criteria = array();
+        if (isset($config['criteria']))
+            $criteria = $config['criteria'];
+
+        return $criteria;
+    }
+
     function getChoice($value) {
 
         $choices = $this->getChoices();
@@ -1041,50 +1703,94 @@ class ChoiceField extends FormField {
             }
         }
         return $this->_choices;
-     }
-}
+    }
 
-class DatetimeField extends FormField {
-    static $widget = 'DatetimePickerWidget';
+    function lookupChoice($value) {
+        return null;
+    }
 
-    function to_database($value) {
-        // Store time in gmt time, unix epoch format
-        return (string) $value;
+    function getSearchMethods() {
+        return array(
+            'set' =>        __('has a value'),
+            'nset' =>     __('does not have a value'),
+            'includes' =>   __('includes'),
+            '!includes' =>  __('does not include'),
+        );
     }
 
-    function to_php($value) {
+    function getSearchMethodWidgets() {
+        return array(
+            'set' => null,
+            'nset' => null,
+            'includes' => array('ChoiceField', array(
+                'choices' => $this->getChoices(),
+                'configuration' => array('multiselect' => true),
+            )),
+            '!includes' => array('ChoiceField', array(
+                'choices' => $this->getChoices(),
+                'configuration' => array('multiselect' => true),
+            )),
+        );
+    }
+
+    function getSearchQ($method, $value, $name=false) {
+        $name = $name ?: $this->get('name');
+        switch ($method) {
+        case '!includes':
+            return Q::not(array("{$name}__in" => array_keys($value)));
+        case 'includes':
+            return new Q(array("{$name}__in" => array_keys($value)));
+        default:
+            return parent::getSearchQ($method, $value, $name);
+        }
+    }
+
+    function describeSearchMethod($method) {
+        switch ($method) {
+        case 'includes':
+            return __('%s includes %s' /* includes -> if a list includes a selection */);
+        case 'includes':
+            return __('%s does not include %s' /* includes -> if a list includes a selection */);
+        default:
+            return parent::describeSearchMethod($method);
+        }
+    }
+}
+
+class DatetimeField extends FormField {
+    static $widget = 'DatetimePickerWidget';
+
+    function to_database($value) {
+        // Store time in gmt time, unix epoch format
+        return date('Y-m-d H:i:s', $value);
+    }
+
+    function to_php($value) {
         if (!$value)
             return $value;
         else
-            return (int) $value;
+            return (int) strtotime($value);
     }
 
-    function parse($value) {
+    function asVar($value, $id=false) {
         if (!$value) return null;
-        $config = $this->getConfiguration();
-        return ($config['gmt']) ? Misc::db2gmtime($value) : $value;
+        return new FormattedDate((int) $value, 'UTC', false, false);
+    }
+    function asVarType() {
+        return 'FormattedDate';
     }
 
     function toString($value) {
         global $cfg;
         $config = $this->getConfiguration();
-        $format = ($config['time'])
-            ? $cfg->getDateTimeFormat() : $cfg->getDateFormat();
-        if ($config['gmt'])
-            // Return time local to user's timezone
-            return Format::userdate($format, $value);
-        else
-            return Format::date($format, $value);
-    }
-
-    function export($value) {
-        $config = $this->getConfiguration();
+        // If GMT is set, convert to local time zone. Otherwise, leave
+        // unchanged (default TZ is UTC)
         if (!$value)
             return '';
-        elseif ($config['gmt'])
-            return Format::userdate('Y-m-d H:i:s', $value);
+        if ($config['time'])
+            return Format::datetime($value, false, !$config['gmt'] ? 'UTC' : false);
         else
-            return Format::date('Y-m-d H:i:s', $value);
+            return Format::date($value, false, false, !$config['gmt'] ? 'UTC' : false);
     }
 
     function getConfigurationOptions() {
@@ -1123,6 +1829,151 @@ class DatetimeField extends FormField {
         elseif ($value === -1 or $value === false)
             $this->_errors[] = __('Enter a valid date');
     }
+
+    // SearchableField interface ------------------------------
+    function getSearchMethods() {
+        return array(
+            'set' =>        __('has a value'),
+            'nset' =>       __('does not have a value'),
+            'equal' =>      __('on'),
+            'nequal' =>     __('not on'),
+            'before' =>     __('before'),
+            'after' =>      __('after'),
+            'between' =>    __('between'),
+            'ndaysago' =>   __('in the last n days'),
+            'ndays' =>      __('in the next n days'),
+        );
+    }
+
+    function getSearchMethodWidgets() {
+        $config_notime = $config = $this->getConfiguration();
+        $config_notime['time'] = false;
+        return array(
+            'set' => null,
+            'nset' => null,
+            'equal' => array('DatetimeField', array(
+                'configuration' => $config_notime,
+            )),
+            'nequal' => array('DatetimeField', array(
+                'configuration' => $config_notime,
+            )),
+            'before' => array('DatetimeField', array(
+                'configuration' => $config,
+            )),
+            'after' => array('DatetimeField', array(
+                'configuration' => $config,
+            )),
+            'between' => array('InlineformField', array(
+                'form' => array(
+                    'left' => new DatetimeField(),
+                    'text' => new FreeTextField(array(
+                        'configuration' => array('content' => 'and'))
+                    ),
+                    'right' => new DatetimeField(),
+                ),
+            )),
+            'ndaysago' => array('InlineformField', array(
+                'form' => array(
+                    'until' => new TextboxField(array(
+                        'configuration' => array('validator'=>'number', 'size'=>4))
+                    ),
+                    'text' => new FreeTextField(array(
+                        'configuration' => array('content' => 'days'))
+                    ),
+                ),
+            )),
+            'ndays' => array('InlineformField', array(
+                'form' => array(
+                    'until' => new TextboxField(array(
+                        'configuration' => array('validator'=>'number', 'size'=>4))
+                    ),
+                    'text' => new FreeTextField(array(
+                        'configuration' => array('content' => 'days'))
+                    ),
+                ),
+            )),
+        );
+    }
+
+    function getSearchQ($method, $value, $name=false) {
+        $name = $name ?: $this->get('name');
+        $config = $this->getConfiguration();
+        $value = is_int($value)
+            ? DateTime::createFromFormat('U', !$config['gmt'] ? Misc::gmtime($value) : $value) ?: $value
+            : $value;
+        switch ($method) {
+        case 'equal':
+            $l = clone $value;
+            $r = $value->add(new DateInterval('P1D'));
+            return new Q(array(
+                "{$name}__gte" => $l,
+                "{$name}__lt" => $r
+            ));
+        case 'nequal':
+            $l = clone $value;
+            $r = $value->add(new DateInterval('P1D'));
+            return Q::any(array(
+                "{$name}__lt" => $l,
+                "{$name}__gte" => $r,
+            ));
+        case 'after':
+            return new Q(array("{$name}__gte" => $value));
+        case 'before':
+            return new Q(array("{$name}__lt" => $value));
+        case 'between':
+            foreach (array('left', 'right') as $side) {
+                $value[$side] = is_int($value[$side])
+                    ? DateTime::createFromFormat('U', !$config['gmt']
+                        ? Misc::gmtime($value[$side]) : $value[$side]) ?: $value[$side]
+                    : $value[$side];
+            }
+            return new Q(array(
+                "{$name}__gte" => $value['left'],
+                "{$name}__lte" => $value['right'],
+            ));
+        case 'ndaysago':
+            $now = Misc::gmtime();
+            return new Q(array(
+                "{$name}__lt" => $now,
+                "{$name}__gte" => SqlExpression::minus($now, SqlInterval::DAY($value['until'])),
+            ));
+        case 'ndays':
+            $now = Misc::gmtime();
+            return new Q(array(
+                "{$name}__gt" => $now,
+                "{$name}__lte" => SqlExpression::plus($now, SqlInterval::DAY($value['until'])),
+            ));
+        default:
+            return parent::getSearchQ($method, $value, $name);
+        }
+    }
+
+    function describeSearchMethod($method) {
+        switch ($method) {
+        case 'before':
+            return __('%1$s before %2$s' /* occurs before a date and time */);
+        case 'after':
+            return __('%1$s after %2$s' /* occurs after a date and time */);
+        case 'ndays':
+            return __('%1$s in the next %2$s' /* occurs within a window (like 3 days) */);
+        case 'ndaysago':
+            return __('%1$s in the last %2$s' /* occurs within a recent window (like 3 days) */);
+        case 'between':
+            return __('%1$s between %2$s and %3$s');
+        default:
+            return parent::describeSearchMethod($method);
+        }
+    }
+
+    function describeSearch($method, $value, $name=false) {
+        if ($method === 'between') {
+            $l = $this->toString($value['left']);
+            $r = $this->toString($value['right']);
+            $desc = $this->describeSearchMethod($method);
+            return sprintf($desc, $name, $l, $r);
+        }
+        return parent::describeSearch($method, $value, $name);
+    }
 }
 
 /**
@@ -1157,10 +2008,18 @@ class ThreadEntryField extends FormField {
         return false;
     }
 
+    function getMedia() {
+        $config = $this->getConfiguration();
+        $media = parent::getMedia() ?: array();
+        if ($config['attachments'])
+            $media = array_merge_recursive($media, FileUploadWidget::$media);
+        return $media;
+    }
+
     function getConfiguration() {
         global $cfg;
         $config = parent::getConfiguration();
-        $config['html'] = (bool) ($cfg && $cfg->isHtmlThreadEnabled());
+        $config['html'] = (bool) ($cfg && $cfg->isRichTextEnabled());
         return $config;
     }
 
@@ -1172,12 +2031,17 @@ class ThreadEntryField extends FormField {
         if ($cfg->getAllowedFileTypes())
             $fileupload_config['extensions']->set('default', $cfg->getAllowedFileTypes());
 
+        foreach ($fileupload_config as $C) {
+            $C->set('visibility', new VisibilityConstraint(new Q(array(
+                'attachments__eq'=>true,
+            )), VisibilityConstraint::HIDDEN));
+        }
         return array(
             'attachments' => new BooleanField(array(
                 'label'=>__('Enable Attachments'),
                 'default'=>$cfg->allowAttachments(),
                 'configuration'=>array(
-                    'desc'=>__('Enables attachments on tickets, regardless of channel'),
+                    'desc'=>__('Enables attachments, regardless of channel'),
                 ),
                 'validators' => function($self, $value) {
                     if (!ini_get('file_uploads'))
@@ -1192,6 +2056,14 @@ class ThreadEntryField extends FormField {
         $config = $this->getConfiguration();
         return $config['attachments'];
     }
+
+    function getWidget($widgetClass=false) {
+        if ($hint = $this->getLocal('hint'))
+            $this->set('placeholder', $hint);
+        $this->set('hint', null);
+        $widget = parent::getWidget($widgetClass);
+        return $widget;
+    }
 }
 
 class PriorityField extends ChoiceField {
@@ -1205,10 +2077,6 @@ class PriorityField extends ChoiceField {
     function hasIdValue() {
         return true;
     }
-    function isChangeable() {
-        return $this->getForm()->get('type') != 'T' ||
-            $this->get('name') != 'priority';
-    }
 
     function getChoices($verbose=false) {
         $sql = 'SELECT priority_id, priority_desc FROM '.PRIORITY_TABLE
@@ -1227,10 +2095,14 @@ class PriorityField extends ChoiceField {
     }
 
     function to_php($value, $id=false) {
+        if ($value instanceof Priority)
+            return $value;
         if (is_array($id)) {
             reset($id);
             $id = key($id);
         }
+        elseif (is_array($value))
+            list($value, $id) = $value;
         elseif ($id === false)
             $id = $value;
         if ($id)
@@ -1243,15 +2115,30 @@ class PriorityField extends ChoiceField {
             : $prio;
     }
 
+    function display($prio) {
+        if (!$prio instanceof Priority)
+            return parent::display($prio);
+        return sprintf('<span style="padding: 2px; background-color: %s">%s</span>',
+            $prio->getColor(), Format::htmlchars($prio->getDesc()));
+    }
+
     function toString($value) {
         return ($value instanceof Priority) ? $value->getDesc() : $value;
     }
 
+    function whatChanged($before, $after) {
+        return FormField::whatChanged($before, $after);
+    }
+
     function searchable($value) {
         // Priority isn't searchable this way
         return null;
     }
 
+    function getKeys($value) {
+        return ($value instanceof Priority) ? array($value->getId()) : null;
+    }
+
     function getConfigurationOptions() {
         $choices = $this->getChoices();
         $choices[''] = __('System Default');
@@ -1286,6 +2173,193 @@ FormField::addFieldTypes(/*@trans*/ 'Dynamic Fields', function() {
 });
 
 
+class DepartmentField extends ChoiceField {
+    function getWidget($widgetClass=false) {
+        $widget = parent::getWidget($widgetClass);
+        if ($widget->value instanceof Dept)
+            $widget->value = $widget->value->getId();
+        return $widget;
+    }
+
+    function hasIdValue() {
+        return true;
+    }
+
+    function getChoices($verbose=false) {
+        global $cfg;
+
+        $choices = array();
+        if (($depts = Dept::getDepartments()))
+            foreach ($depts as $id => $name)
+                $choices[$id] = $name;
+
+        return $choices;
+    }
+
+    function parse($id) {
+        return $this->to_php(null, $id);
+    }
+
+    function to_php($value, $id=false) {
+        if (is_array($id)) {
+            reset($id);
+            $id = key($id);
+        }
+        return $id;
+    }
+
+    function to_database($dept) {
+        return ($dept instanceof Dept)
+            ? array($dept->getName(), $dept->getId())
+            : $dept;
+    }
+
+    function toString($value) {
+        return (string) $value;
+    }
+
+    function searchable($value) {
+        return null;
+    }
+
+    function getConfigurationOptions() {
+        return array(
+            'prompt' => new TextboxField(array(
+                'id'=>2, 'label'=>__('Prompt'), 'required'=>false, 'default'=>'',
+                'hint'=>__('Leading text shown before a value is selected'),
+                'configuration'=>array('size'=>40, 'length'=>40),
+            )),
+        );
+    }
+}
+FormField::addFieldTypes(/*@trans*/ 'Dynamic Fields', function() {
+    return array(
+        'department' => array(__('Department'), DepartmentField),
+    );
+});
+
+
+class AssigneeField extends ChoiceField {
+    var $_choices = null;
+    var $_criteria = null;
+
+    function getWidget($widgetClass=false) {
+        $widget = parent::getWidget($widgetClass);
+        if (is_object($widget->value))
+            $widget->value = $widget->value->getId();
+        return $widget;
+    }
+
+    function getCriteria() {
+
+        if (!isset($this->_criteria)) {
+            $this->_criteria = array('available' => true);
+            if (($c=parent::getCriteria()))
+                $this->_criteria = array_merge($this->_criteria, $c);
+        }
+
+        return $this->_criteria;
+    }
+
+    function hasIdValue() {
+        return true;
+    }
+
+    function setChoices($choices) {
+        $this->_choices = $choices;
+    }
+
+    function getChoices($verbose=false) {
+        global $cfg;
+
+        if (!isset($this->_choices)) {
+            $config = $this->getConfiguration();
+            $choices = array(
+                    __('Agents') => new ArrayObject(),
+                    __('Teams') => new ArrayObject());
+            $A = current($choices);
+            $criteria = $this->getCriteria();
+            $agents = array();
+            if (($dept=$config['dept']) && $dept->assignMembersOnly()) {
+                if (($members = $dept->getMembers($criteria)))
+                    foreach ($members as $member)
+                        $agents[$member->getId()] = $member;
+            } else {
+                $agents = Staff::getStaffMembers($criteria);
+            }
+
+            foreach ($agents as $id => $name)
+                $A['s'.$id] = $name;
+
+            next($choices);
+            $T = current($choices);
+            if (($teams = Team::getActiveTeams()))
+                foreach ($teams as $id => $name)
+                    $T['t'.$id] = $name;
+
+            $this->_choices = $choices;
+        }
+
+        return $this->_choices;
+    }
+
+    function getValue() {
+
+        if (($value = parent::getValue()) && ($id=$this->getClean()))
+           return $value[$id];
+    }
+
+
+    function parse($id) {
+        return $this->to_php(null, $id);
+    }
+
+    function to_php($value, $id=false) {
+        if (is_array($id)) {
+            reset($id);
+            $id = key($id);
+        }
+
+        if ($id[0] == 's')
+            return Staff::lookup(substr($id, 1));
+        elseif ($id[0] == 't')
+            return Team::lookup(substr($id, 1));
+
+        return $id;
+    }
+
+
+    function to_database($value) {
+        return (is_object($value))
+            ? array($value->getName(), $value->getId())
+            : $value;
+    }
+
+    function toString($value) {
+        return (string) $value;
+    }
+
+    function searchable($value) {
+        return null;
+    }
+
+    function getConfigurationOptions() {
+        return array(
+            'prompt' => new TextboxField(array(
+                'id'=>2, 'label'=>__('Prompt'), 'required'=>false, 'default'=>'',
+                'hint'=>__('Leading text shown before a value is selected'),
+                'configuration'=>array('size'=>40, 'length'=>40),
+            )),
+        );
+    }
+}
+FormField::addFieldTypes(/*@trans*/ 'Dynamic Fields', function() {
+    return array(
+        'assignee' => array(__('Assignee'), AssigneeField),
+    );
+});
+
+
 class TicketStateField extends ChoiceField {
 
     static $_states = array(
@@ -1321,9 +2395,13 @@ class TicketStateField extends ChoiceField {
     function getChoices($verbose=false) {
         static $_choices;
 
+        $states = static::$_states;
+        if ($this->options['private_too'])
+            $states += static::$_privatestates;
+
         if (!isset($_choices)) {
             // Translate and cache the choices
-            foreach (static::$_states as $k => $v)
+            foreach ($states as $k => $v)
                 $_choices[$k] =  _P('ticket state name', $v['name']);
 
             $this->ht['default'] =  '';
@@ -1438,8 +2516,16 @@ class FileUploadField extends FormField {
     static function getFileTypes() {
         static $filetypes;
 
-        if (!isset($filetypes))
-            $filetypes = YamlDataParser::load(INCLUDE_DIR . '/config/filetype.yaml');
+        if (!isset($filetypes)) {
+            if (function_exists('apcu_fetch')) {
+                $key = md5(SECRET_SALT . GIT_VERSION . 'filetypes');
+                $filetypes = apcu_fetch($key);
+            }
+            if (!$filetypes)
+                $filetypes = YamlDataParser::load(INCLUDE_DIR . '/config/filetype.yaml');
+            if ($key)
+                apcu_store($key, $filetypes, 7200);
+        }
         return $filetypes;
     }
 
@@ -1525,9 +2611,11 @@ class FileUploadField extends FormField {
         if (!$bypass && $file['size'] > $config['size'])
             Http::response(413, 'File is too large');
 
-        if (!($id = AttachmentFile::upload($file)))
+        if (!($F = AttachmentFile::upload($file)))
             Http::response(500, 'Unable to store file: '. $file['error']);
 
+        $id = $F->getId();
+
         // This file is allowed for attachment in this session
         $_SESSION[':uploadedFiles'][$id] = 1;
 
@@ -1571,10 +2659,10 @@ class FileUploadField extends FormField {
         if ($file['size'] > $config['size'])
             throw new FileUploadError(__('File size is too large'));
 
-        if (!$id = AttachmentFile::save($file))
+        if (!$F = AttachmentFile::create($file))
             throw new FileUploadError(__('Unable to save file'));
 
-        return $id;
+        return $F;
     }
 
     function isValidFileType($name, $type=false) {
@@ -1599,12 +2687,16 @@ class FileUploadField extends FormField {
         if (!isset($this->attachments) && ($a = $this->getAnswer())
             && ($e = $a->getEntry()) && ($e->get('id'))
         ) {
-            $this->attachments = new GenericAttachments(
+            $this->attachments = GenericAttachments::forIdAndType(
                 // Combine the field and entry ids to make the key
                 sprintf('%u', crc32('E'.$this->get('id').$e->get('id'))),
                 'E');
         }
-        return $this->attachments ? $this->attachments->getAll() : array();
+        return $this->attachments ?: array();
+    }
+
+    function setAttachments(GenericAttachments $att) {
+        $this->attachments = $att;
     }
 
     function getConfiguration() {
@@ -1666,19 +2758,7 @@ class FileUploadField extends FormField {
     function to_database($value) {
         $this->getFiles();
         if (isset($this->attachments)) {
-            $ids = array();
-            // Handle deletes
-            foreach ($this->attachments->getAll() as $f) {
-                if (!in_array($f['id'], $value))
-                    $this->attachments->delete($f['id']);
-                else
-                    $ids[] = $f['id'];
-            }
-            // Handle new files
-            foreach ($value as $id) {
-                if (!in_array($id, $ids))
-                    $this->attachments->upload($id);
-            }
+            $this->attachments->keepOnlyFileIds($value);
         }
         return JsonDataEncoder::encode($value);
     }
@@ -1690,32 +2770,222 @@ class FileUploadField extends FormField {
     }
 
     function to_php($value) {
-        return JsonDataParser::decode($value);
+        return is_array($value) ? $value : JsonDataParser::decode($value);
+    }
+
+    function display($value) {
+        $links = array();
+        foreach ($this->getFiles() as $f) {
+            $links[] = sprintf('<a class="no-pjax" href="%s">%s</a>',
+                Format::htmlchars($f->file->getDownloadUrl()),
+                Format::htmlchars($f->file->name));
+        }
+        return implode('<br/>', $links);
+    }
+
+    function toString($value) {
+        $files = array();
+        foreach ($this->getFiles() as $f) {
+            $files[] = $f->file->name;
+        }
+        return implode(', ', $files);
+    }
+
+    function db_cleanup($field=false) {
+        // Delete associated attachments from the database, if any
+        $this->getFiles();
+        if (isset($this->attachments)) {
+            $this->attachments->deleteAll();
+        }
+    }
+
+    function asVar($value, $id=false) {
+        return new FileFieldAttachments($this->getFiles());
+    }
+    function asVarType() {
+        return 'FileFieldAttachments';
+    }
+
+    function whatChanged($before, $after) {
+        $B = (array) $before;
+        $A = (array) $after;
+        $added = array_diff($A, $B);
+        $deleted = array_diff($B, $A);
+        $added = Format::htmlchars(array_keys($added));
+        $deleted = Format::htmlchars(array_keys($deleted));
+
+        if ($added && $deleted) {
+            $desc = sprintf(
+                __('added <strong>%1$s</strong> and removed <strong>%2$s</strong>'),
+                implode(', ', $added), implode(', ', $deleted));
+        }
+        elseif ($added) {
+            $desc = sprintf(
+                __('added <strong>%1$s</strong>'),
+                implode(', ', $added));
+        }
+        elseif ($deleted) {
+            $desc = sprintf(
+                __('removed <strong>%1$s</strong>'),
+                implode(', ', $deleted));
+        }
+        else {
+            $desc = sprintf(
+                __('changed from <strong>%1$s</strong> to <strong>%2$s</strong>'),
+                $this->display($before), $this->display($after));
+        }
+        return $desc;
+    }
+}
+
+class FileFieldAttachments {
+    var $files;
+
+    function __construct($files) {
+        $this->files = $files;
+    }
+
+    function __toString() {
+        $files = array();
+        foreach ($this->files as $f) {
+            $files[] = $f->file->name;
+        }
+        return implode(', ', $files);
+    }
+
+    function getVar($tag) {
+        switch ($tag) {
+        case 'names':
+            return $this->__toString();
+        case 'files':
+            throw new OOBContent(OOBContent::FILES, $this->files->all());
+        }
+    }
+
+    static function getVarScope() {
+        return array(
+            'names' => __('List of file names'),
+            'files' => __('Attached files'),
+        );
+    }
+}
+
+class InlineFormData extends ArrayObject {
+    var $_form;
+
+    function __construct($form, array $data=array()) {
+        parent::__construct($data);
+        $this->_form = $form;
+    }
+
+    function getVar($tag) {
+        foreach ($this->_form->getFields() as $f) {
+            if ($f->get('name') == $tag)
+                return $this[$f->get('id')];
+        }
+    }
+}
+
+
+class InlineFormField extends FormField {
+    static $widget = 'InlineFormWidget';
+
+    var $_iform = null;
+
+    function validateEntry($value) {
+        if (!$this->getInlineForm()->isValid()) {
+            $this->_errors[] = __('Correct errors in the inline form');
+        }
+    }
+
+    function parse($value) {
+        // The InlineFieldWidget returns an array of cleaned data
+        return $value;
+    }
+
+    function to_database($value) {
+        return JsonDataEncoder::encode($value);
+    }
+
+    function to_php($value) {
+        $data = JsonDataParser::decode($value);
+        // The InlineFormData helps with the variable replacer API
+        return new InlineFormData($this->getInlineForm(), $data);
+    }
+
+    function display($data) {
+        $form = $this->getInlineForm();
+        ob_start(); ?>
+        <div><?php
+        foreach ($form->getFields() as $field) { ?>
+            <span style="display:inline-block;padding:0 5px;vertical-align:top">
+                <strong><?php echo Format::htmlchars($field->get('label')); ?></strong>
+                <div><?php
+                    $value = $data[$field->get('id')];
+                    echo $field->display($value); ?></div>
+            </span><?php
+        } ?>
+        </div><?php
+        return ob_get_clean();
+    }
+
+    function getInlineForm($data=false) {
+        $form = $this->get('form');
+        if (is_array($form)) {
+            $form = new SimpleForm($form, $data ?: $this->value ?: $this->getSource());
+        }
+        return $form;
     }
+}
 
-    function display($value) {
-        $links = array();
-        foreach ($this->getFiles() as $f) {
-            $links[] = sprintf('<a class="no-pjax" href="%s">%s</a>',
-                Format::htmlchars($f['download_url']), Format::htmlchars($f['name']));
+class InlineDynamicFormField extends FormField {
+    function getInlineForm($data=false) {
+        if (!isset($this->_iform) || $data) {
+            $config = $this->getConfiguration();
+            $this->_iform = DynamicForm::lookup($config['form']);
+            if ($data)
+                $this->_iform = $this->_iform->getForm($data);
         }
-        return implode('<br/>', $links);
+        return $this->_iform;
     }
 
-    function toString($value) {
-        $files = array();
-        foreach ($this->getFiles() as $f) {
-            $files[] = $f['name'];
+    function getConfigurationOptions() {
+        $forms = DynamicForm::objects()->filter(array('type'=>'G'))
+            ->values_flat('id', 'title');
+        $choices = array();
+        foreach ($forms as $row) {
+            list($id, $title) = $row;
+            $choices[$id] = $title;
         }
-        return implode(', ', $files);
+        return array(
+            'form' => new ChoiceField(array(
+                'id'=>2, 'label'=>'Inline Form', 'required'=>true,
+                'default'=>'', 'choices'=>$choices
+            )),
+        );
     }
+}
 
-    function db_cleanup() {
-        // Delete associated attachments from the database, if any
-        $this->getFiles();
-        if (isset($this->attachments)) {
-            $this->attachments->deleteAll();
-        }
+class InlineFormWidget extends Widget {
+    function render($mode=false) {
+        $form = $this->field->getInlineForm();
+        if (!$form)
+            return;
+        // Handle first-step edits -- load data from $this->value
+        if ($form instanceof DynamicForm && !$form->getSource())
+            $form = $form->getForm($this->value);
+        $inc = ($mode == 'client') ? CLIENTINC_DIR : STAFFINC_DIR;
+        include $inc . 'templates/inline-form.tmpl.php';
+    }
+
+    function getValue() {
+        $data = $this->field->getSource();
+        if (!$data)
+            return null;
+        $form = $this->field->getInlineForm($data);
+        if (!$form)
+            return null;
+        return $form->getClean();
     }
 }
 
@@ -1743,6 +3013,8 @@ class Widget {
             return $data[$this->name];
         elseif (isset($data[$this->field->get('name')]))
             return $data[$this->field->get('name')];
+        elseif (isset($data[$this->field->get('id')]))
+            return $data[$this->field->get('id')];
         return null;
     }
 
@@ -1762,7 +3034,7 @@ class Widget {
 class TextboxWidget extends Widget {
     static $input_type = 'text';
 
-    function render($mode=false, $extraConfig=false) {
+    function render($options=array(), $extraConfig=false) {
         $config = $this->field->getConfiguration();
         if (is_array($extraConfig)) {
             foreach ($extraConfig as $k=>$v)
@@ -1777,22 +3049,54 @@ class TextboxWidget extends Widget {
             $classes = 'class="'.$config['classes'].'"';
         if (isset($config['autocomplete']))
             $autocomplete = 'autocomplete="'.($config['autocomplete']?'on':'off').'"';
+        if (isset($config['autofocus']))
+            $autofocus = 'autofocus';
         if (isset($config['disabled']))
             $disabled = 'disabled="disabled"';
+        if (isset($config['translatable']) && $config['translatable'])
+            $translatable = 'data-translate-tag="'.$config['translatable'].'"';
+        $type = static::$input_type;
+        $types = array(
+            'email' => 'email',
+            'phone' => 'tel',
+        );
+        if ($type == 'text' && isset($types[$config['validator']]))
+            $type = $types[$config['validator']];
+        $placeholder = sprintf('placeholder="%s"', $this->field->getLocal('placeholder',
+            $config['placeholder']));
         ?>
-        <span style="display:inline-block">
-        <input type="<?php echo static::$input_type; ?>"
+        <input type="<?php echo $type; ?>"
             id="<?php echo $this->id; ?>"
             <?php echo implode(' ', array_filter(array(
-                $size, $maxlength, $classes, $autocomplete, $disabled)))
-                .' placeholder="'.$config['placeholder'].'"'; ?>
+                $size, $maxlength, $classes, $autocomplete, $disabled,
+                $translatable, $placeholder, $autofocus))); ?>
             name="<?php echo $this->name; ?>"
             value="<?php echo Format::htmlchars($this->value); ?>"/>
-        </span>
         <?php
     }
 }
 
+
+class TextboxSelectionWidget extends TextboxWidget {
+    //TODO: Support multi-input e.g comma separated inputs
+    function render($options=array(), $extraConfig=array()) {
+
+        if ($this->value && is_array($this->value))
+            $this->value = current($this->value);
+
+        parent::render($options);
+    }
+
+    function getValue() {
+
+        $value = parent::getValue();
+        if ($value && ($item=$this->field->lookupChoice((string) $value)))
+            $value = $item;
+
+        return $value;
+    }
+}
+
 class PasswordWidget extends TextboxWidget {
     static $input_type = 'password';
 
@@ -1814,9 +3118,10 @@ class PasswordWidget extends TextboxWidget {
 }
 
 class TextareaWidget extends Widget {
-    function render($mode=false) {
+    function render($options=array()) {
         $config = $this->field->getConfiguration();
         $class = $cols = $rows = $maxlength = "";
+        $attrs = array();
         if (isset($config['rows']))
             $rows = "rows=\"{$config['rows']}\"";
         if (isset($config['cols']))
@@ -1829,9 +3134,12 @@ class TextareaWidget extends Widget {
             $class = sprintf('class="%s"', implode(' ', $class));
             $this->value = Format::viewableImages($this->value);
         }
+        if (isset($config['context']))
+            $attrs['data-root-context'] = '"'.$config['context'].'"';
         ?>
         <span style="display:inline-block;width:100%">
         <textarea <?php echo $rows." ".$cols." ".$maxlength." ".$class
+                .' '.Format::array_implode('=', ' ', $attrs)
                 .' placeholder="'.$config['placeholder'].'"'; ?>
             id="<?php echo $this->id; ?>"
             name="<?php echo $this->name; ?>"><?php
@@ -1858,11 +3166,11 @@ class TextareaWidget extends Widget {
 }
 
 class PhoneNumberWidget extends Widget {
-    function render($mode=false) {
+    function render($options=array()) {
         $config = $this->field->getConfiguration();
         list($phone, $ext) = explode("X", $this->value);
         ?>
-        <input id="<?php echo $this->id; ?>" type="text" name="<?php echo $this->name; ?>" value="<?php
+        <input id="<?php echo $this->id; ?>" type="tel" name="<?php echo $this->name; ?>" value="<?php
         echo Format::htmlchars($phone); ?>"/><?php
         // Allow display of extension field even if disabled if the phone
         // number being edited has an extension
@@ -1886,13 +3194,13 @@ class PhoneNumberWidget extends Widget {
 }
 
 class ChoicesWidget extends Widget {
-    static $media = array(
-        'css' => array(
-            '/css/jquery.multiselect.css',
-        ),
-    );
+    function render($options=array()) {
 
-    function render($mode=false) {
+        $mode = null;
+        if (isset($options['mode']))
+            $mode = $options['mode'];
+        elseif (isset($this->field->options['render_mode']))
+            $mode = $this->field->options['render_mode'];
 
         if ($mode == 'view') {
             if (!($val = (string) $this->field))
@@ -1910,7 +3218,10 @@ class ChoicesWidget extends Widget {
         // Determine the value for the default (the one listed if nothing is
         // selected)
         $choices = $this->field->getChoices(true);
-        $prompt = $config['prompt'] ?: __('Select');
+        $prompt = ($config['prompt'])
+            ? $this->field->getLocal('prompt', $config['prompt'])
+            : __('Select'
+            /* Used as a default prompt for a custom drop-down list */);
 
         $have_def = false;
         // We don't consider the 'default' when rendering in 'search' mode
@@ -1934,23 +3245,25 @@ class ChoicesWidget extends Widget {
         if (!is_array($values))
             $values = $have_def ? array($def_key => $choices[$def_key]) : array();
 
+        if (isset($config['classes']))
+            $classes = 'class="'.$config['classes'].'"';
         ?>
         <select name="<?php echo $this->name; ?>[]"
+            <?php echo implode(' ', array_filter(array($classes))); ?>
             id="<?php echo $this->id; ?>"
-            data-prompt="<?php echo $prompt; ?>"
+            <?php if (isset($config['data']))
+              foreach ($config['data'] as $D=>$V)
+                echo ' data-'.$D.'="'.Format::htmlchars($V).'"';
+            ?>
+            data-placeholder="<?php echo $prompt; ?>"
             <?php if ($config['multiselect'])
-                echo ' multiple="multiple" class="multiselect"'; ?>>
+                echo ' multiple="multiple"'; ?>>
             <?php if (!$have_def && !$config['multiselect']) { ?>
             <option value="<?php echo $def_key; ?>">&mdash; <?php
                 echo $def_val; ?> &mdash;</option>
-            <?php }
-            foreach ($choices as $key => $name) {
-                if (!$have_def && $key == $def_key)
-                    continue; ?>
-                <option value="<?php echo $key; ?>" <?php
-                    if (isset($values[$key])) echo 'selected="selected"';
-                ?>><?php echo Format::htmlchars($name); ?></option>
-            <?php } ?>
+<?php
+        }
+        $this->emitChoices($choices, $values, $have_def, $def_key); ?>
         </select>
         <?php
         if ($config['multiselect']) {
@@ -1958,13 +3271,44 @@ class ChoicesWidget extends Widget {
         <script type="text/javascript">
         $(function() {
             $("#<?php echo $this->id; ?>")
-            .multiselect({'noneSelectedText':'<?php echo $prompt; ?>'});
+            .select2({'minimumResultsForSearch':10, 'width': '350px'});
         });
         </script>
        <?php
         }
     }
 
+    function emitChoices($choices, $values=array(), $have_def=false, $def_key=null) {
+        reset($choices);
+        if (is_array(current($choices)) || current($choices) instanceof Traversable)
+            return $this->emitComplexChoices($choices, $values, $have_def, $def_key);
+
+        foreach ($choices as $key => $name) {
+            if (!$have_def && $key == $def_key)
+                continue; ?>
+            <option value="<?php echo $key; ?>" <?php
+                if (isset($values[$key])) echo 'selected="selected"';
+            ?>><?php echo Format::htmlchars($name); ?></option>
+        <?php
+        }
+    }
+
+    function emitComplexChoices($choices, $values=array(), $have_def=false, $def_key=null) {
+        foreach ($choices as $label => $group) {
+            if (!count($group)) continue;
+            ?>
+            <optgroup label="<?php echo $label; ?>"><?php
+            foreach ($group as $key => $name) {
+                if (!$have_def && $key == $def_key)
+                    continue; ?>
+            <option value="<?php echo $key; ?>" <?php
+                if (isset($values[$key])) echo 'selected="selected"';
+            ?>><?php echo Format::htmlchars($name); ?></option>
+<?php       } ?>
+            </optgroup><?php
+        }
+    }
+
     function getValue() {
 
         if (!($value = parent::getValue()))
@@ -1976,10 +3320,23 @@ class ChoicesWidget extends Widget {
         // Assume multiselect
         $values = array();
         $choices = $this->field->getChoices();
-        if (is_array($value)) {
-            foreach($value as $k => $v) {
-                if (isset($choices[$v]))
-                    $values[$v] = $choices[$v];
+
+        if ($choices && is_array($value)) {
+            // Complex choices
+            if (is_array(current($choices))
+                    || current($choices) instanceof Traversable) {
+                foreach ($choices as $label => $group) {
+                     foreach ($group as $k => $v)
+                        if (in_array($k, $value))
+                            $values[$k] = $v;
+                }
+            } else {
+                foreach($value as $k => $v) {
+                    if (isset($choices[$v]))
+                        $values[$v] = $choices[$v];
+                    elseif (($i=$this->field->lookupChoice($v)))
+                        $values += $i;
+                }
             }
         }
 
@@ -1991,26 +3348,142 @@ class ChoicesWidget extends Widget {
     }
 }
 
+/**
+ * A widget for the ChoiceField which will render a list of radio boxes or
+ * checkboxes depending on the value of $config['multiple']. Complex choices
+ * are also supported and will be rendered as divs.
+ */
+class BoxChoicesWidget extends Widget {
+    function render($options=array()) {
+        $this->emitChoices($this->field->getChoices());
+    }
+
+    function emitChoices($choices) {
+      static $uid = 1;
+
+      if (!isset($this->value))
+          $this->value = $this->field->get('default');
+      $config = $this->field->getConfiguration();
+      $type = $config['multiple'] ? 'checkbox' : 'radio';
+
+      $classes = array('checkbox');
+      if (isset($config['classes']))
+          $classes = array_merge($classes, (array) $config['classes']);
+
+      foreach ($choices as $k => $v) {
+          if (is_array($v)) {
+              $this->renderSectionBreak($k);
+              $this->emitChoices($v);
+              continue;
+          }
+          $id = sprintf("%s-%s", $this->id, $uid++);
+?>
+        <label class="<?php echo implode(' ', $classes); ?>"
+            for="<?php echo $id; ?>">
+        <input id="<?php echo $id; ?>" type="<?php echo $type; ?>"
+            name="<?php echo $this->name; ?>[]" <?php
+            if ($this->value[$k]) echo 'checked="checked"'; ?> value="<?php
+            echo Format::htmlchars($k); ?>"/>
+        <?php
+        if ($v) {
+            echo Format::viewableImages($v);
+        } ?>
+        </label>
+<?php   }
+    }
+
+    function renderSectionBreak($label) { ?>
+        <div><?php echo Format::htmlchars($label); ?></div>
+<?php
+    }
+
+    function getValue() {
+        $data = $this->field->getSource();
+        if (count($data)) {
+            if (!isset($data[$this->name]))
+                return array();
+            return $this->collectValues($data[$this->name], $this->field->getChoices());
+        }
+        return parent::getValue();
+    }
+
+    function collectValues($data, $choices) {
+        $value = array();
+        foreach ($choices as $k => $v) {
+            if (is_array($v))
+                $value = array_merge($value, $this->collectValues($data, $v));
+            elseif (@in_array($k, $data))
+                $value[$k] = $v;
+        }
+        return $value;
+    }
+}
+
+/**
+ * An extension to the BoxChoicesWidget which will render complex choices in
+ * tabs.
+ */
+class TabbedBoxChoicesWidget extends BoxChoicesWidget {
+    function render($options=array()) {
+        $tabs = array();
+        foreach ($this->field->getChoices() as $label=>$group) {
+            if (is_array($group)) {
+                $tabs[$label] = $group;
+            }
+            else {
+                $this->emitChoices(array($label=>$group));
+            }
+        }
+        if ($tabs) {
+            ?>
+            <div>
+            <ul class="alt tabs">
+<?php       $i = 0;
+            foreach ($tabs as $label => $group) {
+                $active = $i++ == 0; ?>
+                <li <?php if ($active) echo 'class="active"';
+                  ?>><a href="#<?php echo sprintf('%s-%s', $this->name, Format::slugify($label));
+                  ?>"><?php echo Format::htmlchars($label); ?></a></li>
+<?php       } ?>
+            </ul>
+<?php       $i = 0;
+            foreach ($tabs as $label => $group) {
+                $first = $i++ == 0; ?>
+                <div class="tab_content <?php if (!$first) echo 'hidden'; ?>" id="<?php
+                  echo sprintf('%s-%s', $this->name, Format::slugify($label));?>">
+<?php           $this->emitChoices($group); ?>
+                </div>
+<?php       } ?>
+            </div>
+<?php   }
+    }
+}
+
 class CheckboxWidget extends Widget {
     function __construct($field) {
         parent::__construct($field);
         $this->name = '_field-checkboxes';
     }
 
-    function render($mode=false) {
+    function render($options=array()) {
         $config = $this->field->getConfiguration();
         if (!isset($this->value))
             $this->value = $this->field->get('default');
+        $classes = array('checkbox');
+        if (isset($config['classes']))
+            $classes = array_merge($classes, (array) $config['classes']);
         ?>
-        <input id="<?php echo $this->id; ?>" style="vertical-align:top;"
+        <label class="<?php echo implode(' ', $classes); ?>">
+        <input id="<?php echo $this->id; ?>"
             type="checkbox" name="<?php echo $this->name; ?>[]" <?php
             if ($this->value) echo 'checked="checked"'; ?> value="<?php
             echo $this->field->get('id'); ?>"/>
         <?php
-        if ($config['desc']) { ?>
-            <em style="display:inline-block"><?php
-            echo Format::viewableImages($config['desc']); ?></em>
-        <?php }
+        if ($config['desc']) {
+            echo Format::viewableImages($config['desc']);
+        } ?>
+        </label>
+<?php
     }
 
     function getValue() {
@@ -2029,23 +3502,26 @@ class CheckboxWidget extends Widget {
 }
 
 class DatetimePickerWidget extends Widget {
-    function render($mode=false) {
+    function render($options=array()) {
         global $cfg;
 
         $config = $this->field->getConfiguration();
         if ($this->value) {
             $this->value = is_int($this->value) ? $this->value :
                 strtotime($this->value);
-            if ($config['gmt'])
-                $this->value += 3600 *
-                    $_SESSION['TZ_OFFSET']+($_SESSION['TZ_DST']?date('I',$this->value):0);
 
+            if ($config['gmt']) {
+                // Convert to GMT time
+                $tz = new DateTimeZone($cfg->getTimezone());
+                $D = DateTime::createFromFormat('U', $this->value);
+                $this->value += $tz->getOffset($D);
+            }
             list($hr, $min) = explode(':', date('H:i', $this->value));
-            $this->value = Format::date($cfg->getDateFormat(), $this->value);
+            $this->value = Format::date($this->value, false, false, 'UTC');
         }
         ?>
         <input type="text" name="<?php echo $this->name; ?>"
-            id="<?php echo $this->id; ?>"
+            id="<?php echo $this->id; ?>" style="display:inline-block;width:auto"
             value="<?php echo Format::htmlchars($this->value); ?>" size="12"
             autocomplete="off" class="dp" />
         <script type="text/javascript">
@@ -2063,7 +3539,7 @@ class DatetimePickerWidget extends Widget {
                     showButtonPanel: true,
                     buttonImage: './images/cal.png',
                     showOn:'both',
-                    dateFormat: $.translate_format('<?php echo $cfg->getDateFormat(); ?>')
+                    dateFormat: $.translate_format('<?php echo $cfg->getDateFormat(true); ?>')
                 });
             });
         </script>
@@ -2091,48 +3567,54 @@ class DatetimePickerWidget extends Widget {
                 list($hr, $min) = explode(':', $data[$this->name . ':time']);
                 $datetime += $hr * 3600 + $min * 60;
             }
-            if ($datetime && $config['gmt'])
-                $datetime -= (int) (3600 * $_SESSION['TZ_OFFSET'] +
-                    ($_SESSION['TZ_DST'] ? date('I',$datetime) : 0));
+            if ($datetime && $config['gmt']) {
+                // Convert to GMT time
+                $tz = new DateTimeZone($cfg->getTimezone());
+                $D = DateTime::createFromFormat('U', $datetime);
+                $datetime -= $tz->getOffset($D);
+            }
         }
         return $datetime;
     }
 }
 
 class SectionBreakWidget extends Widget {
-    function render($mode=false) {
+    function render($options=array()) {
         ?><div class="form-header section-break"><h3><?php
-        echo Format::htmlchars($this->field->get('label'));
-        ?></h3><em><?php echo Format::htmlchars($this->field->get('hint'));
+        echo Format::htmlchars($this->field->getLocal('label'));
+        ?></h3><em><?php echo Format::htmlchars($this->field->getLocal('hint'));
         ?></em></div>
         <?php
     }
 }
 
 class ThreadEntryWidget extends Widget {
-    function render($client=null) {
+    function render($options=array()) {
 
         $config = $this->field->getConfiguration();
-        ?><div style="margin-bottom:0.5em;margin-top:0.5em"><strong><?php
-        echo Format::htmlchars($this->field->get('label'));
-        ?></strong>:</div>
+        $object_id = false;
+        if ($options['client']) {
+            $namespace = $options['draft-namespace']
+                ?: 'ticket.client';
+             $object_id = substr(session_id(), -12);
+        } else {
+            $namespace = $options['draft-namespace'] ?: 'ticket.staff';
+        }
+
+        list($draft, $attrs) = Draft::getDraftAndDataAttrs($namespace, $object_id, $this->value);
+        ?>
         <textarea style="width:100%;" name="<?php echo $this->field->get('name'); ?>"
-            placeholder="<?php echo Format::htmlchars($this->field->get('hint')); ?>"
-            <?php if (!$client) { ?>
-                data-draft-namespace="ticket.staff"
-            <?php } else { ?>
-                data-draft-namespace="ticket.client"
-                data-draft-object-id="<?php echo substr(session_id(), -12); ?>"
-            <?php } ?>
-            class="<?php if ($config['html']) echo 'richtext'; ?> draft draft-delete ifhtml"
+            placeholder="<?php echo Format::htmlchars($this->field->get('placeholder')); ?>"
+            class="<?php if ($config['html']) echo 'richtext';
+                ?> draft draft-delete" <?php echo $attrs; ?>
             cols="21" rows="8" style="width:80%;"><?php echo
-            Format::htmlchars($this->value); ?></textarea>
+            Format::htmlchars($this->value) ?: $draft; ?></textarea>
     <?php
         if (!$config['attachments'])
             return;
 
         $attachments = $this->getAttachments($config);
-        print $attachments->render($client);
+        print $attachments->render($options);
         foreach ($attachments->getMedia() as $type=>$urls) {
             foreach ($urls as $url)
                 Form::emitMedia($url, $type);
@@ -2175,7 +3657,7 @@ class FileUploadWidget extends Widget {
         ),
     );
 
-    function render($how) {
+    function render($options) {
         $config = $this->field->getConfiguration();
         $name = $this->field->getFormName();
         $id = substr(md5(spl_object_hash($this)), 10);
@@ -2183,25 +3665,27 @@ class FileUploadWidget extends Widget {
         $mimetypes = array_filter($config['__mimetypes'],
             function($t) { return strpos($t, '/') !== false; }
         );
-        $files = array();
-        foreach ($this->value ?: array() as $fid) {
-            $found = false;
-            foreach ($attachments as $f) {
-                if ($f['id'] == $fid) {
-                    $files[] = $f;
-                    $found = true;
-                    break;
-                }
-            }
-            if (!$found && ($file = AttachmentFile::lookup($fid))) {
-                $files[] = array(
-                    'id' => $file->getId(),
-                    'name' => $file->getName(),
-                    'type' => $file->getType(),
-                    'size' => $file->getSize(),
-                    'download_url' => $file->getDownloadUrl(),
-                );
-            }
+        $maxfilesize = ($config['size'] ?: 1048576) / 1048576;
+        $files = $F = array();
+        $new = array_fill_keys($this->field->getClean(), 1);
+        foreach ($attachments as $f) {
+            $F[] = $f->file;
+            unset($new[$f->id]);
+        }
+        // Add in newly added files not yet saved (if redisplaying after an
+        // error)
+        if ($new) {
+            $F = array_merge($F, AttachmentFile::objects()->filter(array(
+                'id__in' => array_keys($new)))->all());
+        }
+        foreach ($F as $file) {
+            $files[] = array(
+                'id' => $file->getId(),
+                'name' => $file->getName(),
+                'type' => $file->getType(),
+                'size' => $file->getSize(),
+                'download_url' => $file->getDownloadUrl(),
+            );
         }
         ?><div id="<?php echo $id;
             ?>" class="filedrop"><div class="files"></div>
@@ -2224,7 +3708,7 @@ class FileUploadWidget extends Widget {
           allowedfiletypes: <?php echo JsonDataEncoder::encode(
             $mimetypes); ?>,
           maxfiles: <?php echo $config['max'] ?: 20; ?>,
-          maxfilesize: <?php echo ($config['size'] ?: 1048576) / 1048576; ?>,
+          maxfilesize: <?php echo $maxfilesize; ?>,
           name: '<?php echo $name; ?>[]',
           files: <?php echo JsonDataEncoder::encode($files); ?>
         });});
@@ -2233,23 +3717,23 @@ class FileUploadWidget extends Widget {
     }
 
     function getValue() {
-        $data = $this->field->getSource();
         $ids = array();
         // Handle manual uploads (IE<10)
         if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_FILES[$this->name])) {
             foreach (AttachmentFile::format($_FILES[$this->name]) as $file) {
                 try {
-                    $ids[] = $this->field->uploadFile($file);
+                    $F = $this->field->uploadFile($file);
+                    $ids[] = $F->getId();
                 }
                 catch (FileUploadError $ex) {}
             }
-            return array_merge($ids, parent::getValue() ?: array());
+            return $ids;
         }
+
         // If no value was sent, assume an empty list
-        elseif ($data && is_array($data) && !isset($data[$this->name]))
+        if (!($files = parent::getValue()))
             return array();
 
-
         // Files uploaded here MUST have been uploaded by this user and
         // identified in the session
         if ($files = parent::getValue()) {
@@ -2257,7 +3741,7 @@ class FileUploadWidget extends Widget {
             // Files already attached to the field are allowed
             foreach ($this->field->getFiles() as $F) {
                 // FIXME: This will need special porting in v1.10
-                $allowed[$F['id']] = 1;
+                $allowed[$F->id] = 1;
             }
             // New files uploaded in this session are allowed
             if (isset($_SESSION[':uploadedFiles'])) {
@@ -2274,7 +3758,29 @@ class FileUploadWidget extends Widget {
                 }
             }
         }
-        return $files;
+
+        // New files uploaded in this session are allowed
+        if (isset($_SESSION[':uploadedFiles']))
+            $allowed += $_SESSION[':uploadedFiles'];
+
+        // Canned attachments initiated by this session
+        if (isset($_SESSION[':cannedFiles']))
+           $allowed += $_SESSION[':cannedFiles'];
+
+        // Parse the files and make sure it's allowed.
+        foreach ($files as $info) {
+            @list($id, $name) = explode(',', $info, 2);
+            if (!isset($allowed[$id]))
+                continue;
+
+            // Keep the values as the IDs
+            if ($name)
+                $ids[$name] = $id;
+            else
+                $ids[] = $id;
+        }
+
+        return $ids;
     }
 }
 
@@ -2282,6 +3788,7 @@ class FileUploadError extends Exception {}
 
 class FreeTextField extends FormField {
     static $widget = 'FreeTextWidget';
+    protected $attachments;
 
     function getConfigurationOptions() {
         return array(
@@ -2290,6 +3797,12 @@ class FreeTextField extends FormField {
                 'label'=>__('Content'), 'required'=>true, 'default'=>'',
                 'hint'=>__('Free text shown in the form, such as a disclaimer'),
             )),
+            'attachments' => new FileUploadField(array(
+                'id'=>'attach',
+                'label' => __('Attachments'),
+                'name'=>'files',
+                'configuration' => array('extensions'=>'')
+            )),
         );
     }
 
@@ -2300,23 +3813,82 @@ class FreeTextField extends FormField {
     function isBlockLevel() {
         return true;
     }
+
+    /* utils */
+
+    function to_config($config) {
+        if ($config && isset($config['attachments']))
+            $keepers = $config['attachments'] = array_values($config['attachments']);
+        $this->getAttachments()->keepOnlyFileIds($keepers);
+
+        return $config;
+    }
+
+    function db_cleanup($field=false) {
+
+        if ($field && $this->getFiles())
+            $this->getAttachments()->deleteAll();
+    }
+
+    function getAttachments() {
+
+        if (!isset($this->attachments))
+            $this->attachments = GenericAttachments::forIdAndType($this->get('id'), 'I');
+
+        return $this->attachments;
+    }
+
+    function getFiles() {
+
+        if (!($attachments = $this->getAttachments()))
+            return array();
+
+        return $attachments->all();
+    }
+
 }
 
 class FreeTextWidget extends Widget {
-    function render($mode=false) {
+    function render($options=array()) {
         $config = $this->field->getConfiguration();
-        ?><div class=""><h3><?php
-            echo Format::htmlchars($this->field->get('label'));
-        ?></h3><em><?php
-            echo Format::htmlchars($this->field->get('hint'));
-        ?></em><div><?php
+        $class = $config['classes'] ?: 'thread-body bleed';
+        ?><div class="<?php echo $class; ?>"><?php
+        if ($label = $this->field->getLocal('label')) { ?>
+            <h3><?php
+            echo Format::htmlchars($label);
+        ?></h3><?php
+        }
+        if ($hint = $this->field->getLocal('hint')) { ?>
+        <em><?php
+            echo Format::htmlchars($hint);
+        ?></em><?php
+        } ?>
+        <div><?php
             echo Format::viewableImages($config['content']); ?></div>
         </div>
         <?php
+        if (($attachments=$this->field->getFiles())) { ?>
+            <section class="freetext-files">
+            <div class="title"><?php echo __('Related Resources'); ?></div>
+            <?php foreach ($attachments as $attach) { ?>
+                <div class="file">
+                <a href="<?php echo $attach->file->getDownloadUrl(); ?>"
+                    target="_blank" download="<?php echo $attach->file->getDownloadUrl();
+                    ?>" class="truncate no-pjax">
+                    <i class="icon-file"></i>
+                    <?php echo Format::htmlchars($attach->getFilename()); ?>
+                </a>
+                </div>
+            <?php } ?>
+        </section>
+        <?php }
     }
 }
 
 class VisibilityConstraint {
+    static $operators = array(
+        'eq' => 1,
+    );
 
     const HIDDEN =      0x0001;
     const VISIBLE =     0x0002;
@@ -2330,6 +3902,10 @@ class VisibilityConstraint {
     }
 
     function emitJavascript($field) {
+
+        if (!$this->constraint->constraints)
+            return;
+
         $func = 'recheck';
         $form = $field->getForm();
 ?>
@@ -2371,18 +3947,35 @@ class VisibilityConstraint {
      * Determines if the field was visible when the form was submitted
      */
     function isVisible($field) {
+
+        // Assume initial visibility if constraint is not provided.
+        if (!$this->constraint->constraints)
+            return $this->initial == self::VISIBLE;
+
+
         return $this->compileQPhp($this->constraint, $field);
     }
 
+    static function splitFieldAndOp($field) {
+        if (false !== ($last = strrpos($field, '__'))) {
+            $op = substr($field, $last + 2);
+            if (isset(static::$operators[$op]))
+                $field = substr($field, 0, strrpos($field, '__'));
+        }
+        return array($field, $op);
+    }
+
     function compileQPhp(Q $Q, $field) {
-        $form = $field->getForm();
+        if (!($form = $field->getForm())) {
+            return $this->initial == self::VISIBLE;
+        }
         $expr = array();
         foreach ($Q->constraints as $c=>$value) {
             if ($value instanceof Q) {
                 $expr[] = $this->compileQPhp($value, $field);
             }
             else {
-                @list($f, $op) = explode('__', $c, 2);
+                @list($f, $op) = self::splitFieldAndOp($c);
                 $field = $form->getField($f);
                 $wval = $field->getClean();
                 switch ($op) {
@@ -2408,7 +4001,7 @@ class VisibilityConstraint {
                 $this->getAllFields($c, $fields);
             }
             else {
-                list($f, $op) = explode('__', $c, 2);
+                @list($f) = self::splitFieldAndOp($c);
                 $fields[$f] = true;
             }
         }
@@ -2422,7 +4015,7 @@ class VisibilityConstraint {
                 $expr[] = $this->compileQ($value, $form);
             }
             else {
-                list($f, $op) = explode('__', $c, 2);
+                list($f, $op) = self::splitFieldAndOp($c);
                 $widget = $form->getField($f)->getWidget();
                 $id = $widget->id;
                 switch ($op) {
@@ -2447,40 +4040,240 @@ class VisibilityConstraint {
     }
 }
 
-class Q {
-    const NEGATED = 0x0001;
-    const ANY =     0x0002;
+class AssignmentForm extends Form {
+
+    static $id = 'assign';
+    var $_assignee = null;
+    var $_assignees = null;
+
+
+    function getFields() {
+
+        if ($this->fields)
+            return $this->fields;
+
+        $fields = array(
+            'assignee' => new AssigneeField(array(
+                    'id'=>1,
+                    'label' => __('Assignee'),
+                    'flags' => hexdec(0X450F3),
+                    'required' => true,
+                    'validator-error' => __('Assignee selection required'),
+                    'configuration' => array(
+                        'criteria' => array(
+                            'available' => true,
+                            ),
+                       ),
+                    )
+                ),
+            'comments' => new TextareaField(array(
+                    'id' => 2,
+                    'label'=> '',
+                    'required'=>false,
+                    'default'=>'',
+                    'configuration' => array(
+                        'html' => true,
+                        'size' => 'small',
+                        'placeholder' => __('Optional reason for the assignment'),
+                        ),
+                    )
+                ),
+            );
+
+
+        if (isset($this->_assignees))
+            $fields['assignee']->setChoices($this->_assignees);
+
+
+        $this->setFields($fields);
+
+        return $this->fields;
+    }
+
+    function getField($name) {
+
+        if (($fields = $this->getFields())
+                && isset($fields[$name]))
+            return $fields[$name];
+    }
+
+    function isValid($include=false) {
+
+        if (!parent::isValid($include) || !($f=$this->getField('assignee')))
+            return false;
+
+        // Do additional assignment validation
+        if (!($assignee = $this->getAssignee())) {
+            $f->addError(__('Unknown assignee'));
+        } elseif ($assignee instanceof Staff) {
+            // Make sure the agent is available
+            if (!$assignee->isAvailable())
+                $f->addError(__('Agent is unavailable for assignment'));
+        } elseif ($assignee instanceof Team) {
+            // Make sure the team is active and has members
+            if (!$assignee->isActive())
+                $f->addError(__('Team is disabled'));
+            elseif (!$assignee->getNumMembers())
+                $f->addError(__('Team does not have members'));
+        }
+
+        return !$this->errors();
+    }
+
+    function render($options) {
+
+        switch(strtolower($options['template'])) {
+        case 'simple':
+            $inc = STAFFINC_DIR . 'templates/dynamic-form-simple.tmpl.php';
+            break;
+        default:
+            throw new Exception(sprintf(__('%s: Unknown template style %s'),
+                        'FormUtils', $options['template']));
+        }
+
+        $form = $this;
+        include $inc;
+    }
+
+    function setAssignees($assignees) {
+        $this->_assignees = $assignees;
+        $this->_fields = array();
+    }
+
+    function getAssignees() {
+        return $this->_assignees;
+    }
+
+    function getAssignee() {
+
+        if (!isset($this->_assignee))
+            $this->_assignee = $this->getField('assignee')->getClean();
+
+        return $this->_assignee;
+    }
+
+    function getComments() {
+        return $this->getField('comments')->getClean();
+    }
+}
+
+class ClaimForm extends AssignmentForm {
+
+    var $_fields;
+
+    function setFields($fields) {
+        $this->_fields = $fields;
+        parent::setFields($fields);
+    }
+
+    function getFields() {
+
+        if ($this->_fields)
+            return $this->_fields;
+
+        $fields = parent::getFields();
 
-    var $constraints;
-    var $flags;
-    var $negated = false;
-    var $ored = false;
+        // Disable && hide assignee field selection
+        if (isset($fields['assignee'])) {
+            $visibility = new VisibilityConstraint(
+                    new Q(array()), VisibilityConstraint::HIDDEN);
 
-    function __construct($filter, $flags=0) {
-        $this->constraints = $filter;
-        $this->negated = $flags & self::NEGATED;
-        $this->ored = $flags & self::ANY;
+            $fields['assignee']->set('visibility', $visibility);
+        }
+
+        // Change coments placeholder to reflect claim
+        if (isset($fields['comments'])) {
+            $fields['comments']->configure('placeholder',
+                    __('Optional reason for the claim'));
+        }
+
+
+        $this->setFields($fields);
+
+        return $this->fields;
     }
 
-    function isNegated() {
-        return $this->negated;
+}
+
+class TransferForm extends Form {
+
+    static $id = 'transfer';
+    var $_dept = null;
+
+    function __construct($source=null, $options=array()) {
+        parent::__construct($source, $options);
     }
 
-    function isOred() {
-        return $this->ored;
+    function getFields() {
+
+        if ($this->fields)
+            return $this->fields;
+
+        $fields = array(
+            'dept' => new DepartmentField(array(
+                    'id'=>1,
+                    'label' => __('Department'),
+                    'flags' => hexdec(0X450F3),
+                    'required' => true,
+                    'validator-error' => __('Department selection required'),
+                    )
+                ),
+            'comments' => new TextareaField(array(
+                    'id' => 2,
+                    'label'=> '',
+                    'required'=>false,
+                    'default'=>'',
+                    'configuration' => array(
+                        'html' => true,
+                        'size' => 'small',
+                        'placeholder' => __('Optional reason for the transfer'),
+                        ),
+                    )
+                ),
+            );
+
+        $this->setFields($fields);
+
+        return $this->fields;
     }
 
-    function negate() {
-        $this->negated = !$this->negated;
-        return $this;
+    function isValid($include=false) {
+
+        if (!parent::isValid($include))
+            return false;
+
+        // Do additional validations
+        if (!($dept = $this->getDept()))
+            $this->getField('dept')->addError(
+                    __('Unknown department'));
+
+        return !$this->errors();
     }
 
-    static function not(array $constraints) {
-        return new static($constraints, self::NEGATED);
+    function render($options) {
+
+        switch(strtolower($options['template'])) {
+        case 'simple':
+            $inc = STAFFINC_DIR . 'templates/dynamic-form-simple.tmpl.php';
+            break;
+        default:
+            throw new Exception(sprintf(__('%s: Unknown template style %s'),
+                        get_class(), $options['template']));
+        }
+
+        $form = $this;
+        include $inc;
+
     }
 
-    static function any(array $constraints) {
-        return new static($constraints, self::ORED);
+    function getDept() {
+
+        if (!isset($this->_dept)) {
+            if (($id = $this->getField('dept')->getClean()))
+                $this->_dept = Dept::lookup($id);
+        }
+
+        return $this->_dept;
     }
 }
 
diff --git a/include/class.group.php b/include/class.group.php
deleted file mode 100644
index 67f392190f0cd357872ad9a1e550cf6258175631..0000000000000000000000000000000000000000
--- a/include/class.group.php
+++ /dev/null
@@ -1,236 +0,0 @@
-<?php
-/*********************************************************************
-    class.group.php
-
-    User Group - Everything about a group!
-
-    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:
-**********************************************************************/
-
-class Group {
-
-    var $id;
-    var $ht;
-
-    var $members;
-    var $departments;
-
-    function Group($id){
-
-        $this->id=0;
-        return $this->load($id);
-    }
-
-    function load($id=0) {
-
-        if(!$id && !($id=$this->getId()))
-            return false;
-
-        $sql='SELECT grp.*,grp.group_name as name, grp.group_enabled as isactive, count(staff.staff_id) as users '
-            .'FROM '.GROUP_TABLE.' grp '
-            .'LEFT JOIN '.STAFF_TABLE.' staff USING(group_id) '
-            .'WHERE grp.group_id='.db_input($id).' GROUP BY grp.group_id ';
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
-
-        $this->ht=db_fetch_array($res);
-        $this->id=$this->ht['group_id'];
-        $this->members=array();
-        $this->departments = array();
-
-        return $this->id;
-    }
-
-    function reload(){
-        return $this->load();
-    }
-
-    function getHashtable() {
-        return $this->ht;
-    }
-
-    function getInfo(){
-        return  $this->getHashtable();
-    }
-
-    function getId(){
-        return $this->id;
-    }
-
-    function getName(){
-        return $this->ht['name'];
-    }
-
-    function getNumUsers(){
-        return $this->ht['users'];
-    }
-
-
-    function isEnabled(){
-        return ($this->ht['isactive']);
-    }
-
-    function isActive(){
-        return $this->isEnabled();
-    }
- 
-    //Get members of the group.
-    function getMembers() {
-
-        if(!$this->members && $this->getNumUsers()) {
-            $sql='SELECT staff_id FROM '.STAFF_TABLE
-                .' WHERE group_id='.db_input($this->getId())
-                .' ORDER BY lastname, firstname';
-            if(($res=db_query($sql)) && db_num_rows($res)) {
-                while(list($id)=db_fetch_row($res))
-                    if(($staff=Staff::lookup($id)))
-                        $this->members[]= $staff;
-            }
-        }
-
-        return $this->members;
-    }
-
-    //Get departments the group is allowed to access.
-    function getDepartments() {
-
-        if(!$this->departments) {
-            $sql='SELECT dept_id FROM '.GROUP_DEPT_TABLE
-                .' WHERE group_id='.db_input($this->getId());
-            if(($res=db_query($sql)) && db_num_rows($res)) {
-                while(list($id)=db_fetch_row($res))
-                    $this->departments[]= $id;
-            }
-        }
-
-        return $this->departments;
-    }
-
-        
-    function updateDeptAccess($depts) {
-
-
-        if($depts && is_array($depts)) {
-            foreach($depts as $k=>$id) {
-                $sql='INSERT IGNORE INTO '.GROUP_DEPT_TABLE
-                    .' SET group_id='.db_input($this->getId())
-                    .', dept_id='.db_input($id);
-                db_query($sql);
-            }
-        }
-
-        $sql='DELETE FROM '.GROUP_DEPT_TABLE.' WHERE group_id='.db_input($this->getId());
-        if($depts && is_array($depts)) // just inserted departments IF any.
-            $sql.=' AND dept_id NOT IN('.implode(',', db_input($depts)).')';
-
-        db_query($sql);
-
-        return true;
-    }
-
-    function update($vars,&$errors) {
-
-        if(!Group::save($this->getId(),$vars,$errors))
-            return false;
-
-        $this->updateDeptAccess($vars['depts']);
-        $this->reload();
-        
-        return true;
-    }
-
-    function delete() {
-
-        //Can't delete with members
-        if($this->getNumUsers())
-            return false;
-
-        $res = db_query('DELETE FROM '.GROUP_TABLE.' WHERE group_id='.db_input($this->getId()).' LIMIT 1');
-        if(!$res || !db_affected_rows($res))
-            return false;
-
-        //Remove dept access entry.
-        db_query('DELETE FROM '.GROUP_DEPT_TABLE.' WHERE group_id='.db_input($this->getId()));
-
-        return true;
-    }
-
-    /*** Static functions ***/
-    function getIdByName($name){
-        $sql='SELECT group_id FROM '.GROUP_TABLE.' WHERE group_name='.db_input(trim($name));
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id)=db_fetch_row($res);
-
-        return $id;
-    }
-
-    function lookup($id){
-        return ($id && is_numeric($id) && ($g= new Group($id)) && $g->getId()==$id)?$g:null;
-    }
-
-    function create($vars, &$errors) { 
-        if(($id=self::save(0,$vars,$errors)) && ($group=self::lookup($id)))
-            $group->updateDeptAccess($vars['depts']);
-
-        return $id;
-    }
-
-    function save($id,$vars,&$errors) {
-        if($id && $vars['id']!=$id)
-            $errors['err']=__('Missing or invalid group ID');
-            
-        if(!$vars['name']) {
-            $errors['name']=__('Group name required');
-        }elseif(strlen($vars['name'])<3) {
-            $errors['name']=__('Group name must be at least 3 chars.');
-        }elseif(($gid=Group::getIdByName($vars['name'])) && $gid!=$id){
-            $errors['name']=__('Group name already exists');
-        }
-        
-        if($errors) return false;
-            
-        $sql=' SET updated=NOW() '
-            .', group_name='.db_input(Format::striptags($vars['name']))
-            .', group_enabled='.db_input($vars['isactive'])
-            .', can_create_tickets='.db_input($vars['can_create_tickets'])
-            .', can_delete_tickets='.db_input($vars['can_delete_tickets'])
-            .', can_edit_tickets='.db_input($vars['can_edit_tickets'])
-            .', can_assign_tickets='.db_input($vars['can_assign_tickets'])
-            .', can_transfer_tickets='.db_input($vars['can_transfer_tickets'])
-            .', can_close_tickets='.db_input($vars['can_close_tickets'])
-            .', can_ban_emails='.db_input($vars['can_ban_emails'])
-            .', can_manage_premade='.db_input($vars['can_manage_premade'])
-            .', can_manage_faq='.db_input($vars['can_manage_faq'])
-            .', can_post_ticket_reply='.db_input($vars['can_post_ticket_reply'])
-            .', can_view_staff_stats='.db_input($vars['can_view_staff_stats'])
-            .', notes='.db_input(Format::sanitize($vars['notes']));
-
-        if($id) {
-            
-            $sql='UPDATE '.GROUP_TABLE.' '.$sql.' WHERE group_id='.db_input($id);
-            if(($res=db_query($sql)))
-                return true;
-
-            $errors['err']=sprintf(__('Unable to update %s.'), __('this group'))
-               .' '.__('Internal error occurred');
-
-        }else{
-            $sql='INSERT INTO '.GROUP_TABLE.' '.$sql.',created=NOW()';
-            if(($res=db_query($sql)) && ($id=db_insert_id()))
-                return $id;
-
-            $errors['err']=sprintf(__('Unable to create %s.'), __('this group'))
-               .' '.__('Internal error occurred');
-        }
-
-        return false;
-    }
-}
-?>
diff --git a/include/class.http.php b/include/class.http.php
index 463387df54e148e0f164321159c203bd59c53540..2616121c24b130f365f58b2b10f1d1810be118c8 100644
--- a/include/class.http.php
+++ b/include/class.http.php
@@ -27,20 +27,26 @@ class Http {
         case 404: return '404 Not Found';
         case 405: return '405 Method Not Allowed';
         case 416: return '416 Requested Range Not Satisfiable';
+        case 418: return "418 I'm a teapot";
         case 422: return '422 Unprocessable Entity';
         default:  return '500 Internal Server Error';
         endswitch;
     }
 
-    function response($code,$content,$contentType='text/html',$charset='UTF-8') {
+    function response($code,$content=false,$contentType='text/html',$charset='UTF-8') {
 
         header('HTTP/1.1 '.Http::header_code_verbose($code));
 		header('Status: '.Http::header_code_verbose($code)."\r\n");
 		header("Connection: Close\r\n");
-		header("Content-Type: $contentType; charset=$charset\r\n");
-        header('Content-Length: '.strlen($content)."\r\n\r\n");
-       	print $content;
-        exit;
+        $ct = "Content-Type: $contentType";
+        if ($charset)
+            $ct .= "; charset=$charset";
+        header($ct);
+        if ($content) {
+            header('Content-Length: '.strlen($content)."\r\n\r\n");
+            print $content;
+            exit;
+        }
     }
 
     function redirect($url,$delay=0,$msg='') {
@@ -61,18 +67,20 @@ class Http {
         exit;
     }
 
-    function cacheable($etag, $modified, $ttl=3600) {
+    function cacheable($etag, $modified=false, $ttl=3600) {
         // Thanks, http://stackoverflow.com/a/1583753/1025836
         // 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);
+        if ($modified) {
+            $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('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) {
+        if (($modified && @strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) == $last_modified)
+            || @trim($_SERVER['HTTP_IF_NONE_MATCH'], '" ') == $etag) {
                 header("HTTP/1.1 304 Not Modified");
                 exit();
         }
@@ -114,8 +122,14 @@ class Http {
     }
 
     static function build_query($vars, $encode=true, $separator='&amp;') {
-        return http_build_query(
-                ($encode ? Format::htmlchars($vars) : $vars), '', $separator);
+
+        if (!$vars)
+            return '';
+
+        if ($encode)
+            $vars = Format::htmlchars($vars);
+
+        return http_build_query($vars, '', $separator);
     }
 }
 ?>
diff --git a/include/class.i18n.php b/include/class.i18n.php
index 4a0d138f295bec3586369127a16d255d3d51e61a..3954ea0d0765a6777afc396c30e87f9fac251c05 100644
--- a/include/class.i18n.php
+++ b/include/class.i18n.php
@@ -23,10 +23,10 @@ class Internationalization {
     // fallback
     var $langs = array('en_US');
 
-    function Internationalization($language=false) {
+    function __construct($language=false) {
         global $cfg;
 
-        if ($cfg && ($lang = $cfg->getSystemLanguage()))
+        if ($cfg && ($lang = $cfg->getPrimaryLanguage()))
             array_unshift($this->langs, $language);
 
         // Detect language filesystem path, case insensitively
@@ -49,22 +49,22 @@ class Internationalization {
     function loadDefaultData() {
         # notrans -- do not translate the contents of this array
         $models = array(
-            'department.yaml' =>    'Dept::create',
-            'sla.yaml' =>           'SLA::create',
+            'department.yaml' =>    'Dept::__create',
+            'sla.yaml' =>           'SLA::__create',
             'form.yaml' =>          'DynamicForm::create',
             'list.yaml' =>          'DynamicList::create',
             // Note that department, sla, and forms are required for
             // help_topic
-            'help_topic.yaml' =>    'Topic::create',
+            'help_topic.yaml' =>    'Topic::__create',
             'filter.yaml' =>        'Filter::create',
-            'team.yaml' =>          'Team::create',
+            'team.yaml' =>          'Team::__create',
             // Organization
             'organization.yaml' =>  'Organization::__create',
             // Ticket
-            'ticket_status.yaml' =>  'TicketStatus::__create',
-            // Note that group requires department
-            'group.yaml' =>         'Group::create',
-            'file.yaml' =>          'AttachmentFile::create',
+            'ticket_status.yaml' => 'TicketStatus::__create',
+            // Role
+            'role.yaml' =>          'Role::__create',
+            'file.yaml' =>          'AttachmentFile::__create',
             'sequence.yaml' =>      'Sequence::__create',
         );
 
@@ -127,7 +127,6 @@ class Internationalization {
             $sql = 'INSERT INTO '.PAGE_TABLE.' SET type='.db_input($type)
                 .', name='.db_input($page['name'])
                 .', body='.db_input($page['body'])
-                .', lang='.db_input($tpl->getLang())
                 .', notes='.db_input($page['notes'])
                 .', created=NOW(), updated=NOW(), isactive=1';
             if (db_query($sql) && ($id = db_insert_id())
@@ -137,19 +136,14 @@ class Internationalization {
         // Default Language
         $_config->set('system_language', $this->langs[0]);
 
-        // content_id defaults to the `id` field value
-        db_query('UPDATE '.PAGE_TABLE.' SET content_id=id');
-
         // Canned response examples
         if (($tpl = $this->getTemplate('templates/premade.yaml'))
                 && ($canned = $tpl->getData())) {
             foreach ($canned as $c) {
-                if (($id = Canned::create($c, $errors))
-                        && isset($c['attachments'])) {
-                    $premade = Canned::lookup($id);
-                    foreach ($c['attachments'] as $a) {
-                        $premade->attachments->save($a, false);
-                    }
+                if (!($premade = Canned::create($c)) || !$premade->save())
+                    continue;
+                if (isset($c['attachments'])) {
+                    $premade->attachments->upload($c['attachments']);
                 }
             }
         }
@@ -238,23 +232,62 @@ class Internationalization {
                     'phar' => substr($f, -5) == '.phar',
                     'code' => $base,
                 );
+                $installed[strtolower($base)]['flag'] = strtolower(
+                    $langs[$code]['flag'] ?: $locale ?: $code
+                );
             }
         }
-        uasort($installed, function($a, $b) { return strcasecmp($a['code'], $b['code']); });
+        ksort($installed);
 
         return $cache = $installed;
     }
 
+    static function isLanguageInstalled($code) {
+        $langs = self::availableLanguages();
+        return isset($langs[strtolower($code)]);
+    }
+
+    static function isLanguageEnabled($code) {
+        $langs = self::getConfiguredSystemLanguages();
+        return isset($langs[$code]);
+    }
+
+    static function getConfiguredSystemLanguages() {
+        global $cfg;
+        static $langs;
+
+        if (!$cfg)
+            return self::availableLanguages();
+
+        if (!isset($langs)) {
+            $langs = array();
+            $pri = $cfg->getPrimaryLanguage();
+            if ($info = self::getLanguageInfo($pri))
+                $langs = array($pri => $info);
+
+            // Honor sorting preference of ::availableLanguages()
+            foreach ($cfg->getSecondaryLanguages() as $l) {
+                if ($info = self::getLanguageInfo($l))
+                    $langs[$l] = $info;
+            }
+        }
+        return $langs;
+    }
+
     // TODO: Move this to the REQUEST class or some middleware when that
     // exists.
     // Algorithm borrowed from Drupal 7 (locale.inc)
     static function getDefaultLanguage() {
         global $cfg;
+        static $lang;
+
+        if (isset($lang))
+            return $lang;
 
         if (empty($_SERVER["HTTP_ACCEPT_LANGUAGE"]))
-            return $cfg->getSystemLanguage();
+            return $cfg ? $cfg->getPrimaryLanguage() : 'en_US';
 
-        $languages = self::availableLanguages();
+        $languages = self::getConfiguredSystemLanguages();
 
         // The Accept-Language header contains information about the
         // language preferences configured in the user's browser / operating
@@ -328,7 +361,9 @@ class Internationalization {
           }
         }
 
-        return $best_match_langcode;
+        return $lang = self::isLanguageInstalled($best_match_langcode)
+            ? $best_match_langcode
+            : $cfg->getPrimaryLanguage();
     }
 
     static function getCurrentLanguage($user=false) {
@@ -336,12 +371,45 @@ class Internationalization {
 
         $user = $user ?: $thisstaff ?: $thisclient;
         if ($user && method_exists($user, 'getLanguage'))
-            return $user->getLanguage();
-        if (isset($_SESSION['client:lang']))
-            return $_SESSION['client:lang'];
+            if (($lang = $user->getLanguage()) && self::isLanguageEnabled($lang))
+                return $lang;
+
+        // Support the flag buttons for guests
+        if ((!$user || $user != $thisstaff) && $_SESSION['::lang'])
+            return $_SESSION['::lang'];
+
         return self::getDefaultLanguage();
     }
 
+    static function getCurrentLocale($user=false) {
+        global $thisstaff, $cfg;
+
+        if ($user) {
+            return self::getCurrentLanguage($user);
+        }
+        // FIXME: Move this majic elsewhere - see upgrade bug note in
+        // class.staff.php
+        if ($thisstaff) {
+            return $thisstaff->getLocale()
+                ?: self::getCurrentLanguage($thisstaff);
+        }
+
+        if (!($locale = $cfg->getDefaultLocale()))
+            $locale = self::getCurrentLanguage();
+
+        return $locale;
+    }
+
+    static function rfc1766($what) {
+        if (is_array($what))
+            return array_map(array(get_called_class(), 'rfc1766'), $what);
+
+        $lr = explode('_', $what);
+        if (isset($lr[1]))
+            $lr[1] = strtoupper($lr[1]);
+        return implode('-', $lr);
+    }
+
     static function getTtfFonts() {
         if (!class_exists('Phar'))
             return;
@@ -365,6 +433,55 @@ class Internationalization {
         return $rv;
     }
 
+    static function setCurrentLanguage($lang) {
+        if (!self::isLanguageInstalled($lang))
+            return false;
+
+        $_SESSION['::lang'] = $lang ?: null;
+        return true;
+    }
+
+    static function allLocales() {
+        $locales = array();
+        if (class_exists('ResourceBundle')) {
+            $current_lang = self::getCurrentLanguage();
+            $langs = array();
+            foreach (self::getConfiguredSystemLanguages() as $code=>$info) {
+                list($lang,) = explode('_', $code, 2);
+                $langs[$lang] = true;
+            }
+            foreach (ResourceBundle::getLocales('') as $code) {
+                list($lang,) = explode('_', $code, 2);
+                if (isset($langs[$lang])) {
+                    $locales[$code] = Locale::getDisplayName($code, $current_lang);
+                }
+            }
+        }
+        return $locales;
+    }
+
+    static function sortKeyedList($list, $case=false) {
+        global $cfg;
+
+        if ($cfg && function_exists('collator_create')) {
+            $coll = Collator::create($cfg->getPrimaryLanguage());
+            if (!$case)
+                $coll->setStrength(Collator::TERTIARY);
+            // UASORT is necessary to preserve the keys
+            uasort($list, function($a, $b) use ($coll) {
+                return $coll->compare($a, $b); });
+        }
+        else {
+            if (!$case)
+                uasort($list, function($a, $b) {
+                    return strcmp(mb_strtoupper($a), mb_strtoupper($b)); });
+            else
+                // Really only works on ascii names
+                asort($list);
+        }
+        return $list;
+    }
+
     static function bootstrap() {
 
         require_once INCLUDE_DIR . 'class.translation.php';
@@ -423,7 +540,7 @@ class DataTemplate {
      * template itself does not have to keep track of the language for which
      * it is defined.
      */
-    function DataTemplate($path, $langs=array('en_US')) {
+    function __construct($path, $langs=array('en_US')) {
         foreach ($langs as $l) {
             if (file_exists("{$this->base}/$l/$path")) {
                 $this->lang = $l;
diff --git a/include/class.import.php b/include/class.import.php
new file mode 100644
index 0000000000000000000000000000000000000000..dc9a027c1d4af5606a6f9f2df293f9e097d5b72f
--- /dev/null
+++ b/include/class.import.php
@@ -0,0 +1,194 @@
+<?php
+/*********************************************************************
+    class.import.php
+
+    Utilities for importing objects and data (usually via CSV)
+
+    Peter Rotich <peter@osticket.com>
+    Jared Hancock <jared@osticket.com>
+    Copyright (c)  2006-2015 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:
+**********************************************************************/
+
+class ImportError extends Exception {}
+class ImportDataError extends ImportError {}
+
+class CsvImporter {
+    var $stream;
+
+    function __construct($stream) {
+        // File upload
+        if (is_array($stream) && !$stream['error']) {
+            // Properly detect Macintosh style line endings
+            ini_set('auto_detect_line_endings', true);
+            $this->stream = fopen($stream['tmp_name'], 'r');
+        }
+        // Open file
+        elseif (is_resource($stream)) {
+            $this->stream = $stream;
+        }
+        // Text from standard-in
+        elseif (is_string($stream)) {
+            $this->stream = fopen('php://temp', 'w+');
+            fwrite($this->stream, $stream);
+            rewind($this->stream);
+        }
+        else {
+            throw new ImportError(__('Unable to parse submitted csv: ').print_r($stream, true));
+        }
+    }
+
+    function __destruct() {
+        fclose($this->stream);
+    }
+
+    function importCsv($all_fields=array(), $defaults=array()) {
+        $named_fields = array();
+        $has_header = true;
+        foreach ($all_fields as $f)
+            if ($f->get('name'))
+                $named_fields[$f->get('name')] = $f;
+
+        // Read the first row and see if it is a header or not
+        if (!($data = fgetcsv($this->stream, 1000, ",")))
+            throw new ImportError(__('Whoops. Perhaps you meant to send some CSV records'));
+
+        $headers = array();
+        foreach ($data as $h) {
+            $h = trim($h);
+            $found = false;
+            foreach ($all_fields as $f) {
+                if (in_array(mb_strtolower($h), array(
+                        mb_strtolower($f->get('name')), mb_strtolower($f->get('label'))))) {
+                    $found = true;
+                    if (!$f->get('name'))
+                        throw new ImportError(sprintf(__(
+                            '%s: Field must have `variable` set to be imported'), $h));
+                    $headers[$f->get('name')] = $f->get('label');
+                    break;
+                }
+            }
+            if (!$found) {
+                $has_header = false;
+                if (count($data) == count($named_fields)) {
+                    // Number of fields in the user form matches the number
+                    // of fields in the data. Assume things line up
+                    $headers = array();
+                    foreach ($named_fields as $f)
+                        $headers[$f->get('name')] = $f->get('label');
+                    break;
+                }
+                else {
+                    throw new ImportError(sprintf(
+                                __('%s: Unable to map header to the object field'), $h));
+                }
+            }
+        }
+
+        if (!$has_header)
+            fseek($this->stream, 0);
+
+        $objects = $fields = array();
+        foreach ($headers as $h => $label) {
+            if (!isset($named_fields[$h]))
+                continue;
+
+            $f = $named_fields[$h];
+            $name = $f->get('name');
+            $fields[$name] = $f;
+        }
+
+        // Add default fields (org_id, etc).
+        foreach ($defaults as $key => $val) {
+            // Don't apply defaults which are also being imported
+            if (isset($header[$key]))
+                unset($defaults[$key]);
+        }
+
+        // Avoid reading the entire CSV before yielding the first record.
+        // Use an iterator. This will also allow row-level errors to be
+        // continuable such that the rows with errors can be handled and the
+        // iterator can continue with the next row.
+        return new CsvImportIterator($this->stream, $headers, $fields, $defaults);
+    }
+}
+
+class CsvImportIterator
+implements Iterator {
+    var $stream;
+    var $start = 0;
+    var $headers;
+    var $fields;
+    var $defaults;
+
+    var $current = true;
+    var $row = 0;
+
+    function __construct($stream, $headers, $fields, $defaults) {
+        $this->stream = $stream;
+        $this->start = ftell($stream);
+        $this->headers = $headers;
+        $this->fields = $fields;
+        $this->defaults = $defaults;
+    }
+
+    // Iterator interface -------------------------------------
+    function rewind() {
+        @fseek($this->stream, $this->start);
+        if (ftell($this->stream) != $this->start)
+            throw new RuntimeException('Stream cannot be rewound');
+        $this->row = 0;
+        $this->next();
+    }
+    function valid() {
+        return $this->current;
+    }
+    function current() {
+        return $this->current;
+    }
+    function key() {
+        return $this->row;
+    }
+
+    function next() {
+        do {
+            if (($csv = fgetcsv($this->stream, 4096, ",")) === false) {
+                // Read error
+                $this->current = false;
+                break;
+            }
+
+            if (count($csv) == 1 && $csv[0] == null)
+                // Skip empty rows
+                continue;
+            elseif (count($csv) != count($this->headers))
+                throw new ImportDataError(sprintf(__('Bad data. Expected: %s'),
+                    implode(', ', $this->headers)));
+
+            // Validate according to field configuration
+            $i = 0;
+            $this->current = $this->defaults;
+            foreach ($this->headers as $h => $label) {
+                $f = $this->fields[$h];
+                $f->_errors = array();
+                $T = $f->parse($csv[$i]);
+                if ($f->validateEntry($T) && $f->errors())
+                    throw new ImportDataError(sprintf(__(
+                        /* 1 will be a field label, and 2 will be error messages */
+                        '%1$s: Invalid data: %2$s'),
+                        $label, implode(', ', $f->errors())));
+                // Convert to database format
+                $this->current[$h] = $f->to_database($T);
+                $i++;
+            }
+        }
+        // Use the do-loop only for the empty line skipping
+        while (false);
+        $this->row++;
+    }
+}
diff --git a/include/class.knowledgebase.php b/include/class.knowledgebase.php
index 9ee15bdac03c431489c55adad7b800884b0b8a66..8c4b010250151d16bbd4a198e910072aab9b8876 100644
--- a/include/class.knowledgebase.php
+++ b/include/class.knowledgebase.php
@@ -17,7 +17,7 @@ require_once("class.file.php");
 
 class Knowledgebase {
 
-    function Knowledgebase($id) {
+    function __construct($id) {
         $res=db_query(
             'SELECT title, isenabled, dept_id, created, updated '
            .'FROM '.CANNED_TABLE.' WHERE canned_id='.db_input($id));
@@ -35,7 +35,7 @@ class Knowledgebase {
     /* ------------------> Getter methods <--------------------- */
     function getTitle() { return $this->title; }
     function isEnabled() { return !!$this->enabled; }
-    function getAnswer() { 
+    function getAnswer() {
         if (!isset($this->answer)) {
             if ($res=db_query('SELECT answer FROM '.CANNED_TABLE
                     .' WHERE canned_id='.db_input($this->id))) {
@@ -92,14 +92,14 @@ class Knowledgebase {
     }
 
     /* -------------> Database access methods <----------------- */
-    function update() { 
+    function update() {
         if (!@$this->validate()) return false;
         db_query(
             'UPDATE '.CANNED_TABLE.' SET title='.db_input($this->title)
                 .', isenabled='.db_input($this->enabled)
                 .', dept_id='.db_input($this->department)
                 .', updated=NOW()'
-                .((isset($this->answer)) 
+                .((isset($this->answer))
                     ? ', answer='.db_input($this->answer) : '')
                 .' WHERE canned_id='.db_input($this->id));
         return db_affected_rows() == 1;
diff --git a/include/class.list.php b/include/class.list.php
index a54277533d6759711bfe768ba2601672fcd2ea05..84322f7006435453f857ee5ca82dd984b4555eff 100644
--- a/include/class.list.php
+++ b/include/class.list.php
@@ -14,8 +14,8 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
-
 require_once(INCLUDE_DIR .'class.dynamic_forms.php');
+require_once(INCLUDE_DIR .'class.variable.php');
 
 /**
  * Interface for Custom Lists
@@ -38,9 +38,12 @@ interface CustomList {
 
     function getItem($id);
     function addItem($vars, &$errors);
+    function isItemUnique($vars);
 
     function getForm(); // Config form
     function hasProperties();
+    function getConfigurationForm();
+    function getSummaryFields();
 
     function getSortModes();
     function getSortMode();
@@ -66,8 +69,6 @@ interface CustomListItem {
     function getSortOrder();
 
     function getConfiguration();
-    function getConfigurationForm($source=null);
-
 
     function isEnabled();
     function isDeletable();
@@ -123,6 +124,16 @@ abstract class CustomListHandler {
     abstract function getItems($criteria);
     abstract function getItem($id);
     abstract function addItem($vars, &$errors);
+
+    static protected $registry = array();
+    static function forList(/* CustomList */ $list) {
+        if ($list->type && ($handler = static::$registry[$list->type]))
+            return new $handler($list);
+        return $list;
+    }
+    static function register($type, $handler) {
+        static::$registry[$type] = $handler;
+    }
 }
 
 /**
@@ -135,6 +146,11 @@ class DynamicList extends VerySimpleModel implements CustomList {
         'table' => LIST_TABLE,
         'ordering' => array('name'),
         'pk' => array('id'),
+        'joins' => array(
+            'items' => array(
+                'reverse' => 'DynamicListItem.list',
+            ),
+        ),
     );
 
     // Required fields
@@ -148,12 +164,6 @@ class DynamicList extends VerySimpleModel implements CustomList {
 
     var $_items;
     var $_form;
-    var $_config;
-
-    function __construct() {
-        call_user_func_array(array('parent', '__construct'), func_get_args());
-        $this->_config = new Config('list.'.$this->getId());
-    }
 
     function getId() {
         return $this->get('id');
@@ -188,14 +198,14 @@ class DynamicList extends VerySimpleModel implements CustomList {
     }
 
     function getName() {
-        return $this->get('name');
+        return $this->getLocal('name');
     }
 
     function getPluralName() {
-        if ($name = $this->get('name_plural'))
+        if ($name = $this->getLocal('name_plural'))
             return $name;
         else
-            return $this->get('name') . 's';
+            return $this->getName() . 's';
     }
 
     function getItemCount() {
@@ -213,6 +223,15 @@ class DynamicList extends VerySimpleModel implements CustomList {
                 ->order_by($this->getListOrderBy());
     }
 
+    function search($q) {
+        $items = clone $this->getAllItems();
+        return $items->filter(Q::any(array(
+            'value__startswith' => $q,
+            'extra__contains' => $q,
+            'properties__contains' => '"'.$q,
+        )));
+    }
+
     function getItems($limit=false, $offset=false) {
         if (!$this->_items) {
             $this->_items = DynamicListItem::objects()->filter(
@@ -227,36 +246,48 @@ class DynamicList extends VerySimpleModel implements CustomList {
         return $this->_items;
     }
 
+    function getItem($val, $extra=false) {
 
+        $items = DynamicListItem::objects()->filter(
+                array('list_id' => $this->getId()));
 
-    function getItem($val) {
-
-        $criteria = array('list_id' => $this->getId());
         if (is_int($val))
-            $criteria['id'] = $val;
+            $items->filter(array('id' => $val));
+        elseif ($extra)
+            $items->filter(array('extra' => $val));
         else
-            $criteria['value'] = $val;
+            $items->filter(array('value' => $val));
 
-         return DynamicListItem::lookup($criteria);
+
+        return $items->first();
     }
 
     function addItem($vars, &$errors) {
+        if (($item=$this->getItem($vars['value'])))
+            return $item;
 
         $item = DynamicListItem::create(array(
             'status' => 1,
             'list_id' => $this->getId(),
             'sort'  => $vars['sort'],
             'value' => $vars['value'],
-            'extra' => $vars['abbrev']
+            'extra' => $vars['extra']
         ));
-
-        $item->save();
-
         $this->_items = false;
 
         return $item;
     }
 
+    function isItemUnique($data) {
+        try {
+            $this->getItems()->filter(array('value'=>$data['value']))->one();
+            return false;
+        }
+        catch (DoesNotExist $e) {
+            return true;
+        }
+    }
+
     function getConfigurationForm($autocreate=false) {
         if (!$this->_form) {
             $this->_form = DynamicForm::lookup(array('type'=>'L'.$this->getId()));
@@ -269,6 +300,44 @@ class DynamicList extends VerySimpleModel implements CustomList {
         return $this->_form;
     }
 
+    function getListItemBasicForm($source=null, $item=false) {
+        return new SimpleForm(array(
+            'value' => new TextboxField(array(
+                'required' => true,
+                'label' => __('Value'),
+                'configuration' => array(
+                    'translatable' => $item ? $item->getTranslateTag('value') : false,
+                    'size' => 60,
+                    'length' => 0,
+                    'autofocus' => true,
+                ),
+            )),
+            'extra' => new TextboxField(array(
+                'label' => __('Abbreviation'),
+                'configuration' => array(
+                    'size' => 60,
+                    'length' => 0,
+                ),
+            )),
+        ), $source);
+    }
+
+    // Fields shown on the list items page
+    function getSummaryFields() {
+        $prop_fields = array();
+        foreach ($this->getConfigurationForm()->getFields() as $f) {
+            if (in_array($f->get('type'), array('text', 'datetime', 'phone')))
+                $prop_fields[] = $f;
+            if (strpos($f->get('type'), 'list-') === 0)
+                $prop_fields[] = $f;
+
+            // 4 property columns max
+            if (count($prop_fields) == 4)
+                break;
+        }
+        return $prop_fields;
+    }
+
     function isDeleteable() {
         return !$this->hasMask(static::MASK_DELETE);
     }
@@ -312,7 +381,16 @@ class DynamicList extends VerySimpleModel implements CustomList {
     }
 
     function getConfiguration() {
-        return JsonDataParser::parse($this->_config->get('configuration'));
+        return JsonDataParser::parse($this->configuration);
+    }
+
+    function getTranslateTag($subtag) {
+        return _H(sprintf('list.%s.%s', $subtag, $this->id));
+    }
+    function getLocal($subtag) {
+        $tag = $this->getTranslateTag($subtag);
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : $this->get($subtag);
     }
 
     function update($vars, &$errors) {
@@ -346,11 +424,20 @@ class DynamicList extends VerySimpleModel implements CustomList {
     function delete() {
         $fields = DynamicFormField::objects()->filter(array(
             'type'=>'list-'.$this->id))->count();
-        if ($fields == 0)
-            return parent::delete();
-        else
-            // Refuse to delete lists that are in use by fields
+
+        // Refuse to delete lists that are in use by fields
+        if ($fields != 0)
             return false;
+
+        if (!parent::delete())
+            return false;
+
+        if (($form = $this->getForm(false))) {
+            $form->delete(false);
+            $form->fields->delete();
+        }
+
+        return true;
     }
 
     private function createForm() {
@@ -387,7 +474,11 @@ class DynamicList extends VerySimpleModel implements CustomList {
     }
 
     static function create($ht=false, &$errors=array()) {
-        $inst = parent::create($ht);
+        if (isset($ht['configuration'])) {
+            $ht['configuration'] = JsonDataEncoder::encode($ht['configuration']);
+        }
+
+        $inst = new static($ht);
         $inst->set('created', new SqlFunction('NOW'));
 
         if (isset($ht['properties'])) {
@@ -397,12 +488,6 @@ class DynamicList extends VerySimpleModel implements CustomList {
             $form->save();
         }
 
-        if (isset($ht['configuration'])) {
-            $inst->save();
-            $c = new Config('list.'.$inst->getId());
-            $c->set('configuration', JsonDataEncoder::encode($ht['configuration']));
-        }
-
         if (isset($ht['items'])) {
             $inst->save();
             foreach ($ht['items'] as $i) {
@@ -438,6 +523,74 @@ class DynamicList extends VerySimpleModel implements CustomList {
         return $selections;
     }
 
+   function importCsv($stream, $defaults=array()) {
+        require_once INCLUDE_DIR . 'class.import.php';
+
+        $form = $this->getConfigurationForm();
+        $fields = array(
+            'value' => new TextboxField(array(
+                'label' => __('Value'),
+                'name' => 'value',
+                'configuration' => array(
+                    'length' => 0,
+                ),
+            )),
+            'abbrev' => new TextboxField(array(
+                'name' => 'extra',
+                'label' => __('Abbreviation'),
+                'configuration' => array(
+                    'length' => 0,
+                ),
+            )),
+        );
+
+        $form = $this->getConfigurationForm();
+        if ($form && ($custom_fields = $form->getFields())
+                && count($custom_fields)) {
+            foreach ($custom_fields as $f)
+                if ($f->get('name'))
+                    $fields[$f->get('name')] = $f;
+        }
+
+        $importer = new CsvImporter($stream);
+        $imported = 0;
+        try {
+            db_autocommit(false);
+            $records = $importer->importCsv($fields, $defaults);
+            foreach ($records as $data) {
+                $errors = array();
+                $item = $this->addItem($data, $errors);
+                if ($item && $item->setConfiguration($data, $errors))
+                    $imported++;
+                else
+                    echo sprintf(__('Unable to import item: %s'), print_r($data, true));
+            }
+            db_autocommit(true);
+        }
+        catch (Exception $ex) {
+            db_rollback();
+            return $ex->getMessage();
+        }
+        return $imported;
+    }
+
+    function importFromPost($stuff, $extra=array()) {
+        if (is_array($stuff) && !$stuff['error']) {
+            // Properly detect Macintosh style line endings
+            ini_set('auto_detect_line_endings', true);
+            $stream = fopen($stuff['tmp_name'], 'r');
+        }
+        elseif ($stuff) {
+            $stream = fopen('php://temp', 'w+');
+            fwrite($stream, $stuff);
+            rewind($stream);
+        }
+        else {
+            return __('Unable to parse submitted items');
+        }
+
+        return self::importCsv($stream, $extra);
+    }
 }
 FormField::addFieldTypes(/* @trans */ 'Custom Lists', array('DynamicList', 'getSelections'));
 
@@ -520,7 +673,7 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem {
     }
 
     function getValue() {
-        return $this->get('value');
+        return $this->getLocal('value');
     }
 
     function getAbbrev() {
@@ -542,23 +695,25 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem {
         return $this->_config;
     }
 
-    function setConfiguration(&$errors=array()) {
+    function setConfiguration($vars, &$errors=array()) {
         $config = array();
-        foreach ($this->getConfigurationForm($_POST)->getFields() as $field) {
+        foreach ($this->getConfigurationForm($vars)->getFields() as $field) {
             $config[$field->get('id')] = $field->to_php($field->getClean());
             $errors = array_merge($errors, $field->errors());
         }
-        if (count($errors) === 0)
-            $this->set('properties', JsonDataEncoder::encode($config));
 
-        return count($errors) === 0;
+        if ($errors)
+            return false;
+
+        $this->set('properties', JsonDataEncoder::encode($config));
+
+        return $this->save();
     }
 
     function getConfigurationForm($source=null) {
         if (!$this->_form) {
             $config = $this->getConfiguration();
-            $this->_form = DynamicForm::lookup(
-                array('type'=>'L'.$this->get('list_id')))->getForm($source);
+            $this->_form = $this->list->getForm()->getForm($source);
             if (!$source && $config) {
                 $fields = $this->_form->getFields();
                 foreach ($fields as $f) {
@@ -583,7 +738,7 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem {
         $name = mb_strtolower($name);
         foreach ($this->getConfigurationForm()->getFields() as $field) {
             if (mb_strtolower($field->get('name')) == $name)
-                return $config[$field->get('id')];
+                return $field->asVar($config[$field->get('id')]);
         }
     }
 
@@ -596,6 +751,15 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem {
         return $data;
     }
 
+    function getTranslateTag($subtag) {
+        return _H(sprintf('listitem.%s.%s', $subtag, $this->id));
+    }
+    function getLocal($subtag) {
+        $tag = $this->getTranslateTag($subtag);
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : $this->get($subtag);
+    }
+
     function toString() {
         return $this->get('value');
     }
@@ -634,8 +798,7 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem {
         if (isset($ht['properties']) && is_array($ht['properties']))
             $ht['properties'] = JsonDataEncoder::encode($ht['properties']);
 
-        $inst = parent::create($ht);
-        $inst->save(true);
+        $inst = new static($ht);
 
         // Auto-config properties if any
         if ($ht['configuration'] && is_array($ht['configuration'])) {
@@ -690,6 +853,14 @@ class TicketStatusList extends CustomListHandler {
         return $this->_items;
     }
 
+    function search($q) {
+        $items = clone $this->getAllItems();
+        return $items->filter(Q::any(array(
+            'name__startswith' => $q,
+            'properties__contains' => '"'.$q,
+        )));
+    }
+
     function getItems($criteria = array()) {
 
         // Default to only enabled items
@@ -726,20 +897,28 @@ class TicketStatusList extends CustomListHandler {
     }
 
     function addItem($vars, &$errors) {
-
         $item = TicketStatus::create(array(
             'mode' => 1,
             'flags' => 0,
             'sort'  => $vars['sort'],
-            'name' => $vars['value'],
+            'name' => $vars['name'],
         ));
-        $item->save();
-
         $this->_items = false;
 
         return $item;
     }
 
+    function isItemUnique($data) {
+        try {
+            $this->getItems()->filter(array('name'=>$data['name']))->one();
+            return false;
+        }
+        catch (DoesNotExist $e) {
+            return true;
+        }
+    }
+
+
     static function getStatuses($criteria=array()) {
 
         $statuses = array();
@@ -774,60 +953,8 @@ class TicketStatusList extends CustomListHandler {
 
         return $o[0];
     }
-}
-
-class TicketStatus  extends VerySimpleModel implements CustomListItem {
-
-    static $meta = array(
-        'table' => TICKET_STATUS_TABLE,
-        'ordering' => array('name'),
-        'pk' => array('id'),
-        'joins' => array(
-            'tickets' => array(
-                'reverse' => 'TicketModel.status',
-                )
-        )
-    );
-
-    var $_list;
-    var $_form;
-    var $_settings;
-    var $_properties;
-
-    const ENABLED   = 0x0001;
-    const INTERNAL  = 0x0002; // Forbid deletion or name and status change.
-
-    function __construct() {
-        call_user_func_array(array('parent', '__construct'), func_get_args());
-    }
-
-    protected function hasFlag($field, $flag) {
-        return 0 !== ($this->get($field) & $flag);
-    }
-
-    protected function clearFlag($field, $flag) {
-        return $this->set($field, $this->get($field) & ~$flag);
-    }
-
-    protected function setFlag($field, $flag) {
-        return $this->set($field, $this->get($field) | $flag);
-    }
-
-    protected function hasProperties() {
-        return ($this->get('properties'));
-    }
-
-    function getForm() {
-        if (!$this->_form && $this->_list) {
-            $this->_form = DynamicForm::lookup(
-                array('type'=>'L'.$this->_list->getId()));
-        }
-        return $this->_form;
-    }
 
     function getExtraConfigOptions($source=null) {
-
-
         $status_choices = array( 0 => __('System Default'));
         if (($statuses=TicketStatusList::getStatuses(
                         array( 'enabled' => true, 'states' =>
@@ -835,10 +962,10 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
             foreach ($statuses as $s)
                 $status_choices[$s->getId()] = $s->getName();
 
-
         return array(
             'allowreopen' => new BooleanField(array(
                 'label' =>__('Allow Reopen'),
+                'editable' => true,
                 'default' => isset($source['allowreopen'])
                     ?  $source['allowreopen']: true,
                 'id' => 'allowreopen',
@@ -853,6 +980,7 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
             )),
             'reopenstatus' => new ChoiceField(array(
                 'label' => __('Reopen Status'),
+                'editable' => true,
                 'required' => false,
                 'default' => isset($source['reopenstatus'])
                     ? $source['reopenstatus'] : 0,
@@ -872,11 +1000,9 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
     }
 
     function getConfigurationForm($source=null) {
-
         if (!($form = $this->getForm()))
             return null;
 
-        $config = $this->getConfiguration();
         $form = $form->getForm($source);
         $fields = $form->getFields();
         foreach ($fields as $k => $f) {
@@ -886,23 +1012,100 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
                     $extra->setForm($form);
                     $fields->insert(++$k, $extra);
                 }
-                break;
             }
-        }
 
-        if (!$source && $config) {
-            foreach ($fields as $f) {
-                $name = $f->get('id');
-                if (isset($config[$name]))
-                    $f->value = $f->to_php($config[$name]);
-                else if ($f->get('default'))
-                    $f->value = $f->get('default');
-            }
+            if (!isset($f->ht['editable']))
+                $f->ht['editable'] = true;
         }
 
+        // Enable selection and display of private states
+        $form->getField('state')->options['private_too'] = true;
+
         return $form;
     }
 
+    function getListItemBasicForm($source=null, $item=false) {
+        return new SimpleForm(array(
+            'name' => new TextboxField(array(
+                'required' => true,
+                'label' => __('Value'),
+                'configuration' => array(
+                    'translatable' => $item ? $item->getTranslateTag('value') : false,
+                    'size' => 60,
+                    'length' => 0,
+                    'autofocus' => true,
+                ),
+            )),
+            'extra' => new TextboxField(array(
+                'label' => __('Abbreviation'),
+                'configuration' => array(
+                    'size' => 60,
+                    'length' => 0,
+                ),
+            )),
+        ), $source);
+    }
+
+    function getSummaryFields() {
+        // Like the main one, except the description and state fields are
+        // welcome on the screen
+        $prop_fields = array();
+        foreach ($this->getConfigurationForm()->getFields() as $f) {
+            if (in_array($f->get('type'), array('state', 'text', 'datetime', 'phone')))
+                $prop_fields[] = $f;
+            elseif (strpos($f->get('type'), 'list-') === 0)
+                $prop_fields[] = $f;
+            elseif ($f->get('name') == 'description')
+                $prop_fields[] = $f;
+
+            // 4 property columns max
+            if (count($prop_fields) == 4)
+                break;
+        }
+        return $prop_fields;
+    }
+}
+CustomListHandler::register('ticket-status', 'TicketStatusList');
+
+class TicketStatus
+extends VerySimpleModel
+implements CustomListItem, TemplateVariable {
+
+    static $meta = array(
+        'table' => TICKET_STATUS_TABLE,
+        'ordering' => array('name'),
+        'pk' => array('id'),
+        'joins' => array(
+            'tickets' => array(
+                'reverse' => 'TicketModel.status',
+                )
+        )
+    );
+
+    var $_list;
+    var $_form;
+    var $_settings;
+    var $_properties;
+
+    const ENABLED   = 0x0001;
+    const INTERNAL  = 0x0002; // Forbid deletion or name and status change.
+
+    protected function hasFlag($field, $flag) {
+        return 0 !== ($this->get($field) & $flag);
+    }
+
+    protected function clearFlag($field, $flag) {
+        return $this->set($field, $this->get($field) & ~$flag);
+    }
+
+    protected function setFlag($field, $flag) {
+        return $this->set($field, $this->get($field) | $flag);
+    }
+
+    protected function hasProperties() {
+        return ($this->get('properties'));
+    }
+
     function isEnabled() {
         return $this->hasFlag('mode', self::ENABLED);
     }
@@ -997,6 +1200,9 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
     function getValue() {
         return $this->getName();
     }
+    function getLocalName() {
+        return $this->getLocal('value', $this->getName());
+    }
 
     function getAbbrev() {
         return '';
@@ -1019,6 +1225,64 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
         return $this->_properties;
     }
 
+    function getTranslateTag($subtag) {
+        return _H(sprintf('status.%s.%s', $subtag, $this->id));
+    }
+    function getLocal($subtag, $default) {
+        $tag = $this->getTranslateTag($subtag);
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : $default;
+    }
+    static function getLocalById($id, $subtag, $default) {
+        $tag = _H(sprintf('status.%s.%s', $subtag, $id));
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : $default;
+    }
+
+    // TemplateVariable interface
+    static function getVarScope() {
+        $base = array(
+            'name' => __('Status label'),
+            'state' => __('State name (e.g. open or closed)'),
+        );
+        return $base;
+    }
+
+    function getList() {
+        if (!isset($this->_list))
+            $this->_list = DynamicList::lookup(array('type' => 'ticket-status'));
+        return $this->_list;
+    }
+
+    function getConfigurationForm($source=null) {
+        if (!$this->_form) {
+            $config = $this->getConfiguration();
+            // Forcefully retain state for internal statuses
+            if ($source && $this->isInternal())
+                $source['state'] = $this->getState();
+            $this->_form = $this->getList()->getConfigurationForm($source);
+            if (!$source && $config) {
+                $fields = $this->_form->getFields();
+                foreach ($fields as $f) {
+                    $val = $config[$f->get('id')] ?: $config[$f->get('name')];
+                    if (isset($val))
+                        $f->value = $f->to_php($val);
+                    elseif ($f->get('default'))
+                        $f->value = $f->get('default');
+                }
+            }
+
+            if ($this->isInternal()
+                    && ($f=$this->_form->getField('state'))) {
+                $f->ht['required'] = $f->ht['editable'] = false;
+                $f->options['render_mode'] = 'view';
+            }
+
+        }
+
+        return $this->_form;
+    }
+
     function getConfiguration() {
 
         if (!$this->_settings) {
@@ -1026,8 +1290,8 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
             if (!$this->_settings)
                 $this->_settings = array();
 
-            if ($this->getForm()) {
-                foreach ($this->getForm()->getFields() as $f)  {
+            if ($form = $this->getList()->getForm()) {
+                foreach ($form->getFields() as $f)  {
                     $name = mb_strtolower($f->get('name'));
                     $id = $f->get('id');
                     switch($name) {
@@ -1050,12 +1314,12 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
         return $this->_settings;
     }
 
-    function setConfiguration(&$errors=array()) {
+    function setConfiguration($vars, &$errors=array()) {
         $properties = array();
-        foreach ($this->getConfigurationForm($_POST)->getFields() as $f) {
-            if ($this->isInternal() //Item is internal.
-                    && !$f->isEditable())
-                continue;
+        foreach ($this->getConfigurationForm($vars)->getFields() as $f) {
+            // Only bother with editable fields
+            if (!$f->isEditable()) continue;
+
             $val = $f->getClean();
             $errors = array_merge($errors, $f->errors());
             if ($f->errors()) continue;
@@ -1076,6 +1340,10 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
                     }
                     break;
                 case 'state':
+                    // Internal statuses cannot change state
+                    if ($this->isInternal())
+                        break;
+
                     if ($val)
                         $this->set('state', $val);
                     else
@@ -1100,14 +1368,12 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
     }
 
     function update($vars, &$errors) {
-
-        $fields = array('value' => 'name', 'sort' => 'sort');
-        foreach($fields as $k => $v) {
+        $fields = array('name', 'sort');
+        foreach($fields as $k) {
             if (isset($vars[$k]))
-                $this->set($v, $vars[$k]);
+                $this->set($k, $vars[$k]);
         }
-
-        return $this->save(true);
+        return $this->save();
     }
 
     function delete() {
@@ -1120,20 +1386,22 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
     }
 
     function __toString() {
-        return __($this->getName());
+        return $this->getName();
     }
 
-    static function create($ht) {
+    static function create($ht=false) {
+        if (!is_array($ht))
+            return null;
 
         if (!isset($ht['mode']))
             $ht['mode'] = 1;
 
         $ht['created'] = new SqlFunction('NOW');
 
-        return  parent::create($ht);
+        return new static($ht);
     }
 
-    static function lookup($var, $list= false) {
+    static function lookup($var, $list=null) {
 
         if (!($item = parent::lookup($var)))
             return null;
@@ -1158,6 +1426,4 @@ class TicketStatus  extends VerySimpleModel implements CustomListItem {
         include(STAFFINC_DIR . 'templates/status-options.tmpl.php');
     }
 }
-
-TicketStatus::_inspect();
 ?>
diff --git a/include/class.lock.php b/include/class.lock.php
index d6bcbad9dc66105803fbb49a267252d068423242..c0e8c2823e696ffd0bdef3fd8c98988bfc6dd815 100644
--- a/include/class.lock.php
+++ b/include/class.lock.php
@@ -18,139 +18,133 @@
  * Mainly used as a helper...
  */
 
-class TicketLock {
-    var $id;
-    var $ht;
-    
-    function TicketLock($id, $tid=0) {
-        $this->id=0;
-        $this->load($id, $tid);
-    }
-
-    function load($id=0, $tid=0) {
-
-        if(!$id && $this->ht['id'])
-            $id=$this->ht['id'];
-
-        $sql='SELECT l.*, TIME_TO_SEC(TIMEDIFF(expire,NOW())) as timeleft '
-            .' ,IF(s.staff_id IS NULL,"staff",CONCAT_WS(" ", s.lastname, s.firstname)) as staff '
-            .' FROM '.TICKET_LOCK_TABLE. ' l '
-            .' LEFT JOIN '.STAFF_TABLE.' s ON(s.staff_id=l.staff_id) '
-            .' WHERE lock_id='.db_input($id);
-
-        if($tid) 
-            $sql.=' AND ticket_id='.db_input($tid);
-
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
-
-        $this->ht=db_fetch_array($res);
-        $this->id=$this->ht['id']=$this->ht['lock_id'];
-        $this->ht['expiretime']=time()+$this->ht['timeleft'];
-        
-        return true;
-    }
-  
-    function reload() {
-        return $this->load();
-    }
+class Lock extends VerySimpleModel {
+
+    static $meta = array(
+        'table' => LOCK_TABLE,
+        'pk' => array('lock_id'),
+        'joins' => array(
+            'ticket' => array(
+                'reverse' => 'TicketModel.lock',
+                'list' => false,
+            ),
+            'task' => array(
+                'reverse' => 'Task.lock',
+                'list' => false,
+            ),
+            'staff' => array(
+                'constraint' => array('staff_id' => 'Staff.staff_id'),
+            ),
+        ),
+    );
+
+    const MODE_DISABLED = 0;
+    const MODE_ON_VIEW = 1;
+    const MODE_ON_ACTIVITY = 2;
 
     function getId() {
-        return $this->id;
+        return $this->lock_id;
     }
 
     function getStaffId() {
-        return $this->ht['staff_id'];
+        return $this->staff_id;
     }
 
     function getStaffName() {
-        return $this->ht['staff'];
+        return $this->staff->getName();
+    }
+
+    function getStaff() {
+        return $this->staff;
     }
 
     function getCreateTime() {
-        return $this->ht['created'];
+        return $this->created;
     }
 
     function getExpireTime() {
-        return $this->ht['expire'];
+        return strtotime($this->expire);
     }
     //Get remaiming time before the lock expires
     function getTime() {
-        return $this->isExpired()?0:($this->ht['expiretime']-time());
+        return max(0, $this->getExpireTime() - Misc::dbtime());
     }
 
     //Should we be doing realtime check here? (Ans: not really....expiretime is local & based on loadtime)
     function isExpired() {
-        return (time()>$this->ht['expiretime']);
+        return (Misc::dbtime() > $this->getExpireTime());
     }
-   
+
+    function getCode() {
+        return $this->code;
+    }
+
     //Renew existing lock.
     function renew($lockTime=0) {
+        global $cfg;
 
         if(!$lockTime || !is_numeric($lockTime)) //XXX: test to  make it works.
-            $lockTime = '(TIME_TO_SEC(TIMEDIFF(expire,created))/60)';
-            
-
-        $sql='UPDATE '.TICKET_LOCK_TABLE
-            .' SET expire=DATE_ADD(NOW(),INTERVAL '.$lockTime.' MINUTE) '
-            .' WHERE lock_id='.db_input($this->getId());
-        //echo $sql;
-        if(!db_query($sql) || !db_affected_rows())
-            return false;
-        
-        $this->reload();
-        
-        return true;
+            $lockTime = $cfg->getLockTime();
+
+        $this->expire = SqlExpression::plus(
+            SqlFunction::NOW(),
+            SqlInterval::MINUTE($lockTime)
+        );
+        return $this->save(true);
     }
 
     //release aka delete a lock.
     function release() {
-        //FORCED release - we don't give a ....
-        $sql='DELETE FROM '.TICKET_LOCK_TABLE.' WHERE lock_id='.db_input($this->getId()).' LIMIT 1';
-        return (db_query($sql) && db_affected_rows());
+        return $this->delete();
     }
 
     /* ----------------------- Static functions ---------------------------*/
-    function lookup($id, $tid) {
-        return ($id  && ($lock = new TicketLock($id,$tid)) && $lock->getId()==$id)?$lock:null;
-    }
-
-    //Create a ticket lock...this function assumes the caller checked for access & validity of ticket & staff x-ship.    
-    function acquire($ticketId, $staffId, $lockTime) {
-
-        if(!$ticketId or !$staffId or !$lockTime)
-            return 0;
-
-
-        //Cleanup any expired locks on the ticket.
-        db_query('DELETE FROM '.TICKET_LOCK_TABLE.' WHERE ticket_id='.db_input($ticketId).' AND expire<NOW()');
-        //create the new lock.
-        $sql='INSERT IGNORE INTO '.TICKET_LOCK_TABLE.' SET created=NOW() '
-            .',ticket_id='.db_input($ticketId)
-            .',staff_id='.db_input($staffId)
-            .',expire=DATE_ADD(NOW(),INTERVAL '.$lockTime.' MINUTE) ';
-
-        return db_query($sql)?db_insert_id():0;
-    }
-
-    function create($ticketId, $staffId, $lockTime) {
-        if(($id=self::acquire($ticketId, $staffId, $lockTime)))
-            return self::lookup($id);
-    }
-
-    //Simply remove ALL locks a user (staff) holds on a ticket(s).
-    function removeStaffLocks($staffId, $ticketId=0) {
-        $sql='DELETE FROM '.TICKET_LOCK_TABLE.' WHERE staff_id='.db_input($staffId);
-        if($ticketId)
-            $sql.=' AND ticket_id='.db_input($ticketId);
-
-        return db_query($sql);
-    }
-
-    //Called  via cron
-    function cleanup() {
-        //Cleanup any expired locks.
-        db_query('DELETE FROM '.TICKET_LOCK_TABLE.' WHERE expire<NOW()');
+    static function lookup($id, $object=false) {
+        if ($object instanceof Ticket)
+            return parent::lookup(array('lock_id' => $id, 'ticket__ticket_id' => $object->getId()));
+        elseif ($object instanceof Task)
+            return parent::lookup(array('lock_id' => $id, 'task__id' => $object->getId()));
+        else
+            return parent::lookup($id);
+    }
+
+    //Create a ticket lock...this function assumes the caller checked for access & validity of ticket & staff x-ship.
+    static function acquire($staffId, $lockTime) {
+
+        if (!$staffId or !$lockTime)
+            return null;
+
+        // Create the new lock.
+        $lock = new static(array(
+            'created' => SqlFunction::NOW(),
+            'staff_id' => $staffId,
+            'expire' => SqlExpression::plus(
+                SqlFunction::NOW(),
+                SqlInterval::MINUTE($lockTime)
+            ),
+            'code' => Misc::randCode(10)
+        ));
+        if ($lock->save(true))
+            return $lock;
+    }
+
+    // Simply remove ALL locks a user (staff) holds on a ticket(s).
+    static function removeStaffLocks($staffId, $object=false) {
+        $locks = static::objects()->filter(array(
+            'staff_id' => $staffId,
+        ));
+        if ($object instanceof Ticket)
+            $locks->filter(array('ticket__ticket_id' => $object->getId()));
+        elseif ($object instanceof Task)
+            $locks->filter(array('task__id' => $object->getId()));
+
+        return $locks->delete();
+    }
+
+    static function cleanup() {
+        return static::objects()->filter(array(
+            'expire__lt' => SqlFunction::NOW()
+        ))->delete();
     }
 }
 ?>
diff --git a/include/class.log.php b/include/class.log.php
index 4fea7e4c1ffbf5662bbca89e9216f2ce7650470a..eec2192b128477513754aefc0d184e62c039a4a8 100644
--- a/include/class.log.php
+++ b/include/class.log.php
@@ -19,7 +19,7 @@ class Log {
     var $id;
     var $info;
 
-    function Log($id){
+    function __construct($id){
         $this->id=0;
         return $this->load($id);
     }
diff --git a/include/class.mailer.php b/include/class.mailer.php
index 316d32e0905a0b9a735e1493859dc465eff05fe3..cffaec5ed6f5c1b54923bb63c168e0c291f1980f 100644
--- a/include/class.mailer.php
+++ b/include/class.mailer.php
@@ -30,7 +30,7 @@ class Mailer {
     var $smtp = array();
     var $eol="\n";
 
-    function Mailer($email=null, array $options=array()) {
+    function __construct($email=null, array $options=array()) {
         global $cfg;
 
         if(is_object($email) && $email->isSMTPEnabled() && ($info=$email->getSMTPInfo())) { //is SMTP enabled for the current email?
@@ -66,10 +66,14 @@ class Mailer {
         $this->ht['from'] = $from;
     }
 
-    function getFromAddress() {
+    function getFromAddress($options=array()) {
 
-        if(!$this->ht['from'] && ($email=$this->getEmail()))
-            $this->ht['from'] =sprintf('"%s" <%s>', ($email->getName()?$email->getName():$email->getEmail()), $email->getEmail());
+        if (!$this->ht['from'] && ($email=$this->getEmail())) {
+            if (($name = $options['from_name'] ?: $email->getName()))
+                $this->ht['from'] =sprintf('"%s" <%s>', $name, $email->getEmail());
+            else
+                $this->ht['from'] =sprintf('<%s>', $email->getEmail());
+        }
 
         return $this->ht['from'];
     }
@@ -79,15 +83,25 @@ class Mailer {
         return $this->attachments;
     }
 
-    function addAttachment($attachment) {
+    function addAttachment(Attachment $attachment) {
+        // XXX: This looks too assuming; however, the attachment processor
+        // in the ::send() method seems hard coded to expect this format
+        $this->attachments[$attachment->file_id] = $attachment;
+    }
+
+    function addFile(AttachmentFile $file) {
         // XXX: This looks too assuming; however, the attachment processor
         // in the ::send() method seems hard coded to expect this format
-        $this->attachments[$attachment['file_id']] = $attachment;
+        $this->attachments[$file->file_id] = $file;
     }
 
     function addAttachments($attachments) {
-        foreach ($attachments as $a)
-            $this->addAttachment($a);
+        foreach ($attachments as $a) {
+            if ($a instanceof Attachment)
+                $this->addAttachment($a);
+            elseif ($a instanceof AttachmentFile)
+                $this->addFile($a);
+        }
     }
 
     /**
@@ -108,44 +122,65 @@ class Mailer {
      *      'thread' element, the threadId will be recorded in the TAG
      *
      * Returns:
-     * (string) - email message id, with leading and trailing <> chars. See
-     * the format below for the structure.
+     * (string) - email message id, without leading and trailing <> chars.
+     * See the Format below for the structure.
      *
      * Format:
-     * VA-B-C-D, with dash separators and A-D explained below:
+     * VA-B-C, with dash separators and A-C explained below:
      *
      * V: Version code of the generated Message-Id
-     * A: Predictable random code — used for loop detection
-     * B: Random data for unique identifier
-     *    Version Code: A (at char position 10)
-     * C: TAG: Base64(Pack(userid, entryId, type)), = chars discarded
-     * D: Signature:
-     *   '@' + Signed Tag value, last 10 chars from
-     *        HMAC(sha1, tag+rand, SECRET_SALT)
-     *   -or- Original From email address
+     * A: Predictable random code — used for loop detection (sysid)
+     * B: Random data for unique identifier (rand)
+     * C: TAG: Base64(Pack(userid, entryId, threadId, type, Signature)),
+     *    '=' chars discarded
+     * where Signature is:
+     *   Signed Tag value, last 5 chars from
+     *        HMAC(sha1, Tag + rand + sysid, SECRET_SALT),
+     *   where Tag is:
+     *     pack(userId, entryId, threadId, type)
      */
-    function getMessageId($recipient, $options=array(), $version='A') {
-        $rand = Misc::randCode(9,
+    function getMessageId($recipient, $options=array(), $version='B') {
+        $tag = '';
+        $rand = Misc::randCode(5,
             // RFC822 specifies the LHS of the addr-spec can have any char
             // except the specials — ()<>@,;:\".[], dash is reserved as the
             // section separator, and + is reserved for historical reasons
             'abcdefghiklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_=');
+        $sig = $this->getEmail()?$this->getEmail()->getEmail():'@osTicketMailer';
+        $sysid = static::getSystemMessageIdCode();
         // Create a tag for the outbound email
-        $tag = pack('VVa',
-            ($recipient instanceof EmailContact) ? $recipient->getUserId() : 0,
-            (isset($options['thread']) && $options['thread'] instanceof ThreadEntry)
-                ? $options['thread']->getId() : 0,
-            ($recipient instanceof Staff ? 'S'
-                : ($recipient instanceof TicketOwner ? 'U'
-                : ($recipient instanceof Collaborator ? 'C'
-                : '?')))
+        $entry = (isset($options['thread']) && $options['thread'] instanceof ThreadEntry)
+            ? $options['thread'] : false;
+        $thread = $entry ? $entry->getThread()
+            : (isset($options['thread']) && $options['thread'] instanceof Thread
+                ? $options['thread'] : false);
+
+        switch (true) {
+        case $recipient instanceof Staff:
+            $utype = 'S';
+            break;
+        case $recipient instanceof TicketOwner:
+            $utype = 'U';
+            break;
+        case $recipient instanceof Collaborator:
+            $utype = 'C';
+            break;
+        default:
+            $utype = $options['utype'] ?: '?';
+        }
+
+
+        $tag = pack('VVVa',
+            $recipient instanceof EmailContact ? $recipient->getUserId() : 0,
+            $entry ? $entry->getId() : 0,
+            $thread ? $thread->getId() : 0,
+            $utype ?: '?'
         );
-        $tag = str_replace('=','',base64_encode($tag));
         // Sign the tag with the system secret salt
-        $sig = '@' . substr(hash_hmac('sha1', $tag.$rand, SECRET_SALT), -10);
-        return sprintf('<A%s-%s-%s-%s>',
-            static::getSystemMessageIdCode(),
-            $rand, $tag, $sig);
+        $tag .= substr(hash_hmac('sha1', $tag.$rand.$sysid, SECRET_SALT, true), -5);
+        $tag = str_replace('=','',base64_encode($tag));
+        return sprintf('B%s-%s-%s-%s',
+            $sysid, $rand, $tag, $sig);
     }
 
     /**
@@ -166,6 +201,7 @@ class Mailer {
      *      'version' - (string|FALSE) version code of the message id
      *      'code' - (string) unique but predictable help desk message-id
      *      'id' - (string) random characters serving as the unique id
+     *      'entryId' - (int) thread-entry-id from which the message originated
      *      'threadId' - (int) thread-id from which the message originated
      *      'staffId' - (int|null) staff the email was originally sent to
      *      'userId' - (int|null) user the email was originally sent to
@@ -190,33 +226,65 @@ class Mailer {
         if (count($parts) < 2)
             return $rv;
 
-        // Detect the MessageId version, which should be the tenth char of
-        // the second segment
-        $rv['version'] = @$parts[0][0];
-        switch ($rv['version']) {
-            case 'A':
-            default:
-                list($rv['code'], $rv['id'], $tag) = $parts;
-                // Drop the leading version code
-                $rv['code'] = substr($rv['code'], 1);
-                // Verify tag signature
-                $chksig = substr(hash_hmac('sha1', $tag.$rv['id'], SECRET_SALT), -10);
-                if ($tag && $sig == $chksig && ($tag = base64_decode($tag))) {
-                    // Find user and ticket id
-                    $rv += unpack('Vuid/VthreadId/auserClass', $tag);
-                    // Attempt to make the user-id more specific
-                    $classes = array(
-                        'S' => 'staffId', 'U' => 'userId', 'C' => 'userId',
-                    );
-                    if (isset($classes[$rv['userClass']]))
-                        $rv[$classes[$rv['userClass']]] = $rv['uid'];
+        $self = get_called_class();
+        $decoders = array(
+        'A' => function($id, $tag) use ($sig) {
+            // Old format was VA-B-C-D@sig, where C was the packed tag and D
+            // was blank
+            $format = 'Vuid/VentryId/auserClass';
+            $chksig = substr(hash_hmac('sha1', $tag.$id, SECRET_SALT), -10);
+            if ($tag && $sig == $chksig && ($tag = base64_decode($tag))) {
+                // Find user and ticket id
+                return unpack($format, $tag);
+            }
+            return false;
+        },
+        'B' => function($id, $tag) use ($self) {
+            $format = 'Vuid/VentryId/VthreadId/auserClass/a*sig';
+            if ($tag && ($tag = base64_decode($tag))) {
+                if (!($info = @unpack($format, $tag)) || !isset($info['sig']))
+                    return false;
+                $sysid = $self::getSystemMessageIdCode();
+                $shorttag = substr($tag, 0, 13);
+                $chksig = substr(hash_hmac('sha1', $shorttag.$id.$sysid,
+                    SECRET_SALT, true), -5);
+                if ($chksig == $info['sig']) {
+                    return $info;
                 }
-                // Round-trip detection - the first section is the local
-                // system's message-id code
-                $rv['loopback'] = (0 === strcasecmp($rv['code'],
-                    static::getSystemMessageIdCode()));
-                break;
-        }
+            }
+            return false;
+        },
+        );
+
+        // Detect the MessageId version, which should be the first char
+        $rv['version'] = @$parts[0][0];
+        if (!isset($decoders[$rv['version']]))
+            // invalid version code
+            return null;
+
+        // Drop the leading version code
+        list($rv['code'], $rv['id'], $tag) = $parts;
+        $rv['code'] = substr($rv['code'], 1);
+
+        // Verify tag signature and unpack the tag
+        $info = $decoders[$rv['version']]($rv['id'], $tag);
+        if ($info === false)
+            return $rv;
+
+        $rv += $info;
+
+        // Attempt to make the user-id more specific
+        $classes = array(
+            'S' => 'staffId', 'U' => 'userId', 'C' => 'userId',
+        );
+        if (isset($classes[$rv['userClass']]))
+            $rv[$classes[$rv['userClass']]] = $rv['uid'];
+
+        // Round-trip detection - the first section is the local
+        // system's message-id code
+        $rv['loopback'] = (0 === strcmp($rv['code'],
+            static::getSystemMessageIdCode()));
+
         return $rv;
     }
 
@@ -226,25 +294,27 @@ class Mailer {
             0, 6);
     }
 
-    function send($to, $subject, $message, $options=null) {
+    function send($recipient, $subject, $message, $options=null) {
         global $ost, $cfg;
 
         //Get the goodies
         require_once (PEAR_DIR.'Mail.php'); // PEAR Mail package
         require_once (PEAR_DIR.'Mail/mime.php'); // PEAR Mail_Mime packge
 
-        $messageId = $this->getMessageId($to, $options);
+        $messageId = $this->getMessageId($recipient, $options);
 
-        if (is_object($to) && is_callable(array($to, 'getEmail'))) {
+        if (is_object($recipient) && is_callable(array($recipient, 'getEmail'))) {
             // Add personal name if available
-            if (is_callable(array($to, 'getName'))) {
+            if (is_callable(array($recipient, 'getName'))) {
                 $to = sprintf('"%s" <%s>',
-                    $to->getName()->getOriginal(), $to->getEmail()
+                    $recipient->getName()->getOriginal(), $recipient->getEmail()
                 );
             }
             else {
-                $to = $to->getEmail();
+                $to = $recipient->getEmail();
             }
+        } else {
+            $to = $recipient;
         }
 
         //do some cleanup
@@ -252,27 +322,69 @@ class Mailer {
         $subject = preg_replace("/(\r\n|\r|\n)/s",'', trim($subject));
 
         $headers = array (
-            'From' => $this->getFromAddress(),
+            'From' => $this->getFromAddress($options),
             'To' => $to,
             'Subject' => $subject,
             'Date'=> date('D, d M Y H:i:s O'),
-            'Message-ID' => $messageId,
+            'Message-ID' => "<{$messageId}>",
             'X-Mailer' =>'osTicket Mailer',
         );
 
         // Add in the options passed to the constructor
         $options = ($options ?: array()) + $this->options;
 
+        // Message Id Token
+        $mid_token = '';
+        // Check if the email is threadable
+        if (isset($options['thread'])
+            && $options['thread'] instanceof ThreadEntry
+            && ($thread = $options['thread']->getThread())) {
+
+            // Add email in-reply-to references if not set
+            if (!isset($options['inreplyto'])) {
+
+                $entry = null;
+                switch (true) {
+                case $recipient instanceof TicketOwner:
+                case $recipient instanceof Collaborator:
+                    $entry = $thread->getLastEmailMessage(array(
+                                'user_id' => $recipient->getUserId()));
+                    break;
+                case $recipient instanceof Staff:
+                    //XXX: is it necessary ??
+                    break;
+                }
+
+                if ($entry && ($mid=$entry->getEmailMessageId())) {
+                    $options['inreplyto'] = $mid;
+                    $options['references'] = $entry->getEmailReferences();
+                }
+            }
+
+            // Embedded message id token
+            $mid_token = $messageId;
+            // Set Reply-Tag
+            if (!isset($options['reply-tag'])) {
+                if ($cfg && $cfg->stripQuotedReply())
+                    $options['reply-tag'] = $cfg->getReplySeparator() . '<br/><br/>';
+                else
+                    $options['reply-tag'] = '';
+            } elseif ($options['reply-tag'] === false) {
+                $options['reply-tag'] = '';
+            }
+        }
+
+        // Return-Path
         if (isset($options['nobounce']) && $options['nobounce'])
             $headers['Return-Path'] = '<>';
         elseif ($this->getEmail() instanceof Email)
             $headers['Return-Path'] = $this->getEmail()->getEmail();
 
-        //Bulk.
+        // Bulk.
         if (isset($options['bulk']) && $options['bulk'])
             $headers+= array('Precedence' => 'bulk');
 
-        //Auto-reply - mark as autoreply and supress all auto-replies
+        // Auto-reply - mark as autoreply and supress all auto-replies
         if (isset($options['autoreply']) && $options['autoreply']) {
             $headers+= array(
                     'Precedence' => 'auto_reply',
@@ -281,51 +393,22 @@ class Mailer {
                     'Auto-Submitted' => 'auto-replied');
         }
 
-        //Notice (sort of automated - but we don't want auto-replies back
+        // Notice (sort of automated - but we don't want auto-replies back
         if (isset($options['notice']) && $options['notice'])
             $headers+= array(
                     'X-Auto-Response-Suppress' => 'OOF, AutoReply',
                     'Auto-Submitted' => 'auto-generated');
-
-        if ($options) {
-            if (isset($options['inreplyto']) && $options['inreplyto'])
-                $headers += array('In-Reply-To' => $options['inreplyto']);
-            if (isset($options['references']) && $options['references']) {
-                if (is_array($options['references']))
-                    $headers += array('References' =>
-                        implode(' ', $options['references']));
-                else
-                    $headers += array('References' => $options['references']);
-            }
-        }
-
-        // Make the best effort to add In-Reply-To and References headers
-        $reply_tag = $mid_token = '';
-        if (isset($options['thread'])
-            && $options['thread'] instanceof ThreadEntry
-        ) {
-            if ($irt = $options['thread']->getEmailMessageId()) {
-                // This is an response from an email, like and autoresponse.
-                // Web posts will not have a email message-id
-                $headers += array(
-                    'In-Reply-To' => $irt,
-                    'References' => $options['thread']->getEmailReferences()
-                );
-            }
-            elseif ($parent = $options['thread']->getParent()) {
-                // Use the parent item as the email information source. This
-                // will apply for staff replies
-                $headers += array(
-                    'In-Reply-To' => $parent->getEmailMessageId(),
-                    'References' => $parent->getEmailReferences(),
-                );
-            }
-
-            // Configure the reply tag and embedded message id token
-            $mid_token = $options['thread']->asMessageId($to);
-            if ($cfg && $cfg->stripQuotedReply()
-                    && (!isset($options['reply-tag']) || $options['reply-tag']))
-                $reply_tag = $cfg->getReplySeparator() . '<br/><br/>';
+        // In-Reply-To
+        if (isset($options['inreplyto']) && $options['inreplyto'])
+            $headers += array('In-Reply-To' => $options['inreplyto']);
+
+        // References
+        if (isset($options['references']) && $options['references']) {
+            if (is_array($options['references']))
+                $headers += array('References' =>
+                    implode(' ', $options['references']));
+            else
+                $headers += array('References' => $options['references']);
         }
 
         // Use general failsafe default initially
@@ -337,17 +420,34 @@ class Mailer {
         }
         $mime = new Mail_mime($eol);
 
+        // Add in extra attachments, if any from template variables
+        if ($message instanceof TextWithExtras
+            && ($files = $message->getFiles())
+        ) {
+            foreach ($files as $F) {
+                $file = $F->getFile();
+                $mime->addAttachment($file->getData(),
+                    $file->getType(), $file->getName(), false);
+            }
+        }
+
         // If the message is not explicitly declared to be a text message,
         // then assume that it needs html processing to create a valid text
         // body
         $isHtml = true;
         if (!(isset($options['text']) && $options['text'])) {
-            if ($reply_tag || $mid_token) {
-                $message = "<div style=\"display:none\"
-                    class=\"mid-$mid_token\">$reply_tag</div>$message";
+            // Embed the data-mid in such a way that it should be included
+            // in a response
+            if ($options['reply-tag'] || $mid_token) {
+                $message = sprintf('<div style="display:none"
+                        class="mid-%s">%s</div>%s',
+                        $mid_token,
+                        $options['reply-tag'],
+                        $message);
             }
+
             $txtbody = rtrim(Format::html2text($message, 90, false))
-                . ($mid_token ? "\nRef-Mid: $mid_token\n" : '');
+                . ($messageId ? "\nRef-Mid: $messageId\n" : '');
             $mime->setTXTBody($txtbody);
         }
         else {
@@ -355,7 +455,7 @@ class Mailer {
             $isHtml = false;
         }
 
-        if ($isHtml && $cfg && $cfg->isHtmlThreadEnabled()) {
+        if ($isHtml && $cfg && $cfg->isRichTextEnabled()) {
             // Pick a domain compatible with pear Mail_Mime
             $matches = array();
             if (preg_match('#(@[0-9a-zA-Z\-\.]+)#', $this->getFromAddress(), $matches)) {
@@ -368,7 +468,19 @@ class Mailer {
             $self = $this;
             $message = preg_replace_callback('/cid:([\w.-]{32})/',
                 function($match) use ($domain, $mime, $self) {
-                    if (!($file = AttachmentFile::lookup($match[1])))
+                    $file = false;
+                    foreach ($self->attachments as $id=>$F) {
+                        if ($F instanceof Attachment)
+                            $F = $F->getFile();
+                        if (strcasecmp($F->getKey(), $match[1]) === 0) {
+                            $file = $F;
+                            break;
+                        }
+                    }
+                    if (!$file)
+                        // Not attached yet attempt to attach it inline
+                        $file = AttachmentFile::lookup($match[1]);
+                    if (!$file)
                         return $match[0];
                     $mime->addHTMLImage($file->getData(),
                         $file->getType(), $file->getName(), false,
@@ -382,12 +494,17 @@ class Mailer {
         }
         //XXX: Attachments
         if(($attachments=$this->getAttachments())) {
-            foreach($attachments as $attachment) {
-                if ($attachment['file_id']
-                        && ($file=AttachmentFile::lookup($attachment['file_id']))) {
-                    $mime->addAttachment($file->getData(),
-                        $file->getType(), $file->getName(),false);
+            foreach($attachments as $id=>$file) {
+                // Read the filename from the Attachment if possible
+                if ($file instanceof Attachment) {
+                    $filename = $file->getFilename();
+                    $file = $file->getFile();
+                }
+                else {
+                    $filename = $file->getName();
                 }
+                $mime->addAttachment($file->getData(),
+                    $file->getType(), $filename, false);
             }
         }
 
@@ -436,7 +553,8 @@ class Mailer {
             // Force reconnect on next ->send()
             unset($smtp_connections[$key]);
 
-            $alert=sprintf(__("Unable to email via SMTP:%1\$s:%2\$d [%3\$s]\n\n%4\$s\n"),
+            $alert=_S("Unable to email via SMTP")
+                    .sprintf(":%1\$s:%2\$d [%3\$s]\n\n%4\$s\n",
                     $smtp['host'], $smtp['port'], $smtp['username'], $result->getMessage());
             $this->logError($alert);
         }
@@ -445,14 +563,21 @@ class Mailer {
         $mail = mail::factory('mail');
         // Ensure the To: header is properly encoded.
         $to = $headers['To'];
-        return PEAR::isError($mail->send($to, $headers, $body))?false:$messageId;
-
+        $result = $mail->send($to, $headers, $body);
+        if(!PEAR::isError($result))
+            return $messageId;
+
+        $alert=_S("Unable to email via php mail function")
+                .sprintf(":%1\$s\n\n%2\$s\n",
+                $to, $result->getMessage());
+        $this->logError($alert);
+        return false;
     }
 
     function logError($error) {
         global $ost;
         //NOTE: Admin alert override - don't email when having email trouble!
-        $ost->logError(__('Mailer Error'), $error, false);
+        $ost->logError(_S('Mailer Error'), $error, false);
     }
 
     /******* Static functions ************/
diff --git a/include/class.mailfetch.php b/include/class.mailfetch.php
index 03cebe2f23e6c1dcc0ecf50df1d66004aaa07dc9..325300f7b6e09e310324e6002d2f72844b75552e 100644
--- a/include/class.mailfetch.php
+++ b/include/class.mailfetch.php
@@ -19,6 +19,7 @@ require_once(INCLUDE_DIR.'class.ticket.php');
 require_once(INCLUDE_DIR.'class.dept.php');
 require_once(INCLUDE_DIR.'class.email.php');
 require_once(INCLUDE_DIR.'class.filter.php');
+require_once(INCLUDE_DIR.'class.banlist.php');
 require_once(INCLUDE_DIR.'tnef_decoder.php');
 
 class MailFetcher {
@@ -32,7 +33,7 @@ class MailFetcher {
 
     var $tnef = false;
 
-    function MailFetcher($email, $charset='UTF-8') {
+    function __construct($email, $charset='UTF-8') {
 
 
         if($email && is_numeric($email)) //email_id
@@ -554,7 +555,7 @@ class MailFetcher {
                     if ($body = $this->getPart(
                         $mid, 'text/plain', $this->charset, $struct, false, 3, false
                     )) {
-                        return new TextThreadBody($body);
+                        return new TextThreadEntryBody($body);
                     }
                 }
             }
@@ -602,21 +603,21 @@ class MailFetcher {
     function getBody($mid) {
         global $cfg;
 
-        if ($cfg->isHtmlThreadEnabled()) {
+        if ($cfg->isRichTextEnabled()) {
             if ($html=$this->getPart($mid, 'text/html', $this->charset))
-                $body = new HtmlThreadBody($html);
+                $body = new HtmlThreadEntryBody($html);
             elseif ($text=$this->getPart($mid, 'text/plain', $this->charset))
-                $body = new TextThreadBody($text);
+                $body = new TextThreadEntryBody($text);
         }
         elseif ($text=$this->getPart($mid, 'text/plain', $this->charset))
-            $body = new TextThreadBody($text);
+            $body = new TextThreadEntryBody($text);
         elseif ($html=$this->getPart($mid, 'text/html', $this->charset))
-            $body = new TextThreadBody(
+            $body = new TextThreadEntryBody(
                     Format::html2text(Format::safe_html($html),
                         100, false));
 
         if (!isset($body))
-            $body = new TextThreadBody('');
+            $body = new TextThreadEntryBody('');
 
         if ($cfg->stripQuotedReply())
             $body->stripQuotedReply($cfg->getReplySeparator());
@@ -650,7 +651,7 @@ class MailFetcher {
         }
 
 	    //Is the email address banned?
-        if($mailinfo['email'] && TicketFilter::isBanned($mailinfo['email'])) {
+        if($mailinfo['email'] && Banlist::isBanned($mailinfo['email'])) {
 	        //We need to let admin know...
             $ost->logWarning(_S('Ticket denied'),
                 sprintf(_S('Banned email — %s'),$mailinfo['email']), false);
@@ -683,22 +684,22 @@ class MailFetcher {
         $vars['subject'] = $mailinfo['subject'] ?: '[No Subject]';
         $vars['emailId'] = $mailinfo['emailId'] ?: $this->getEmailId();
         $vars['to-email-id'] = $mailinfo['emailId'] ?: 0;
-        $vars['flags'] = new ArrayObject();
+        $vars['mailflags'] = new ArrayObject();
 
         if ($this->isBounceNotice($mid)) {
             // Fetch the original References and assign to 'references'
             if ($headers = $this->getOriginalMessageHeaders($mid)) {
                 $vars['references'] = $headers['references'];
-                $vars['in-reply-to'] = @$headers['in-reply-to'] ?: null;
+                $vars['in-reply-to'] = $headers['message-id'] ?: @$headers['in-reply-to'] ?: null;
             }
             // Fetch deliver status report
             $vars['message'] = $this->getDeliveryStatusMessage($mid) ?: $this->getBody($mid);
             $vars['thread-type'] = 'N';
-            $vars['flags']['bounce'] = true;
+            $vars['mailflags']['bounce'] = true;
         }
         else {
             $vars['message'] = $this->getBody($mid);
-            $vars['flags']['bounce'] = TicketFilter::isBounce($info);
+            $vars['mailflags']['bounce'] = TicketFilter::isBounce($info);
         }
 
 
@@ -770,23 +771,31 @@ class MailFetcher {
         Signal::send('mail.processed', $this, $vars);
 
         $seen = false;
-        if (($thread = ThreadEntry::lookupByEmailHeaders($vars, $seen))
-                && ($t=$thread->getTicket())
-                && ($vars['staffId']
-                    || !$t->isClosed()
-                    || $t->isReopenable())
-                && ($message = $thread->postEmail($vars))) {
+        if (($entry = ThreadEntry::lookupByEmailHeaders($vars, $seen))
+            && ($message = $entry->postEmail($vars))
+        ) {
             if (!$message instanceof ThreadEntry)
                 // Email has been processed previously
                 return $message;
-            $ticket = $message->getTicket();
-        } elseif ($seen) {
+            // NOTE: This might not be a "ticket"
+            $ticket = $message->getThread()->getObject();
+        }
+        elseif ($seen) {
             // Already processed, but for some reason (like rejection), no
             // thread item was created. Ignore the email
             return true;
-        } elseif (($ticket=Ticket::create($vars, $errors, 'Email'))) {
+        }
+        // Allow continuation of thread without initial message or note
+        elseif (($thread = Thread::lookupByEmailHeaders($vars))
+            && ($message = $thread->postEmail($vars))
+        ) {
+            // NOTE: This might not be a "ticket"
+            $ticket = $thread->getObject();
+        }
+        elseif (($ticket=Ticket::create($vars, $errors, 'Email'))) {
             $message = $ticket->getLastMessage();
-        } else {
+        }
+        else {
             //Report success if the email was absolutely rejected.
             if(isset($errors['errno']) && $errors['errno'] == 403) {
                 // Never process this email again!
diff --git a/include/class.mailparse.php b/include/class.mailparse.php
index eecbf5656898c7c2c1bd7faedc97d1cd2ef917f2..1564cc07735a2d7460d65eaff8d4935c15ecd668 100644
--- a/include/class.mailparse.php
+++ b/include/class.mailparse.php
@@ -32,7 +32,7 @@ class Mail_Parse {
 
     var $tnef = false;      // TNEF encoded mail
 
-    function Mail_parse(&$mimeMessage, $charset=null){
+    function __construct(&$mimeMessage, $charset=null){
 
         $this->mime_message = &$mimeMessage;
 
@@ -208,14 +208,14 @@ class Mail_Parse {
         if (!($header = $this->struct->headers['from']))
             return null;
 
-        return Mail_Parse::parseAddressList($header);
+        return Mail_Parse::parseAddressList($header, $this->charset);
     }
 
     function getDeliveredToAddressList() {
         if (!($header = $this->struct->headers['delivered-to']))
             return null;
 
-        return Mail_Parse::parseAddressList($header);
+        return Mail_Parse::parseAddressList($header, $this->charset);
     }
 
     function getToAddressList(){
@@ -223,7 +223,7 @@ class Mail_Parse {
         $tolist = array();
         if ($header = $this->struct->headers['to'])
             $tolist = array_merge($tolist,
-                Mail_Parse::parseAddressList($header));
+                Mail_Parse::parseAddressList($header, $this->charset));
         return $tolist ? $tolist : null;
     }
 
@@ -231,14 +231,14 @@ class Mail_Parse {
         if (!($header = @$this->struct->headers['cc']))
             return null;
 
-        return Mail_Parse::parseAddressList($header);
+        return Mail_Parse::parseAddressList($header, $this->charset);
     }
 
     function getBccAddressList(){
         if (!($header = @$this->struct->headers['bcc']))
             return null;
 
-        return Mail_Parse::parseAddressList($header);
+        return Mail_Parse::parseAddressList($header, $this->charset);
     }
 
     function getMessageId(){
@@ -258,7 +258,7 @@ class Mail_Parse {
         if (!($header = @$this->struct->headers['reply-to']))
             return null;
 
-        return Mail_Parse::parseAddressList($header);
+        return Mail_Parse::parseAddressList($header, $this->charset);
     }
 
     function isBounceNotice() {
@@ -279,7 +279,7 @@ class Mail_Parse {
             && $this->struct->ctype_parameters['report-type'] == 'delivery-status'
         ) {
             if ($body = $this->getPart($this->struct, 'text/plain', 3, false))
-                return new TextThreadBody($body);
+                return new TextThreadEntryBody($body);
         }
         return false;
     }
@@ -303,21 +303,21 @@ class Mail_Parse {
     function getBody(){
         global $cfg;
 
-        if ($cfg && $cfg->isHtmlThreadEnabled()) {
+        if ($cfg && $cfg->isRichTextEnabled()) {
             if ($html=$this->getPart($this->struct,'text/html'))
-                $body = new HtmlThreadBody($html);
+                $body = new HtmlThreadEntryBody($html);
             elseif ($text=$this->getPart($this->struct,'text/plain'))
-                $body = new TextThreadBody($text);
+                $body = new TextThreadEntryBody($text);
         }
         elseif ($text=$this->getPart($this->struct,'text/plain'))
-            $body = new TextThreadBody($text);
+            $body = new TextThreadEntryBody($text);
         elseif ($html=$this->getPart($this->struct,'text/html'))
-            $body = new TextThreadBody(
+            $body = new TextThreadEntryBody(
                     Format::html2text(Format::safe_html($html),
                         100, false));
 
         if (!isset($body))
-            $body = new TextThreadBody('');
+            $body = new TextThreadEntryBody('');
 
         if ($cfg && $cfg->stripQuotedReply())
             $body->stripQuotedReply($cfg->getReplySeparator());
@@ -543,7 +543,7 @@ class Mail_Parse {
     	return 0;
     }
 
-    function parseAddressList($address){
+    function parseAddressList($address, $charset='UTF-8'){
         if (!$address)
             return array();
 
@@ -558,11 +558,11 @@ class Mail_Parse {
 
         // Decode name and mailbox
         foreach ($parsed as $p) {
-            $p->personal = Format::mimedecode($p->personal, $this->charset);
+            $p->personal = Format::mimedecode($p->personal, $charset);
             // Some mail clients may send ISO-8859-1 strings without proper encoding.
             // Also, handle the more sane case where the mailbox is properly encoded
             // against RFC2047
-            $p->mailbox = Format::mimedecode($p->mailbox, $this->charset);
+            $p->mailbox = Format::mimedecode($p->mailbox, $charset);
         }
 
         return $parsed;
@@ -581,7 +581,7 @@ class EmailDataParser {
     var $stream;
     var $error;
 
-    function EmailDataParser($stream=null) {
+    function __construct($stream=null) {
         $this->stream = $stream;
     }
 
@@ -608,7 +608,7 @@ class EmailDataParser {
         $data['header'] = $parser->getHeader();
         $data['mid'] = $parser->getMessageId();
         $data['priorityId'] = $parser->getPriority();
-        $data['flags'] = new ArrayObject();
+        $data['mailflags'] = new ArrayObject();
 
         //FROM address: who sent the email.
         if(($fromlist = $parser->getFromAddressList())) {
@@ -694,19 +694,19 @@ class EmailDataParser {
             // Fetch the original References and assign to 'references'
             if ($headers = $parser->getOriginalMessageHeaders()) {
                 $data['references'] = $headers['references'];
-                $data['in-reply-to'] = @$headers['in-reply-to'] ?: null;
+                $data['in-reply-to'] = $headers['message-id'] ?: @$headers['in-reply-to'] ?: null;
             }
             // Fetch deliver status report
             $data['message'] = $parser->getDeliveryStatusMessage() ?: $parser->getBody();
             $data['thread-type'] = 'N';
-            $data['flags']['bounce'] = true;
+            $data['mailflags']['bounce'] = true;
         }
         else {
             // Typical email
             $data['message'] = $parser->getBody();
             $data['in-reply-to'] = @$parser->struct->headers['in-reply-to'];
             $data['references'] = @$parser->struct->headers['references'];
-            $data['flags']['bounce'] = TicketFilter::isBounce($data['header']);
+            $data['mailflags']['bounce'] = TicketFilter::isBounce($data['header']);
         }
 
         $data['to-email-id'] = $data['emailId'];
diff --git a/include/class.message.php b/include/class.message.php
new file mode 100644
index 0000000000000000000000000000000000000000..e516bce3df30fb2569dac985a1af6535ba84c144
--- /dev/null
+++ b/include/class.message.php
@@ -0,0 +1,248 @@
+<?php
+/*********************************************************************
+    class.message.php
+
+    Simple messages interface used to stash messages for display in a future
+    request. Mainly useful for the post-redirect-get pattern.
+
+    Usage:
+
+    <?php Messages::success('It worked!!'); ?>
+
+    // In a later request
+    <?php
+    foreach (Messages::getMessages() as $msg) {
+        include 'path/to/message-template.tmp.php';
+    }
+    ?>
+
+    Jared Hancock <jared@osticket.com>
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  2006-2015 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:
+**********************************************************************/
+
+interface Message {
+    function getTags();
+    function getLevel();
+    function __toString();
+}
+
+class Messages {
+    const ERROR = 50;
+    const WARNING = 40;
+    const WARN = self::WARNING;
+    const SUCCESS = 30;
+    const INFO = 20;
+    const DEBUG = 10;
+    const NOTSET = 0;
+
+    static $_levelNames = array(
+        self::ERROR => 'ERROR',
+        self::WARNING => 'WARNING',
+        self::SUCCESS => 'SUCCESS',
+        self::INFO => 'INFO',
+        self::DEBUG => 'DEBUG',
+        self::NOTSET => 'NOTSET',
+        'ERROR' => self::ERROR,
+        'WARN' => self::WARNING,
+        'WARNING' => self::WARNING,
+        'SUCCESS' => self::SUCCESS,
+        'INFO' => self::INFO,
+        'DEBUG' => self::DEBUG,
+        'NOTSET' => self::NOTSET,
+    );
+
+    static $messageClass = 'SimpleMessage';
+    static $backend = 'SessionMessageStorage';
+
+    static function debug($message) {
+        static::addMessage(self::DEBUG, $message);
+    }
+    static function info($message) {
+        static::addMessage(self::INFO, $message);
+    }
+    static function success($message) {
+        static::addMessage(self::SUCCESS, $message);
+    }
+    static function warning($message) {
+        static::addMessage(self::WARNING, $message);
+    }
+    static function error($message) {
+        static::addMessage(self::ERROR, $message);
+    }
+
+    static function addMessage($level, $message) {
+        $msg = new static::$messageClass($level, $message);
+        $bk = static::getMessages();
+        $bk->add($level, $msg);
+    }
+
+    static function getMessages() {
+        static $messages;
+        if (!isset($messages))
+            $messages = new static::$backend();
+        return $messages;
+    }
+
+    static function setMessageClass($class) {
+        if (!is_subclass_of($class, 'Message'))
+            throw new InvalidArgumentException('Class must extend Message');
+        self::$messageClass = $class;
+    }
+
+    static function checkLevel($level) {
+        if (is_int($level)) {
+            $rv = $level;
+        }
+        elseif ((string) $level == $level) {
+            if (!isset(static::$_levelNames[$level]))
+                throw new InvalidArgumentException(
+                    sprintf('Unknown level: %s', $level));
+            $rv = static::$_levelNames[$level];
+        }
+        else {
+            throw new InvalidArgumentException(
+                sprintf('Level not an integer or a valid string: %s',
+                    $level));
+        }
+        return $rv;
+    }
+
+    static function getLevelName($level) {
+        return @self::$_levelNames[$level];
+    }
+}
+
+class SimpleMessage implements Message {
+    var $tags;
+    var $level;
+    var $msg;
+
+    function __construct($level, $message, $extra_tags=array()) {
+        $this->level = $level;
+        $this->msg = $message;
+        $this->tags = $extra_tags ?: null;
+    }
+
+    function getTags() {
+        $tags = array_merge(
+            array(strtolower(Messages::getLevelName($this->level))),
+            $this->tags ?: array());
+        return implode(' ', $tags);
+    }
+
+    function getLevel() {
+        return Messages::getLevelName($this->level);
+    }
+
+    function __toString() {
+        return $this->msg;
+    }
+}
+
+interface MessageStorageBackend extends \IteratorAggregate {
+    function setLevel($level);
+    function getLevel();
+
+    function update();
+    function add($level, $message);
+}
+
+abstract class BaseMessageStorage implements MessageStorageBackend {
+    var $level = Messages::NOTSET;
+    var $queued = array();
+    var $used = false;
+    var $added_new = false;
+
+    function isEnabledFor($level) {
+        Messages::checkLevel($level);
+        return $level >= $this->getLevel();
+    }
+
+    function setLevel($level) {
+        Messages::checkLevel($level);
+        $this->level = $level;
+    }
+
+    function getLevel() {
+        return $this->level;
+    }
+
+    function load() {
+        static $messages = false;
+
+        if (!$messages) {
+            $messages = new ListObject($this->get());
+        }
+        return $messages;
+    }
+
+    function getIterator() {
+        $this->used = true;
+        $messages = $this->load();
+        if ($this->queued) {
+            $messages->extend($this->queued);
+            $this->queued = array();
+        }
+        if ($messages instanceof ListObject)
+            return $messages->getIterator();
+        else
+            return new \ArrayIterator($messages);
+    }
+
+    function update() {
+        if ($this->used) {
+            return $this->store($this->queued);
+        }
+        else {
+            $messages = $this->load();
+            $messages->extend($this->queued);
+            return $this->store($messages);
+        }
+    }
+
+    function add($level, $message) {
+        if (!$message)
+            return;
+        elseif (!$this->isEnabledFor($level))
+            return;
+
+        $this->added_new = true;
+        $this->queued[] = $message;
+    }
+
+    abstract function get();
+    abstract function store($messages);
+}
+
+class SessionMessageStorage extends BaseMessageStorage {
+    var $list;
+
+    function __construct() {
+        $this->list = @$_SESSION[':msgs'] ?: array();
+        // Since no middleware exists in this framework, register a
+        // pre-shutdown hook
+        $self = $this;
+        Signal::connect('session.close', function($null, $info) use ($self) {
+            // Whether or not the session data should be re-encoded to
+            // reflect changes made in this routine
+            $info['touched'] = $self->added_new || ($self->used && count($self->list));
+            $self->update();
+        });
+    }
+
+    function get() {
+        return $this->list;
+    }
+
+    function store($messages) {
+        $_SESSION[':msgs'] = $messages;
+        return array();
+    }
+}
diff --git a/include/class.migrater.php b/include/class.migrater.php
index b9dbdca2e663c01b9e7f506319367b4a16548175..0348f1b310bebe466bef8d195f7a4de26dc53017 100644
--- a/include/class.migrater.php
+++ b/include/class.migrater.php
@@ -33,12 +33,10 @@ class DatabaseMigrater {
     var $end;
     var $sqldir;
 
-    function DatabaseMigrater($start, $end, $sqldir) {
-
+    function __construct($start, $end, $sqldir) {
         $this->start = $start;
         $this->end = $end;
         $this->sqldir = $sqldir;
-
     }
 
     function getPatches($stop=null) {
diff --git a/include/class.misc.php b/include/class.misc.php
index adbefde1a7cebaaabf5ee196d33966053717e560..d8f2523045c2628f0fe796ace96ed44e761549ae 100644
--- a/include/class.misc.php
+++ b/include/class.misc.php
@@ -62,31 +62,80 @@ class Misc {
 
     /* misc date helpers...this will go away once we move to php 5 */
     function db2gmtime($var){
+        static $dbtz;
         global $cfg;
-        if(!$var) return;
 
-        $dbtime=is_int($var)?$var:strtotime($var);
-        return $dbtime-($cfg->getDBTZoffset()*3600);
+        if (!$var || !$cfg)
+            return;
+
+        if (!isset($dbtz))
+            $dbtz = new DateTimeZone($cfg->getDbTimezone());
+
+        $dbtime = is_int($var) ? $var : strtotime($var);
+        $D = DateTime::createFromFormat('U', $dbtime);
+        if (!$D)
+            // This happens e.g. from negative timestamps
+            return $var;
+
+        return $dbtime - $dbtz->getOffset($D);
+    }
+
+    // Take user's time and return GMT time.
+    function user2gmtime($timestamp=null, $user=null) {
+        global $cfg;
+
+        $tz = new DateTimeZone($cfg->getTimezone($user));
+
+        if (!$timestamp)
+            $timestamp = 'now';
+
+        if (is_int($timestamp)) {
+            $time = $timestamp;
+        } else {
+            if (!($date = new DateTime($timestamp, $tz))) {
+                // Timestamp might be invalid
+                return $timestamp;
+            }
+            $time = $date->format('U');
+        }
+
+        if (!($D = DateTime::createFromFormat('U', $time)))
+            return $time;
+
+        return $time - $tz->getOffset($D);
     }
 
     //Take user time or gmtime and return db (mysql) time.
     function dbtime($var=null){
-         global $cfg;
-
-        if(is_null($var) || !$var)
-            $time=Misc::gmtime(); //gm time.
-        else{ //user time to GM.
-            $time=is_int($var)?$var:strtotime($var);
-            $offset=$_SESSION['TZ_OFFSET']+($_SESSION['TZ_DST']?date('I',$time):0);
-            $time=$time-($offset*3600);
+        static $dbtz;
+        global $cfg;
+
+        if (is_null($var) || !$var) {
+            // Default timezone is set to UTC
+            $time = time();
+        } else {
+            // User time to UTC
+            $time = self::user2gmtime($var);
         }
-        //gm to db time
-        return $time+($cfg->getDBTZoffset()*3600);
+
+        if (!isset($dbtz)) {
+            $dbtz = new DateTimeZone($cfg->getDbTimezone());
+        }
+        // UTC to db time
+        $D = DateTime::createFromFormat('U', $time);
+        return $time + $dbtz->getOffset($D);
     }
 
     /*Helper get GM time based on timezone offset*/
-    function gmtime() {
-        return time()-date('Z');
+    function gmtime($time=false, $user=false) {
+        global $cfg;
+
+        $tz = new DateTimeZone($user ? $cfg->getDbTimezone($user) : 'UTC');
+        if (!($time = new DateTime($time ?: 'now'))) {
+            // Old standard
+            return time() - date('Z');
+        }
+        return $time->getTimestamp() - $tz->getOffset($time);
     }
 
     /* Needed because of PHP 4 support */
@@ -141,15 +190,14 @@ class Misc {
             $min=0;
 
         ob_start();
-        echo sprintf('<select name="%s" id="%s">',$name,$name);
+        echo sprintf('<select name="%s" id="%s" style="display:inline-block;width:auto">',$name,$name);
         echo '<option value="" selected>'.__('Time').'</option>';
-        $format = $cfg->getTimeFormat();
         for($i=23; $i>=0; $i--) {
             for($minute=45; $minute>=0; $minute-=15) {
                 $sel=($hr==$i && $min==$minute)?'selected="selected"':'';
                 $_minute=str_pad($minute, 2, '0',STR_PAD_LEFT);
                 $_hour=str_pad($i, 2, '0',STR_PAD_LEFT);
-                $disp = gmdate($format, $i*3600 + $minute*60);
+                $disp = Format::time($i*3600 + $minute*60 + 1);
                 echo sprintf('<option value="%s:%s" %s>%s</option>',$_hour,$_minute,$sel,$disp);
             }
         }
diff --git a/include/class.model.php b/include/class.model.php
new file mode 100644
index 0000000000000000000000000000000000000000..5d41a05d099684b943c9ad754f2b8653b443324d
--- /dev/null
+++ b/include/class.model.php
@@ -0,0 +1,63 @@
+<?php
+/*********************************************************************
+    class.model.php
+
+    Peter Rotich <peter@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:
+**********************************************************************/
+
+// TODO:  Make ObjectModel models base class and extend VerySimpleModel
+class ObjectModel {
+
+    const OBJECT_TYPE_TICKET = 'T';
+    const OBJECT_TYPE_THREAD = 'H';
+    const OBJECT_TYPE_USER   = 'U';
+    const OBJECT_TYPE_ORG    = 'O';
+    const OBJECT_TYPE_FAQ    = 'K';
+    const OBJECT_TYPE_FILE   = 'F';
+    const OBJECT_TYPE_TASK   = 'A';
+
+    private function objects() {
+        static $objects = false;
+        if ($objects == false) {
+            $objects = array(
+                    self::OBJECT_TYPE_TICKET  => 'Ticket',
+                    self::OBJECT_TYPE_THREAD  => 'ThreadEntry',
+                    self::OBJECT_TYPE_USER    => 'User',
+                    self::OBJECT_TYPE_ORG     => 'Organization',
+                    self::OBJECT_TYPE_FAQ     => 'FAQ',
+                    self::OBJECT_TYPE_FILE    => 'AttachmentFile',
+                    self::OBJECT_TYPE_TASK    => 'Task',
+                    );
+        }
+
+        return $objects;
+    }
+
+    static function getType($model) {
+
+        foreach (self::objects() as $t => $c) {
+            if ($model instanceof $c)
+                return $t;
+        }
+    }
+
+    static function lookup($id, $type) {
+        $model = null;
+        if ($id
+                && ($objects=self::objects())
+                && ($class=$objects[$type])
+                && class_exists($class)
+                && is_callable(array($class, 'lookup')))
+            $model = $class::lookup($id);
+
+        return $model;
+    }
+}
+?>
diff --git a/include/class.nav.php b/include/class.nav.php
index 2b54c32d6c59cfc958ac60dda7f4f4ce1f4a54ff..dfe308f796f6538be1177c10da4febc0c6e79af7 100644
--- a/include/class.nav.php
+++ b/include/class.nav.php
@@ -23,7 +23,7 @@ class StaffNav {
 
     var $staff;
 
-    function StaffNav($staff, $panel='staff'){
+    function __construct($staff, $panel='staff'){
         $this->staff=$staff;
         $this->panel=strtolower($panel);
     }
@@ -111,11 +111,21 @@ class StaffNav {
 
 
     function getTabs(){
+        global $thisstaff;
+
         if(!$this->tabs) {
-            $this->tabs=array();
-            $this->tabs['dashboard'] = array('desc'=>__('Dashboard'),'href'=>'dashboard.php','title'=>__('Agent Dashboard'), "class"=>"no-pjax");
-            $this->tabs['users'] = array('desc' => __('Users'), 'href' => 'users.php', 'title' => __('User Directory'));
+            $this->tabs = array();
+            $this->tabs['dashboard'] = array(
+                'desc'=>__('Dashboard'),'href'=>'dashboard.php','title'=>__('Agent Dashboard'), "class"=>"no-pjax"
+            );
+            if ($thisstaff->hasPerm(User::PERM_DIRECTORY)) {
+                $this->tabs['users'] = array(
+                    'desc' => __('Users'), 'href' => 'users.php', 'title' => __('User Directory')
+                );
+            }
+            $this->tabs['tasks'] = array('desc'=>__('Tasks'), 'href'=>'tasks.php', 'title'=>__('Task Queue'));
             $this->tabs['tickets'] = array('desc'=>__('Tickets'),'href'=>'tickets.php','title'=>__('Ticket Queue'));
+
             $this->tabs['kbase'] = array('desc'=>__('Knowledgebase'),'href'=>'kb.php','title'=>__('Knowledgebase'));
             if (count($this->getRegisteredApps()))
                 $this->tabs['apps']=array('desc'=>__('Applications'),'href'=>'apps.php','title'=>__('Applications'));
@@ -132,6 +142,9 @@ class StaffNav {
         foreach($this->getTabs() as $k=>$tab){
             $subnav=array();
             switch(strtolower($k)){
+                case 'tasks':
+                    $subnav[]=array('desc'=>__('Tasks'), 'href'=>'tasks.php', 'iconclass'=>'Ticket', 'droponly'=>true);
+                    break;
                 case 'tickets':
                     $subnav[]=array('desc'=>__('Tickets'),'href'=>'tickets.php','iconclass'=>'Ticket', 'droponly'=>true);
                     if($staff) {
@@ -141,7 +154,7 @@ class StaffNav {
                                             'iconclass'=>'assignedTickets',
                                             'droponly'=>true);
 
-                        if($staff->canCreateTickets())
+                        if ($staff->hasPerm(TicketModel::PERM_CREATE, false))
                             $subnav[]=array('desc'=>__('New Ticket'),
                                             'title' => __('Open a New Ticket'),
                                             'href'=>'tickets.php?a=open',
@@ -162,9 +175,9 @@ class StaffNav {
                 case 'kbase':
                     $subnav[]=array('desc'=>__('FAQs'),'href'=>'kb.php', 'urls'=>array('faq.php'), 'iconclass'=>'kb');
                     if($staff) {
-                        if($staff->canManageFAQ())
+                        if ($staff->hasPerm(FAQ::PERM_MANAGE))
                             $subnav[]=array('desc'=>__('Categories'),'href'=>'categories.php','iconclass'=>'faq-categories');
-                        if ($cfg->isCannedResponseEnabled() && $staff->canManageCannedResponses())
+                        if ($cfg->isCannedResponseEnabled() && $staff->hasPerm(Canned::PERM_MANAGE, false))
                             $subnav[]=array('desc'=>__('Canned Responses'),'href'=>'canned.php','iconclass'=>'canned');
                     }
                    break;
@@ -193,8 +206,8 @@ class StaffNav {
 
 class AdminNav extends StaffNav{
 
-    function AdminNav($staff){
-        parent::StaffNav($staff, 'admin');
+    function __construct($staff){
+        parent::__construct($staff, 'admin');
     }
 
     function getRegisteredApps() {
@@ -233,11 +246,10 @@ class AdminNav extends StaffNav{
                     $subnav[]=array('desc'=>__('Company'),'href'=>'settings.php?t=pages','iconclass'=>'pages');
                     $subnav[]=array('desc'=>__('System'),'href'=>'settings.php?t=system','iconclass'=>'preferences');
                     $subnav[]=array('desc'=>__('Tickets'),'href'=>'settings.php?t=tickets','iconclass'=>'ticket-settings');
-                    $subnav[]=array('desc'=>__('Emails'),'href'=>'settings.php?t=emails','iconclass'=>'email-settings');
-                    $subnav[]=array('desc'=>__('Access'),'href'=>'settings.php?t=access','iconclass'=>'users');
+                    $subnav[]=array('desc'=>__('Tasks'),'href'=>'settings.php?t=tasks','iconclass'=>'lists');
+                    $subnav[]=array('desc'=>__('Agents'),'href'=>'settings.php?t=agents','iconclass'=>'teams');
+                    $subnav[]=array('desc'=>__('Users'),'href'=>'settings.php?t=users','iconclass'=>'groups');
                     $subnav[]=array('desc'=>__('Knowledgebase'),'href'=>'settings.php?t=kb','iconclass'=>'kb-settings');
-                    $subnav[]=array('desc'=>__('Autoresponder'),'href'=>'settings.php?t=autoresp','iconclass'=>'email-autoresponders');
-                    $subnav[]=array('desc'=>__('Alerts and Notices'),'href'=>'settings.php?t=alerts','iconclass'=>'alert-settings');
                     break;
                 case 'manage':
                     $subnav[]=array('desc'=>__('Help Topics'),'href'=>'helptopics.php','iconclass'=>'helpTopics');
@@ -252,6 +264,7 @@ class AdminNav extends StaffNav{
                     break;
                 case 'emails':
                     $subnav[]=array('desc'=>__('Emails'),'href'=>'emails.php', 'title'=>__('Email Addresses'), 'iconclass'=>'emailSettings');
+                    $subnav[]=array('desc'=>__('Settings'),'href'=>'emailsettings.php','iconclass'=>'email-settings');
                     $subnav[]=array('desc'=>__('Banlist'),'href'=>'banlist.php',
                                         'title'=>__('Banned Emails'),'iconclass'=>'emailDiagnostic');
                     $subnav[]=array('desc'=>__('Templates'),'href'=>'templates.php','title'=>__('Email Templates'),'iconclass'=>'emailTemplates');
@@ -260,7 +273,7 @@ class AdminNav extends StaffNav{
                 case 'staff':
                     $subnav[]=array('desc'=>__('Agents'),'href'=>'staff.php','iconclass'=>'users');
                     $subnav[]=array('desc'=>__('Teams'),'href'=>'teams.php','iconclass'=>'teams');
-                    $subnav[]=array('desc'=>__('Groups'),'href'=>'groups.php','iconclass'=>'groups');
+                    $subnav[]=array('desc'=>__('Roles'),'href'=>'roles.php','iconclass'=>'lists');
                     $subnav[]=array('desc'=>__('Departments'),'href'=>'departments.php','iconclass'=>'departments');
                     break;
                 case 'apps':
@@ -283,7 +296,7 @@ class UserNav {
 
     var $user;
 
-    function UserNav($user=null, $active=''){
+    function __construct($user=null, $active=''){
 
         $this->user=$user;
         $this->navs=$this->getNavs();
@@ -331,7 +344,7 @@ class UserNav {
                 $navs['new']=array('desc'=>__('Open a New Ticket'),'href'=>'open.php','title'=>'');
             if($user && $user->isValid()) {
                 if(!$user->isGuest()) {
-                    $navs['tickets']=array('desc'=>sprintf(__('Tickets (%d)'),$user->getNumTickets()),
+                    $navs['tickets']=array('desc'=>sprintf(__('Tickets (%d)'),$user->getNumTickets($user->canSeeOrgTickets())),
                                            'href'=>'tickets.php',
                                             'title'=>__('Show all tickets'));
                 } else {
diff --git a/include/class.note.php b/include/class.note.php
index d16112d4ee2926b8a01b1d94cfd6d49e23a229c7..4b354944e72821618bdd4e699269786aa13c69d1 100644
--- a/include/class.note.php
+++ b/include/class.note.php
@@ -44,7 +44,7 @@ class QuickNote extends QuickNoteModel {
     }
 
     function getFormattedTime() {
-        return Format::db_datetime(strpos($this->updated, '0000-') !== 0
+        return Format::datetime(strpos($this->updated, '0000-') !== 0
             ? $this->updated : $this->created);
     }
 
diff --git a/include/class.organization.php b/include/class.organization.php
index 9e5bedb7553b92d4b2c4964be08dbcf915fdf49f..f41e04225c5ce1bf017b647644c7e73d99f3a968 100644
--- a/include/class.organization.php
+++ b/include/class.organization.php
@@ -25,6 +25,9 @@ class OrganizationModel extends VerySimpleModel {
             'users' => array(
                 'reverse' => 'User.org',
             ),
+            'cdata' => array(
+                'constraint' => array('id' => 'OrganizationCdata.org_id'),
+            ),
         )
     );
 
@@ -32,6 +35,31 @@ class OrganizationModel extends VerySimpleModel {
     const COLLAB_PRIMARY_CONTACT =  0x0002;
     const ASSIGN_AGENT_MANAGER =    0x0004;
 
+    const SHARE_PRIMARY_CONTACT =   0x0008;
+    const SHARE_EVERYBODY =         0x0010;
+
+    const PERM_CREATE =     'org.create';
+    const PERM_EDIT =       'org.edit';
+    const PERM_DELETE =     'org.delete';
+
+    static protected $perms = array(
+        self::PERM_CREATE => array(
+            'title' => /* @trans */ 'Create',
+            'desc' => /* @trans */ 'Ability to create new organizations',
+            'primary' => true,
+        ),
+        self::PERM_EDIT => array(
+            'title' => /* @trans */ 'Edit',
+            'desc' => /* @trans */ 'Ability to manage organizations',
+            'primary' => true,
+        ),
+        self::PERM_DELETE => array(
+            'title' => /* @trans */ 'Delete',
+            'desc' => /* @trans */ 'Ability to delete organizations',
+            'primary' => true,
+        ),
+    );
+
     var $_manager;
 
     function getId() {
@@ -42,6 +70,10 @@ class OrganizationModel extends VerySimpleModel {
         return $this->name;
     }
 
+    function getNumUsers() {
+        return $this->users->count();
+    }
+
     function getAccountManager() {
         if (!isset($this->_manager)) {
             if ($this->manager[0] == 't')
@@ -75,6 +107,14 @@ class OrganizationModel extends VerySimpleModel {
         return $this->check(self::ASSIGN_AGENT_MANAGER);
     }
 
+    function shareWithPrimaryContacts() {
+        return $this->check(self::SHARE_PRIMARY_CONTACT);
+    }
+
+    function shareWithEverybody() {
+        return $this->check(self::SHARE_EVERYBODY);
+    }
+
     function getUpdateDate() {
         return $this->updated;
     }
@@ -98,9 +138,29 @@ class OrganizationModel extends VerySimpleModel {
     function allMembers() {
         return $this->users;
     }
+
+    static function getPermissions() {
+        return self::$perms;
+    }
+}
+include_once INCLUDE_DIR.'class.role.php';
+RolePermission::register(/* @trans */ 'Organizations',
+    OrganizationModel::getPermissions());
+
+class OrganizationCdata extends VerySimpleModel {
+    static $meta = array(
+        'table' => ORGANIZATION_CDATA_TABLE,
+        'pk' => array('org_id'),
+        'joins' => array(
+            'org' => array(
+                'constraint' => array('ord_id' => 'OrganizationModel.id'),
+            ),
+        ),
+    );
 }
 
-class Organization extends OrganizationModel {
+class Organization extends OrganizationModel
+implements TemplateVariable {
     var $_entries;
     var $_forms;
 
@@ -115,7 +175,7 @@ class Organization extends OrganizationModel {
 
     function getDynamicData($create=true) {
         if (!isset($this->_entries)) {
-            $this->_entries = DynamicFormEntry::forOrganization($this->id)->all();
+            $this->_entries = DynamicFormEntry::forObject($this->id, 'O')->all();
             if (!$this->_entries && $create) {
                 $g = OrganizationForm::getInstance($this->id, true);
                 $g->save();
@@ -130,18 +190,18 @@ class Organization extends OrganizationModel {
 
         if (!isset($this->_forms)) {
             $this->_forms = array();
-            foreach ($this->getDynamicData() as $cd) {
-                $cd->addMissingFields();
+            foreach ($this->getDynamicData() as $entry) {
+                $entry->addMissingFields();
                 if(!$data
-                        && ($form = $cd->getForm())
+                        && ($form = $entry->getDynamicForm())
                         && $form->get('type') == 'O' ) {
-                    foreach ($cd->getFields() as $f) {
+                    foreach ($entry->getFields() as $f) {
                         if ($f->get('name') == 'name')
                             $f->value = $this->getName();
                     }
                 }
 
-                $this->_forms[] = $cd->getForm();
+                $this->_forms[] = $entry;
             }
         }
 
@@ -158,6 +218,8 @@ class Organization extends OrganizationModel {
                 'collab-all-flag' => Organization::COLLAB_ALL_MEMBERS,
                 'collab-pc-flag' => Organization::COLLAB_PRIMARY_CONTACT,
                 'assign-am-flag' => Organization::ASSIGN_AGENT_MANAGER,
+                'sharing-primary' => Organization::SHARE_PRIMARY_CONTACT,
+                'sharing-all' => Organization::SHARE_EVERYBODY,
         ) as $ck=>$flag) {
             if ($this->check($flag))
                 $base[$ck] = true;
@@ -196,7 +258,7 @@ class Organization extends OrganizationModel {
         }
     }
 
-    function addForm($form, $sort=1, $data) {
+    function addForm($form, $sort=1, $data=null) {
         $entry = $form->instanciate($sort, $data);
         $entry->set('object_type', 'O');
         $entry->set('object_id', $this->getId());
@@ -207,11 +269,11 @@ class Organization extends OrganizationModel {
     function getFilterData() {
         $vars = array();
         foreach ($this->getDynamicData() as $entry) {
-            if ($entry->getForm()->get('type') != 'O')
+            if ($entry->getDynamicForm()->get('type') != 'O')
                 continue;
             $vars += $entry->getFilterData();
             // Add special `name` field
-            $f = $entry->getForm()->getField('name');
+            $f = $entry->getField('name');
             $vars['field.'.$f->get('id')] = $this->getName();
         }
         return $vars;
@@ -251,25 +313,43 @@ class Organization extends OrganizationModel {
     }
 
     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;
+
+        switch ($tag) {
+        case 'members':
+            return new UserList($this->users);
+        case 'manager':
+            return $this->getAccountManager();
+        case 'contacts':
+            return new UserList($this->users->filter(array(
+                'flags__hasbit' => User::PRIMARY_ORG_CONTACT
+            )));
+        }
+    }
+
+    static function getVarScope() {
+        $base = array(
+            'contacts' => array('class' => 'UserList', 'desc' => __('Primary Contacts')),
+            'manager' => __('Account Manager'),
+            'members' => array('class' => 'UserList', 'desc' => __('Organization Members')),
+            'name' => __('Name'),
+        );
+        $extra = VariableReplacer::compileFormScope(OrganizationForm::getInstance());
+        return $base + $extra;
     }
 
     function update($vars, &$errors) {
 
         $valid = true;
         $forms = $this->getForms($vars);
-        foreach ($forms as $cd) {
-            if (!$cd->isValid())
+        foreach ($forms as $entry) {
+            if (!$entry->isValid())
                 $valid = false;
-            if ($cd->get('type') == 'O'
-                        && ($form= $cd->getForm($vars))
-                        && ($f=$form->getField('name'))
+            if ($entry->getDynamicForm()->get('type') == 'O'
+                        && ($f = $entry->getField('name'))
                         && $f->getClean()
                         && ($o=Organization::lookup(array('name'=>$f->getClean())))
                         && $o->id != $this->getId()) {
@@ -303,15 +383,15 @@ class Organization extends OrganizationModel {
         if (!$valid || $errors)
             return false;
 
-        foreach ($this->getDynamicData() as $cd) {
-            if (($f=$cd->getForm())
-                    && ($f->get('type') == 'O')
-                    && ($name = $f->getField('name'))) {
-                    $this->name = $name->getClean();
-                    $this->save();
-                }
-            $cd->setSource($vars);
-            if ($cd->save())
+        foreach ($this->getDynamicData() as $entry) {
+            if ($entry->getDynamicForm()->get('type') == 'O'
+               && ($name = $entry->getField('name'))
+            ) {
+                $this->name = $name->getClean();
+                $this->save();
+            }
+            $entry->setSource($vars);
+            if ($entry->save())
                 $this->updated = SqlFunction::NOW();
         }
 
@@ -327,6 +407,16 @@ class Organization extends OrganizationModel {
                 $this->clearStatus($flag);
         }
 
+        foreach (array(
+                'sharing-primary' => Organization::SHARE_PRIMARY_CONTACT,
+                'sharing-all' => Organization::SHARE_EVERYBODY,
+        ) as $ck=>$flag) {
+            if ($vars['sharing'] == $ck)
+                $this->setStatus($flag);
+            else
+                $this->clearStatus($flag);
+        }
+
         // Set staff and primary contacts
         $this->set('domain', $vars['domain']);
         $this->set('manager', $vars['manager'] ?: '');
@@ -337,11 +427,6 @@ class Organization extends OrganizationModel {
             }
         }
 
-        // Send signal for search engine updating if not modifying the
-        // fields specific to the organization
-        if (count($this->dirty) === 0)
-            Signal::send('model.updated', $this);
-
         return $this->save();
     }
 
@@ -349,24 +434,31 @@ class Organization extends OrganizationModel {
         if (!parent::delete())
             return false;
 
+        // Remove users from this organization
+        User::objects()
+            ->filter(array('org' => $this))
+            ->update(array('org_id' => 0));
+
         foreach ($this->getDynamicData(false) as $entry) {
-            $entry->delete();
+            if (!$entry->delete())
+                return false;
         }
+        return true;
     }
 
     static function fromVars($vars) {
 
         $vars['name'] = Format::striptags($vars['name']);
-        if (!($org = Organization::lookup(array('name' => $vars['name'])))) {
-            $org = Organization::create(array(
+        if (!($org = static::lookup(array('name' => $vars['name'])))) {
+            $org = static::create(array(
                 'name' => $vars['name'],
-                'created' => new SqlFunction('NOW'),
                 'updated' => new SqlFunction('NOW'),
             ));
             $org->save(true);
             $org->addDynamicData($vars);
         }
 
+        Signal::send('organization.created', $org);
         return $org;
     }
 
@@ -383,7 +475,7 @@ class Organization extends OrganizationModel {
         // Make sure the name is not in-use
         if (($field=$form->getField('name'))
                 && $field->getClean()
-                && Organization::lookup(array('name' => $field->getClean()))) {
+                && static::lookup(array('name' => $field->getClean()))) {
             $field->addError(__('Organization with the same name already exists'));
             $valid = false;
         }
@@ -391,11 +483,18 @@ class Organization extends OrganizationModel {
         return $valid ? self::fromVars($form->getClean()) : null;
     }
 
+    static function create($vars=false) {
+        $org = new static($vars);
+
+        $org->created = new SqlFunction('NOW');
+        $org->setStatus(self::SHARE_PRIMARY_CONTACT);
+        return $org;
+    }
+
     // Custom create called by installer/upgrader to load initial data
     static function __create($ht, &$error=false) {
 
-        $ht['created'] = new SqlFunction('NOW');
-        $org = Organization::create($ht);
+        $org = static::create($ht);
         // Add dynamic data (if any)
         if ($ht['fields']) {
             $org->save(true);
@@ -410,6 +509,12 @@ class OrganizationForm extends DynamicForm {
     static $instance;
     static $form;
 
+    static $cdata = array(
+            'table' => ORGANIZATION_CDATA_TABLE,
+            'object_id' => 'org_id',
+            'object_type' => ObjectModel::OBJECT_TYPE_ORG,
+        );
+
     static function objects() {
         $os = parent::objects();
         return $os->filter(array('type'=>'O'));
@@ -478,5 +583,4 @@ Filter::addSupportedMatches(/*@trans*/ 'Organization Data', function() {
     }
     return $matches;
 },40);
-Organization::_inspect();
 ?>
diff --git a/include/class.orm.php b/include/class.orm.php
index e7e54d95e4b6e10c680cb1848aee668234bebed0..e0120814d3a72170c4412c8fa59b7b067204778b 100644
--- a/include/class.orm.php
+++ b/include/class.orm.php
@@ -18,6 +18,262 @@
 
 class OrmException extends Exception {}
 class OrmConfigurationException extends Exception {}
+// Database fields/tables do not match codebase
+class InconsistentModelException extends OrmException {
+    function __construct() {
+        // Drop the model cache (just incase)
+        ModelMeta::flushModelCache();
+        call_user_func_array(array('parent', '__construct'), func_get_args());
+    }
+}
+
+/**
+ * Meta information about a model including edges (relationships), table
+ * name, default sorting information, database fields, etc.
+ *
+ * This class is constructed and built automatically from the model's
+ * ::getMeta() method using a class's ::$meta array.
+ */
+class ModelMeta implements ArrayAccess {
+
+    static $base = array(
+        'pk' => false,
+        'table' => false,
+        'defer' => array(),
+        'select_related' => array(),
+        'view' => false,
+        'joins' => array(),
+        'foreign_keys' => array(),
+    );
+    static $model_cache;
+
+    var $model;
+    var $meta = array();
+    var $new;
+    var $subclasses = array();
+    var $fields;
+
+    function __construct($model) {
+        $this->model = $model;
+
+        // Merge ModelMeta from parent model (if inherited)
+        $parent = get_parent_class($this->model);
+        $meta = $model::$meta;
+        if ($model::$meta instanceof self)
+            $meta = $meta->meta;
+        if (is_subclass_of($parent, 'VerySimpleModel')) {
+            $this->parent = $parent::getMeta();
+            $meta = $this->parent->extend($this, $meta);
+        }
+        else {
+            $meta = $meta + self::$base;
+        }
+
+        // Short circuit the meta-data processing if APCu is available.
+        // This is preferred as the meta-data is unlikely to change unless
+        // osTicket is upgraded, (then the upgrader calls the
+        // flushModelCache method to clear this cache). Also, GIT_VERSION is
+        // used in the APC key which should be changed if new code is
+        // deployed.
+        if (function_exists('apcu_store')) {
+            $loaded = false;
+            $apc_key = SECRET_SALT.GIT_VERSION."/orm/{$this->model}";
+            $this->meta = apcu_fetch($apc_key, $loaded);
+            if ($loaded)
+                return;
+        }
+
+        if (!$meta['view']) {
+            if (!$meta['table'])
+                throw new OrmConfigurationException(
+                    sprintf(__('%s: Model does not define meta.table'), $this->model));
+            elseif (!$meta['pk'])
+                throw new OrmConfigurationException(
+                    sprintf(__('%s: Model does not define meta.pk'), $this->model));
+        }
+
+        // Ensure other supported fields are set and are arrays
+        foreach (array('pk', 'ordering', 'defer', 'select_related') as $f) {
+            if (!isset($meta[$f]))
+                $meta[$f] = array();
+            elseif (!is_array($meta[$f]))
+                $meta[$f] = array($meta[$f]);
+        }
+
+        // Break down foreign-key metadata
+        foreach ($meta['joins'] as $field => &$j) {
+            $this->processJoin($j);
+            if ($j['local'])
+                $meta['foreign_keys'][$j['local']] = $field;
+        }
+        unset($j);
+        $this->meta = $meta;
+
+        if (function_exists('apcu_store')) {
+            apcu_store($apc_key, $this->meta, 1800);
+        }
+    }
+
+    function extend(ModelMeta $child, $meta) {
+        $this->subclasses[$child->model] = $child;
+        return $meta + $this->meta + self::$base;
+    }
+
+    function isSuperClassOf($model) {
+        if (isset($this->subclasses[$model]))
+            return true;
+        foreach ($this->subclasses as $M=>$meta)
+            if ($meta->isSuperClassOf($M))
+                return true;
+    }
+
+    function isSubclassOf($model) {
+        if (!isset($this->parent))
+            return false;
+
+        if ($this->parent->model === $model)
+            return true;
+
+        return $this->parent->isSubclassOf($model);
+    }
+
+    /**
+     * Adds some more information to a declared relationship. If the
+     * relationship is a reverse relation, then the information from the
+     * reverse relation is loaded into the local definition
+     *
+     * Compiled-Join-Structure:
+     * 'constraint' => array(local => array(foreign_field, foreign_class)),
+     *      Constraint used to construct a JOIN in an SQL query
+     * 'list' => boolean
+     *      TRUE if an InstrumentedList should be employed to fetch a list
+     *      of related items
+     * 'broker' => Handler for the 'list' property. Usually a subclass of
+     *      'InstrumentedList'
+     * 'null' => boolean
+     *      TRUE if relation is nullable
+     * 'fkey' => array(class, pk)
+     *      Classname and field of the first item in the constraint that
+     *      points to a PK field of a foreign model
+     * 'local' => string
+     *      The local field corresponding to the 'fkey' property
+     */
+    function processJoin(&$j) {
+        $constraint = array();
+        if (isset($j['reverse'])) {
+            list($fmodel, $key) = explode('.', $j['reverse']);
+            // NOTE: It's ok if the forein meta data is not yet inspected.
+            $info = $fmodel::$meta['joins'][$key];
+            if (!is_array($info['constraint']))
+                throw new OrmConfigurationException(sprintf(__(
+                    // `reverse` here is the reverse of an ORM relationship
+                    '%s: Reverse does not specify any constraints'),
+                    $j['reverse']));
+            foreach ($info['constraint'] as $foreign => $local) {
+                list($L,$field) = is_array($local) ? $local : explode('.', $local);
+                $constraint[$field ?: $L] = array($fmodel, $foreign);
+            }
+            if (!isset($j['list']))
+                $j['list'] = true;
+            if (!isset($j['null']))
+                // By default, reverse releationships can be empty lists
+                $j['null'] = true;
+        }
+        else {
+            foreach ($j['constraint'] as $local => $foreign) {
+                list($class, $field) = $constraint[$local]
+                    = is_array($foreign) ? $foreign : explode('.', $foreign);
+            }
+        }
+        if ($j['list'] && !isset($j['broker'])) {
+            $j['broker'] = 'InstrumentedList';
+        }
+        if ($j['broker'] && !class_exists($j['broker'])) {
+            throw new OrmException($j['broker'] . ': List broker does not exist');
+        }
+        foreach ($constraint as $local => $foreign) {
+            list($class, $field) = $foreign;
+            if ($local[0] == "'" || $field[0] == "'" || !class_exists($class))
+                continue;
+            $j['fkey'] = $foreign;
+            $j['local'] = $local;
+        }
+        $j['constraint'] = $constraint;
+    }
+
+    function addJoin($name, array $join) {
+        $this->meta['joins'][$name] = $join;
+        $this->processJoin($this->meta['joins'][$name]);
+    }
+
+    function offsetGet($field) {
+        return $this->meta[$field];
+    }
+    function offsetSet($field, $what) {
+        $this->meta[$field] = $what;
+    }
+    function offsetExists($field) {
+        return isset($this->meta[$field]);
+    }
+    function offsetUnset($field) {
+        throw new Exception('Model MetaData is immutable');
+    }
+
+    /**
+     * Fetch the column names of the table used to persist instances of this
+     * model in the database.
+     */
+    function getFieldNames() {
+        if (!isset($this->fields))
+            $this->fields = self::inspectFields();
+        return $this->fields;
+    }
+
+    /**
+     * Create a new instance of the model, optionally hydrating it with the
+     * given hash table. The constructor is not called, which leaves the
+     * default constructor free to assume new object status.
+     *
+     * Three methods were considered, with runtime for 10000 iterations
+     *   * unserialze('O:9:"ModelBase":0:{}') - 0.0671s
+     *   * new ReflectionClass("ModelBase")->newInstanceWithoutConstructor()
+     *      - 0.0478s
+     *   * and a hybrid by cloning the reflection class instance - 0.0335s
+     */
+    function newInstance($props=false) {
+        if (!isset($this->new)) {
+            $rc = new ReflectionClass($this->model);
+            $this->new = $rc->newInstanceWithoutConstructor();
+        }
+        $instance = clone $this->new;
+        // Hydrate if props were included
+        if (is_array($props)) {
+            $instance->ht = $props;
+        }
+        return $instance;
+    }
+
+    function inspectFields() {
+        if (!isset(self::$model_cache))
+            self::$model_cache = function_exists('apcu_fetch');
+        if (self::$model_cache) {
+            $key = SECRET_SALT.GIT_VERSION."/orm/{$this['table']}";
+            if ($fields = apcu_fetch($key)) {
+                return $fields;
+            }
+        }
+        $fields = DbEngine::getCompiler()->inspectTable($this['table']);
+        if (self::$model_cache) {
+            apcu_store($key, $fields, 1800);
+        }
+        return $fields;
+    }
+
+    static function flushModelCache() {
+        if (self::$model_cache)
+            @apcu_clear_cache('user');
+    }
+}
 
 class VerySimpleModel {
     static $meta = array(
@@ -27,32 +283,91 @@ class VerySimpleModel {
     );
 
     var $ht;
-    var $dirty;
+    var $dirty = array();
     var $__new__ = false;
+    var $__deleted__ = false;
+    var $__deferred__ = array();
 
-    function __construct($row) {
-        $this->ht = $row;
-        $this->__setupForeignLists();
-        $this->dirty = array();
+    function __construct($row=false) {
+        if (is_array($row))
+            foreach ($row as $field=>$value)
+                if (!is_array($value))
+                    $this->set($field, $value);
+        $this->__new__ = true;
     }
 
     function get($field, $default=false) {
         if (array_key_exists($field, $this->ht))
             return $this->ht[$field];
-        elseif (isset(static::$meta['joins'][$field])) {
-            // TODO: Support instrumented lists and such
-            $j = static::$meta['joins'][$field];
-            // Make sure joins were inspected
-            if (isset($j['fkey'])
-                    && ($class = $j['fkey'][0])
-                    && class_exists($class)) {
-                $v = $this->ht[$field] = $class::lookup(
-                    array($j['fkey'][1] => $this->ht[$j['local']]));
+        elseif (($joins = static::getMeta('joins')) && isset($joins[$field])) {
+            $j = $joins[$field];
+            // Support instrumented lists and such
+            if (isset($j['list']) && $j['list']) {
+                $class = $j['fkey'][0];
+                $fkey = array();
+                // Localize the foreign key constraint
+                foreach ($j['constraint'] as $local=>$foreign) {
+                    list($_klas,$F) = $foreign;
+                    $fkey[$F ?: $_klas] = ($local[0] == "'")
+                        ? trim($local, "'") : $this->ht[$local];
+                }
+                $v = $this->ht[$field] = new $j['broker'](
+                    // Send Model, [Foriegn-Field => Local-Id]
+                    array($class, $fkey)
+                );
+                return $v;
+            }
+            // Support relationships
+            elseif (isset($j['fkey'])) {
+                $criteria = array();
+                foreach ($j['constraint'] as $local => $foreign) {
+                    list($klas,$F) = $foreign;
+                    if (class_exists($klas))
+                        $class = $klas;
+                    if ($local[0] == "'") {
+                        $criteria[$F] = trim($local,"'");
+                    }
+                    elseif ($F[0] == "'") {
+                        // Does not affect the local model
+                        continue;
+                    }
+                    else {
+                        $criteria[$F] = $this->ht[$local];
+                    }
+                }
+                try {
+                    $v = $this->ht[$field] = $class::lookup($criteria);
+                }
+                catch (DoesNotExist $e) {
+                    $v = null;
+                }
                 return $v;
             }
         }
+        elseif (isset($this->__deferred__[$field])) {
+            // Fetch deferred field
+            $row = static::objects()->filter($this->getPk())
+                // XXX: Seems like all the deferred fields should be fetched
+                ->values_flat($field)
+                ->one();
+            if ($row)
+                return $this->ht[$field] = $row[0];
+        }
+        elseif ($field == 'pk') {
+            return $this->getPk();
+        }
+
         if (isset($default))
             return $default;
+
+        // For new objects, assume the field is NULLable
+        if ($this->__new__)
+            return null;
+
+        // Check to see if the column referenced is actually valid
+        if (in_array($field, static::getMeta()->getFieldNames()))
+            return null;
+
         throw new OrmException(sprintf(__('%s: %s: Field not defined'),
             get_class($this), $field));
     }
@@ -60,53 +375,89 @@ class VerySimpleModel {
         return $this->get($field, null);
     }
 
+    function getByPath($path) {
+        if (is_string($path))
+            $path = explode('__', $path);
+        $root = $this;
+        foreach ($path as $P)
+            $root = $root->get($P);
+        return $root;
+    }
+
     function __isset($field) {
         return array_key_exists($field, $this->ht)
             || isset(static::$meta['joins'][$field]);
     }
     function __unset($field) {
-        unset($this->ht[$field]);
+        if ($this->__isset($field))
+            unset($this->ht[$field]);
+        else
+            unset($this->{$field});
     }
 
     function set($field, $value) {
         // Update of foreign-key by assignment to model instance
-        if (isset(static::$meta['joins'][$field])) {
-            if (!isset(static::$meta['joins'][$field]['fkey']))
-                static::_inspect();
-            $j = static::$meta['joins'][$field];
+        $related = false;
+        $joins = static::getMeta('joins');
+        if (isset($joins[$field])) {
+            $j = $joins[$field];
             if ($j['list'] && ($value instanceof InstrumentedList)) {
                 // Magic list property
                 $this->ht[$field] = $value;
                 return;
             }
             if ($value === null) {
+                $this->ht[$field] = $value;
+                if (in_array($j['local'], static::$meta['pk'])) {
+                    // Reverse relationship — don't null out local PK
+                    return;
+                }
                 // Pass. Set local field to NULL in logic below
             }
-            elseif ($value instanceof $j['fkey'][0]) {
-                if ($value->__new__)
-                    $value->save();
+            elseif ($value instanceof VerySimpleModel) {
+                // Ensure that the model being assigned as a relationship is
+                // an instance of the foreign model given in the
+                // relationship, or is a super class thereof. The super
+                // class case is used primary for the xxxThread classes
+                // which all extend from the base Thread class.
+                if (!$value instanceof $j['fkey'][0]
+                    && !$value::getMeta()->isSuperClassOf($j['fkey'][0])
+                ) {
+                    throw new InvalidArgumentException(
+                        sprintf(__('Expecting NULL or instance of %s. Got a %s instead'),
+                        $j['fkey'][0], is_object($value) ? get_class($value) : gettype($value)));
+                }
                 // Capture the object under the object's field name
                 $this->ht[$field] = $value;
-                $value = $value->get($j['fkey'][1]);
+                if ($value->__new__)
+                    // save() will be performed when saving this object
+                    $value = null;
+                else
+                    $value = $value->get($j['fkey'][1]);
                 // Fall through to the standard logic below
             }
-            else
-                throw new InvalidArgumentException(
-                    sprintf(__('Expecting NULL or instance of %s'), $j['fkey'][0]));
-
             // Capture the foreign key id value
             $field = $j['local'];
         }
-        // XXX: Fully support or die if updating pk
-        // XXX: The contents of $this->dirty should be the value after the
-        // previous fetch or save. For instance, if the value is changed more
-        // than once, the original value should be preserved in the dirty list
-        // on the second edit.
+        // elseif $field is in a relationship, adjust the relationship
+        elseif (isset(static::$meta['foreign_keys'][$field])) {
+            // meta->foreign_keys->{$field} points to the property of the
+            // foreign object. For instance 'object_id' points to 'object'
+            $related = static::$meta['foreign_keys'][$field];
+        }
         $old = isset($this->ht[$field]) ? $this->ht[$field] : null;
         if ($old != $value) {
-            $this->dirty[$field] = $old;
-            $this->ht[$field] = $value;
+            // isset should not be used here, because `null` should not be
+            // replaced in the dirty array
+            if (!array_key_exists($field, $this->dirty))
+                $this->dirty[$field] = $old;
+            if ($related)
+                // $related points to a foreign object propery. If setting a
+                // new object_id value, the relationship to object should be
+                // cleared and rebuilt
+                unset($this->ht[$related]);
         }
+        $this->ht[$field] = $value;
     }
     function __set($field, $value) {
         return $this->set($field, $value);
@@ -117,117 +468,146 @@ class VerySimpleModel {
             $this->set($field, $value);
     }
 
-    function __setupForeignLists() {
-        // Construct related lists
-        if (isset(static::$meta['joins'])) {
-            foreach (static::$meta['joins'] as $name => $j) {
-                if (isset($this->ht[$j['local']])
-                        && isset($j['list']) && $j['list']) {
-                    $fkey = $j['fkey'];
-                    $this->set($name, new InstrumentedList(
-                        // Send Model, Foriegn-Field, Local-Id
-                        array($fkey[0], $fkey[1], $this->get($j['local'])))
-                    );
-                }
-            }
-        }
-    }
-
     function __onload() {}
 
-    static function _inspect() {
-        if (!static::$meta['table'])
-            throw new OrmConfigurationException(
-                __('Model does not define meta.table'), get_called_class());
-
-        // Break down foreign-key metadata
-        foreach (static::$meta['joins'] as $field => &$j) {
-            if (isset($j['reverse'])) {
-                list($model, $key) = explode('.', $j['reverse']);
-                $info = $model::$meta['joins'][$key];
-                $constraint = array();
-                if (!is_array($info['constraint']))
-                    throw new OrmConfigurationException(sprintf(__(
-                        // `reverse` here is the reverse of an ORM relationship
-                        '%s: Reverse does not specify any constraints'),
-                        $j['reverse']));
-                foreach ($info['constraint'] as $foreign => $local) {
-                    list(,$field) = explode('.', $local);
-                    $constraint[$field] = "$model.$foreign";
-                }
-                $j['constraint'] = $constraint;
-                if (!isset($j['list']))
-                    $j['list'] = true;
-            }
-            // XXX: Make this better (ie. composite keys)
-            $keys = array_keys($j['constraint']);
-            $foreign = $j['constraint'][$keys[0]];
-            $j['fkey'] = explode('.', $foreign);
-            $j['local'] = $keys[0];
+    static function getMeta($key=false) {
+        if (!static::$meta instanceof ModelMeta
+            || get_called_class() != static::$meta->model
+        ) {
+            static::$meta = new ModelMeta(get_called_class());
         }
+        $M = static::$meta;
+        return ($key) ? $M->offsetGet($key) : $M;
     }
 
+    /**
+     * objects
+     *
+     * Retrieve a QuerySet for this model class which can be used to fetch
+     * models from the connected database. Subclasses can override this
+     * method to apply forced constraints on the QuerySet.
+     */
     static function objects() {
         return new QuerySet(get_called_class());
     }
 
+    /**
+     * lookup
+     *
+     * Retrieve a record by its primary key. This method may be short
+     * circuited by model caching if the record has already been loaded by
+     * the database. In such a case, the database will not be consulted for
+     * the model's data.
+     *
+     * This method can be called with an array of keyword arguments matching
+     * the PK of the object or the values of the primary key. Both of these
+     * usages are correct:
+     *
+     * >>> User::lookup(1)
+     * >>> User::lookup(array('id'=>1))
+     *
+     * For composite primary keys and the first usage, pass the values in
+     * the order they are given in the Model's 'pk' declaration in its meta
+     * data.
+     *
+     * Parameters:
+     * $criteria - (mixed) primary key for the sought model either as
+     *      arguments or key/value array as the function's first argument
+     *
+     * Returns:
+     * (Object<Model>|null) a single instance of the sought model or null if
+     * no such instance exists.
+     */
     static function lookup($criteria) {
-        if (!is_array($criteria))
-            // Model::lookup(1), where >1< is the pk value
-            $criteria = array(static::$meta['pk'][0] => $criteria);
-        return static::objects()->filter($criteria)->one();
+        // Model::lookup(1), where >1< is the pk value
+        $args = func_get_args();
+        if (!is_array($criteria)) {
+            $criteria = array();
+            $pk = static::getMeta('pk');
+            foreach ($args as $i=>$f)
+                $criteria[$pk[$i]] = $f;
+
+            // Only consult cache for PK lookup, which is assumed if the
+            // values are passed as args rather than an array
+            if ($cached = ModelInstanceManager::checkCache(get_called_class(),
+                    $criteria))
+                return $cached;
+        }
+        try {
+            return static::objects()->filter($criteria)->one();
+        }
+        catch (DoesNotExist $e) {
+            return null;
+        }
     }
 
-    function delete($pk=false) {
-        $table = static::$meta['table'];
-        $sql = 'DELETE FROM '.$table;
-        $filter = array();
-
-        if (!$pk) $pk = static::$meta['pk'];
-        if (!is_array($pk)) $pk=array($pk);
-
-        foreach ($pk as $p)
-            $filter[] = $p.' = '.db_input($this->get($p));
-        $sql .= ' WHERE '.implode(' AND ', $filter).' LIMIT 1';
-        if (!db_query($sql) || db_affected_rows() != 1)
-            throw new Exception(db_error());
-        Signal::send('model.deleted', $this);
+    function delete() {
+        $ex = DbEngine::delete($this);
+        try {
+            $ex->execute();
+            if ($ex->affected_rows() != 1)
+                return false;
+
+            $this->__deleted__ = true;
+            Signal::send('model.deleted', $this);
+        }
+        catch (OrmException $e) {
+            return false;
+        }
         return true;
     }
 
     function save($refetch=false) {
-        $pk = static::$meta['pk'];
-        if (!is_array($pk)) $pk=array($pk);
-        if ($this->__new__)
-            $sql = 'INSERT INTO '.static::$meta['table'];
-        else
-            $sql = 'UPDATE '.static::$meta['table'];
-        $filter = $fields = array();
+        if ($this->__deleted__)
+            throw new OrmException('Trying to update a deleted object');
+
+        $pk = static::getMeta('pk');
+        $wasnew = $this->__new__;
+
+        // First, if any foreign properties of this object are connected to
+        // another *new* object, then save those objects first and set the
+        // local foreign key field values
+        foreach (static::getMeta('joins') as $prop => $j) {
+            if (isset($this->ht[$prop])
+                && ($foreign = $this->ht[$prop])
+                && $foreign instanceof VerySimpleModel
+                && !in_array($j['local'], $pk)
+                && null === $this->get($j['local'])
+            ) {
+                if ($foreign->__new__ && !$foreign->save())
+                    return false;
+                $this->set($j['local'], $foreign->get($j['fkey'][1]));
+            }
+        }
+
+        // If there's nothing in the model to be saved, then we're done
         if (count($this->dirty) === 0)
             return true;
-        foreach ($this->dirty as $field=>$old) {
-            if ($this->__new__ or !in_array($field, $pk)) {
-                if (@get_class($this->get($field)) == 'SqlFunction')
-                    $fields[] = "`$field` = ".$this->get($field)->toSql();
+
+        $ex = DbEngine::save($this);
+        try {
+            $ex->execute();
+            if ($ex->affected_rows() != 1) {
+                // This doesn't really signify an error. It just means that
+                // the database believes that the row did not change. For
+                // inserts though, it's a deal breaker
+                if ($this->__new__)
+                    return false;
                 else
-                    $fields[] = "`$field` = ".db_input($this->get($field));
+                    // No need to reload the record if requested — the
+                    // database didn't update anything
+                    $refetch = false;
             }
         }
-        $sql .= ' SET '.implode(', ', $fields);
-        if (!$this->__new__) {
-            foreach ($pk as $p)
-                $filter[] = $p.' = '.db_input($this->get($p));
-            $sql .= ' WHERE '.implode(' AND ', $filter);
-            $sql .= ' LIMIT 1';
+        catch (OrmException $e) {
+            return false;
         }
-        if (!db_query($sql) || db_affected_rows() != 1)
-            throw new Exception(db_error());
-        if ($this->__new__) {
+
+        if ($wasnew) {
             if (count($pk) == 1)
-                $this->ht[$pk[0]] = db_insert_id();
+                // XXX: Ensure AUTO_INCREMENT is set for the field
+                $this->ht[$pk[0]] = $ex->insert_id();
             $this->__new__ = false;
-            // Setup lists again
-            $this->__setupForeignLists();
             Signal::send('model.created', $this);
         }
         else {
@@ -235,38 +615,125 @@ class VerySimpleModel {
             Signal::send('model.updated', $this, $data);
         }
         # Refetch row from database
-        # XXX: Too much voodoo
         if ($refetch) {
-            # XXX: Support composite PK
-            $criteria = array($pk[0] => $this->get($pk[0]));
-            $self = static::lookup($criteria);
-            $this->ht = $self->ht;
+            // Preserve non database information such as list relationships
+            // across the refetch
+            $this->ht =
+                static::objects()->filter($this->getPk())->values()->one()
+                + $this->ht;
+        }
+        if ($wasnew) {
+            // Attempt to update foreign, unsaved objects with the PK of
+            // this newly created object
+            foreach (static::getMeta('joins') as $prop => $j) {
+                if (isset($this->ht[$prop])
+                    && ($foreign = $this->ht[$prop])
+                    && in_array($j['local'], $pk)
+                ) {
+                    if ($foreign instanceof VerySimpleModel
+                        && null === $foreign->get($j['fkey'][1])
+                    ) {
+                        $foreign->set($j['fkey'][1], $this->get($j['local']));
+                    }
+                    elseif ($foreign instanceof InstrumentedList) {
+                        foreach ($foreign as $item) {
+                            if (null === $item->get($j['fkey'][1]))
+                                $item->set($j['fkey'][1], $this->get($j['local']));
+                        }
+                    }
+                }
+            }
+            $this->__onload();
         }
         $this->dirty = array();
-        return $this->get($pk[0]);
+        return true;
+    }
+
+    private function getPk() {
+        $pk = array();
+        foreach ($this::getMeta('pk') as $f)
+            $pk[$f] = $this->ht[$f];
+        return $pk;
+    }
+}
+
+/**
+ * AnnotatedModel
+ *
+ * Simple wrapper class which allows wrapping and write-protecting of
+ * annotated fields retrieved from the database. Instances of this class
+ * will delegate most all of the heavy lifting to the wrapped Model instance.
+ */
+class AnnotatedModel {
+
+    var $model;
+    var $annotations;
+
+    function __construct($model, $annotations) {
+        $this->model = $model;
+        $this->annotations = $annotations;
     }
 
-    static function create($ht=false) {
-        if (!$ht) $ht=array();
-        $class = get_called_class();
-        $i = new $class(array());
-        $i->__new__ = true;
-        foreach ($ht as $field=>$value)
-            if (!is_array($value))
-                $i->set($field, $value);
-        return $i;
+    function __get($what) {
+        return $this->get($what);
+    }
+    function get($what) {
+        if (isset($this->annotations[$what]))
+            return $this->annotations[$what];
+        return $this->model->get($what, null);
+    }
+
+    function __set($what, $to) {
+        return $this->set($what, $to);
+    }
+    function set($what, $to) {
+        if (isset($this->annotations[$what]))
+            throw new OrmException('Annotated fields are read-only');
+        return $this->model->set($what, $to);
+    }
+
+    function __isset($what) {
+        return isset($this->annotations[$what]) || $this->model->__isset($what);
+    }
+
+    // Delegate everything else to the model
+    function __call($what, $how) {
+        return call_user_func_array(array($this->model, $what), $how);
     }
 }
 
 class SqlFunction {
-    function SqlFunction($name) {
+    var $alias;
+
+    function __construct($name) {
         $this->func = $name;
         $this->args = array_slice(func_get_args(), 1);
     }
 
-    function toSql($compiler=false) {
-        $args = (count($this->args)) ? implode(',', db_input($this->args)) : "";
-        return sprintf('%s(%s)', $this->func, $args);
+    function input($what, $compiler, $model) {
+        if ($what instanceof SqlFunction)
+            $A = $what->toSql($compiler, $model);
+        elseif ($what instanceof Q)
+            $A = $compiler->compileQ($what, $model);
+        else
+            $A = $compiler->input($what);
+        return $A;
+    }
+
+    function toSql($compiler, $model=false, $alias=false) {
+        $args = array();
+        foreach ($this->args as $A) {
+            $args[] = $this->input($A, $compiler, $model);
+        }
+        return sprintf('%s(%s)%s', $this->func, implode(', ', $args),
+            $alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : '');
+    }
+
+    function getAlias() {
+        return $this->alias;
+    }
+    function setAlias($alias) {
+        $this->alias = $alias;
     }
 
     static function __callStatic($func, $args) {
@@ -274,28 +741,249 @@ class SqlFunction {
         $I->args = $args;
         return $I;
     }
+
+    function __call($operator, $other) {
+        array_unshift($other, $this);
+        return SqlExpression::__callStatic($operator, $other);
+    }
+}
+
+class SqlCase extends SqlFunction {
+    var $cases = array();
+    var $else = false;
+
+    static function N() {
+        return new static('CASE');
+    }
+
+    function when($expr, $result) {
+        if (is_array($expr))
+            $expr = new Q($expr);
+        $this->cases[] = array($expr, $result);
+        return $this;
+    }
+    function otherwise($result) {
+        $this->else = $result;
+        return $this;
+    }
+
+    function toSql($compiler, $model=false, $alias=false) {
+        $cases = array();
+        foreach ($this->cases as $A) {
+            list($expr, $result) = $A;
+            $expr = $this->input($expr, $compiler, $model);
+            $result = $this->input($result, $compiler, $model);
+            $cases[] = "WHEN {$expr} THEN {$result}";
+        }
+        if ($this->else) {
+            $else = $this->input($this->else, $compiler, $model);
+            $cases[] = "ELSE {$else}";
+        }
+        return sprintf('CASE %s END%s', implode(' ', $cases),
+            $alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : '');
+    }
+}
+
+class SqlExpr extends SqlFunction {
+    function __construct($args) {
+        $this->args = $args;
+    }
+
+    function toSql($compiler, $model=false, $alias=false) {
+        $O = array();
+        foreach ($this->args as $field=>$value) {
+            list($field, $op) = $compiler->getField($field, $model);
+            if (is_callable($op))
+                $O[] = call_user_func($op, $field, $value, $model);
+            else
+                $O[] = sprintf($op, $field, $compiler->input($value));
+        }
+        return implode(' ', $O) . ($alias ? ' AS ' . $alias : '');
+    }
+}
+
+class SqlExpression extends SqlFunction {
+    var $operator;
+    var $operands;
+
+    function toSql($compiler, $model=false, $alias=false) {
+        $O = array();
+        foreach ($this->args as $operand) {
+            if ($operand instanceof SqlFunction)
+                $O[] = $operand->toSql($compiler, $model);
+            else
+                $O[] = $compiler->input($operand);
+        }
+        return implode(' '.$this->func.' ', $O)
+            . ($alias ? ' AS '.$compiler->quote($alias) : '');
+    }
+
+    static function __callStatic($operator, $operands) {
+        switch ($operator) {
+            case 'minus':
+                $operator = '-'; break;
+            case 'plus':
+                $operator = '+'; break;
+            case 'times':
+                $operator = '*'; break;
+            case 'bitand':
+                $operator = '&'; break;
+            case 'bitor':
+                $operator = '|'; break;
+            default:
+                throw new InvalidArgumentException('Invalid operator specified');
+        }
+        return parent::__callStatic($operator, $operands);
+    }
+}
+
+class SqlInterval extends SqlFunction {
+    var $type;
+
+    function toSql($compiler, $model=false, $alias=false) {
+        $A = $this->args[0];
+        if ($A instanceof SqlFunction)
+            $A = $A->toSql($compiler, $model);
+        else
+            $A = $compiler->input($A);
+        return sprintf('INTERVAL %s %s',
+            $A,
+            $this->func)
+            . ($alias ? ' AS '.$compiler->quote($alias) : '');
+    }
+
+    static function __callStatic($interval, $args) {
+        if (count($args) != 1) {
+            throw new InvalidArgumentException("Interval expects a single interval value");
+        }
+        return parent::__callStatic($interval, $args);
+    }
+}
+
+class SqlField extends SqlFunction {
+    var $level;
+
+    function __construct($field, $level=0) {
+        $this->field = $field;
+        $this->level = $level;
+    }
+
+    function toSql($compiler, $model=false, $alias=false) {
+        $L = $this->level;
+        while ($L--)
+            $compiler = $compiler->getParent();
+        list($field) = $compiler->getField($this->field, $model);
+        return $field;
+    }
+}
+
+class SqlCode extends SqlFunction {
+    function __construct($code) {
+        $this->code = $code;
+    }
+
+    function toSql($compiler, $model=false, $alias=false) {
+        return $this->code.($alias ? ' AS '.$alias : '');
+    }
 }
 
-class QuerySet implements IteratorAggregate, ArrayAccess {
+class SqlAggregate extends SqlFunction {
+
+    var $func;
+    var $expr;
+    var $distinct=false;
+    var $constraint=false;
+
+    function __construct($func, $expr, $distinct=false, $constraint=false) {
+        $this->func = $func;
+        $this->expr = $expr;
+        $this->distinct = $distinct;
+        if ($constraint instanceof Q)
+            $this->constraint = $constraint;
+        elseif ($constraint)
+            $this->constraint = new Q($constraint);
+    }
+
+    static function __callStatic($func, $args) {
+        $distinct = @$args[1] ?: false;
+        $constraint = @$args[2] ?: false;
+        return new static($func, $args[0], $distinct, $constraint);
+    }
+
+    function toSql($compiler, $model=false, $alias=false) {
+        $options = array('constraint' => $this->constraint, 'model' => true);
+
+        // For DISTINCT, require a field specification — not a relationship
+        // specification.
+        $E = $this->expr;
+        if ($E instanceof SqlFunction) {
+            $field = $E->toSql($compiler, $model);
+        }
+        else {
+        list($field, $rmodel) = $compiler->getField($E, $model, $options);
+        if ($this->distinct) {
+            $pk = false;
+            $fpk  = $rmodel::getMeta('pk');
+            foreach ($fpk as $f) {
+                $pk |= false !== strpos($field, $f);
+            }
+            if (!$pk) {
+                // Try and use the foriegn primary key
+                if (count($fpk) == 1) {
+                    list($field) = $compiler->getField(
+                        $this->expr . '__' . $fpk[0],
+                        $model, $options);
+                }
+                else {
+                    throw new OrmException(
+                        sprintf('%s :: %s', $rmodel, $field) .
+                        ': DISTINCT aggregate expressions require specification of a single primary key field of the remote model'
+                    );
+                }
+            }
+        }
+        }
+
+        return sprintf('%s(%s%s)%s', $this->func,
+            $this->distinct ? 'DISTINCT ' : '', $field,
+            $alias && $this->alias ? ' AS '.$compiler->quote($this->alias) : '');
+    }
+
+    function getFieldName() {
+        return strtolower(sprintf('%s__%s', $this->args[0], $this->func));
+    }
+}
+
+class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countable {
     var $model;
 
     var $constraints = array();
-    var $exclusions = array();
+    var $path_constraints = array();
     var $ordering = array();
     var $limit = false;
     var $offset = 0;
     var $related = array();
     var $values = array();
+    var $defer = array();
+    var $aggregated = false;
+    var $annotations = array();
+    var $extra = array();
+    var $distinct = array();
     var $lock = false;
+    var $chain = array();
+    var $options = array();
 
     const LOCK_EXCLUSIVE = 1;
     const LOCK_SHARED = 2;
 
+    const ASC = 'ASC';
+    const DESC = 'DESC';
+
     var $compiler = 'MySqlCompiler';
-    var $iterator = 'ModelInstanceIterator';
+    var $iterator = 'ModelInstanceManager';
 
-    var $params;
     var $query;
+    var $count;
 
     function __construct($model) {
         $this->model = $model;
@@ -303,19 +991,79 @@ class QuerySet implements IteratorAggregate, ArrayAccess {
 
     function filter() {
         // Multiple arrays passes means OR
-        $this->constraints[] = func_get_args();
+        foreach (func_get_args() as $Q) {
+            $this->constraints[] = $Q instanceof Q ? $Q : new Q($Q);
+        }
         return $this;
     }
 
     function exclude() {
-        $this->exclusions[] = func_get_args();
+        foreach (func_get_args() as $Q) {
+            $this->constraints[] = $Q instanceof Q ? $Q->negate() : Q::not($Q);
+        }
+        return $this;
+    }
+
+    /**
+     * Add a path constraint for the query. This is different from ::filter
+     * in that the constraint is added to a join clause which is normally
+     * built from the model meta data. The ::filter() method on the other
+     * hand adds the constraint to the where clause. This is generally useful
+     * for aggregate queries and left join queries where multiple rows might
+     * match a filter in the where clause and would produce incorrect results.
+     *
+     * Example:
+     * Find users with personal email hosted with gmail.
+     * >>> $Q = User::objects();
+     * >>> $Q->constrain(['user__emails' => new Q(['type' => 'personal']))
+     * >>> $Q->filter(['user__emails__address__contains' => '@gmail.com'])
+     */
+    function constrain() {
+        foreach (func_get_args() as $I) {
+            foreach ($I as $path => $Q) {
+                if (!is_array($Q) && !$Q instanceof Q) {
+                    // ->constrain(array('field__path__op' => val));
+                    $Q = array($path => $Q);
+                    list(, $path) = SqlCompiler::splitCriteria($path);
+                    $path = implode('__', $path);
+                }
+                $this->path_constraints[$path][] = $Q instanceof Q ? $Q : Q::all($Q);
+            }
+        }
         return $this;
     }
 
-    function order_by() {
-        $this->ordering = array_merge($this->ordering, func_get_args());
+    function defer() {
+        foreach (func_get_args() as $f)
+            $this->defer[$f] = true;
         return $this;
     }
+    function order_by($order, $direction=false) {
+        if ($order === false)
+            return $this->options(array('nosort' => true));
+
+        $args = func_get_args();
+        if (in_array($direction, array(self::ASC, self::DESC))) {
+            $args = array($args[0]);
+        }
+        else
+            $direction = false;
+
+        $new = is_array($order) ?  $order : $args;
+        if ($direction) {
+            foreach ($new as $i=>$x) {
+                $new[$i] = array($x, $direction);
+            }
+        }
+        $this->ordering = array_merge($this->ordering, $new);
+        return $this;
+    }
+    function getSortFields() {
+        $ordering = $this->ordering;
+        if ($this->extra['order_by'])
+            $ordering = array_merge($ordering, $this->extra['order_by']);
+        return $ordering;
+    }
 
     function lock($how=false) {
         $this->lock = $how ?: self::LOCK_EXCLUSIVE;
@@ -332,46 +1080,194 @@ class QuerySet implements IteratorAggregate, ArrayAccess {
         return $this;
     }
 
+    function isWindowed() {
+        return $this->limit || $this->offset || (count($this->values) + count($this->annotations) + @count($this->extra['select'])) > 1;
+    }
+
     function select_related() {
         $this->related = array_merge($this->related, func_get_args());
         return $this;
     }
 
+    function extra(array $extra) {
+        foreach ($extra as $section=>$info) {
+            $this->extra[$section] = array_merge($this->extra[$section] ?: array(), $info);
+        }
+        return $this;
+    }
+
+    function distinct() {
+        foreach (func_get_args() as $D)
+            $this->distinct[] = $D;
+        return $this;
+    }
+
+    function models() {
+        $this->iterator = 'ModelInstanceManager';
+        $this->values = $this->related = array();
+        return $this;
+    }
+
     function values() {
-        $this->values = func_get_args();
+        foreach (func_get_args() as $A)
+            $this->values[$A] = $A;
         $this->iterator = 'HashArrayIterator';
+        // This disables related models
+        $this->related = false;
         return $this;
     }
 
     function values_flat() {
         $this->values = func_get_args();
         $this->iterator = 'FlatArrayIterator';
+        // This disables related models
+        $this->related = false;
         return $this;
     }
 
+    function copy() {
+        return clone $this;
+    }
+
     function all() {
         return $this->getIterator()->asArray();
     }
 
-    function one() {
+    function first() {
         $list = $this->limit(1)->all();
-        // TODO: Throw error if more than one result from database
-        return $this[0];
+        return $list[0];
+    }
+
+    /**
+     * one
+     *
+     * Finds and returns a single model instance based on the criteria in
+     * this QuerySet instance.
+     *
+     * Throws:
+     * DoesNotExist - if no such model exists with the given criteria
+     * ObjectNotUnique - if more than one model matches the given criteria
+     *
+     * Returns:
+     * (Object<Model>) a single instance of the sought model is guarenteed.
+     * If no such model or multiple models exist, an exception is thrown.
+     */
+    function one() {
+        $list = $this->all();
+        if (count($list) == 0)
+            throw new DoesNotExist();
+        elseif (count($list) > 1)
+            throw new ObjectNotUnique('One object was expected; however '
+                .'multiple objects in the database matched the query. '
+                .sprintf('In fact, there are %d matching objects.', count($list))
+            );
+        return $list[0];
     }
 
     function count() {
+        // Defer to the iterator if fetching already started
+        if (isset($this->_iterator)) {
+            return $this->_iterator->count();
+        }
+        elseif (isset($this->count)) {
+            return $this->count;
+        }
         $class = $this->compiler;
         $compiler = new $class();
-        return $compiler->compileCount($this);
+        return $this->_count = $compiler->compileCount($this);
     }
 
-    function exists() {
+    function toSql($compiler, $model, $alias=false) {
+        // FIXME: Force root model of the compiler to $model
+        $exec = $this->getQuery(array('compiler' => get_class($compiler),
+             'parent' => $compiler, 'subquery' => true));
+        // Rewrite the parameter numbers so they fit the parameter numbers
+        // of the current parameters of the $compiler
+        $sql = preg_replace_callback("/:(\d+)/",
+        function($m) use ($compiler, $exec) {
+            $compiler->params[] = $exec->params[$m[1]-1];
+            return ':'.count($compiler->params);
+        }, $exec->sql);
+        return "({$sql})".($alias ? " AS {$alias}" : '');
+    }
+
+    /**
+     * exists
+     *
+     * Determines if there are any rows in this QuerySet. This can be
+     * achieved either by evaluating a SELECT COUNT(*) query or by
+     * attempting to fetch the first row from the recordset and return
+     * boolean success.
+     *
+     * Parameters:
+     * $fetch - (bool) TRUE if a compile and fetch should be attempted
+     *      instead of a SELECT COUNT(*). This would be recommended if an
+     *      accurate count is not required and the records would be fetched
+     *      if this method returns TRUE.
+     *
+     * Returns:
+     * (bool) TRUE if there would be at least one record in this QuerySet
+     */
+    function exists($fetch=false) {
+        if ($fetch) {
+            return (bool) $this[0];
+        }
         return $this->count() > 0;
     }
 
+    function annotate($annotations) {
+        if (!is_array($annotations))
+            $annotations = func_get_args();
+        foreach ($annotations as $name=>$A) {
+            if ($A instanceof SqlAggregate) {
+                if (is_int($name))
+                    $name = $A->getFieldName();
+                $A->setAlias($name);
+            }
+            $this->annotations[$name] = $A;
+        }
+        return $this;
+    }
+
+    function aggregate($annotations) {
+        // Aggregate works like annotate, except that it sets up values
+        // fetching which will disable model creation
+        $this->annotate($annotations);
+        $this->values();
+        // Disable other fields from being fetched
+        $this->aggregated = true;
+        $this->related = false;
+        return $this;
+    }
+
+    function options($options) {
+        $this->options = array_merge($this->options, $options);
+        return $this;
+    }
+
+    function countSelectFields() {
+        $count = count($this->values) + count($this->annotations);
+        if (isset($this->extra['select']))
+            foreach (@$this->extra['select'] as $S)
+                $count += count($S);
+        return $count;
+    }
+
+    function union(QuerySet $other, $all=true) {
+        // Values and values_list _must_ match for this to work
+        if ($this->countSelectFields() != $other->countSelectFields())
+            throw new OrmException('Union queries must have matching values counts');
+
+        // TODO: Clear OFFSET and LIMIT in the $other query
+
+        $this->chain[] = array($other, $all);
+        return $this;
+    }
+
     function delete() {
         $class = $this->compiler;
         $compiler = new $class();
+        // XXX: Mark all in-memory cached objects as deleted
         $ex = $compiler->compileBulkDelete($this);
         $ex->execute();
         return $ex->affected_rows();
@@ -388,6 +1284,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess {
     function __clone() {
         unset($this->_iterator);
         unset($this->query);
+        unset($this->count);
     }
 
     // IteratorAggregate interface
@@ -422,50 +1319,97 @@ class QuerySet implements IteratorAggregate, ArrayAccess {
 
         // Load defaults from model
         $model = $this->model;
-        if (!$this->ordering && isset($model::$meta['ordering']))
-            $this->ordering = $model::$meta['ordering'];
-
-        $class = $this->compiler;
+        $meta = $model::getMeta();
+        $query = clone $this;
+        $options += $this->options;
+        if ($options['nosort'])
+            $query->ordering = array();
+        elseif (!$query->ordering && $meta['ordering'])
+            $query->ordering = $meta['ordering'];
+        if (false !== $query->related && !$query->values && $meta['select_related'])
+            $query->related = $meta['select_related'];
+        if (!$query->defer && $meta['defer'])
+            $query->defer = $meta['defer'];
+
+        $class = $options['compiler'] ?: $this->compiler;
         $compiler = new $class($options);
-        $this->query = $compiler->compileSelect($this);
+        $this->query = $compiler->compileSelect($query);
 
         return $this->query;
     }
+
+    /**
+     * Fetch a model class which can be used to render the QuerySet as a
+     * subquery to be used as a JOIN.
+     */
+    function asView() {
+        $unique = spl_object_hash($this);
+        $classname = "QueryView{$unique}";
+
+        if (class_exists($classname))
+            return $classname;
+
+        $class = <<<EOF
+class {$classname} extends VerySimpleModel {
+    static \$meta = array(
+        'view' => true,
+    );
+    static \$queryset;
+
+    static function getQuery(\$compiler) {
+        return ' ('.static::\$queryset->getQuery().') ';
+    }
+
+    static function getSqlAddParams(\$compiler) {
+        return static::\$queryset->toSql(\$compiler, self::\$queryset->model);
+    }
+}
+EOF;
+        eval($class); // Ugh
+        $classname::$queryset = $this;
+        return $classname;
+    }
+
+    function serialize() {
+        $info = get_object_vars($this);
+        unset($info['query']);
+        unset($info['limit']);
+        unset($info['offset']);
+        unset($info['_iterator']);
+        unset($info['count']);
+        return serialize($info);
+    }
+
+    function unserialize($data) {
+        $data = unserialize($data);
+        foreach ($data as $name => $val) {
+            $this->{$name} = $val;
+        }
+    }
 }
 
-class ModelInstanceIterator implements Iterator, ArrayAccess {
-    var $model;
+class DoesNotExist extends Exception {}
+class ObjectNotUnique extends Exception {}
+
+abstract class ResultSet implements Iterator, ArrayAccess, Countable {
     var $resource;
-    var $cache = array();
     var $position = 0;
     var $queryset;
+    var $cache = array();
 
     function __construct($queryset=false) {
         $this->queryset = $queryset;
         if ($queryset) {
             $this->model = $queryset->model;
-            $this->resource = $queryset->getQuery();
         }
     }
 
-    function buildModel($row) {
-        // TODO: Traverse to foreign keys
-        $model = new $this->model($row); # nolint
-        $model->__onload();
-        return $model;
+    function prime() {
+        if (!isset($this->resource) && $this->queryset)
+            $this->resource = $this->queryset->getQuery();
     }
 
-    function fillTo($index) {
-        while ($this->resource && $index >= count($this->cache)) {
-            if ($row = $this->resource->getArray()) {
-                $this->cache[] = $this->buildModel($row);
-            } else {
-                $this->resource->close();
-                $this->resource = null;
-                break;
-            }
-        }
-    }
+    abstract function fillTo($index);
 
     function asArray() {
         $this->fillTo(PHP_INT_MAX);
@@ -506,63 +1450,367 @@ class ModelInstanceIterator implements Iterator, ArrayAccess {
     function offsetSet($a, $b) {
         throw new Exception(sprintf(__('%s is read-only'), get_class($this)));
     }
-}
 
-class FlatArrayIterator extends ModelInstanceIterator {
-    function __construct($queryset) {
-        $this->resource = $queryset->getQuery();
+    // Countable interface
+    function count() {
+        return count($this->asArray());
+    }
+}
+
+class ModelInstanceManager extends ResultSet {
+    var $model;
+    var $map;
+
+    static $objectCache = array();
+
+    function cache($model) {
+        $key = sprintf('%s.%s',
+            $model::$meta->model, implode('.', $model->get('pk')));
+        self::$objectCache[$key] = $model;
+    }
+
+    /**
+     * uncache
+     *
+     * Drop the cached reference to the model. If the model is deleted
+     * database-side. Lookups for the same model should not be short
+     * circuited to retrieve the cached reference.
+     */
+    static function uncache($model) {
+        $key = sprintf('%s.%s',
+            $model::$meta->model, implode('.', $model->pk));
+        unset(self::$objectCache[$key]);
+    }
+
+    static function flushCache() {
+        self::$objectCache = array();
+    }
+
+    static function checkCache($modelClass, $fields) {
+        $key = $modelClass::$meta->model;
+        foreach ($modelClass::getMeta('pk') as $f)
+            $key .= '.'.$fields[$f];
+        return @self::$objectCache[$key];
+    }
+
+    /**
+     * getOrBuild
+     *
+     * Builds a new model from the received fields or returns the model
+     * already stashed in the model cache. Caching helps to ensure that
+     * multiple lookups for the same model identified by primary key will
+     * fetch the exact same model. Therefore, changes made to the model
+     * anywhere in the project will be reflected everywhere.
+     *
+     * For annotated models (models build from querysets with annotations),
+     * the built or cached model is wrapped in an AnnotatedModel instance.
+     * The annotated fields are in the AnnotatedModel instance and the
+     * database-backed fields are managed by the Model instance.
+     */
+    function getOrBuild($modelClass, $fields, $cache=true) {
+        // Check for NULL primary key, used with related model fetching. If
+        // the PK is NULL, then consider the object to also be NULL
+        foreach ($modelClass::getMeta('pk') as $pkf) {
+            if (!isset($fields[$pkf])) {
+                return null;
+            }
+        }
+        $annotations = $this->queryset->annotations;
+        $extras = array();
+        // For annotations, drop them from the $fields list and add them to
+        // an $extras list. The fields passed to the root model should only
+        // be the root model's fields. The annotated fields will be wrapped
+        // using an AnnotatedModel instance.
+        if ($annotations && $modelClass == $this->model) {
+            foreach ($annotations as $name=>$A) {
+                if (array_key_exists($name, $fields)) {
+                    $extras[$name] = $fields[$name];
+                    unset($fields[$name]);
+                }
+            }
+        }
+        // Check the cache for the model instance first
+        if (!($m = self::checkCache($modelClass, $fields))) {
+            // Construct and cache the object
+            $m = $modelClass::$meta->newInstance($fields);
+            // XXX: defer may refer to fields not in this model
+            $m->__deferred__ = $this->queryset->defer;
+            $m->__onload();
+            if ($cache)
+                $this->cache($m);
+        }
+        // Wrap annotations in an AnnotatedModel
+        if ($extras) {
+            $m = new AnnotatedModel($m, $extras);
+        }
+        // TODO: If the model has deferred fields which are in $fields,
+        // those can be resolved here
+        return $m;
+    }
+
+    /**
+     * buildModel
+     *
+     * This method builds the model including related models from the record
+     * received. For related recordsets, a $map should be setup inside this
+     * object prior to using this method. The $map is assumed to have this
+     * configuration:
+     *
+     * array(array(<fieldNames>, <modelClass>, <relativePath>))
+     *
+     * Where $modelClass is the name of the foreign (with respect to the
+     * root model ($this->model), $fieldNames is the number and names of
+     * fields in the row for this model, $relativePath is the path that
+     * describes the relationship between the root model and this model,
+     * 'user__account' for instance.
+     */
+    function buildModel($row) {
+        // TODO: Traverse to foreign keys
+        if ($this->map) {
+            if ($this->model != $this->map[0][1])
+                throw new OrmException('Internal select_related error');
+
+            $offset = 0;
+            foreach ($this->map as $info) {
+                @list($fields, $model_class, $path) = $info;
+                $values = array_slice($row, $offset, count($fields));
+                $record = array_combine($fields, $values);
+                if (!$path) {
+                    // Build the root model
+                    $model = $this->getOrBuild($this->model, $record);
+                }
+                elseif ($model) {
+                    $i = 0;
+                    // Traverse the declared path and link the related model
+                    $tail = array_pop($path);
+                    $m = $model;
+                    foreach ($path as $field) {
+                        if (!($m = $m->get($field)))
+                            break;
+                    }
+                    if ($m)
+                        $m->set($tail, $this->getOrBuild($model_class, $record));
+                }
+                $offset += count($fields);
+            }
+        }
+        else {
+            $model = $this->getOrBuild($this->model, $row);
+        }
+        return $model;
+    }
+
+    function fillTo($index) {
+        $this->prime();
+        $func = ($this->map) ? 'getRow' : 'getArray';
+        while ($this->resource && $index >= count($this->cache)) {
+            if ($row = $this->resource->{$func}()) {
+                $this->cache[] = $this->buildModel($row);
+            } else {
+                $this->resource->close();
+                $this->resource = false;
+                break;
+            }
+        }
+    }
+
+    function prime() {
+        parent::prime();
+        if ($this->resource) {
+            $this->map = $this->resource->getMap();
+        }
     }
+}
+
+class FlatArrayIterator extends ResultSet {
     function fillTo($index) {
+        $this->prime();
         while ($this->resource && $index >= count($this->cache)) {
             if ($row = $this->resource->getRow()) {
                 $this->cache[] = $row;
             } else {
                 $this->resource->close();
-                $this->resource = null;
+                $this->resource = false;
+                break;
+            }
+        }
+    }
+}
+
+class HashArrayIterator extends ResultSet {
+    function fillTo($index) {
+        $this->prime();
+        while ($this->resource && $index >= count($this->cache)) {
+            if ($row = $this->resource->getArray()) {
+                $this->cache[] = $row;
+            } else {
+                $this->resource->close();
+                $this->resource = false;
                 break;
             }
         }
     }
 }
 
-class InstrumentedList extends ModelInstanceIterator {
+class InstrumentedList extends ModelInstanceManager {
     var $key;
-    var $id;
-    var $model;
 
     function __construct($fkey, $queryset=false) {
-        list($model, $this->key, $this->id) = $fkey;
-        if (!$queryset)
-            $queryset = $model::objects()->filter(array($this->key=>$this->id));
+        list($model, $this->key) = $fkey;
+        if (!$queryset) {
+            $queryset = $model::objects()->filter($this->key);
+            if ($related = $model::getMeta('select_related'))
+                $queryset->select_related($related);
+        }
         parent::__construct($queryset);
         $this->model = $model;
-        if (!$this->id)
-            $this->resource = null;
     }
 
     function add($object, $at=false) {
         if (!$object || !$object instanceof $this->model)
-            throw new Exception(__('Attempting to add invalid object to list'));
+            throw new Exception(sprintf(
+                'Attempting to add invalid object to list. Expected <%s>, but got <%s>',
+                $this->model,
+                get_class($object)
+            ));
 
-        $object->set($this->key, $this->id);
-        $object->save();
+        foreach ($this->key as $field=>$value)
+            $object->set($field, $value);
+
+        if (!$object->__new__)
+            $object->save();
 
         if ($at !== false)
             $this->cache[$at] = $object;
         else
             $this->cache[] = $object;
+
+        return $object;
     }
-    function remove($object) {
-        $object->delete();
+    function remove($object, $delete=true) {
+        if ($delete)
+            $object->delete();
+        else
+            foreach ($this->key as $field=>$value)
+                $object->set($field, null);
     }
 
     function reset() {
         $this->cache = array();
+        unset($this->resource);
+    }
+
+    /**
+     * Slight edit to the standard ::next() iteration method which will skip
+     * deleted items.
+     */
+    function next() {
+        do {
+            parent::next();
+        }
+        while ($this->valid() && $this->current()->__deleted__);
+    }
+
+    /**
+     * Reduce the list to a subset using a simply key/value constraint. New
+     * items added to the subset will have the constraint automatically
+     * added to all new items.
+     */
+    function window($constraint) {
+        $model = $this->model;
+        $fields = $model::getMeta()->getFieldNames();
+        $key = $this->key;
+        foreach ($constraint as $field=>$value) {
+            if (!is_string($field) || false === in_array($field, $fields))
+                throw new OrmException('InstrumentedList windowing must be performed on local fields only');
+            $key[$field] = $value;
+        }
+        return new static(array($this->model, $key), $this->filter($constraint));
+    }
+
+    /**
+     * Find the first item in the current set which matches the given criteria.
+     * This would be used in favor of ::filter() which might trigger another
+     * database query. The criteria is intended to be quite simple and should
+     * not traverse relationships which have not already been fetched.
+     * Otherwise, the ::filter() or ::window() methods would provide better
+     * performance.
+     *
+     * Example:
+     * >>> $a = new User();
+     * >>> $a->roles->add(Role::lookup(['name' => 'administator']));
+     * >>> $a->roles->findFirst(['roles__name__startswith' => 'admin']);
+     * <Role: administrator>
+     */
+    function findFirst(array $criteria) {
+        foreach ($this as $record) {
+            $matches = true;
+            foreach ($criteria as $field=>$check) {
+                if (!SqlCompiler::evaluate($record, $field, $check)) {
+                    $matches = false;
+                    break;
+                }
+            }
+            if ($matches)
+                return $record;
+        }
+    }
+
+    /**
+     * Sort the instrumented list in place. This would be useful to change the
+     * sorting order of the items in the list without fetching the list from
+     * the database again.
+     *
+     * Parameters:
+     * $key - (callable|int) A callable function to produce the sort keys
+     *      or one of the SORT_ constants used by the array_multisort
+     *      function
+     * $reverse - (bool) true if the list should be sorted descending
+     *
+     * Returns:
+     * This instrumented list for chaining and inlining.
+     */
+    function sort($key=false, $reverse=false) {
+        // Fetch all records into the cache
+        $this->asArray();
+        if (is_callable($key)) {
+            array_multisort(
+                array_map($key, $this->cache),
+                $reverse ? SORT_DESC : SORT_ASC,
+                $this->cache);
+        }
+        elseif ($key) {
+            array_multisort($this->cache,
+                $reverse ? SORT_DESC : SORT_ASC, $key);
+        }
+        elseif ($reverse) {
+            rsort($this->cache);
+        }
+        else
+            sort($this->cache);
+        return $this;
+    }
+
+    /**
+     * Reverse the list item in place. Returns this object for chaining
+     */
+    function reverse() {
+        $this->asArray();
+        array_reverse($this->cache);
+        return $this;
+    }
+
+    // Save all changes made to any list items
+    function saveAll() {
+        foreach ($this as $I)
+            if (!$I->save())
+                return false;
+        return true;
     }
 
     // QuerySet delegates
     function count() {
-        return $this->queryset->count();
+        return $this->objects()->count();
     }
     function exists() {
         return $this->queryset->exists();
@@ -589,6 +1837,11 @@ class InstrumentedList extends ModelInstanceIterator {
         $this->cache[$a]->delete();
         $this->add($b, $a);
     }
+
+    // QuerySet overriedes
+    function __call($what, $how) {
+        return call_user_func_array(array($this->objects(), $what), $how);
+    }
 }
 
 class SqlCompiler {
@@ -605,6 +1858,63 @@ class SqlCompiler {
     function __construct($options=false) {
         if ($options)
             $this->options = array_merge($this->options, $options);
+        if ($options['subquery'])
+            $this->alias_num += 150;
+    }
+
+    function getParent() {
+        return $this->options['parent'];
+    }
+
+    /**
+     * Split a criteria item into the identifying pieces: path, field, and
+     * operator.
+     */
+    static function splitCriteria($criteria) {
+        static $operators = array(
+            'exact' => 1, 'isnull' => 1,
+            'gt' => 1, 'lt' => 1, 'gte' => 1, 'lte' => 1, 'range' => 1,
+            'contains' => 1, 'like' => 1, 'startswith' => 1, 'endswith' => 1, 'regex' => 1,
+            'in' => 1, 'intersect' => 1,
+            'hasbit' => 1,
+        );
+        $path = explode('__', $criteria);
+        if (!isset($options['table'])) {
+            $field = array_pop($path);
+            if (isset($operators[$field])) {
+                $operator = $field;
+                $field = array_pop($path);
+            }
+        }
+        return array($field, $path, $operator ?: 'exact');
+    }
+
+    /**
+     * Check if the values match given the operator.
+     *
+     * Throws:
+     * OrmException - if $operator is not supported
+     */
+    static function evaluate($record, $field, $check) {
+        static $ops; if (!isset($ops)) { $ops = array(
+            'exact' => function($a, $b) { return is_string($a) ? strcasecmp($a, $b) == 0 : $a == $b; },
+            'isnull' => function($a, $b) { return is_null($a) == $b; },
+            'gt' => function($a, $b) { return $a > $b; },
+            'gte' => function($a, $b) { return $a >= $b; },
+            'lt' => function($a, $b) { return $a < $b; },
+            'lte' => function($a, $b) { return $a <= $b; },
+            'contains' => function($a, $b) { return stripos($a, $b) !== false; },
+            'startswith' => function($a, $b) { return stripos($a, $b) === 0; },
+            'hasbit' => function($a, $b) { return $a & $b == $b; },
+        ); }
+        list($field, $path, $operator) = self::splitCriteria($field);
+        if (!isset($ops[$operator]))
+            throw new OrmException($operator.': Unsupported operator');
+
+        if ($path)
+            $record = $record->getByPath($path);
+        // TODO: Support Q expressions
+        return $ops[$operator]($record->get($field), $check);
     }
 
     /**
@@ -652,54 +1962,88 @@ class SqlCompiler {
      *
      * If no comparison function is declared in the field descriptor,
      * 'exact' is assumed.
+     *
+     * Parameters:
+     * $field - (string) name of the field to join
+     * $model - (VerySimpleModel) root model for references in the $field
+     *      parameter
+     * $options - (array) extra options for the compiler
+     *      'table' => return the table alias rather than the field-name
+     *      'model' => return the target model class rather than the operator
+     *      'constraint' => extra constraint for join clause
+     *
+     * Returns:
+     * (mixed) Usually array<field-name, operator> where field-name is the
+     * name of the field in the destination model, and operator is the
+     * requestion comparison method.
      */
     function getField($field, $model, $options=array()) {
-        $joins = array();
-
         // Break apart the field descriptor by __ (double-underbars). The
         // first part is assumed to be the root field in the given model.
         // The parts after each of the __ pieces are links to other tables.
         // The last item (after the last __) is allowed to be an operator
         // specifiction.
-        $parts = explode('__', $field);
-        $operator = static::$operators['exact'];
-        if (!isset($options['table'])) {
-            $field = array_pop($parts);
-            if (array_key_exists($field, static::$operators)) {
-                $operator = static::$operators[$field];
-                $field = array_pop($parts);
+        list($field, $parts, $op) = static::splitCriteria($field);
+        $operator = static::$operators[$op];
+        $path = '';
+        $rootModel = $model;
+
+        // Call pushJoin for each segment in the join path. A new JOIN
+        // fragment will need to be emitted and/or cached
+        $joins = array();
+        $push = function($p, $model) use (&$joins, &$path) {
+            $J = $model::getMeta('joins');
+            if (!($info = $J[$p])) {
+                throw new OrmException(sprintf(
+                   'Model `%s` does not have a relation called `%s`',
+                    $model, $p));
             }
+            $crumb = $path;
+            $path = ($path) ? "{$path}__{$p}" : $p;
+            $joins[] = array($crumb, $path, $model, $info);
+            // Roll to foreign model
+            return $info['fkey'];
+        };
+
+        foreach ($parts as $p) {
+            list($model) = $push($p, $model);
         }
 
-        $path = array();
-        $crumb = '';
-        $alias = $this->quote($model::$meta['table']);
-
-        // Traverse through the parts and establish joins between the tables
-        // if the field is joined to a foreign model
-        if (count($parts) && isset($model::$meta['joins'][$parts[0]])) {
-            // Call pushJoin for each segment in the join path. A new
-            // JOIN fragment will need to be emitted and/or cached
-            foreach ($parts as $p) {
-                $path[] = $p;
-                $tip = implode('__', $path);
-                $info = $model::$meta['joins'][$p];
-                $alias = $this->pushJoin($crumb, $tip, $model, $info);
-                // Roll to foreign model
-                foreach ($info['constraint'] as $local => $foreign) {
-                    list($model, $f) = explode('.', $foreign);
-                    if (class_exists($model))
-                        break;
-                }
-                $crumb = $tip;
-            }
+        // If comparing a relationship, join the foreign table
+        // This is a comparison with a relationship — use the foreign key
+        $J = $model::getMeta('joins');
+        if (isset($J[$field])) {
+            list($model, $field) = $push($field, $model);
+        }
+
+        // Apply the joins list to $this->pushJoin
+        $last = count($joins) - 1;
+        $constraint = false;
+        foreach ($joins as $i=>$A) {
+            // Add the conststraint as the last arg to the last join
+            if ($i == $last)
+                $constraint = $options['constraint'];
+            $alias = $this->pushJoin($A[0], $A[1], $A[2], $A[3], $constraint);
         }
+
+        if (!isset($alias)) {
+            // Determine the alias for the root model table
+            $alias = (isset($this->joins['']))
+                ? $this->joins['']['alias']
+                : $this->quote($rootModel::getMeta('table'));
+        }
+
         if (isset($options['table']) && $options['table'])
             $field = $alias;
+        elseif (isset($this->annotations[$field]))
+            $field = $this->annotations[$field];
         elseif ($alias)
             $field = $alias.'.'.$this->quote($field);
         else
             $field = $this->quote($field);
+
+        if (isset($options['model']) && $options['model'])
+            $operator = $model;
         return array($field, $operator);
     }
 
@@ -711,14 +2055,14 @@ class SqlCompiler {
      * algorithm is short-circuited and the originally-assigned table alias
      * is returned immediately.
      */
-    function pushJoin($tip, $path, $model, $info) {
+    function pushJoin($tip, $path, $model, $info, $constraint=false) {
         // TODO: Build the join statement fragment and return the table
         // alias. The table alias will be useful where the join is used in
         // the WHERE and ORDER BY clauses
 
         // If the join already exists for the statement-being-compiled, just
         // return the alias being used.
-        if (isset($this->joins[$path]))
+        if (!$constraint && isset($this->joins[$path]))
             return $this->joins[$path]['alias'];
 
         // TODO: Support only using aliases if necessary. Use actual table
@@ -736,46 +2080,123 @@ class SqlCompiler {
         // TODO: Always use a table alias. This will further help with
         // coordination between the data returned from the database (where
         // table alias is available) and the corresponding data.
-        $this->joins[$path] = array(
-            'alias' => $alias,
-            'sql'=> $this->compileJoin($tip, $model, $alias, $info),
-        );
+        $T = array('alias' => $alias);
+        $this->joins[$path] = $T;
+        $this->joins[$path]['sql'] = $this->compileJoin($tip, $model, $alias, $info, $constraint);
         return $alias;
     }
 
-    function compileConstraints($where, $model) {
-        $constraints = array();
-        foreach ($where as $constraint) {
-            $filter = array();
-            foreach ($constraint as $field=>$value) {
+    /**
+     * compileQ
+     *
+     * Build a constraint represented in an arbitrarily nested Q instance.
+     * The placement of the compiled constraint is also considered and
+     * represented in the resulting CompiledExpression instance.
+     *
+     * Parameters:
+     * $Q - (Q) constraint represented in a Q instance
+     * $model - (VerySimpleModel) root model for all the field references in
+     *      the Q instance
+     * $slot - (int) slot for inputs to be placed. Useful to differenciate
+     *      inputs placed in the joins and where clauses for SQL engines
+     *      which do not support named parameters.
+     *
+     * Returns:
+     * (CompiledExpression) object containing the compiled expression (with
+     * AND, OR, and NOT operators added). Furthermore, the $type attribute
+     * of the CompiledExpression will allow the compiler to place the
+     * constraint properly in the WHERE or HAVING clause appropriately.
+     */
+    function compileQ(Q $Q, $model, $slot=false) {
+        $filter = array();
+        $type = CompiledExpression::TYPE_WHERE;
+        foreach ($Q->constraints as $field=>$value) {
+            // Handle nested constraints
+            if ($value instanceof Q) {
+                $filter[] = $T = $this->compileQ($value, $model, $slot);
+                // Bubble up HAVING constraints
+                if ($T instanceof CompiledExpression
+                        && $T->type == CompiledExpression::TYPE_HAVING)
+                    $type = $T->type;
+            }
+            // Handle relationship comparisons with model objects
+            elseif ($value instanceof VerySimpleModel) {
+                $criteria = array();
+                foreach ($value->pk as $f=>$v) {
+                    $f = $field . '__' . $f;
+                    $criteria[$f] = $v;
+                }
+                $filter[] = $this->compileQ(new Q($criteria), $model, $slot);
+            }
+            // Handle simple field = <value> constraints
+            else {
                 list($field, $op) = $this->getField($field, $model);
-                // Allow operators to be callable rather than sprintf
-                // strings
+                if ($field instanceof SqlAggregate) {
+                    // This constraint has to go in the HAVING clause
+                    $field = $field->toSql($this, $model);
+                    $type = CompiledExpression::TYPE_HAVING;
+                }
                 if ($value === null)
                     $filter[] = sprintf('%s IS NULL', $field);
+                elseif ($value instanceof SqlField)
+                    $filter[] = sprintf($op, $field, $value->toSql($this, $model));
+                // Allow operators to be callable rather than sprintf
+                // strings
                 elseif (is_callable($op))
-                    $filter[] = call_user_func($op, $field, $value);
+                    $filter[] = call_user_func($op, $field, $value, $model);
                 else
-                    $filter[] = sprintf($op, $field, $this->input($value));
+                    $filter[] = sprintf($op, $field, $this->input($value, $slot));
             }
-            // Multiple constraints here are ANDed together
-            $constraints[] = implode(' AND ', $filter);
         }
-        // Multiple constrains here are ORed together
-        $filter = implode(' OR ', $constraints);
-        if (count($constraints) > 1)
-            $filter = '(' . $filter . ')';
-        return $filter;
+        $glue = $Q->isOred() ? ' OR ' : ' AND ';
+        $clause = implode($glue, $filter);
+        if (count($filter) > 1)
+            $clause = '(' . $clause . ')';
+        if ($Q->isNegated())
+            $clause = 'NOT '.$clause;
+        return new CompiledExpression($clause, $type);
+    }
+
+    function compileConstraints($where, $model) {
+        $constraints = array();
+        foreach ($where as $Q) {
+            $constraints[] = $this->compileQ($Q, $model);
+        }
+        return $constraints;
     }
 
     function getParams() {
         return $this->params;
     }
 
-    function getJoins() {
+    function getJoins($queryset) {
         $sql = '';
-        foreach ($this->joins as $j)
-            $sql .= $j['sql'];
+        foreach ($this->joins as $path => $j) {
+            if (!$j['sql'])
+                continue;
+            list($base, $constraints) = $j['sql'];
+            // Add in path-specific constraints, if any
+            if (isset($queryset->path_constraints[$path])) {
+                foreach ($queryset->path_constraints[$path] as $Q) {
+                    $constraints[] = $this->compileQ($Q, $queryset->model);
+                }
+            }
+            $sql .= $base;
+            if ($constraints)
+                $sql .= ' ON ('.implode(' AND ', $constraints).')';
+        }
+        // Add extra items from QuerySet
+        if (isset($queryset->extra['tables'])) {
+            foreach ($queryset->extra['tables'] as $S) {
+                $join = ' JOIN ';
+                // Left joins require an ON () clause
+                if ($lastparen = strrpos($S, '(')) {
+                    if (preg_match('/\bon\b/i', substr($S, $lastparen - 4, 4)))
+                        $join = ' LEFT' . $join;
+                }
+                $sql .= $join.$S;
+            }
+        }
         return $sql;
     }
 
@@ -787,8 +2208,26 @@ class SqlCompiler {
     }
 }
 
+class CompiledExpression /* extends SplString */ {
+    const TYPE_WHERE =   0x0001;
+    const TYPE_HAVING =  0x0002;
+
+    var $text = '';
+
+    function __construct($clause, $type=self::TYPE_WHERE) {
+        $this->text = $clause;
+        $this->type = $type;
+    }
+
+    function __toString() {
+        return $this->text;
+    }
+}
+
 class DbEngine {
 
+    static $compiler = 'MySqlCompiler';
+
     function __construct($info) {
     }
 
@@ -797,7 +2236,22 @@ class DbEngine {
 
     // Gets a compiler compatible with this database engine that can compile
     // and execute a queryset or DML request.
-    function getCompiler() {
+    static function getCompiler() {
+        $class = static::$compiler;
+        return new $class();
+    }
+
+    static function delete(VerySimpleModel $model) {
+        ModelInstanceManager::uncache($model);
+        return static::getCompiler()->compileDelete($model);
+    }
+
+    static function save(VerySimpleModel $model) {
+        $compiler = static::getCompiler();
+        if ($model->__new__)
+            return $compiler->compileInsert($model);
+        else
+            return $compiler->compileUpdate($model);
     }
 }
 
@@ -806,30 +2260,62 @@ class MySqlCompiler extends SqlCompiler {
     static $operators = array(
         'exact' => '%1$s = %2$s',
         'contains' => array('self', '__contains'),
+        'startswith' => array('self', '__startswith'),
+        'endswith' => array('self', '__endswith'),
         'gt' => '%1$s > %2$s',
         'lt' => '%1$s < %2$s',
         'gte' => '%1$s >= %2$s',
         'lte' => '%1$s <= %2$s',
+        'range' => array('self', '__range'),
         'isnull' => array('self', '__isnull'),
         'like' => '%1$s LIKE %2$s',
         'hasbit' => '%1$s & %2$s != 0',
         'in' => array('self', '__in'),
+        'intersect' => array('self', '__find_in_set'),
+        'regex' => array('self', '__regex'),
     );
 
+    // Thanks, http://stackoverflow.com/a/3683868
+    function like_escape($what, $e='\\') {
+        return str_replace(array($e, '%', '_'), array($e.$e, $e.'%', $e.'_'), $what);
+    }
+
     function __contains($a, $b) {
         # {%a} like %{$b}%
-        return sprintf('%s LIKE %s', $a, $this->input($b = "%$b%"));
+        # Escape $b
+        $b = $this->like_escape($b);
+        return sprintf('%s LIKE %s', $a, $this->input("%$b%"));
+    }
+    function __startswith($a, $b) {
+        $b = $this->like_escape($b);
+        return sprintf('%s LIKE %s', $a, $this->input("$b%"));
+    }
+    function __endswith($a, $b) {
+        $b = $this->like_escape($b);
+        return sprintf('%s LIKE %s', $a, $this->input("%$b"));
     }
 
     function __in($a, $b) {
         if (is_array($b)) {
             $vals = array_map(array($this, 'input'), $b);
-            $b = implode(', ', $vals);
+            $b = '('.implode(', ', $vals).')';
+        }
+        // MySQL is almost always faster with a join. Use one if possible
+        // MySQL doesn't support LIMIT or OFFSET in subqueries. Instead, add
+        // the query as a JOIN and add the join constraint into the WHERE
+        // clause.
+        elseif ($b instanceof QuerySet
+            && ($b->isWindowed() || $b->countSelectFields() > 1 || $b->chain)
+        ) {
+            $f1 = $b->values[0];
+            $view = $b->asView();
+            $alias = $this->pushJoin($view, $a, $view, array('constraint'=>array()));
+            return sprintf('%s = %s.%s', $a, $alias, $this->quote($f1));
         }
         else {
             $b = $this->input($b);
         }
-        return sprintf('%s IN (%s)', $a, $b);
+        return sprintf('%s IN %s', $a, $b);
     }
 
     function __isnull($a, $b) {
@@ -838,7 +2324,32 @@ class MySqlCompiler extends SqlCompiler {
             : sprintf('%s IS NOT NULL', $a);
     }
 
-    function compileJoin($tip, $model, $alias, $info) {
+    function __find_in_set($a, $b) {
+        if (is_array($b)) {
+            $sql = array();
+            foreach (array_map(array($this, 'input'), $b) as $b) {
+                $sql[] = sprintf('FIND_IN_SET(%s, %s)', $b, $a);
+            }
+            $parens = count($sql) > 1;
+            $sql = implode(' OR ', $sql);
+            return $parens ? ('('.$sql.')') : $sql;
+        }
+        return sprintf('FIND_IN_SET(%s, %s)', $b, $a);
+    }
+
+    function __regex($a, $b) {
+        // Strip slashes and options
+        if ($b[0] == '/')
+            $b = preg_replace('`/[^/]*$`', '', substr($b, 1));
+        return sprintf('%s REGEXP %s', $a, $this->input($b));
+    }
+
+    function __range($a, $b) {
+        // XXX: Crash if $b is not array of two items
+        return sprintf('%s BETWEEN %s AND %s', $a, $b[0], $b[1]);
+    }
+
+    function compileJoin($tip, $model, $alias, $info, $extra=false) {
         $constraints = array();
         $join = ' JOIN ';
         if (isset($info['null']) && $info['null'])
@@ -846,31 +2357,83 @@ class MySqlCompiler extends SqlCompiler {
         if (isset($this->joins[$tip]))
             $table = $this->joins[$tip]['alias'];
         else
-            $table = $this->quote($model::$meta['table']);
+            $table = $this->quote($model::getMeta('table'));
         foreach ($info['constraint'] as $local => $foreign) {
-            list($rmodel, $right) = explode('.', $foreign);
-            // TODO: Support a constant constraint
-            $constraints[] = sprintf("%s.%s = %s.%s",
-                $table, $this->quote($local), $alias,
-                $this->quote($right)
-            );
+            list($rmodel, $right) = $foreign;
+            // Support a constant constraint with
+            // "'constant'" => "Model.field_name"
+            if ($local[0] == "'") {
+                $constraints[] = sprintf("%s.%s = %s",
+                    $alias, $this->quote($right),
+                    $this->input(trim($local, '\'"'))
+                );
+            }
+            // Support local constraint
+            // field_name => "'constant'"
+            elseif ($rmodel[0] == "'" && !$right) {
+                $constraints[] = sprintf("%s.%s = %s",
+                    $table, $this->quote($local),
+                    $this->input(trim($rmodel, '\'"'))
+                );
+            }
+            else {
+                $constraints[] = sprintf("%s.%s = %s.%s",
+                    $table, $this->quote($local), $alias,
+                    $this->quote($right)
+                );
+            }
         }
-        return $join.$this->quote($rmodel::$meta['table'])
-            .' '.$alias.' ON ('.implode(' AND ', $constraints).')';
+        // Support extra join constraints
+        if ($extra instanceof Q) {
+            $constraints[] = $this->compileQ($extra, $model);
+        }
+        if (!isset($rmodel))
+            $rmodel = $model;
+        // Support inline views
+        $rmeta = $rmodel::getMeta();
+        $table = ($rmeta['view'])
+            // XXX: Support parameters from the nested query
+            ? $rmodel::getSqlAddParams($this)
+            : $this->quote($rmeta['table']);
+        $base = "{$join}{$table} {$alias}";
+        return array($base, $constraints);
     }
 
-    function input(&$what) {
+    /**
+     * input
+     *
+     * Generate a parameterized input for a database query.
+     *
+     * Parameters:
+     * $what - (mixed) value to be sent to the database. No escaping is
+     *      necessary. Pass a raw value here.
+     *
+     * Returns:
+     * (string) token to be placed into the compiled SQL statement. This
+     * is a colon followed by a number
+     */
+    function input($what, $slot=false, $model=false) {
         if ($what instanceof QuerySet) {
-            $q = $what->getQuery(array('nosort'=>true));
-            $this->params += $q->params;
-            return (string)$q;
+            $q = $what->getQuery(array('nosort'=>!($what->limit || $what->offset)));
+            // Rewrite the parameter numbers so they fit the parameter numbers
+            // of the current parameters of the $compiler
+            $self = $this;
+            $sql = preg_replace_callback("/:(\d+)/",
+            function($m) use ($self, $q) {
+                $self->params[] = $q->params[$m[1]-1];
+                return ':'.count($self->params);
+            }, $q->sql);
+            return "({$sql})";
         }
         elseif ($what instanceof SqlFunction) {
-            return $what->toSql($this);
+            return $what->toSql($this, $model);
+        }
+        elseif (!isset($what)) {
+            return 'NULL';
         }
         else {
             $this->params[] = $what;
-            return '?';
+            return ':'.(count($this->params));
         }
     }
 
@@ -885,78 +2448,228 @@ class MySqlCompiler extends SqlCompiler {
      * called before ::getJoins(), because it may add joins into the
      * statement based on the relationships used in the where clause
      */
-    protected function getWhereClause($queryset) {
+    protected function getWhereHavingClause($queryset) {
         $model = $queryset->model;
-        $where_pos = array();
-        $where_neg = array();
-        foreach ($queryset->constraints as $where) {
-            $where_pos[] = $this->compileConstraints($where, $model);
-        }
-        foreach ($queryset->exclusions as $where) {
-            $where_neg[] = $this->compileConstraints($where, $model);
+        $constraints = $this->compileConstraints($queryset->constraints, $model);
+        $where = $having = array();
+        foreach ($constraints as $C) {
+            if ($C->type == CompiledExpression::TYPE_WHERE)
+                $where[] = $C;
+            else
+                $having[] = $C;
         }
-
-        $where = '';
-        if ($where_pos || $where_neg) {
-            $where = ' WHERE '.implode(' AND ', $where_pos)
-                .implode(' AND NOT ', $where_neg);
+        if (isset($queryset->extra['where'])) {
+            foreach ($queryset->extra['where'] as $S) {
+                $where[] = "($S)";
+            }
         }
-        return $where;
+        if ($where)
+            $where = ' WHERE '.implode(' AND ', $where);
+        if ($having)
+            $having = ' HAVING '.implode(' AND ', $having);
+        return array($where ?: '', $having ?: '');
     }
 
     function compileCount($queryset) {
-        $model = $queryset->model;
-        $table = $model::$meta['table'];
-        $where = $this->getWhereClause($queryset);
-        $joins = $this->getJoins();
-        $sql = 'SELECT COUNT(*) AS count FROM '.$this->quote($table).$joins.$where;
-        $exec = new MysqlExecutor($sql, $this->params);
-        $row = $exec->getArray();
-        return $row['count'];
+        $q = clone $queryset;
+        // Drop extra fields from the queryset
+        $q->related = $q->anotations = false;
+        $model = $q->model;
+        $q->values = $model::getMeta('pk');
+        $exec = $q->getQuery(array('nosort' => true));
+        $exec->sql = 'SELECT COUNT(*) FROM ('.$exec->sql.') __';
+        $row = $exec->getRow();
+        return $row ? $row[0] : null;
     }
 
     function compileSelect($queryset) {
         $model = $queryset->model;
-        $where = $this->getWhereClause($queryset);
+        // Use an alias for the root model table
+        $this->joins[''] = array('alias' => ($rootAlias = $this->nextAlias()));
+
+        // Compile the WHERE clause
+        $this->annotations = $queryset->annotations ?: array();
+        list($where, $having) = $this->getWhereHavingClause($queryset);
 
+        // Compile the ORDER BY clause
         $sort = '';
-        if ($queryset->ordering && !isset($this->options['nosort'])) {
+        if ($columns = $queryset->getSortFields()) {
             $orders = array();
-            foreach ($queryset->ordering as $sort) {
+            foreach ($columns as $sort) {
                 $dir = 'ASC';
-                if (substr($sort, 0, 1) == '-') {
-                    $dir = 'DESC';
-                    $sort = substr($sort, 1);
+                if (is_array($sort)) {
+                    list($sort, $dir) = $sort;
+                }
+                if ($sort instanceof SqlFunction) {
+                    $field = $sort->toSql($this, $model);
+                }
+                else {
+                    if ($sort[0] == '-') {
+                        $dir = 'DESC';
+                        $sort = substr($sort, 1);
+                    }
+                    // If the field is already an annotation, then don't
+                    // compile the annotation again below. It's included in
+                    // the select clause, which is sufficient
+                    if (isset($this->annotations[$sort]))
+                        $field = $this->quote($sort);
+                    else
+                        list($field) = $this->getField($sort, $model);
                 }
-                list($field) = $this->getField($sort, $model);
-                $orders[] = $field.' '.$dir;
+                if ($field instanceof SqlFunction)
+                    $field = $field->toSql($this, $model);
+                // TODO: Throw exception if $field can be indentified as
+                //       invalid
+
+                $orders[] = "{$field} {$dir}";
             }
             $sort = ' ORDER BY '.implode(', ', $orders);
         }
 
-        // Include related tables
-        $fields = array();
-        $table = $model::$meta['table'];
+        // Compile the field listing
+        $fields = $group_by = array();
+        $meta = $model::getMeta();
+        $table = $this->quote($meta['table']).' '.$rootAlias;
+        // Handle related tables
         if ($queryset->related) {
-            $fields = array($this->quote($table).'.*');
-            foreach ($queryset->related as $rel) {
-                // XXX: This is ugly
-                list($t) = $this->getField($rel, $model,
-                    array('table'=>true));
-                $fields[] = $t.'.*';
+            $count = 0;
+            $fieldMap = $theseFields = array();
+            $defer = $queryset->defer ?: array();
+            // Add local fields first
+            foreach ($meta->getFieldNames() as $f) {
+                // Handle deferreds
+                if (isset($defer[$f]))
+                    continue;
+                $fields[$rootAlias . '.' . $this->quote($f)] = true;
+                $theseFields[] = $f;
+            }
+            $fieldMap[] = array($theseFields, $model);
+            // Add the JOINs to this query
+            foreach ($queryset->related as $sr) {
+                // XXX: Sort related by the paths so that the shortest paths
+                //      are resolved first when building out the models.
+                $full_path = '';
+                $parts = array();
+                // Track each model traversal and fetch data for each of the
+                // models in the path of the related table
+                foreach (explode('__', $sr) as $field) {
+                    $full_path .= $field;
+                    $parts[] = $field;
+                    $theseFields = array();
+                    list($alias, $fmodel) = $this->getField($full_path, $model,
+                        array('table'=>true, 'model'=>true));
+                    foreach ($fmodel::getMeta()->getFieldNames() as $f) {
+                        // Handle deferreds
+                        if (isset($defer[$sr . '__' . $f]))
+                            continue;
+                        elseif (isset($fields[$alias.'.'.$this->quote($f)]))
+                            continue;
+                        $fields[$alias . '.' . $this->quote($f)] = true;
+                        $theseFields[] = $f;
+                    }
+                    if ($theseFields) {
+                        $fieldMap[] = array($theseFields, $fmodel, $parts);
+                    }
+                    $full_path .= '__';
+                }
+            }
+        }
+        // Support retrieving only a list of values rather than a model
+        elseif ($queryset->values) {
+            foreach ($queryset->values as $alias=>$v) {
+                list($f) = $this->getField($v, $model);
+                $unaliased = $f;
+                if ($f instanceof SqlFunction)
+                    $fields[$f->toSql($this, $model, $alias)] = true;
+                else {
+                    if (!is_int($alias))
+                        $f .= ' AS '.$this->quote($alias);
+                    $fields[$f] = true;
+                }
+                // If there are annotations, add in these fields to the
+                // GROUP BY clause
+                if ($queryset->annotations && !$queryset->distinct)
+                    $group_by[] = $unaliased;
+            }
+        }
+        // Simple selection from one table
+        elseif (!$queryset->aggregated) {
+            if ($queryset->defer) {
+                foreach ($meta->getFieldNames() as $f) {
+                    if (isset($queryset->defer[$f]))
+                        continue;
+                    $fields[$rootAlias .'.'. $this->quote($f)] = true;
+                }
+            }
+            else {
+                $fields[$rootAlias.'.*'] = true;
+            }
+        }
+        $fields = array_keys($fields);
+        // Add in annotations
+        if ($queryset->annotations) {
+            foreach ($queryset->annotations as $alias=>$A) {
+                // The root model will receive the annotations, add in the
+                // annotation after the root model's fields
+                $T = $A->toSql($this, $model, $alias);
+                if ($fieldMap) {
+                    array_splice($fields, count($fieldMap[0][0]), 0, array($T));
+                    $fieldMap[0][0][] = $alias;
+                }
+                else {
+                    // No field map — just add to end of field list
+                    $fields[] = $T;
+                }
+            }
+            // If no group by has been set yet, use the root model pk
+            if (!$group_by && !$queryset->aggregated && !$queryset->distinct) {
+                foreach ($meta['pk'] as $pk)
+                    $group_by[] = $rootAlias .'.'. $pk;
             }
-        // Support only retrieving a list of values rather than a model
-        } elseif ($queryset->values) {
-            foreach ($queryset->values as $v) {
-                list($fields[]) = $this->getField($v, $model);
+        }
+        // Add in SELECT extras
+        if (isset($queryset->extra['select'])) {
+            foreach ($queryset->extra['select'] as $name=>$expr) {
+                if ($expr instanceof SqlFunction)
+                    $expr = $expr->toSql($this, false, $name);
+                else
+                    $expr = sprintf('%s AS %s', $expr, $this->quote($name));
+                $fields[] = $expr;
             }
-        } else {
-            $fields[] = $this->quote($table).'.*';
         }
+        if (isset($queryset->distinct)) {
+            foreach ($queryset->distinct as $d)
+                list($group_by[]) = $this->getField($d, $model);
+        }
+        $group_by = $group_by ? ' GROUP BY '.implode(', ', $group_by) : '';
+
+        $joins = $this->getJoins($queryset);
 
-        $joins = $this->getJoins();
         $sql = 'SELECT '.implode(', ', $fields).' FROM '
-            .$this->quote($table).$joins.$where.$sort;
+            .$table.$joins.$where.$group_by.$having.$sort;
+        // UNIONS
+        if ($queryset->chain) {
+            // If the main query is sorted, it will need parentheses
+            if ($parens = (bool) $sort)
+                $sql = "($sql)";
+            foreach ($queryset->chain as $qs) {
+                list($qs, $all) = $qs;
+                $q = $qs->getQuery(array('nosort' => true));
+                // Rewrite the parameter numbers so they fit the parameter numbers
+                // of the current parameters of the $compiler
+                $self = $this;
+                $S = preg_replace_callback("/:(\d+)/",
+                function($m) use ($self, $q) {
+                    $self->params[] = $q->params[$m[1]-1];
+                    return ':'.count($self->params);
+                }, $q->sql);
+                // Wrap unions in parentheses if they are windowed or sorted
+                if ($parens || $qs->isWindowed() || count($qs->getSortFields()))
+                    $S = "($S)";
+                $sql .= ' UNION '.($all ? 'ALL ' : '').$S;
+            }
+        }
+
         if ($queryset->limit)
             $sql .= ' LIMIT '.$queryset->limit;
         if ($queryset->offset)
@@ -970,20 +2683,57 @@ class MySqlCompiler extends SqlCompiler {
             break;
         }
 
-        return new MysqlExecutor($sql, $this->params);
+        return new MysqlExecutor($sql, $this->params, $fieldMap);
     }
 
-    function compileUpdate() {
+    function __compileUpdateSet($model, array $pk) {
+        $fields = array();
+        foreach ($model->dirty as $field=>$old) {
+            if ($model->__new__ or !in_array($field, $pk)) {
+                $fields[] = sprintf('%s = %s', $this->quote($field),
+                    $this->input($model->get($field)));
+            }
+        }
+        return ' SET '.implode(', ', $fields);
+    }
+
+    function compileUpdate(VerySimpleModel $model) {
+        $pk = $model::getMeta('pk');
+        $sql = 'UPDATE '.$this->quote($model::getMeta('table'));
+        $sql .= $this->__compileUpdateSet($model, $pk);
+        // Support PK updates
+        $criteria = array();
+        foreach ($pk as $f) {
+            $criteria[$f] = @$model->dirty[$f] ?: $model->get($f);
+        }
+        $sql .= ' WHERE '.$this->compileQ(new Q($criteria), $model);
+        $sql .= ' LIMIT 1';
+
+        return new MySqlExecutor($sql, $this->params);
+    }
+
+    function compileInsert(VerySimpleModel $model) {
+        $pk = $model::getMeta('pk');
+        $sql = 'INSERT INTO '.$this->quote($model::getMeta('table'));
+        $sql .= $this->__compileUpdateSet($model, $pk);
+
+        return new MySqlExecutor($sql, $this->params);
     }
 
-    function compileInsert() {
+    function compileDelete($model) {
+        $table = $model::getMeta('table');
+
+        $where = ' WHERE '.implode(' AND ',
+            $this->compileConstraints(array(new Q($model->pk)), $model));
+        $sql = 'DELETE FROM '.$this->quote($table).$where.' LIMIT 1';
+        return new MySqlExecutor($sql, $this->params);
     }
 
     function compileBulkDelete($queryset) {
         $model = $queryset->model;
-        $table = $model::$meta['table'];
-        $where = $this->getWhereClause($queryset);
-        $joins = $this->getJoins();
+        $table = $model::getMeta('table');
+        list($where, $having) = $this->getWhereHavingClause($queryset);
+        $joins = $this->getJoins($queryset);
         $sql = 'DELETE '.$this->quote($table).'.* FROM '
             .$this->quote($table).$joins.$where;
         return new MysqlExecutor($sql, $this->params);
@@ -991,33 +2741,68 @@ class MySqlCompiler extends SqlCompiler {
 
     function compileBulkUpdate($queryset, array $what) {
         $model = $queryset->model;
-        $table = $model::$meta['table'];
+        $table = $model::getMeta('table');
         $set = array();
         foreach ($what as $field=>$value)
-            $set[] = sprintf('%s = %s', $this->quote($field), $this->input($value));
+            $set[] = sprintf('%s = %s', $this->quote($field), $this->input($value, false, $model));
         $set = implode(', ', $set);
-        $where = $this->getWhereClause($queryset);
-        $joins = $this->getJoins();
-        $sql = 'UPDATE '.$this->quote($table).' SET '.$set.$joins.$where;
+        list($where, $having) = $this->getWhereHavingClause($queryset);
+        $joins = $this->getJoins($queryset);
+        $sql = 'UPDATE '.$this->quote($table).$joins.' SET '.$set.$where;
         return new MysqlExecutor($sql, $this->params);
     }
 
     // Returns meta data about the table used to build queries
     function inspectTable($table) {
+        static $cache = array();
+
+        // XXX: Assuming schema is not changing — add support to track
+        //      current schema
+        if (isset($cache[$table]))
+            return $cache[$table];
+
+        $sql = 'SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS '
+            .'WHERE TABLE_NAME = '.db_input($table).' AND TABLE_SCHEMA = DATABASE() '
+            .'ORDER BY ORDINAL_POSITION';
+        $ex = new MysqlExecutor($sql, array());
+        $columns = array();
+        while (list($column) = $ex->getRow()) {
+            $columns[] = $column;
+        }
+        return $cache[$table] = $columns;
     }
 }
 
-class MysqlExecutor {
+class MySqlExecutor {
 
     var $stmt;
     var $fields = array();
-
     var $sql;
     var $params;
+    // Array of [count, model] values representing which fields in the
+    // result set go with witch model.  Useful for handling select_related
+    // queries
+    var $map;
 
-    function __construct($sql, $params) {
+    function __construct($sql, $params, $map=null) {
         $this->sql = $sql;
         $this->params = $params;
+        $this->map = $map;
+    }
+
+    function getMap() {
+        return $this->map;
+    }
+
+    function fixupParams() {
+        $self = $this;
+        $params = array();
+        $sql = preg_replace_callback("/:(\d+)/",
+        function($m) use ($self, &$params) {
+            $params[] = $self->params[$m[1]-1];
+            return '?';
+        }, $this->sql);
+        return array($sql, $params);
     }
 
     function _prepare() {
@@ -1026,11 +2811,12 @@ class MysqlExecutor {
     }
 
     function execute() {
-        if (!($this->stmt = db_prepare($this->sql)))
-            throw new Exception('Unable to prepare query: '.db_error()
-                .' '.$this->sql);
-        if (count($this->params))
-            $this->_bind($this->params);
+        list($sql, $params) = $this->fixupParams();
+        if (!($this->stmt = db_prepare($sql)))
+            throw new InconsistentModelException(
+                'Unable to prepare query: '.db_error().' '.$sql);
+        if (count($params))
+            $this->_bind($params);
         if (!$this->stmt->execute() || ! $this->stmt->store_result()) {
             throw new OrmException('Unable to execute query: ' . $this->stmt->error);
         }
@@ -1043,13 +2829,21 @@ class MysqlExecutor {
 
         $types = '';
         $ps = array();
-        foreach ($params as &$p) {
+        foreach ($params as $i=>&$p) {
             if (is_int($p) || is_bool($p))
                 $types .= 'i';
             elseif (is_float($p))
                 $types .= 'd';
             elseif (is_string($p))
                 $types .= 's';
+            elseif ($p instanceof DateTime) {
+                $types .= 's';
+                $p = $p->format('Y-m-d H:i:s');
+            }
+            elseif (is_object($p)) {
+                $types .= 's';
+                $p = (string) $p;
+            }
             // TODO: Emit error if param is null
             $ps[] = &$p;
         }
@@ -1061,8 +2855,7 @@ class MysqlExecutor {
     function _setup_output() {
         if (!($meta = $this->stmt->result_metadata()))
             throw new OrmException('Unable to fetch statment metadata: ', $this->stmt->error);
-        while ($f = $meta->fetch_field())
-            $this->fields[] = $f;
+        $this->fields = $meta->fetch_fields();
         $meta->free_result();
     }
 
@@ -1120,23 +2913,6 @@ class MysqlExecutor {
         return $output;
     }
 
-    function getStruct() {
-        $output = array();
-        $variables = array();
-
-        if (!isset($this->stmt))
-            $this->_prepare();
-
-        foreach ($this->fields as $f)
-            $variables[] = &$output[$f->table][$f->name]; // pass by reference
-
-        // TODO: Figure out what the table alias for the root model will be
-        call_user_func_array(array($this->stmt, 'bind_result'), $variables);
-        if (!$this->next())
-            return false;
-        return $output;
-    }
-
     function close() {
         if (!$this->stmt)
             return;
@@ -1154,7 +2930,79 @@ class MysqlExecutor {
     }
 
     function __toString() {
-        return $this->sql;
+        $self = $this;
+        return preg_replace_callback("/:(\d+)(?=([^']*'[^']*')*[^']*$)/",
+        function($m) use ($self) {
+            $p = $self->params[$m[1]-1];
+            if ($p instanceof DateTime) {
+                $p = $p->format('Y-m-d H:i:s');
+            }
+            return db_real_escape($p, is_string($p));
+        }, $this->sql);
+    }
+}
+
+class Q implements Serializable {
+    const NEGATED = 0x0001;
+    const ANY =     0x0002;
+
+    var $constraints;
+    var $negated = false;
+    var $ored = false;
+
+    function __construct($filter=array(), $flags=0) {
+        if (!is_array($filter))
+            $filter = array($filter);
+        $this->constraints = $filter;
+        $this->negated = $flags & self::NEGATED;
+        $this->ored = $flags & self::ANY;
+    }
+
+    function isNegated() {
+        return $this->negated;
+    }
+
+    function isOred() {
+        return $this->ored;
+    }
+
+    function negate() {
+        $this->negated = !$this->negated;
+        return $this;
+    }
+
+    function union() {
+        $this->ored = true;
+    }
+
+    function add($constraints) {
+        if (is_array($constraints))
+            $this->constraints = array_merge($this->constraints, $constraints);
+        elseif ($constraints instanceof static)
+            $this->constraints[] = $constraints;
+        else
+            throw new InvalidArgumentException('Expected an instance of Q or an array thereof');
+        return $this;
+    }
+
+    static function not($constraints) {
+        return new static($constraints, self::NEGATED);
+    }
+
+    static function any($constraints) {
+        return new static($constraints, self::ANY);
+    }
+
+    static function all($constraints) {
+        return new static($constraints);
+    }
+
+    function serialize() {
+        return serialize(array($this->negated, $this->ored, $this->constraints));
+    }
+
+    function unserialize($data) {
+        list($this->negated, $this->ored, $this->constraints) = unserialize($data);
     }
 }
 ?>
diff --git a/include/class.osticket.php b/include/class.osticket.php
index 44a354ed06072e48d9ba90939ba9713c7f8e6595..40c939c0493a30d685ea7c51fac0d808eb5b39a5 100644
--- a/include/class.osticket.php
+++ b/include/class.osticket.php
@@ -21,6 +21,7 @@
 require_once(INCLUDE_DIR.'class.csrf.php'); //CSRF token class.
 require_once(INCLUDE_DIR.'class.migrater.php');
 require_once(INCLUDE_DIR.'class.plugin.php');
+require_once INCLUDE_DIR . 'class.message.php';
 
 define('LOG_WARN',LOG_WARNING);
 
@@ -50,12 +51,13 @@ class osTicket {
     var $company;
     var $plugins;
 
-    function osTicket() {
+    function __construct() {
 
         require_once(INCLUDE_DIR.'class.config.php'); //Config helper
         require_once(INCLUDE_DIR.'class.company.php');
 
-        $this->session = osTicketSession::start(SESSION_TTL); // start DB based session
+        if (!defined('DISABLE_SESSION') || !DISABLE_SESSION)
+            $this->session = osTicketSession::start(SESSION_TTL); // start DB based session
 
         $this->config = new OsticketConfig();
 
@@ -110,8 +112,8 @@ class osTicket {
         return ($token && $this->getCSRF()->validateToken($token));
     }
 
-    function checkCSRFToken($name='') {
-        $name = $name?$name:$this->getCSRF()->getTokenName();
+    function checkCSRFToken($name=false) {
+        $name = $name ?: $this->getCSRF()->getTokenName();
         if(isset($_POST[$name]) && $this->validateCSRFToken($_POST[$name]))
             return true;
 
@@ -145,6 +147,13 @@ class osTicket {
         return $replacer->replaceVars($input);
     }
 
+    static function getVarScope() {
+        return array(
+            'url' => __("osTicket's base url (FQDN)"),
+            'company' => array('class' => 'Company', 'desc' => __('Company Information')),
+        );
+    }
+
     function addExtraHeader($header, $pjax_script=false) {
         $this->headers[md5($header)] = $header;
         $this->pjax_extra[md5($header)] = $pjax_script;
@@ -263,6 +272,10 @@ class osTicket {
             $e->getTraceAsString());
         $error .= nl2br("\n\n---- "._S('Backtrace')." ----\n".$bt);
 
+        // Prevent recursive loops through this code path
+        if (substr_count($bt, __FUNCTION__) > 1)
+            return;
+
         return $this->log(LOG_ERR, $title, $error, $alert);
     }
 
@@ -302,7 +315,8 @@ class osTicket {
             return false;
 
         //Alert admin if enabled...
-        if($alert && $this->getConfig()->getLogLevel() >= $level)
+        $alert = $alert && !$this->isUpgradePending();
+        if ($alert && $this->getConfig()->getLogLevel() >= $level)
             $this->alertAdmin($title, $message);
 
         //Save log based on system log level settings.
@@ -485,7 +499,7 @@ class osTicket {
     }
 
     /* returns true if script is being executed via commandline */
-    function is_cli() {
+    static function is_cli() {
         return (!strcasecmp(substr(php_sapi_name(), 0, 3), 'cli')
                 || (!isset($_SERVER['REQUEST_METHOD']) &&
                     !isset($_SERVER['HTTP_HOST']))
@@ -501,10 +515,6 @@ class osTicket {
         if(!($ost = new osTicket()))
             return null;
 
-        //Set default time zone... user/staff settting will override it (on login).
-        $_SESSION['TZ_OFFSET'] = $ost->getConfig()->getTZoffset();
-        $_SESSION['TZ_DST'] = $ost->getConfig()->observeDaylightSaving();
-
         // Bootstrap installed plugins
         $ost->plugins->bootstrap();
 
diff --git a/include/class.ostsession.php b/include/class.ostsession.php
index f649955494a8ba85460fcf2ff463bbc46ce3a9e6..8581965eb5f538e343875a0e0ea2df94a02748c2 100644
--- a/include/class.ostsession.php
+++ b/include/class.ostsession.php
@@ -27,7 +27,7 @@ class osTicketSession {
     var $id = '';
     var $backend;
 
-    function osTicketSession($ttl=0){
+    function __construct($ttl=0){
         $this->ttl = $ttl ?: ini_get('session.gc_maxlifetime') ?: SESSION_TTL;
 
         // Set osTicket specific session name.
@@ -151,66 +151,75 @@ abstract class SessionBackend {
         return $this->ttl;
     }
 
+    function write($id, $data) {
+        // Last chance session update
+        $i = new ArrayObject(array('touched' => false));
+        Signal::send('session.close', null, $i);
+        return $this->update($id, $i['touched'] ? session_encode() : $data);
+    }
+
     abstract function read($id);
-    abstract function write($id, $data);
+    abstract function update($id, $data);
     abstract function destroy($id);
     abstract function gc($maxlife);
 }
 
+class SessionData
+extends VerySimpleModel {
+    static $meta = array(
+        'table' => SESSION_TABLE,
+        'pk' => array('session_id'),
+    );
+}
+
 class DbSessionBackend
 extends SessionBackend {
-
-    function read($id){
-        $this->isnew = false;
-        if (!$this->data || $this->id != $id) {
-            $sql='SELECT session_data FROM '.SESSION_TABLE
-                .' WHERE session_id='.db_input($id)
-                .'  AND session_expire>NOW()';
-            if(!($res=db_query($sql)))
-                return false;
-            elseif (db_num_rows($res))
-                list($this->data)=db_fetch_row($res);
-            else
-                // No session data on record -- new session
-                $this->isnew = true;
+    function read($id) {
+        try {
+            $this->data = SessionData::objects()->filter([
+                'session_id' => $id,
+                'session_expire__gt' => SqlFunction::NOW(),
+            ])->one();
             $this->id = $id;
         }
-        $this->data_hash = md5($id.$this->data);
-        return $this->data;
+        catch (DoesNotExist $e) {
+            $this->data = new SessionData(['session_id' => $id]);
+        }
+        catch (OrmException $e) {
+            return false;
+        }
+        return $this->data->session_data;
     }
 
-    function write($id, $data){
+    function update($id, $data){
         global $thisstaff;
 
-        if (md5($id.$data) == $this->data_hash)
-            return;
-
-        elseif (defined('DISABLE_SESSION') && $this->isnew)
-            return;
+        if (defined('DISABLE_SESSION') && $this->data->__new__)
+            return true;
 
-        $ttl = ($this && get_class($this) == 'osTicketSession')
+        $ttl = $this && method_exists($this, 'getTTL')
             ? $this->getTTL() : SESSION_TTL;
 
-        $sql='REPLACE INTO '.SESSION_TABLE.' SET session_updated=NOW() '.
-             ',session_id='.db_input($id).
-             ',session_data=0x'.bin2hex($data).
-             ',session_expire=(NOW() + INTERVAL '.$ttl.' SECOND)'.
-             ',user_id='.db_input($thisstaff?$thisstaff->getId():0).
-             ',user_ip='.db_input($_SERVER['REMOTE_ADDR']).
-             ',user_agent='.db_input($_SERVER['HTTP_USER_AGENT']);
+        assert($this->data->session_id == $id);
 
-        $this->data = '';
-        return (db_query($sql) && db_affected_rows());
+        $this->data->session_data = $data;
+        $this->data->session_expire =
+            SqlFunction::NOW()->plus(SqlInterval::SECOND($ttl));
+        $this->data->user_id = $thisstaff ? $thisstaff->getId() : 0;
+        $this->data->user_ip = $_SERVER['REMOTE_ADDR'];
+        $this->data->user_agent = $_SERVER['HTTP_USER_AGENT'];
+
+        return $this->data->save();
     }
 
     function destroy($id){
-        $sql='DELETE FROM '.SESSION_TABLE.' WHERE session_id='.db_input($id);
-        return (db_query($sql) && db_affected_rows());
+        return SessionData::objects()->filter(['session_id' => $id])->delete();
     }
 
     function gc($maxlife){
-        $sql='DELETE FROM '.SESSION_TABLE.' WHERE session_expire<NOW()';
-        db_query($sql);
+        SessionData::objects()->filter([
+            'session_expire__lte' => SqlFunction::NOW()
+        ])->delete();
     }
 }
 
@@ -272,7 +281,7 @@ extends SessionBackend {
         return $data;
     }
 
-    function write($id, $data) {
+    function update($id, $data) {
         if (defined('DISABLE_SESSION') && $this->isnew)
             return;
 
diff --git a/include/class.page.php b/include/class.page.php
index 7b11830897697a140adfded1e6611bacbddeda2e..920c2ee88ecfd3cd06c3eb8bf36756bad487c76f 100644
--- a/include/class.page.php
+++ b/include/class.page.php
@@ -13,73 +13,95 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-class Page {
-
-    var $id;
-    var $ht;
-    var $attachments;
-
-    function Page($id, $lang=false) {
-        $this->id=0;
-        $this->ht = array();
-        $this->load($id, $lang);
-    }
-
-    function load($id=0, $lang=false) {
-
-        if(!$id && !($id=$this->getId()))
-            return false;
-
-        $sql='SELECT page.*, count(topic.page_id) as topics '
-            .' FROM '.PAGE_TABLE.' page '
-            .' LEFT JOIN '.TOPIC_TABLE. ' topic ON(topic.page_id=page.id) '
-            . ' WHERE page.content_id='.db_input($id)
-            . ($lang ? ' AND lang='.db_input($lang) : '')
-            .' GROUP By page.id';
-
-        if (!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
-
-        $this->ht = db_fetch_array($res);
-        $this->id = $this->ht['id'];
-        $this->attachments = new GenericAttachments($this->id, 'P');
-
-        return true;
-    }
-
-    function reload() {
-        return $this->load();
-    }
+class Page extends VerySimpleModel {
+
+    static $meta = array(
+        'table' => PAGE_TABLE,
+        'pk' => array('id'),
+        'ordering' => array('name'),
+        'defer' => array('body'),
+        'joins' => array(
+            'topics' => array(
+                'reverse' => 'Topic.page',
+            ),
+            'attachments' => array(
+                'constraint' => array(
+                    "'P'" => 'Attachment.type',
+                    'id' => 'Attachment.object_id',
+                ),
+                'list' => true,
+                'null' => true,
+                'broker' => 'GenericAttachments',
+            ),
+        ),
+    );
+
+    var $_local;
 
     function getId() {
         return $this->id;
     }
 
     function getHashtable() {
-        return $this->ht;
+        $base = $this->ht;
+        unset($base['topics']);
+        unset($base['attachments']);
+        return $base;
     }
 
     function getType() {
-        return $this->ht['type'];
+        return $this->type;
     }
 
     function getName() {
-        return $this->ht['name'];
+        return $this->name;
+    }
+    function getLocalName($lang=false) {
+        return $this->getLocal('name', $lang);
+    }
+    function getNameAsSlug() {
+        return urlencode(Format::slugify($this->name));
     }
 
     function getBody() {
-        return $this->ht['body'];
+        return $this->body;
+    }
+    function getLocalBody($lang=false) {
+        return $this->_getLocal('body', $lang);
     }
     function getBodyWithImages() {
-        return Format::viewableImages($this->getBody());
+        return Format::viewableImages($this->getLocalBody());
+    }
+
+    function _getLocal($what, $lang=false) {
+        if (!$lang) {
+            $lang = Internationalization::getCurrentLanguage();
+        }
+        $translations = $this->getAllTranslations();
+        foreach ($translations as $t) {
+            if ($lang == $t->lang) {
+                $data = $t->getComplex();
+                if (isset($data[$what]))
+                    return $data[$what];
+            }
+        }
+        return $this->ht[$what];
+    }
+
+    function getAllTranslations() {
+        if (!isset($this->_local)) {
+            $tag = $this->getTranslateTag('name:body');
+            $this->_local = CustomDataTranslation::allTranslations($tag, 'article');
+        }
+        return $this->_local;
     }
 
     function getNotes() {
-        return $this->ht['notes'];
+        return $this->notes;
     }
 
     function isActive() {
-        return ($this->ht['isactive']);
+        return ($this->isactive);
     }
 
     function isInUse() {
@@ -91,30 +113,24 @@ class Page {
 
 
     function getCreateDate() {
-        return $this->ht['created'];
+        return $this->created;
     }
 
     function getUpdateDate() {
-        return $this->ht['updated'];
+        return $this->updated;
     }
 
     function getNumTopics() {
-        return $this->ht['topics'];
+        return $this->topics->count();
     }
 
-    function update($vars, &$errors) {
-
-        if(!$vars['isactive'] && $this->isInUse()) {
-            $errors['err'] = __('A page currently in-use CANNOT be disabled!');
-            $errors['isactive'] = __('Page is in-use!');
-        }
-
-        if($errors || !$this->save($this->getId(), $vars, $errors))
-            return false;
-
-        $this->reload();
-
-        return true;
+    function getTranslateTag($subtag) {
+        return _H(sprintf('page.%s.%s', $subtag, $this->getId()));
+    }
+    function getLocal($subtag) {
+        $tag = $this->getTranslateTag($subtag);
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : $this->ht[$subtag];
     }
 
     function disable() {
@@ -126,36 +142,37 @@ class Page {
             return false;
 
 
-        $sql=' UPDATE '.PAGE_TABLE.' SET isactive=0 '
-            .' WHERE id='.db_input($this->getId());
-
-        if(!db_query($sql) || !db_affected_rows())
-            return false;
-
-        $this->reload();
-
-        return true;
+        $this->isactive = 0;
+        return $this->save();
     }
 
     function delete() {
 
-        if($this->isInUse())
+        if ($this->isInUse())
             return false;
 
-        $sql='DELETE FROM '.PAGE_TABLE
-            .' WHERE id='.db_input($this->getId())
-            .' LIMIT 1';
-
-        if(!db_query($sql) || !db_affected_rows())
+        if (!parent::delete())
             return false;
 
-        db_query('UPDATE '.TOPIC_TABLE.' SET page_id=0 WHERE page_id='.db_input($this->getId()));
+        return Topic::objects()
+            ->filter(array('page_id'=>$this->getId()))
+            ->update(array('page_id'=>0));
+    }
 
-        return true;
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($refetch || $this->dirty);
     }
 
     /* ------------------> Static methods <--------------------- */
 
+    static function create($vars=false) {
+        $page = new static($vars);
+        $page->created = SqlFunction::NOW();
+        return $page;
+    }
+
     function add($vars, &$errors) {
         if(!($id=self::create($vars, $errors)))
             return false;
@@ -163,79 +180,80 @@ class Page {
         return self::lookup($id);
     }
 
-    function create($vars, &$errors) {
-        return self::save(0, $vars, $errors);
-    }
-
-    function getPages($criteria=array()) {
-
-        $sql = ' SELECT id FROM '.PAGE_TABLE.' WHERE 1';
+    static function getPages($criteria=array()) {
+        $pages = self::objects();
         if(isset($criteria['active']))
-            $sql.=' AND  isactive='.db_input($criteria['active']?1:0);
+            $pages = $pages->filter(array('isactive'=>$criteria['active']));
         if(isset($criteria['type']))
-            $sql.=' AND `type`='.db_input($criteria['type']);
-
-        $sql.=' ORDER BY name';
-
-        $pages = array();
-        if(($res=db_query($sql)) && db_num_rows($res))
-            while(list($id) = db_fetch_row($res))
-                $pages[] = Page::lookup($id);
+            $pages = $pages->filter(array('type'=>$criteria['type']));
 
-        return array_filter($pages);
+        return $pages;
     }
 
-    function getActivePages($criteria=array()) {
+    static function getActivePages($criteria=array()) {
 
         $criteria = array_merge($criteria, array('active'=>true));
 
         return self::getPages($criteria);
     }
 
-    function getActiveThankYouPages() {
+    static function getActiveThankYouPages() {
         return self::getActivePages(array('type' => 'thank-you'));
     }
 
-    function getIdByName($name, $lang=false) {
-
-        $id = 0;
-        $sql = ' SELECT id FROM '.PAGE_TABLE.' WHERE name='.db_input($name);
-        if ($lang)
-            $sql .= ' AND lang='.db_input($lang);
-
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id) = db_fetch_row($res);
-
-        return $id;
+    static function lookup($id, $lang=false) {
+        try {
+            $qs = self::objects()->filter(is_array($id) ? $id : array('id'=>$id));
+            if ($lang)
+                $qs = $qs->filter(array('lang'=>$lang));
+            return $qs->one();
+        }
+        catch (DoesNotExist $ex) {
+            return null;
+        }
     }
 
-    function getIdByType($type, $lang=false) {
-        $id = 0;
-        $sql = ' SELECT id FROM '.PAGE_TABLE.' WHERE `type`='.db_input($type);
-        if ($lang)
-            $sql .= ' AND lang='.db_input($lang);
-
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id) = db_fetch_row($res);
-
-        return $id;
+    static function getIdByName($name, $lang=false) {
+        try {
+            $qs = self::objects()->filter(array('name'=>$name))
+                ->values_flat('id');
+            list($id) = $qs->one();
+            return $id;
+        }
+        catch (DoesNotExist $ex) {
+            return null;
+        }
+        catch (InconsistentModelException $ex) {
+            // This largely happens on upgrades, and may specifically cause
+            // the staff login box to crash
+            return null;
+        }
     }
 
-    function lookup($id) {
-        return ($id
-                && is_numeric($id)
-                && ($p= new Page($id))
-                && $p->getId()==$id)
-            ? $p : null;
+    static function lookupByType($type, $lang=false) {
+        try {
+            return self::objects()->filter(array('type'=>$type))->one();
+        }
+        catch (DoesNotExist $ex) {
+            return null;
+        }
+        catch (InconsistentModelException $ex) {
+            return null;
+        }
     }
 
-    function save($id, $vars, &$errors, $allowempty=false) {
+    function update($vars, &$errors, $allowempty=false) {
 
         //Cleanup.
         $vars['name']=Format::striptags(trim($vars['name']));
 
         //validate
-        if($id && $id!=$vars['id'])
+        if (isset($this->id) && !$vars['isactive'] && $this->isInUse()) {
+            $errors['err'] = __('A page currently in-use CANNOT be disabled!');
+            $errors['isactive'] = __('Page is in-use!');
+        }
+
+        if (isset($this->id) && $this->getId() != $vars['id'])
             $errors['err'] = __('Internal error. Try again');
 
         if(!$vars['type'])
@@ -243,7 +261,7 @@ class Page {
 
         if(!$vars['name'])
             $errors['name'] = __('Name is required');
-        elseif(($pid=self::getIdByName($vars['name'])) && $pid!=$id)
+        elseif(($pid=self::getIdByName($vars['name'])) && $pid!=$this->getId())
             $errors['name'] = __('Name already exists');
 
         if(!$vars['body'] && !$allowempty)
@@ -251,38 +269,102 @@ class Page {
 
         if($errors) return false;
 
-        //save
-        $sql=' updated=NOW() '
-            .', `type`='.db_input($vars['type'])
-            .', name='.db_input($vars['name'])
-            .', body='.db_input(Format::sanitize($vars['body']))
-            .', isactive='.db_input($vars['isactive'] ? 1 : 0)
-            .', notes='.db_input(Format::sanitize($vars['notes']));
-
-        if($id) {
-            $sql='UPDATE '.PAGE_TABLE.' SET '.$sql.' WHERE id='.db_input($id);
-            if(db_query($sql))
-                return true;
-
-            $errors['err']=sprintf(__('Unable to update %s.'), __('this site page'));
-
-        } else {
-            $sql='INSERT INTO '.PAGE_TABLE.' SET '.$sql.', created=NOW()';
-            if (!db_query($sql) || !($id=db_insert_id())) {
-                $errors['err']=sprintf(__('Unable to create %s.'), __('this site page'))
-                   .' '.__('Internal error occurred');
-                return false;
-            }
+        $this->type = $vars['type'];
+        $this->name = $vars['name'];
+        $this->body = Format::sanitize($vars['body']);
+        $this->isactive = (bool) $vars['isactive'];
+        $this->notes = Format::sanitize($vars['notes']);
 
-            $sql = 'UPDATE '.PAGE_TABLE.' SET `content_id`=`id`'
-                .' WHERE id='.db_input($id);
-            if (!db_query($sql))
-                return false;
+        $isnew = !isset($this->id);
+        $rv = $this->save();
+        if (!$isnew)
+            $rv = $this->saveTranslations($vars, $errors);
 
-            return $id;
-        }
+        // Attach inline attachments from the editor
+        $keepers = Draft::getAttachmentIds($vars['body']);
+        $keepers = array_map(function($i) { return $i['id']; }, $keepers);
+        $this->attachments->keepOnlyFileIds($keepers, true);
 
+        if ($rv)
+            return $rv;
+
+        $errors['err']=sprintf(__('Unable to update %s.'), __('this site page'));
         return false;
     }
+
+    function saveTranslations($vars, &$errors) {
+        global $thisstaff;
+
+        $tag = $this->getTranslateTag('name:body');
+        $translations = CustomDataTranslation::allTranslations($tag, 'article');
+        foreach ($translations as $t) {
+            $title = @$vars['trans'][$t->lang]['title'];
+            $body = @$vars['trans'][$t->lang]['body'];
+            if (!$title && !$body)
+                continue;
+
+            // Content is not new and shouldn't be added below
+            unset($vars['trans'][$t->lang]['title']);
+            unset($vars['trans'][$t->lang]['body']);
+            $content = array('name' => $title, 'body' => Format::sanitize($body));
+
+            // Don't update content which wasn't updated
+            if ($content == $t->getComplex())
+                continue;
+
+            $t->text = $content;
+            $t->agent_id = $thisstaff->getId();
+            $t->updated = SqlFunction::NOW();
+            if (!$t->save())
+                return false;
+        }
+        // New translations (?)
+        foreach ($vars['trans'] as $lang=>$parts) {
+            $content = array('name' => @$parts['title'], 'body' => Format::sanitize(@$parts['body']));
+            if (!array_filter($content))
+                continue;
+            $t = CustomDataTranslation::create(array(
+                'type'      => 'article',
+                'object_hash' => $tag,
+                'lang'      => $lang,
+                'text'      => $content,
+                'revision'  => 1,
+                'agent_id'  => $thisstaff->getId(),
+                'updated'   => SqlFunction::NOW(),
+            ));
+            if (!$t->save())
+                return false;
+        }
+        return true;
+    }
+
+    static function getContext($type) {
+        $context = array(
+        'thank-you' => array('ticket'),
+        'registration-staff' => array(
+            // 'token' => __('Special authentication token'),
+            'staff' => array('class' => 'Staff', 'desc' => __('Message recipient')),
+            'recipient' => array('class' => 'Staff', 'desc' => __('Message recipient')),
+            'link',
+        ),
+        'pwreset-staff' => array(
+            'staff' => array('class' => 'Staff', 'desc' => __('Message recipient')),
+            'recipient' => array('class' => 'Staff', 'desc' => __('Message recipient')),
+            'link',
+        ),
+        'registration-client' => array(
+            // 'token' => __('Special authentication token'),
+            'recipient' => array('class' => 'User', 'desc' => __('Message recipient')),
+            'link', 'user',
+        ),
+        'pwreset-client' => array(
+            'recipient' => array('class' => 'User', 'desc' => __('Message recipient')),
+            'link', 'user',
+        ),
+        'access-link' => array('ticket', 'recipient'),
+        );
+
+        return $context[$type];
+    }
 }
 ?>
diff --git a/include/class.pagenate.php b/include/class.pagenate.php
index f6446dd92a9d2647882fb0155b9a572f22c472d2..69368393e431e4d9edb95da7940e90101d9c66b4 100644
--- a/include/class.pagenate.php
+++ b/include/class.pagenate.php
@@ -18,12 +18,13 @@ class PageNate {
 
     var $start;
     var $limit;
+    var $slack = 0;
     var $total;
     var $page;
     var $pages;
 
 
-    function PageNate($total,$page,$limit=20,$url='') {
+    function __construct($total,$page,$limit=20,$url='') {
         $this->total = intval($total);
         $this->limit = max($limit, 1 );
         $this->page  = max($page, 1 );
@@ -54,13 +55,24 @@ class PageNate {
     }
 
     function getStart() {
-        return $this->start;
+        return max($this->start - $this->slack, 0);
+    }
+
+    function getStop() {
+        return min($this->getStart() + $this->getLimit(), $this->total);
+    }
+
+    function getCount() {
+        return $this->total;
     }
 
     function getLimit() {
         return $this->limit;
     }
 
+    function setSlack($count) {
+        $this->slack = $count;
+    }
 
     function getNumPages(){
         return $this->pages;
@@ -72,27 +84,28 @@ class PageNate {
 
     function showing() {
         $html = '';
-        $from= $this->start+1;
-        if ($this->start + $this->limit < $this->total) {
-            $to= $this->start + $this->limit;
+        $start = $this->getStart() + 1;
+        $end = min($start + $this->limit + $this->slack - 1, $this->total);
+        if ($end < $this->total) {
+            $to= $end;
         } else {
             $to= $this->total;
         }
-        $html="&nbsp;".__('Showing')."&nbsp;&nbsp;";
+        $html=__('Showing')."&nbsp;";
         if ($this->total > 0) {
             $html .= sprintf(__('%1$d - %2$d of %3$d' /* Used in pagination output */),
-               $from, $to, $this->total);
+               $start, $end, $this->total);
         }else{
             $html .= " 0 ";
         }
         return $html;
     }
 
-    function getPageLinks() {
+    function getPageLinks($hash=false, $pjax=false) {
         $html                 = '';
         $file                =$this->url;
         $displayed_span     = 5;
-        $total_pages         = ceil( $this->total / $this->limit );
+        $total_pages         = ceil( ($this->total - $this->slack) / $this->limit );
         $this_page             = ceil( ($this->start+1) / $this->limit );
 
         $last=$this_page-1;
@@ -116,15 +129,24 @@ class PageNate {
 
         for ($i=$start_loop; $i <= $stop_loop; $i++) {
             $page = ($i - 1) * $this->limit;
+            $href = "{$file}&amp;p={$i}";
+            if ($hash)
+                $href .= '#'.$hash;
             if ($i == $this_page) {
                 $html .= "\n<b>[$i]</b>";
+            }
+            elseif ($pjax) {
+                $html .= " <a href=\"{$href}\" data-pjax-container=\"{$pjax}\"><b>$i</b></a>";
             } else {
-                $html .= "\n<a href=\"$file&p=$i\" ><b>$i</b></a>";
+                $html .= "\n<a href=\"{$href}\" ><b>$i</b></a>";
             }
         }
         if($stop_loop<$total_pages){
             $nextspan=($stop_loop+$displayed_span>$total_pages)?$total_pages-$displayed_span:$stop_loop+$displayed_span;
-            $html .= "\n<a href=\"$file&p=$nextspan\" ><strong>&raquo;</strong></a>";
+            $href = "{$file}&amp;p={$nextspan}";
+            if ($hash)
+                $href .= '#'.$hash;
+            $html .= "\n<a href=\"{$href}\" ><strong>&raquo;</strong></a>";
         }
 
 
@@ -132,5 +154,11 @@ class PageNate {
         return $html;
     }
 
+    function paginate(QuerySet $qs) {
+        $start = $this->getStart();
+        $end = min($start + $this->getLimit() + $this->slack + ($start > 0 ? $this->slack : 0), $this->total);
+        return $qs->limit($end-$start)->offset($start);
+    }
+
 }
 ?>
diff --git a/include/class.pdf.php b/include/class.pdf.php
index f0422b47bc5e242b89c56c52266b35948f066eda..c12d071e82dd48058513fad2d1a89dcf5efb25a7 100644
--- a/include/class.pdf.php
+++ b/include/class.pdf.php
@@ -18,7 +18,36 @@ define('THIS_DIR', str_replace('\\', '/', Misc::realpath(dirname(__FILE__))) . '
 
 require_once(INCLUDE_DIR.'mpdf/mpdf.php');
 
-class Ticket2PDF extends mPDF
+class mPDFWithLocalImages extends mPDF {
+    function WriteHtml($html) {
+        static $filenumber = 1;
+        $args = func_get_args();
+        $self = $this;
+        $images = $cids = array();
+        // Try and get information for all the files in one query
+        if (preg_match_all('/"cid:([\w._-]{32})"/', $html, $cids)) {
+            foreach (AttachmentFile::objects()
+                ->filter(array('key__in' => $cids[1]))
+                as $file
+            ) {
+                $images[strtolower($file->getKey())] = $file;
+            }
+        }
+        $args[0] = preg_replace_callback('/"cid:([\w.-]{32})"/',
+            function($match) use ($self, $images, &$filenumber) {
+                if (!($file = @$images[strtolower($match[1])]))
+                    return $match[0];
+                $key = "__attached_file_".$filenumber++;
+                $self->{$key} = $file->getData();
+                return 'var:'.$key;
+            },
+            $html
+        );
+        return call_user_func_array(array('parent', 'WriteHtml'), $args);
+    }
+}
+
+class Ticket2PDF extends mPDFWithLocalImages
 {
 
 	var $includenotes = false;
@@ -27,20 +56,14 @@ class Ticket2PDF extends mPDF
 
     var $ticket = null;
 
-	function Ticket2PDF($ticket, $psize='Letter', $notes=false) {
+	function __construct($ticket, $psize='Letter', $notes=false) {
         global $thisstaff;
 
-
         $this->ticket = $ticket;
         $this->includenotes = $notes;
 
         parent::__construct('', $psize);
 
-        $this->SetMargins(10,10,10);
-		$this->AliasNbPages();
-		$this->AddPage();
-		$this->cMargin = 3;
-
         $this->_print();
 	}
 
@@ -48,272 +71,53 @@ class Ticket2PDF extends mPDF
         return $this->ticket;
     }
 
-    function getLogoFile() {
-        global $ost;
-
-        if (!function_exists('imagecreatefromstring')
-                || (!($logo = $ost->getConfig()->getClientLogo()))) {
-            return INCLUDE_DIR.'fpdf/print-logo.png';
-        }
-
-        $tmp = tempnam(sys_get_temp_dir(), 'pdf') . '.jpg';
-        $img = imagecreatefromstring($logo->getData());
-        // Handle transparent images with white background
-        $img2 = imagecreatetruecolor(imagesx($img), imagesy($img));
-        $white = imagecolorallocate($img2, 255, 255, 255);
-        imagefill($img2, 0, 0, $white);
-        imagecopy($img2, $img, 0, 0, 0, 0, imagesx($img), imagesy($img));
-        imagejpeg($img2, $tmp);
-        return $tmp;
-    }
-
-	//report header...most stuff are hard coded for now...
-	function Header() {
-        global $cfg;
-
-		//Common header
-        $logo = $this->getLogoFile();
-		$this->Image($logo, $this->lMargin, $this->tMargin, 0, 20);
-        if (strpos($logo, INCLUDE_DIR) === false)
-            unlink($logo);
-		$this->SetFont('Arial', 'B', 16);
-		$this->SetY($this->tMargin + 20);
-        $this->SetX($this->lMargin);
-        $this->WriteCell(0, 0, '', "B", 2, 'L');
-		$this->Ln(1);
-        $this->SetFont('Arial', 'B',10);
-        $this->WriteCell(0, 5, $cfg->getTitle(), 0, 0, 'L');
-        $this->SetFont('Arial', 'I',10);
-        $this->WriteCell(0, 5, Format::date($cfg->getDateTimeFormat(), Misc::gmtime(),
-            $_SESSION['TZ_OFFSET'], $_SESSION['TZ_DST'])
-            .' GMT '.$_SESSION['TZ_OFFSET'], 0, 1, 'R');
-		$this->Ln(5);
-	}
-
-	//Page footer baby
-	function Footer() {
-        global $thisstaff;
-
-		$this->SetY(-15);
-        $this->WriteCell(0, 2, '', "T", 2, 'L');
-		$this->SetFont('Arial', 'I', 9);
-        $this->WriteCell(0, 7, sprintf(__('Ticket #%1$s printed by %2$s on %3$s'),
-            $this->getTicket()->getNumber(), $thisstaff->getUserName(), date('r')), 0, 0, 'L');
-		//$this->WriteCell(0,10,'Page '.($this->PageNo()-$this->pageOffset).' of {nb} '.$this->pageOffset.' '.$this->PageNo(),0,0,'R');
-		$this->WriteCell(0, 7, sprintf(__('Page %d'), ($this->PageNo() - $this->pageOffset)), 0, 0, 'R');
-	}
-
-    function Cell($w, $h=0, $txt='', $border=0, $ln=0, $align='', $fill=false, $link='') {
-        parent::Cell($w, $h, $txt, $border, $ln, $align, $fill, $link);
-    }
-
-    function WriteText($w, $text, $border) {
-
-        $this->SetFont('Arial','',11);
-        $this->MultiCell($w, 7, $text, $border, 'L');
-
-    }
-
-    function WriteHtml() {
-        static $filenumber = 1;
-        $args = func_get_args();
-        $text = &$args[0];
-        $self = $this;
-        $text = preg_replace_callback('/cid:([\w.-]{32})/',
-            function($match) use ($self, &$filenumber) {
-                if (!($file = AttachmentFile::lookup($match[1])))
-                    return $match[0];
-                $key = "__attached_file_".$filenumber++;
-                $self->{$key} = $file->getData();
-                return 'var:'.$key;
-            },
-            $text
-        );
-        call_user_func_array(array('parent', 'WriteHtml'), $args);
-    }
-
     function _print() {
+        global $thisstaff, $thisclient, $cfg;
 
         if(!($ticket=$this->getTicket()))
             return;
 
-        $w =(($this->w/2)-$this->lMargin);
-        $l = 35;
-        $c = $w-$l;
-
-        // Setup HTML writing and load default thread stylesheet
-        $this->WriteHtml(
-            '<style>'.file_get_contents(ROOT_DIR.'css/thread.css')
-            .'</style>', 1, true, false);
-
-        $this->SetFont('Arial', 'B', 11);
-        $this->cMargin = 0;
-        $this->SetFont('Arial', 'B', 11);
-        $this->SetTextColor(10, 86, 142);
-        $this->WriteCell($w, 7,sprintf(__('Ticket #%s'),$ticket->getNumber()), 0, 0, 'L');
-        $this->Ln(7);
-        $this->cMargin = 3;
-        $this->SetTextColor(0);
-        $this->SetDrawColor(220, 220, 220);
-        $this->SetFillColor(244, 250, 255);
-        $this->SetX($this->lMargin);
-        $this->SetFont('Arial', 'B', 11);
-        $this->WriteCell($l, 7, __('Status'), 1, 0, 'L', true);
-        $this->SetFont('');
-        $this->WriteCell($c, 7, (string)$ticket->getStatus(), 1, 0, 'L', true);
-        $this->SetFont('Arial', 'B', 11);
-        $this->WriteCell($l, 7, __('Name'), 1, 0, 'L', true);
-        $this->SetFont('');
-        $this->WriteCell($c, 7, (string)$ticket->getName(), 1, 1, 'L', true);
-        $this->SetFont('Arial', 'B', 11);
-        $this->WriteCell($l, 7, __('Priority'), 1, 0, 'L', true);
-        $this->SetFont('');
-        $this->WriteCell($c, 7, $ticket->getPriority(), 1, 0, 'L', true);
-        $this->SetFont('Arial', 'B', 11);
-        $this->WriteCell($l, 7, __('Email'), 1, 0, 'L', true);
-        $this->SetFont('');
-        $this->WriteCell($c, 7, $ticket->getEmail(), 1, 1, 'L', true);
-        $this->SetFont('Arial', 'B', 11);
-        $this->WriteCell($l, 7, __('Department'), 1, 0, 'L', true);
-        $this->SetFont('');
-        $this->WriteCell($c, 7, $ticket->getDeptName(), 1, 0, 'L', true);
-        $this->SetFont('Arial', 'B', 11);
-        $this->WriteCell($l, 7, __('Phone'), 1, 0, 'L', true);
-        $this->SetFont('');
-        $this->WriteCell($c, 7, $ticket->getPhoneNumber(), 1, 1, 'L', true);
-        $this->SetFont('Arial', 'B', 11);
-        $this->WriteCell($l, 7, __('Create Date'), 1, 0, 'L', true);
-        $this->SetFont('');
-        $this->WriteCell($c, 7, Format::db_datetime($ticket->getCreateDate()), 1, 0, 'L', true);
-        $this->SetFont('Arial', 'B', 11);
-        $this->WriteCell($l, 7, __('Source'), 1, 0, 'L', true);
-        $this->SetFont('');
-        $source = ucfirst($ticket->getSource());
-        if($ticket->getIP())
-            $source.='  ('.$ticket->getIP().')';
-        $this->WriteCell($c, 7, $source, 1, 0, 'L', true);
-        $this->Ln(12);
-
-        $this->SetFont('Arial', 'B', 11);
-        if($ticket->isOpen()) {
-            $this->WriteCell($l, 7, __('Assigned To'), 1, 0, 'L', true);
-            $this->SetFont('');
-            $this->WriteCell($c, 7, $ticket->isAssigned()?$ticket->getAssigned():' -- ', 1, 0, 'L', true);
-        } else {
-
-            $closedby = __('unknown');
-            if(($staff = $ticket->getStaff()))
-                $closedby = (string) $staff->getName();
-
-            $this->WriteCell($l, 7, __('Closed By'), 1, 0, 'L', true);
-            $this->SetFont('');
-            $this->WriteCell($c, 7, $closedby, 1, 0, 'L', true);
-        }
+        ob_start();
+        if ($thisstaff)
+            include STAFFINC_DIR.'templates/ticket-print.tmpl.php';
+        elseif ($thisclient)
+            include CLIENTINC_DIR.'templates/ticket-print.tmpl.php';
+        else
+            return;
+        $html = ob_get_clean();
 
-        $this->SetFont('Arial', 'B', 11);
-        $this->WriteCell($l, 7, __('Help Topic'), 1, 0, 'L', true);
-        $this->SetFont('');
-        $this->WriteCell($c, 7, $ticket->getHelpTopic(), 1, 1, 'L', true);
-        $this->SetFont('Arial', 'B', 11);
-        $this->WriteCell($l, 7, __('SLA Plan'), 1, 0, 'L', true);
-        $this->SetFont('');
-        $sla = $ticket->getSLA();
-        $this->WriteCell($c, 7, $sla?$sla->getName():' -- ', 1, 0, 'L', true);
-        $this->SetFont('Arial', 'B', 11);
-        $this->WriteCell($l, 7, __('Last Response'), 1, 0, 'L', true);
-        $this->SetFont('');
-        $this->WriteCell($c, 7, Format::db_datetime($ticket->getLastRespDate()), 1, 1, 'L', true);
-        $this->SetFont('Arial', 'B', 11);
-        if($ticket->isOpen()) {
-            $this->WriteCell($l, 7, __('Due Date'), 1, 0, 'L', true);
-            $this->SetFont('');
-            $this->WriteCell($c, 7, Format::db_datetime($ticket->getEstDueDate()), 1, 0, 'L', true);
-        } else {
-            $this->WriteCell($l, 7, __('Close Date'), 1, 0, 'L', true);
-            $this->SetFont('');
-            $this->WriteCell($c, 7, Format::db_datetime($ticket->getCloseDate()), 1, 0, 'L', true);
-        }
+        $this->WriteHtml($html, 0, true, true);
+    }
+}
 
-        $this->SetFont('Arial', 'B', 11);
-        $this->WriteCell($l, 7, __('Last Message'), 1, 0, 'L', true);
-        $this->SetFont('');
-        $this->WriteCell($c, 7, Format::db_datetime($ticket->getLastMsgDate()), 1, 1, 'L', true);
 
-        $this->SetFillColor(255, 255, 255);
-        foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) {
-            $idx = 0;
-            foreach ($form->getAnswers() as $a) {
-                if (in_array($a->getField()->get('name'),
-                            array('email','name','subject','phone','priority')))
-                    continue;
-                $this->SetFont('Arial', 'B', 11);
-                if ($idx++ === 0) {
-                    $this->Ln(5);
-                    $this->SetFillColor(244, 250, 255);
-                    $this->WriteCell(($l+$c)*2, 7, $a->getForm()->get('title'),
-                        1, 0, 'L', true);
-                    $this->SetFillColor(255, 255, 255);
-                }
-                if ($val = $a->toString()) {
-                    $this->Ln(7);
-                    $this->WriteCell($l*2, 7, $a->getField()->get('label'), 1, 0, 'L', true);
-                    $this->SetFont('');
-                    $this->WriteCell($c*2, 7, $val, 1, 0, 'L', true);
-                }
-            }
-        }
-        $this->SetFillColor(244, 250, 255);
-        $this->Ln(10);
+// Task print
+class Task2PDF extends mPDFWithLocalImages {
 
-        $this->SetFont('Arial', 'B', 11);
-        $this->cMargin = 0;
-        $this->SetTextColor(10, 86, 142);
-        $this->WriteCell($w, 7,trim($ticket->getSubject()), 0, 0, 'L');
-        $this->Ln(7);
-        $this->SetTextColor(0);
-        $this->cMargin = 3;
+    var $options = array();
+    var $task = null;
 
-        //Table header colors (RGB)
-        $colors = array('M'=>array(195, 217, 255),
-                        'R'=>array(255, 224, 179),
-                        'N'=>array(250, 250, 210));
-        //Get ticket thread
-        $types = array('M', 'R');
-        if($this->includenotes)
-            $types[] = 'N';
+    function __construct($task, $options=array()) {
 
-        if(($entries = $ticket->getThreadEntries($types))) {
-            foreach($entries as $entry) {
+        $this->task = $task;
+        $this->options = $options;
 
-                $color = $colors[$entry['thread_type']];
+        parent::__construct('', $this->options['psize']);
+        $this->_print();
+    }
 
-                $this->SetFillColor($color[0], $color[1], $color[2]);
-                $this->SetFont('Arial', 'B', 11);
-                $this->WriteCell($w/2, 7, Format::db_datetime($entry['created']), 'LTB', 0, 'L', true);
-                $this->SetFont('Arial', '', 10);
-                $this->WriteCell($w, 7, Format::truncate($entry['title'], 50), 'TB', 0, 'L', true);
-                $this->WriteCell($w/2, 7, $entry['name'] ?: $entry['poster'], 'TBR', 1, 'L', true);
-                $this->SetFont('');
-                $text = $entry['body']->display('pdf');
-                if($entry['attachments']
-                        && ($tentry=$ticket->getThreadEntry($entry['id']))
-                        && ($attachments = $tentry->getAttachments())) {
-                    $files = array();
-                    foreach($attachments as $attachment)
-                        if (!$attachment['inline'])
-                            $files[]= $attachment['name'];
+    function _print() {
+        global $thisstaff, $cfg;
 
-                    if ($files)
-                        $text.="<div>Files Attached: [".implode(', ',$files)."]</div>";
-                        $text.="<div>".sprintf(__('Files Attached: [%s]'),implode(', ',$files))."</div>";
-                }
-                $this->WriteHtml('<div class="thread-body">'.$text.'</div>', 2, false, false);
-                $this->Ln(5);
-            }
-        }
+        if (!($task=$this->task) || !$thisstaff)
+            return;
 
-        $this->WriteHtml('', 2, false, true);
+        ob_start();
+        include STAFFINC_DIR.'templates/task-print.tmpl.php';
+        $html = ob_get_clean();
+        $this->WriteHtml($html, 0, true, true);
 
     }
 }
+
 ?>
diff --git a/include/class.plugin.php b/include/class.plugin.php
index dd9b5bceb1d0d7cb71104766654296c0e01c6d80..9d15bd3595a97adac8c6f0c67d45152ea8cb159b 100644
--- a/include/class.plugin.php
+++ b/include/class.plugin.php
@@ -8,10 +8,10 @@ class PluginConfig extends Config {
     function __construct($name) {
         // Use parent constructor to place configurable information into the
         // central config table in a namespace of "plugin.<id>"
-        parent::Config("plugin.$name");
+        parent::__construct("plugin.$name");
         foreach ($this->getOptions() as $name => $field) {
             if ($this->exists($name))
-                $this->config[$name]['value'] = $field->to_php($this->get($name));
+                $this->config[$name]->value = $field->to_php($this->get($name));
             elseif ($default = $field->get('default'))
                 $this->defaults[$name] = $default;
         }
@@ -32,7 +32,7 @@ class PluginConfig extends Config {
      */
     function getForm() {
         if (!isset($this->form)) {
-            $this->form = new Form($this->getOptions());
+            $this->form = new SimpleForm($this->getOptions());
             if ($_SERVER['REQUEST_METHOD'] != 'POST')
                 $this->form->data($this->getInfo());
         }
@@ -170,15 +170,23 @@ class PluginManager {
             //      read all plugins
             $info = static::getInfoForPath(
                 INCLUDE_DIR . $ht['install_path'], $ht['isphar']);
+
             list($path, $class) = explode(':', $info['plugin']);
             if (!$class)
                 $class = $path;
             elseif ($ht['isphar'])
-                require_once('phar://' . INCLUDE_DIR . $ht['install_path']
+                @include_once('phar://' . INCLUDE_DIR . $ht['install_path']
                     . '/' . $path);
             else
-                require_once(INCLUDE_DIR . $ht['install_path']
+                @include_once(INCLUDE_DIR . $ht['install_path']
                     . '/' . $path);
+
+            if (!class_exists($class)) {
+                $class = 'DefunctPlugin';
+                $ht['isactive'] = false;
+                $info = array('name' => $ht['name'] . ' '. __('(defunct — missing)'));
+            }
+
             if ($ht['isactive']) {
                 static::$plugin_list[$ht['install_path']]
                     = new $class($ht['id']);
@@ -277,7 +285,9 @@ class PluginManager {
         if (!isset(static::$plugin_info[$install_path])) {
             // plugin.php is require to return an array of informaiton about
             // the plugin.
-            $info = array_merge($defaults, (include $path . '/plugin.php'));
+            if (!file_exists($path . '/plugin.php'))
+                return false;
+            $info = array_merge($defaults, (@include $path . '/plugin.php'));
             $info['install_path'] = $install_path;
 
             // XXX: Ensure 'id' key isset
@@ -290,11 +300,14 @@ class PluginManager {
         static $instances = array();
         if (!isset($instances[$path])
                 && ($ps = static::allInstalled())
-                && ($ht = $ps[$path])
-                && ($info = static::getInfoForPath($path))) {
+                && ($ht = $ps[$path])) {
+
+            $info = static::getInfoForPath($path);
+
             // $ht may be the plugin instance
             if ($ht instanceof Plugin)
                 return $ht;
+
             // Usually this happens when the plugin is being enabled
             list($path, $class) = explode(':', $info['plugin']);
             if (!$class)
@@ -357,7 +370,7 @@ abstract class Plugin {
 
     static $verify_domain = 'updates.osticket.com';
 
-    function Plugin($id) {
+    function __construct($id) {
         $this->id = $id;
         $this->load();
     }
@@ -408,7 +421,9 @@ abstract class Plugin {
         if (!db_query($sql) || !db_affected_rows())
             return false;
 
-        $this->getConfig()->purge();
+        if ($config = $this->getConfig())
+            $config->purge();
+
         return true;
     }
 
@@ -681,4 +696,11 @@ abstract class Plugin {
     }
 }
 
+class DefunctPlugin extends Plugin {
+    function bootstrap() {}
+
+    function enable() {
+        return false;
+    }
+}
 ?>
diff --git a/include/class.priority.php b/include/class.priority.php
index 63a7434c479f88075ec1170d2a6410768891a0ef..8721ad81182028b942d7bc565ca71458f7bd3620 100644
--- a/include/class.priority.php
+++ b/include/class.priority.php
@@ -14,55 +14,45 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-class Priority {
+class Priority extends VerySimpleModel
+implements TemplateVariable {
 
-    var $id;
-    var $ht;
-
-    function Priority($id){
-
-        $this->id =0;
-        $this->load($id);
-    }
-
-    function load($id) {
-        if(!$id && !($id=$this->getId()))
-            return false;
-
-
-        $sql='SELECT *  FROM '.PRIORITY_TABLE
-            .' WHERE priority_id='.db_input($id);
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
-
-        $this->ht= db_fetch_array($res);
-        $this->id= $this->ht['priority_id'];
-
-        return true;;
-    }
+    static $meta = array(
+        'table' => PRIORITY_TABLE,
+        'pk' => array('priority_id'),
+        'ordering' => array('-priority_urgency')
+    );
 
     function getId() {
-        return $this->id;
+        return $this->priority_id;
     }
 
     function getTag() {
-        return $this->ht['priority'];
+        return $this->priority;
     }
 
     function getDesc() {
-        return $this->ht['priority_desc'];
+        return $this->priority_desc;
     }
 
     function getColor() {
-        return $this->ht['priority_color'];
+        return $this->priority_color;
     }
 
     function getUrgency() {
-        return $this->ht['priority_urgency'];
+        return $this->priority_urgency;
     }
 
     function isPublic() {
-        return ($this->ht['ispublic']);
+        return $this->ispublic;
+    }
+
+    // TemplateVariable interface
+    function asVar() { return $this->getDesc(); }
+    static function getVarScope() {
+        return array(
+            'desc' => __('Priority Level'),
+        );
     }
 
     function __toString() {
@@ -70,20 +60,15 @@ class Priority {
     }
 
     /* ------------- Static ---------------*/
-    function lookup($id) {
-        return ($id && is_numeric($id) && ($p=new Priority($id)) && $p->getId()==$id)?$p:null;
-    }
-
-    function getPriorities( $publicOnly=false) {
-
+    static function getPriorities( $publicOnly=false) {
         $priorities=array();
-        $sql ='SELECT priority_id, priority_desc FROM '.PRIORITY_TABLE;
-        if($publicOnly)
-            $sql.=' WHERE ispublic=1';
 
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while(list($id, $name)=db_fetch_row($res))
-                $priorities[$id] = $name;
+        $objects = static::objects()->values_flat('priority_id', 'priority_desc');
+        if ($publicOnly)
+            $objects->filter(array('ispublic'=>1));
+
+        foreach ($objects as $row) {
+            $priorities[$row[0]] = $row[1];
         }
 
         return $priorities;
diff --git a/include/class.report.php b/include/class.report.php
new file mode 100644
index 0000000000000000000000000000000000000000..d65fd1cc5a63fac47ff221763c104f148fc71c5d
--- /dev/null
+++ b/include/class.report.php
@@ -0,0 +1,251 @@
+<?php
+
+class ReportModel {
+
+    const PERM_AGENTS = 'stats.agents';
+
+    static protected $perms = array(
+            self::PERM_AGENTS => array(
+                'title' =>
+                /* @trans */ 'Stats',
+                'desc'  =>
+                /* @trans */ 'Ability to view stats of other agents in allowed departments',
+                'primary' => true,
+            ));
+
+    static function getPermissions() {
+        return self::$perms;
+    }
+}
+
+RolePermission::register(/* @trans */ 'Miscellaneous', ReportModel::getPermissions());
+
+class OverviewReport {
+    var $start;
+    var $end;
+
+    var $format;
+
+    function __construct($start, $end='now', $format=null) {
+        global $cfg;
+
+        $this->start = $start;
+        $this->end = $end;
+        $this->format = $format ?: $cfg->getDateFormat(true);
+    }
+
+
+    function getStartDate($format=null, $translate=true) {
+
+        if (!$this->start)
+            return '';
+
+        $format =  $format ?: $this->format;
+        if ($translate) {
+            $format = str_replace(
+                    array('y', 'Y', 'm'),
+                    array('yy', 'yyyy', 'mm'),
+                    $format);
+        }
+
+        return Format::date(Misc::dbtime($this->start), false, $format);
+    }
+
+
+    function getDateRange() {
+        global $cfg;
+
+        $start = $this->start ?: 'last month';
+        $stop = $this->end ?: 'now';
+
+        // Convert user time to db time
+        $start = Misc::dbtime($start);
+        // Stop time can be relative.
+        if ($stop[0] == '+') {
+            // $start time + time(X days)
+            $now = time();
+            $stop = $start + (strtotime($stop, $now)-$now);
+        } else {
+            $stop = Misc::dbtime($stop);
+        }
+
+        $start = 'FROM_UNIXTIME('.$start.')';
+        $stop = 'FROM_UNIXTIME('.$stop.')';
+
+        return array($start, $stop);
+    }
+
+    function getPlotData() {
+        list($start, $stop) = $this->getDateRange();
+
+        # Fetch all types of events over the timeframe
+        $res = db_query('SELECT DISTINCT(state) FROM '.THREAD_EVENT_TABLE
+            .' WHERE timestamp BETWEEN '.$start.' AND '.$stop
+            .' AND state IN ("created", "closed", "reopened", "assigned", "overdue", "transferred")'
+            .' ORDER BY 1');
+        $events = array();
+        while ($row = db_fetch_row($res)) $events[] = $row[0];
+
+        # TODO: Handle user => db timezone offset
+        # XXX: Implement annulled column from the %ticket_event table
+        $res = db_query('SELECT state, DATE_FORMAT(timestamp, \'%Y-%m-%d\'), '
+                .'COUNT(DISTINCT T.id)'
+            .' FROM '.THREAD_EVENT_TABLE. ' E '
+            .' JOIN '.THREAD_TABLE. ' T
+                ON (T.id = E.thread_id AND T.object_type = "T") '
+            .' WHERE E.timestamp BETWEEN '.$start.' AND '.$stop
+            .' AND NOT annulled'
+            .' AND E.state IN ("created", "closed", "reopened", "assigned", "overdue", "transferred")'
+            .' GROUP BY E.state, DATE_FORMAT(E.timestamp, \'%Y-%m-%d\')'
+            .' ORDER BY 2, 1');
+        # Initialize array of plot values
+        $plots = array();
+        foreach ($events as $e) { $plots[$e] = array(); }
+
+        $time = null; $times = array();
+        # Iterate over result set, adding zeros for missing ticket events
+        $slots = array();
+        while ($row = db_fetch_row($res)) {
+            $row_time = strtotime($row[1]);
+            if ($time != $row_time) {
+                # New time (and not the first), figure out which events did
+                # not have any tickets associated for this time slot
+                if ($time !== null) {
+                    # Not the first record -- add zeros all the arrays that
+                    # did not have at least one entry for the timeframe
+                    foreach (array_diff($events, $slots) as $slot)
+                        $plots[$slot][] = 0;
+                }
+                $slots = array();
+                $times[] = $time = $row_time;
+            }
+            # Keep track of states for this timeframe
+            $slots[] = $row[0];
+            $plots[$row[0]][] = (int)$row[2];
+        }
+        foreach (array_diff($events, $slots) as $slot)
+            $plots[$slot][] = 0;
+
+        return array("times" => $times, "plots" => $plots, "events" => $events);
+    }
+
+    function enumTabularGroups() {
+        return array("dept"=>__("Department"), "topic"=>__("Topics"),
+            # XXX: This will be relative to permissions based on the
+            # logged-in-staff. For basic staff, this will be 'My Stats'
+            "staff"=>__("Agent"));
+    }
+
+    function getTabularData($group='dept') {
+        global $thisstaff;
+
+        list($start, $stop) = $this->getDateRange();
+        $times = Ticket::objects()
+            ->constrain(array(
+                'thread__entries' => array(
+                    'thread__entries__type' => 'R'
+                ),
+            ))
+            ->aggregate(array(
+                'ServiceTime' => SqlAggregate::AVG(SqlFunction::DATEDIFF(
+                    new SqlField('closed'), new SqlField('created')
+                )),
+                'ResponseTime' => SqlAggregate::AVG(SqlFunction::DATEDIFF(
+                    new SqlField('thread__entries__created'), new SqlField('thread__entries__parent__created')
+                )),
+            ));
+
+        $stats = Ticket::objects()
+            ->constrain(array(
+                'thread__events' => array(
+                    'thread__events__annulled' => 0,
+                    'thread__events__timestamp__range' => array($start, $stop),
+                ),
+            ))
+            ->aggregate(array(
+                'Opened' => SqlAggregate::COUNT(
+                    SqlCase::N()
+                        ->when(new Q(array('thread__events__state' => 'created')), 1)
+                ),
+                'Assigned' => SqlAggregate::COUNT(
+                    SqlCase::N()
+                        ->when(new Q(array('thread__events__state' => 'assigned')), 1)
+                ),
+                'Overdue' => SqlAggregate::COUNT(
+                    SqlCase::N()
+                        ->when(new Q(array('thread__events__state' => 'overdue')), 1)
+                ),
+                'Closed' => SqlAggregate::COUNT(
+                    SqlCase::N()
+                        ->when(new Q(array('thread__events__state' => 'closed')), 1)
+                ),
+                'Reopened' => SqlAggregate::COUNT(
+                    SqlCase::N()
+                        ->when(new Q(array('thread__events__state' => 'reopened')), 1)
+                ),
+            ));
+
+        switch ($group) {
+        case 'dept':
+            $headers = array(__('Department'));
+            $header = function($row) { return Dept::getLocalNameById($row['dept_id'], $row['dept__name']); };
+            $pk = 'dept_id';
+            $stats = $stats
+                ->filter(array('dept_id__in' => $thisstaff->getDepts()))
+                ->values('dept__id', 'dept__name');
+            $times = $times
+                ->filter(array('dept_id__in' => $thisstaff->getDepts()))
+                ->values('dept__id');
+            break;
+        case 'topic':
+            $headers = array(__('Help Topic'));
+            $header = function($row) { return Topic::getLocalNameById($row['topic_id'], $row['topic__topic']); };
+            $pk = 'topic_id';
+            $stats = $stats
+                ->values('topic_id', 'topic__topic')
+                ->filter(array('topic_id__gt' => 0));
+            $times = $times
+                ->values('topic_id')
+                ->filter(array('topic_id__gt' => 0));
+            break;
+        case 'staff':
+            $headers = array(__('Agent'));
+            $header = function($row) { return new AgentsName(array(
+                'first' => $row['staff__firstname'], 'last' => $row['staff__lastname'])); };
+            $pk = 'staff_id';
+            $stats = $stats->values('staff_id', 'staff__firstname', 'staff__lastname');
+            $times = $times->values('staff_id');
+            $depts = $thisstaff->getManagedDepartments();
+            if ($thisstaff->hasPerm(ReportModel::PERM_AGENTS))
+                $depts = array_merge($depts, $thisstaff->getDepts());
+            $Q = Q::any(array(
+                'staff_id' => $thisstaff->getId(),
+            ));
+            if ($depts)
+                $Q->add(array('dept_id__in' => $depts));
+            $stats = $stats->filter(array('staff_id__gt'=>0))->filter($Q);
+            $times = $times->filter(array('staff_id__gt'=>0))->filter($Q);
+            break;
+        default:
+            # XXX: Die if $group not in $groups
+        }
+
+        $timings = array();
+        foreach ($times as $T) {
+            $timings[$T[$pk]] = $T;
+        }
+
+        $rows = array();
+        foreach ($stats as $R) {
+            $T = $timings[$R[$pk]];
+            $rows[] = array($header($R), $R['Opened'], $R['Assigned'],
+                $R['Overdue'], $R['Closed'], $R['Reopened'],
+                number_format($T['ServiceTime'], 1),
+                number_format($T['ResponseTime'], 1));
+        }
+        return array("columns" => array_merge($headers,
+                        array(__('Opened'),__('Assigned'),__('Overdue'),__('Closed'),__('Reopened'),
+                              __('Service Time'),__('Response Time'))),
+                     "data" => $rows);
+    }
+}
diff --git a/include/class.role.php b/include/class.role.php
new file mode 100644
index 0000000000000000000000000000000000000000..f584cfa430af8f7c25ba990cf302d601a3439dcf
--- /dev/null
+++ b/include/class.role.php
@@ -0,0 +1,389 @@
+<?php
+/*********************************************************************
+    class.role.php
+
+    Role-based access
+
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  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:
+**********************************************************************/
+
+class RoleModel extends VerySimpleModel {
+    static $meta = array(
+        'table' => ROLE_TABLE,
+        'pk' => array('id'),
+        'joins' => array(
+            'extensions' => array(
+                'null' => true,
+                'list' => true,
+                'reverse' => 'StaffDeptAccess.role',
+            ),
+            'agents' => array(
+                'reverse' => 'Staff.role',
+            ),
+        ),
+    );
+
+    // Flags
+    const FLAG_ENABLED   = 0x0001;
+
+    protected function hasFlag($flag) {
+        return ($this->get('flags') & $flag) !== 0;
+    }
+
+    protected function clearFlag($flag) {
+        return $this->set('flags', $this->get('flags') & ~$flag);
+    }
+
+    protected function setFlag($flag) {
+        return $this->set('flags', $this->get('flags') | $flag);
+    }
+
+    function getId() {
+        return $this->id;
+    }
+
+    function getName() {
+        return $this->name;
+    }
+
+    function getCreateDate() {
+        return $this->created;
+    }
+
+    function getUpdateDate() {
+        return $this->updated;
+    }
+
+    function getInfo() {
+        return $this->ht;
+    }
+
+    function isEnabled() {
+        return $this->hasFlag(self::FLAG_ENABLED);
+    }
+
+    function isDeleteable() {
+        return $this->extensions->count() + $this->agents->count() == 0;
+    }
+
+}
+
+class Role extends RoleModel {
+    var $form;
+    var $entry;
+
+    var $_perm;
+
+    function hasPerm($perm) {
+        return $this->getPermission()->has($perm);
+    }
+
+    function getPermission() {
+        if (!$this->_perm) {
+            $this->_perm = new RolePermission(
+                isset($this->permissions) ? $this->permissions : array()
+            );
+        }
+        return $this->_perm;
+    }
+
+    function getPermissionInfo() {
+        return $this->getPermission()->getInfo();
+    }
+
+    function getTranslateTag($subtag) {
+        return _H(sprintf('role.%s.%s', $subtag, $this->getId()));
+    }
+    function getLocal($subtag) {
+        $tag = $this->getTranslateTag($subtag);
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : $this->ht[$subtag];
+    }
+
+    function to_json() {
+
+        $info = array(
+                'id'    => $this->getId(),
+                'name'  => $this->getName()
+                );
+
+        return JsonDataEncoder::encode($info);
+    }
+
+    function __toString() {
+        return (string) $this->getName();
+    }
+
+    function __call($what, $args) {
+        $rv = null;
+        if($this->getPermission() && is_callable(array($this->_perm, $what)))
+            $rv = $args
+                ? call_user_func_array(array($this->_perm, $what), $args)
+                : call_user_func(array($this->_perm, $what));
+
+        return $rv;
+    }
+
+    private function updatePerms($vars, &$errors=array()) {
+
+        $config = array();
+        $permissions = $this->getPermission();
+        foreach (RolePermission::allPermissions() as $g => $perms) {
+            foreach($perms as $k => $v) {
+                $permissions->set($k, in_array($k, $vars) ? 1 : 0);
+            }
+        }
+        $this->permissions = $permissions->toJson();
+    }
+
+    function update($vars, &$errors) {
+
+        if (!$vars['name'])
+            $errors['name'] = __('Name required');
+        elseif (($r=Role::lookup(array('name'=>$vars['name'])))
+                && $r->getId() != $vars['id'])
+            $errors['name'] = __('Name already in-use');
+        elseif (!$vars['perms'] || !count($vars['perms']))
+            $errors['err'] = __('Must check at least one permission for the role');
+
+        if ($errors)
+            return false;
+
+        $this->name = $vars['name'];
+        $this->notes = $vars['notes'];
+
+        $this->updatePerms($vars['perms'], $errors);
+
+        if (!$this->save(true))
+            return false;
+
+        return true;
+    }
+
+    function save($refetch=false) {
+        if (count($this->dirty))
+            $this->set('updated', new SqlFunction('NOW'));
+        if (isset($this->dirty['notes']))
+            $this->notes = Format::sanitize($this->notes);
+
+        return parent::save($refetch | $this->dirty);
+    }
+
+    function delete() {
+
+        if (!$this->isDeleteable())
+            return false;
+
+        if (!parent::delete())
+            return false;
+
+        // Remove dept access entries
+        StaffDeptAccess::objects()
+            ->filter(array('role_id'=>$this->getId()))
+            ->update(array('role_id' => 0));
+
+        return true;
+    }
+
+    static function create($vars=false) {
+        $role = new static($vars);
+        $role->created = SqlFunction::NOW();
+        return $role;
+    }
+
+    static function __create($vars, &$errors) {
+        $role = self::create($vars);
+        if ($vars['permissions'])
+            $role->updatePerms($vars['permissions']);
+
+        $role->save();
+        return $role;
+    }
+
+    static function getRoles($criteria=null, $localize=true) {
+        static $roles = null;
+
+        if (!isset($roles) || $criteria) {
+
+            $filters = array();
+            if (isset($criteria['enabled'])) {
+                $q = new Q(array('flags__hasbit' => self::FLAG_ENABLED));
+                if (!$criteria['enabled'])
+                    $q->negate();
+                $filters[] = $q;
+            }
+
+            $query = self::objects()
+                ->order_by('name')
+                ->values_flat('id', 'name');
+
+            if ($filters)
+                $query->filter($filters);
+
+            $localize_this = function($id, $default) use ($localize) {
+                if (!$localize)
+                    return $default;
+                $tag = _H("role.name.{$id}");
+                $T = CustomDataTranslation::translate($tag);
+                return $T != $tag ? $T : $default;
+            };
+
+            $names = array();
+            foreach ($query as $row)
+                $names[$row[0]] = $localize_this($row[0], $row[1]);
+
+            if ($criteria || !$localize)
+                return $names;
+
+            $roles = $names;
+        }
+
+        return $roles;
+    }
+
+    static function getActiveRoles() {
+        static $roles = null;
+
+        if (!isset($roles))
+            $roles = self::getRoles(array('enabled' => true));
+
+        return $roles;
+    }
+}
+
+
+class RolePermission {
+
+    // Predefined groups are for sort order.
+    // New groups will be appended to the bottom
+    static protected $_permissions = array(
+            /* @trans */ 'Tickets' => array(),
+            /* @trans */ 'Tasks' => array(),
+            /* @trans */ 'Users' => array(),
+            /* @trans */ 'Organizations' => array(),
+            /* @trans */ 'Knowledgebase' => array(),
+            /* @trans */ 'Miscellaneous' => array(),
+            );
+
+    var $perms;
+
+
+    function __construct($perms) {
+        $this->perms = $perms;
+        if (is_string($this->perms))
+            $this->perms = JsonDataParser::parse($this->perms);
+        elseif (!$this->perms)
+            $this->perms = array();
+    }
+
+    function has($perm) {
+        return (bool) $this->get($perm);
+    }
+
+    function get($perm) {
+        return @$this->perms[$perm];
+    }
+
+    function set($perm, $value) {
+        if (!$value)
+            unset($this->perms[$perm]);
+        else
+            $this->perms[$perm] = $value;
+    }
+
+    function toJson() {
+        return JsonDataEncoder::encode($this->perms);
+    }
+
+    function getInfo() {
+        return $this->perms;
+    }
+
+    function merge($perms) {
+        if ($perms instanceof self)
+            $perms = $perms->getInfo();
+        foreach ($perms as $perm=>$value) {
+            if (is_numeric($perm)) {
+                // Array of perm names
+                $perm = $value;
+                $value = true;
+            }
+            $this->set($perm, $value);
+        }
+    }
+
+    static function allPermissions() {
+        return static::$_permissions;
+    }
+
+    static function register($group, $perms, $prepend=false) {
+        if ($prepend) {
+            static::$_permissions[$group] = array_merge(
+                $perms, static::$_permissions[$group] ?: array());
+        }
+        else {
+            static::$_permissions[$group] = array_merge(
+                static::$_permissions[$group] ?: array(), $perms);
+        }
+    }
+}
+
+class RoleQuickAddForm
+extends AbstractForm {
+    function buildFields() {
+        $permissions = array();
+        foreach (RolePermission::allPermissions() as $g => $perms) {
+            foreach ($perms as $k => $v) {
+                if ($v['primary'])
+                    continue;
+                $permissions[$g][$k] = "{$v['title']} — {$v['desc']}";
+            }
+        }
+        return array(
+            'name' => new TextboxField(array(
+                'required' => true,
+                'configuration' => array(
+                    'placeholder' => __('Name'),
+                    'classes' => 'span12',
+                    'autofocus' => true,
+                    'length' => 128,
+                ),
+            )),
+            'clone' => new ChoiceField(array(
+                'default' => 0,
+                'choices' =>
+                    array(0 => '— '.__('Clone an existing role').' —')
+                    + Role::getRoles(),
+                'configuration' => array(
+                    'classes' => 'span12',
+                ),
+            )),
+            'perms' => new ChoiceField(array(
+                'choices' => $permissions,
+                'widget' => 'TabbedBoxChoicesWidget',
+                'configuration' => array(
+                    'multiple' => true,
+                    'classes' => 'vertical-pad',
+                ),
+            )),
+        );
+    }
+
+    function getClean() {
+        $clean = parent::getClean();
+        // Index permissions as ['ticket.edit' => 1]
+        $clean['perms'] = array_keys($clean['perms']);
+        return $clean;
+    }
+
+    function render($staff=true, $title=false, $options=array()) {
+        return parent::render($staff, $title, $options + array('template' => 'dynamic-form-simple.tmpl.php'));
+    }
+}
diff --git a/include/class.search.php b/include/class.search.php
index 903486ba90ed82b673f70312ab1309fff09e2634..25617b27bf401995c9bd14c5afda2ba2d36e346f 100644
--- a/include/class.search.php
+++ b/include/class.search.php
@@ -30,8 +30,18 @@ abstract class SearchBackend {
     const SORT_RECENT = 2;
     const SORT_OLDEST = 3;
 
+    const PERM_EVERYTHING = 'search.all';
+
+    static protected $perms = array(
+        self::PERM_EVERYTHING => array(
+            'title' => /* @trans */ 'Search',
+            'desc'  => /* @trans */ 'See all tickets in search results, regardless of access',
+            'primary' => true,
+        ),
+    );
+
     abstract function update($model, $id, $content, $new=false, $attrs=array());
-    abstract function find($query, $criteria, $model=false, $sort=array());
+    abstract function find($query, QuerySet $criteria, $addRelevance=true);
 
     static function register($backend=false) {
         $backend = $backend ?: get_called_class();
@@ -48,7 +58,12 @@ abstract class SearchBackend {
 
         return new self::$registry[$id]();
     }
+
+    static function getPermissions() {
+        return self::$perms;
+    }
 }
+RolePermission::register(/* @trans */ 'Miscellaneous', SearchBackend::getPermissions());
 
 // Register signals to intercept saving of various content throughout the
 // system
@@ -61,22 +76,25 @@ class SearchInterface {
         $this->bootstrap();
     }
 
-    function find($query, $criteria, $model=false, $sort=array()) {
+    function find($query, QuerySet $criteria, $addRelevance=true) {
         $query = Format::searchable($query);
-        return $this->backend->find($query, $criteria, $model, $sort);
+        return $this->backend->find($query, $criteria, $addRelevance);
     }
 
     function update($model, $id, $content, $new=false, $attrs=array()) {
-        if (!$this->backend)
-            return;
-
-        $this->backend->update($model, $id, $content, $new, $attrs);
+        if ($this->backend)
+            $this->backend->update($model, $id, $content, $new, $attrs);
     }
 
     function createModel($model) {
         return $this->updateModel($model, true);
     }
 
+    function deleteModel($model) {
+        if ($this->backend)
+            $this->backend->delete($model);
+    }
+
     function updateModel($model, $new=false) {
         // The MySQL backend does not need to index attributes of the
         // various models, because those other attributes are available in
@@ -89,10 +107,10 @@ class SearchInterface {
                 break;
 
             $this->update($model, $model->getId(),
-                $model->getBody()->getSearchable(), $new,
+                $model->getBody()->getSearchable(),
+                $new === true,
                 array(
                     'title' =>      $model->getTitle(),
-                    'ticket_id' =>  $model->getTicketId(),
                     'created' =>    $model->getCreateDate(),
                 )
             );
@@ -105,7 +123,7 @@ class SearchInterface {
                     $cdata[] = $v;
             $this->update($model, $model->getId(),
                 trim(implode("\n", $cdata)),
-                $new,
+                $new === true,
                 array(
                     'title'=>       Format::searchable($model->getSubject()),
                     'number'=>      $model->getNumber(),
@@ -132,7 +150,7 @@ class SearchInterface {
                         $cdata[] = $v;
             $this->update($model, $model->getId(),
                 trim(implode("\n", $cdata)),
-                $new,
+                $new === true,
                 array(
                     'title'=>       Format::searchable($model->getFullName()),
                     'emails'=>      $model->emails->asArray(),
@@ -150,7 +168,7 @@ class SearchInterface {
                         $cdata[] = $v;
             $this->update($model, $model->getId(),
                 trim(implode("\n", $cdata)),
-                $new,
+                $new === true,
                 array(
                     'title'=>       Format::searchable($model->getName()),
                     'created'=>     $model->getCreateDate(),
@@ -161,7 +179,7 @@ class SearchInterface {
         case $model instanceof FAQ:
             $this->update($model, $model->getId(),
                 $model->getSearchableAnswer(),
-                $new,
+                $new === true,
                 array(
                     'title'=>       Format::searchable($model->getQuestion()),
                     'keywords'=>    $model->getKeywords(),
@@ -196,9 +214,14 @@ class SearchInterface {
         // Tickets, which can be edited as well
         // Knowledgebase articles (FAQ and canned responses)
         // Users, organizations
-        Signal::connect('model.created', array($this, 'createModel'));
+        Signal::connect('threadentry.created', array($this, 'createModel'));
+        Signal::connect('ticket.created', array($this, 'createModel'));
+        Signal::connect('user.created', array($this, 'createModel'));
+        Signal::connect('organization.created', array($this, 'createModel'));
+        Signal::connect('model.created', array($this, 'createModel'), 'FAQ');
+
         Signal::connect('model.updated', array($this, 'updateModel'));
-        #Signal::connect('model.deleted', array($this, 'deleteModel'));
+        Signal::connect('model.deleted', array($this, 'deleteModel'));
     }
 }
 
@@ -207,7 +230,7 @@ class MySqlSearchConfig extends Config {
     var $table = CONFIG_TABLE;
 
     function __construct() {
-        parent::Config("mysqlsearch");
+        parent::__construct("mysqlsearch");
     }
 }
 
@@ -218,6 +241,7 @@ class MysqlSearchBackend extends SearchBackend {
     // Only index 20 batches per cron run
     var $max_batches = 60;
     var $_reindexed = 0;
+    var $SEARCH_TABLE;
 
     function __construct() {
         $this->SEARCH_TABLE = TABLE_PREFIX . '_search';
@@ -236,49 +260,53 @@ class MysqlSearchBackend extends SearchBackend {
     }
 
     function update($model, $id, $content, $new=false, $attrs=array()) {
-        switch (true) {
-        case $model instanceof ThreadEntry:
-            $type = 'H';
-            break;
-        case $model instanceof Ticket:
-            $attrs['title'] = $attrs['number'].' '.$attrs['title'];
-            $type = 'T';
-            break;
-        case $model instanceof User:
-            $content .= implode("\n", $attrs['emails']);
-            $type = 'U';
-            break;
-        case $model instanceof Organization:
-            $type = 'O';
-            break;
-        case $model instanceof FAQ:
-            $type = 'K';
-            break;
-        case $model instanceof AttachmentFile:
-            $type = 'F';
-            break;
-        default:
-            // Not indexed
+        if (!($type=ObjectModel::getType($model)))
             return;
-        }
+
+        if ($model instanceof Ticket)
+            $attrs['title'] = $attrs['number'].' '.$attrs['title'];
+        elseif ($model instanceof User)
+            $content .=' '.implode("\n", $attrs['emails']);
 
         $title = $attrs['title'] ?: '';
 
         if (!$content && !$title)
             return;
+        if (!$id)
+            return;
 
         $sql = 'REPLACE INTO '.$this->SEARCH_TABLE
             . ' SET object_type='.db_input($type)
             . ', object_id='.db_input($id)
             . ', content='.db_input($content)
             . ', title='.db_input($title);
-        return db_query($sql);
+        return db_query($sql, false);
+    }
+
+    function delete($model) {
+        switch (true) {
+        case $model instanceof Thread:
+            $sql = 'DELETE s.* FROM '.$this->SEARCH_TABLE
+                . " s JOIN ".THREAD_ENTRY_TABLE." h ON (h.id = s.object_id) "
+                . " WHERE s.object_type='H'"
+                . ' AND h.thread_id='.db_input($model->getId());
+            return db_query($sql);
+
+        default:
+            if (!($type = ObjectModel::getType($model)))
+                return;
+
+            $sql = 'DELETE FROM '.$this->SEARCH_TABLE
+                . ' WHERE object_type='.db_input($type)
+                . ' AND object_id='.db_input($model->getId());
+            return db_query($sql);
+        }
     }
 
     // Quote things like email addresses
     function quote($query) {
         $parts = array();
-        if (!preg_match_all('`([^\s"\']+)|"[^"]*"|\'[^\']*\'`', $query, $parts,
+        if (!preg_match_all('`(?:([^\s"\']+)|"[^"]*"|\'[^\']*\')(\s*)`', $query, $parts,
                 PREG_SET_ORDER))
             return $query;
 
@@ -291,155 +319,107 @@ class MysqlSearchBackend extends SearchBackend {
                 $char = strpos($m[1], '"') ? "'" : '"';
                 $m[0] = $char . $m[0] . $char;
             }
-            $results[] = $m[0];
+            $results[] = $m[0].$m[2];
         }
-        return implode(' ', $results);
+        return implode('', $results);
     }
 
-    function find($query, $criteria=array(), $model=false, $sort=array()) {
+    function find($query, QuerySet $criteria, $addRelevance=true) {
         global $thisstaff;
 
-        $mode = ' IN BOOLEAN MODE';
-        #if (count(explode(' ', $query)) == 1)
-        #    $mode = ' WITH QUERY EXPANSION';
-        $query = $this->quote($query);
-        $search = 'MATCH (search.title, search.content) AGAINST ('
-            .db_input($query)
-            .$mode.')';
-        $tables = array();
-        $P = TABLE_PREFIX;
-        $sort = '';
-
-        if ($query) {
-            $tables[] = "(
-                SELECT object_type, object_id, $search AS `relevance`
-                FROM `{$P}_search` `search`
-                WHERE $search
-            ) `search`";
-            $sort = 'ORDER BY `search`.`relevance`';
+        // MySQL usually doesn't handle words shorter than three letters
+        // (except with special configuration)
+        if (strlen($query) < 3)
+            return $criteria;
+
+        $criteria = clone $criteria;
+
+        $mode = ' IN NATURAL LANGUAGE MODE';
+
+        // According to the MySQL full text boolean mode, this grammar is
+        // assumed:
+        // see http://dev.mysql.com/doc/refman/5.6/en/fulltext-boolean.html
+        //
+        // PREOP    = [<>~+-]
+        // POSTOP   = [*]
+        // WORD     = [\w][\w-]*
+        // TERM     = PREOP? WORD POSTOP?
+        // QWORD    = " [^"]+ "
+        // PARENS   = \( { { TERM | QWORD } { \s+ { TERM | QWORD } }+ } \)
+        // EXPR     = { PREOP? PARENS | TERM | QWORD }
+        // BOOLEAN  = EXPR { \s+ EXPR }*
+        //
+        // Changing '{' for (?: and '}' for ')', collapsing whitespace, we
+        // have this regular expression
+        $BOOLEAN = '(?:[<>~+-]?\((?:(?:[<>~+-]?[\w][\w-]*[*]?|"[^"]+")(?:\s+(?:[<>~+-]?[\w][\w-]*[*]?|"[^"]+"))+)\)|[<>~+-]?[\w][\w-]*[*]?|"[^"]+")(?:\s+(?:[<>~+-]?\((?:(?:[<>~+-]?[\w][\w-]*[*]?|"[^"]+")(?:\s+(?:[<>~+-]?[\w][\w-]*[*]?|"[^"]+"))+)\)|[<>~+-]?[\w][\w-]*[*]?|"[^"]+"))*';
+
+        // Require the use of at least one operator and conform to the
+        // boolean mode grammar
+        if (preg_match('`(^|\s)["()<>~+-]`u', $query, $T = array())
+            && preg_match("`^{$BOOLEAN}$`u", $query, $T = array())
+        ) {
+            // If using boolean operators, search in boolean mode. This regex
+            // will ensure proper placement of operators, whitespace, and quotes
+            // in an effort to avoid crashing the query at MySQL
+            $query = $this->quote($query);
+            $mode = ' IN BOOLEAN MODE';
         }
+        #elseif (count(explode(' ', $query)) == 1)
+        #    $mode = ' WITH QUERY EXPANSION';
+        $search = 'MATCH (Z1.title, Z1.content) AGAINST ('.db_input($query).$mode.')';
 
-        switch ($model) {
+        switch ($criteria->model) {
         case false:
-        case 'Ticket':
-            $tables[] = "(select ticket_id as ticket_id from {$P}ticket
-            ) B1 ON (B1.ticket_id = search.object_id and search.object_type = 'T')";
-            $tables[] = "(select A2.id as thread_id, A1.ticket_id from {$P}ticket A1
-                join {$P}ticket_thread A2 on (A1.ticket_id = A2.ticket_id)
-            ) B2 ON (B2.thread_id = search.object_id and search.object_type = 'H')";
-            $tables[] = "(select A3.id as user_id, A1.ticket_id from {$P}user A3
-                join {$P}ticket A1 on (A1.user_id = A3.id)
-            ) B3 ON (B3.user_id = search.object_id and search.object_type = 'U')";
-            $tables[] = "(select A4.id as org_id, A1.ticket_id from {$P}organization A4
-                join {$P}user A3 on (A3.org_id = A4.id) join {$P}ticket A1 on (A1.user_id = A3.id)
-            ) B4 ON (B4.org_id = search.object_id and search.object_type = 'O')";
-            $key = 'COALESCE(B1.ticket_id, B2.ticket_id, B3.ticket_id, B4.ticket_id)';
-            $tables[] = "{$P}ticket A1 ON (A1.ticket_id = {$key})";
-            $tables[] = "{$P}ticket_status A2 ON (A1.status_id = A2.id)";
-            $cdata_search = false;
-            $where = array();
-
-            if ($criteria) {
-                foreach ($criteria as $name=>$value) {
-                    switch ($name) {
-                    case 'status_id':
-                        $where[] = 'A2.id = '.db_input($value);
-                        break;
-                    case 'state':
-                        $where[] = 'A2.state = '.db_input($value);
-                        break;
-                    case 'state__in':
-                        $where[] = 'A2.state IN ('.implode(',',db_input($value)).')';
-                        break;
-                    case 'topic_id':
-                    case 'staff_id':
-                    case 'team_id':
-                    case 'dept_id':
-                    case 'user_id':
-                    case 'isanswered':
-                    case 'isoverdue':
-                    case 'number':
-                        $where[] = sprintf('A1.%s = %s', $name, db_input($value));
-                        break;
-                    case 'created__gte':
-                        $where[] = sprintf('A1.created >= %s', db_input($value));
-                        break;
-                    case 'created__lte':
-                        $where[] = sprintf('A1.created <= %s', db_input($value));
-                        break;
-                    case 'email':
-                    case 'org_id':
-                    case 'form_id':
-                    default:
-                        if (strpos($name, 'cdata.') === 0) {
-                            // Search ticket CDATA table
-                            $cdata_search = true;
-                            $name = substr($name, 6);
-                            if (is_array($value)) {
-                                $where[] = '(' . implode(' OR ', array_map(
-                                    function($k) use ($name) {
-                                        return sprintf('FIND_IN_SET(%s, cdata.`%s`)',
-                                            db_input($k), $name);
-                                    }, $value)
-                                ) . ')';
-                            }
-                            else {
-                                $where[] = sprintf("cdata.%s = %s", $name, db_input($value));
-                            }
-                        }
-                    }
-                }
+        case 'TicketModel':
+            if ($addRelevance) {
+                $criteria = $criteria->extra(array(
+                    'select' => array(
+                        '__relevance__' => 'Z1.`relevance`',
+                    ),
+                ));
             }
-            if ($cdata_search)
-                $tables[] = TABLE_PREFIX.'ticket__cdata cdata'
-                    .' ON (cdata.ticket_id = A1.ticket_id)';
-
-            // Always consider the current staff's access
-            $thisstaff->getDepts();
-            $access = array();
-            $access[] = '(A1.staff_id=' . db_input($thisstaff->getId())
-                .' AND A2.state="open")';
-
-            if (!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts()))
-                $access[] = 'A1.dept_id IN ('
-                    . ($depts ? implode(',', db_input($depts)) : 0)
-                    . ')';
-
-            if (($teams = $thisstaff->getTeams()) && count(array_filter($teams)))
-                $access[] = 'A1.team_id IN ('
-                    .implode(',', db_input(array_filter($teams)))
-                    .') AND A2.state="open"';
-
-            $where[] = '(' . implode(' OR ', $access) . ')';
-
-            // TODO: Consider sorting preferences
-
-            $sql = 'SELECT DISTINCT '
-                . $key
-                . ' FROM '
-                . implode(' LEFT JOIN ', $tables)
-                . ' WHERE ' . implode(' AND ', $where)
-                . $sort
-                . ' LIMIT 500';
-        }
-
-        $class = get_class();
-        $auto_create = function($db_error) use ($class) {
-
-            if ($db_error != 1146)
-                // Perform the standard error handling
-                return true;
+            $criteria->extra(array(
+                'tables' => array(
+                    str_replace(array(':', '{}'), array(TABLE_PREFIX, $search),
+                    "(SELECT COALESCE(Z3.`object_id`, Z5.`ticket_id`, Z8.`ticket_id`) as `ticket_id`, SUM({}) AS `relevance` FROM `:_search` Z1 LEFT JOIN `:thread_entry` Z2 ON (Z1.`object_type` = 'H' AND Z1.`object_id` = Z2.`id`) LEFT JOIN `:thread` Z3 ON (Z2.`thread_id` = Z3.`id` AND Z3.`object_type` = 'T') LEFT JOIN `:ticket` Z5 ON (Z1.`object_type` = 'T' AND Z1.`object_id` = Z5.`ticket_id`) LEFT JOIN `:user` Z6 ON (Z6.`id` = Z1.`object_id` and Z1.`object_type` = 'U') LEFT JOIN `:organization` Z7 ON (Z7.`id` = Z1.`object_id` AND Z7.`id` = Z6.`org_id` AND Z1.`object_type` = 'O') LEFT JOIN :ticket Z8 ON (Z8.`user_id` = Z6.`id`) WHERE {} GROUP BY `ticket_id`) Z1"),
+                )
+            ));
+            $criteria->filter(array('ticket_id'=>new SqlCode('Z1.`ticket_id`')));
+            break;
 
-            // Create the search table automatically
-            $class::createSearchTable();
-        };
-        $res = db_query($sql, $auto_create);
-        $object_ids = array();
+        case 'User':
+            $criteria->extra(array(
+                'select' => array(
+                    '__relevance__' => 'Z1.`relevance`',
+                ),
+                'tables' => array(
+                    str_replace(array(':', '{}'), array(TABLE_PREFIX, $search),
+                    "(SELECT Z6.`id` as `user_id`, {} AS `relevance` FROM `:_search` Z1 LEFT JOIN `:user` Z6 ON (Z6.`id` = Z1.`object_id` and Z1.`object_type` = 'U') LEFT JOIN `:organization` Z7 ON (Z7.`id` = Z1.`object_id` AND Z7.`id` = Z6.`org_id` AND Z1.`object_type` = 'O') WHERE {}) Z1"),
+                )
+            ));
+            $criteria->filter(array('id'=>new SqlCode('Z1.`user_id`')));
+            break;
 
-        while ($row = db_fetch_row($res))
-            $object_ids[] = $row[0];
+        case 'Organization':
+            $criteria->extra(array(
+                'select' => array(
+                    '__relevance__' => 'Z1.`relevance`',
+                ),
+                'tables' => array(
+                    str_replace(array(':', '{}'), array(TABLE_PREFIX, $search),
+                    "(SELECT Z2.`id` as `org_id`, {} AS `relevance` FROM `:_search` Z1 LEFT JOIN `:organization` Z2 ON (Z2.`id` = Z1.`object_id` AND Z1.`object_type` = 'O') WHERE {}) Z1"),
+                )
+            ));
+            $criteria->filter(array('id'=>new SqlCode('Z1.`org_id`')));
+            break;
+        }
 
-        return $object_ids;
+        // TODO: Ensure search table exists;
+        if (false) {
+            // TODO: Create the search table automatically
+            // $class::createSearchTable();
+        }
+        return $criteria;
     }
 
     static function createSearchTable() {
@@ -494,8 +474,7 @@ class MysqlSearchBackend extends SearchBackend {
         };
 
         // THREADS ----------------------------------
-
-        $sql = "SELECT A1.`id`, A1.`title`, A1.`body`, A1.`format` FROM `".TICKET_THREAD_TABLE."` A1
+        $sql = "SELECT A1.`id`, A1.`title`, A1.`body`, A1.`format` FROM `".THREAD_ENTRY_TABLE."` A1
             LEFT JOIN `".TABLE_PREFIX."_search` A2 ON (A1.`id` = A2.`object_id` AND A2.`object_type`='H')
             WHERE A2.`object_id` IS NULL AND (A1.poster <> 'SYSTEM')
             AND (LENGTH(A1.`title`) + LENGTH(A1.`body`) > 0)
@@ -504,7 +483,7 @@ class MysqlSearchBackend extends SearchBackend {
             return false;
 
         while ($row = db_fetch_row($res)) {
-            $body = ThreadBody::fromFormattedText($row[2], $row[3]);
+            $body = ThreadEntryBody::fromFormattedText($row[2], $row[3]);
             $body = $body->getSearchable();
             $title = Format::searchable($row[1]);
             if (!$body && !$title)
@@ -524,7 +503,8 @@ class MysqlSearchBackend extends SearchBackend {
             return false;
 
         while ($row = db_fetch_row($res)) {
-            $ticket = Ticket::lookup($row[0]);
+            if (!($ticket = Ticket::lookup($row[0])))
+                continue;
             $cdata = $ticket->loadDynamicData();
             $content = array();
             foreach ($cdata as $k=>$a)
@@ -659,3 +639,604 @@ Signal::connect('system.install',
         array('MysqlSearchBackend', '__init'));
 
 MysqlSearchBackend::register();
+
+// Saved search system
+
+/**
+ *
+ * Fields:
+ * id - (int:unsigned:auto:pk) unique identifier
+ * flags - (int:unsigned) flags for this queue
+ * staff_id - (int:unsigned) Agent to whom this queue belongs (can be null
+ *      for public saved searches)
+ * title - (text:60) name of the queue
+ * config - (text) JSON encoded search configuration for the queue
+ * created - (date) date initially created
+ * updated - (date:auto_update) time of last update
+ */
+class SavedSearch extends VerySimpleModel {
+
+    static $meta = array(
+        'table' => QUEUE_TABLE,
+        'pk' => array('id'),
+        'ordering' => array('sort'),
+    );
+
+    const FLAG_PUBLIC =     0x0001;
+    const FLAG_QUEUE =      0x0002;
+
+    static function forStaff(Staff $agent) {
+        return static::objects()->filter(Q::any(array(
+            'staff_id' => $agent->getId(),
+            'flags__hasbit' => self::FLAG_PUBLIC,
+        )));
+    }
+
+    function loadFromState($source=false) {
+        // Pull out 'other' fields from the state so the fields will be
+        // added to the form. The state will be loaded below
+        $state = $source ?: array();
+        foreach ($state as $k=>$v) {
+            $info = array();
+            if (!preg_match('/^:(\w+)(?:!(\d+))?\+search/', $k, $info)) {
+                continue;
+            }
+            list($k,) = explode('+', $k, 2);
+            $state['fields'][] = $k;
+        }
+        return $this->getForm($state);
+    }
+
+    function getFormFromSession($key) {
+        if (isset($_SESSION[$key])) {
+            return $this->loadFromState($_SESSION[$key]);
+        }
+    }
+
+    function getForm($source=false) {
+        // XXX: Ensure that the UIDs generated for these fields are
+        //      consistent between requests
+
+        $searchable = $this->getCurrentSearchFields($source);
+        $fields = array(
+            'keywords' => new TextboxField(array(
+                'id' => 3001,
+                'configuration' => array(
+                    'size' => 40,
+                    'length' => 400,
+                    'autofocus' => true,
+                    'classes' => 'full-width headline',
+                    'placeholder' => __('Keywords — Optional'),
+                ),
+            )),
+        );
+        foreach ($searchable as $name=>$field) {
+            $fields = array_merge($fields, self::getSearchField($field, $name));
+        }
+
+        // Don't send the state as the souce because it is not in the
+        // ::parse format (it's in ::to_php format). Instead, source is set
+        // via ::loadState() below
+        $form = new AdvancedSearchForm($fields, $source);
+        $form->addValidator(function($form) {
+            $selected = 0;
+            foreach ($form->getFields() as $F) {
+                if (substr($F->get('name'), -7) == '+search' && $F->getClean())
+                    $selected += 1;
+                // Consider keyword searches
+                elseif ($F->get('name') == 'keywords' && $F->getClean())
+                    $selected += 1;
+            }
+            if (!$selected)
+                $form->addError(__('No fields selected for searching'));
+        });
+        if ($source)
+            $form->loadState($source);
+        return $form;
+    }
+
+    function getCurrentSearchFields($source=false) {
+        $core = array(
+            'status_id' =>  new TicketStatusChoiceField(array(
+                'id' => 3101,
+                'label' => __('Status'),
+            )),
+            'dept_id'   =>  new DepartmentChoiceField(array(
+                'id' => 3102,
+                'label' => __('Department'),
+            )),
+            'assignee'  =>  new AssigneeChoiceField(array(
+                'id' => 3103,
+                'label' => __('Assignee'),
+            )),
+            'topic_id'  =>  new HelpTopicChoiceField(array(
+                'id' => 3104,
+                'label' => __('Help Topic'),
+            )),
+            'created'   =>  new DateTimeField(array(
+                'id' => 3105,
+                'label' => __('Created'),
+            )),
+            'est_duedate'   =>  new DateTimeField(array(
+                'id' => 3106,
+                'label' => __('Due Date'),
+            )),
+        );
+
+        // Add 'other' fields added dynamically
+        if (is_array($source) && isset($source['fields'])) {
+            $extended = self::getExtendedTicketFields();
+            foreach ($source['fields'] as $f) {
+                $info = array();
+                if (isset($extended[$f])) {
+                    $core[$f] = $extended[$f];
+                    continue;
+                }
+                if (!preg_match('/^:(\w+)!(\d+)/', $f, $info)) {
+                    continue;
+                }
+                $id = $info[2];
+                if (is_numeric($id) && ($field = DynamicFormField::lookup($id))) {
+                    $impl = $field->getImpl();
+                    $impl->set('label', sprintf('%s / %s',
+                        $field->form->getLocal('title'), $field->getLocal('label')
+                    ));
+                    $core[":{$info[1]}!{$info[2]}"] = $impl;
+                }
+            }
+        }
+        return $core;
+    }
+
+    static function getExtendedTicketFields() {
+        return array(
+#            ':user' =>       new UserChoiceField(array(
+#                'label' => __('Ticket Owner'),
+#            )),
+#            ':org' =>        new OrganizationChoiceField(array(
+#                'label' => __('Organization'),
+#            )),
+            ':closed' =>     new DatetimeField(array(
+                'id' => 3204,
+                'label' => __('Closed Date'),
+            )),
+            ':thread__lastresponse' => new DatetimeField(array(
+                'id' => 3205,
+                'label' => __('Last Response'),
+            )),
+            ':thread__lastmessage' => new DatetimeField(array(
+                'id' => 3206,
+                'label' => __('Last Message'),
+            )),
+            ':source' =>     new TicketSourceChoiceField(array(
+                'id' => 3201,
+                'label' => __('Source'),
+            )),
+            ':state' =>      new TicketStateChoiceField(array(
+                'id' => 3202,
+                'label' => __('State'),
+            )),
+            ':flags' =>      new TicketFlagChoiceField(array(
+                'id' => 3203,
+                'label' => __('Flags'),
+            )),
+        );
+    }
+
+    static function getSearchField($field, $name) {
+        $baseId = $field->getId() * 20;
+        $pieces = array();
+        $pieces["{$name}+search"] = new BooleanField(array(
+            'id' => $baseId + 50000,
+            'configuration' => array(
+                'desc' => $field->getLocal('label'),
+                'classes' => 'inline',
+            ),
+        ));
+        $methods = $field->getSearchMethods();
+        $pieces["{$name}+method"] = new ChoiceField(array(
+            'id' => $baseId + 50001,
+            'choices' => $methods,
+            'default' => key($methods),
+            'visibility' => new VisibilityConstraint(new Q(array(
+                "{$name}+search__eq" => true,
+            )), VisibilityConstraint::HIDDEN),
+        ));
+        $offs = 0;
+        foreach ($field->getSearchMethodWidgets() as $m=>$w) {
+            if (!$w)
+                continue;
+            list($class, $args) = $w;
+            $args['id'] = $baseId + 50002 + $offs++;
+            $args['required'] = true;
+            $args['__searchval__'] = true;
+            $args['visibility'] = new VisibilityConstraint(new Q(array(
+                    "{$name}+method__eq" => $m,
+                )), VisibilityConstraint::HIDDEN);
+            $pieces["{$name}+{$m}"] = new $class($args);
+        }
+        return $pieces;
+    }
+
+    /**
+     * Collect information on the search form.
+     *
+     * Returns:
+     * (<array(name => array('field' => <FormField>, 'method' => <string>,
+     *      'value' => <mixed>, 'active' => <bool>))>), which will help to
+     * explain each field active in the search form.
+     */
+    function getSearchFields($form=false) {
+        $form = $form ?: $this->getForm();
+        $searchable = $this->getCurrentSearchFields($form->state);
+        $info = array();
+        foreach ($form->getFields() as $f) {
+            if (substr($f->get('name'), -7) == '+search') {
+                $name = substr($f->get('name'), 0, -7);
+                $value = null;
+                // Determine the search method and fetch the original field
+                if (($M = $form->getField("{$name}+method"))
+                    && ($method = $M->getClean())
+                    && ($field = $searchable[$name])
+                ) {
+                    // Request the field to generate a search Q for the
+                    // search method and given value
+                    if ($value = $form->getField("{$name}+{$method}"))
+                        $value = $value->getClean();
+                }
+                $info[$name] = array(
+                    'field' => $field,
+                    'method' => $method,
+                    'value' => $value,
+                    'active' =>  $f->getClean(),
+                );
+            }
+        }
+        return $info;
+    }
+
+    /**
+     * Get a description of a field in a search. Expects an entry from the
+     * array retrieved in ::getSearchFields()
+     */
+    function describeField($info, $name=false) {
+        return $info['field']->describeSearch($info['method'], $info['value'], $name);
+    }
+
+    function mangleQuerySet(QuerySet $qs, $form=false) {
+        $form = $form ?: $this->getForm();
+        $searchable = $this->getCurrentSearchFields($form->state);
+        $qs = clone $qs;
+
+        // Figure out fields to search on
+        foreach ($this->getSearchFields($form) as $name=>$info) {
+            if (!$info['active'])
+                continue;
+            $field = $info['field'];
+            $filter = new Q();
+            if ($name[0] == ':') {
+                // This was an 'other' field, fetch a special "name"
+                // for it which will be the ORM join path
+                static $other_paths = array(
+                    ':ticket' => 'cdata__',
+                    ':user' => 'user__cdata__',
+                    ':organization' => 'user__org__cdata__',
+                );
+                $column = $field->get('name') ?: 'field_'.$field->get('id');
+                list($type,$id) = explode('!', $name, 2);
+                // XXX: Last mile — find a better idea
+                switch (array($type, $column)) {
+                case array(':user', 'name'):
+                    $name = 'user__name';
+                    break;
+                case array(':user', 'email'):
+                    $name = 'user__emails__address';
+                    break;
+                case array(':organization', 'name'):
+                    $name = 'user__org__name';
+                    break;
+                default:
+                    if ($type == ':field' && $id) {
+                        $name = 'entries__answers__value';
+                        $filter->add(array('entries__answers__field_id' => $id));
+                        break;
+                    }
+                    if ($OP = $other_paths[$type])
+                        $name = $OP . $column;
+                    else
+                        $name = substr($name, 1);
+                }
+            }
+
+            // Add the criteria to the QuerySet
+            if ($Q = $field->getSearchQ($info['method'], $info['value'], $name)) {
+                $filter->add($Q);
+                $qs = $qs->filter($filter);
+            }
+        }
+
+        // Consider keyword searching
+        if ($keywords = $form->getField('keywords')->getClean()) {
+            global $ost;
+
+            $qs = $ost->searcher->find($keywords, $qs);
+        }
+
+        return $qs;
+    }
+
+    function checkAccess(Staff $agent) {
+        return $agent->getId() == $this->staff_id
+            || $this->hasFlag(self::FLAG_PUBLIC);
+    }
+
+    protected function hasFlag($flag) {
+        return $this->get('flag') & $flag !== 0;
+    }
+
+    protected function clearFlag($flag) {
+        return $this->set('flag', $this->get('flag') & ~$flag);
+    }
+
+    protected function setFlag($flag) {
+        return $this->set('flag', $this->get('flag') | $flag);
+    }
+
+    static function create($vars=array()) {
+        $inst = new static($vars);
+        $inst->created = SqlFunction::NOW();
+        return $inst;
+    }
+
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($refetch || $this->dirty);
+    }
+}
+
+class AdvancedSearchForm extends SimpleForm {
+    var $state;
+
+    function __construct($fields, $state) {
+        parent::__construct($fields);
+        $this->state = $state;
+    }
+}
+
+// Advanced search special fields
+
+class HelpTopicChoiceField extends ChoiceField {
+    function hasIdValue() {
+        return true;
+    }
+
+    function getChoices($verbose=false) {
+        return Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED);
+    }
+}
+
+require_once INCLUDE_DIR . 'class.dept.php';
+class DepartmentChoiceField extends ChoiceField {
+    function getChoices($verbose=false) {
+        return Dept::getDepartments();
+    }
+
+    function getSearchMethods() {
+        return array(
+            'includes' =>   __('is'),
+            '!includes' =>  __('is not'),
+        );
+    }
+}
+
+class AssigneeChoiceField extends ChoiceField {
+    function getChoices($verbose=false) {
+        global $thisstaff;
+
+        $items = array(
+            'M' => __('Me'),
+            'T' => __('One of my teams'),
+        );
+        foreach (Staff::getStaffMembers() as $id=>$name) {
+            // Don't include $thisstaff (since that's 'Me')
+            if ($thisstaff && $thisstaff->getId() == $id)
+                continue;
+            $items['s' . $id] = $name;
+        }
+        foreach (Team::getTeams() as $id=>$name) {
+            $items['t' . $id] = $name;
+        }
+        return $items;
+    }
+
+    function getSearchMethods() {
+        return array(
+            'assigned' =>   __('assigned'),
+            '!assigned' =>  __('unassigned'),
+            'includes' =>   __('includes'),
+            '!includes' =>  __('does not include'),
+        );
+    }
+
+    function getSearchMethodWidgets() {
+        return array(
+            'assigned' => null,
+            '!assigned' => null,
+            'includes' => array('ChoiceField', array(
+                'choices' => $this->getChoices(),
+                'configuration' => array('multiselect' => true),
+            )),
+            '!includes' => array('ChoiceField', array(
+                'choices' => $this->getChoices(),
+                'configuration' => array('multiselect' => true),
+            )),
+        );
+    }
+
+    function getSearchQ($method, $value, $name=false) {
+        global $thisstaff;
+
+        $Q = new Q();
+        switch ($method) {
+        case 'assigned':
+            $Q->negate();
+        case '!assigned':
+            $Q->add(array('team_id' => 0,
+                'staff_id' => 0));
+            break;
+        case '!includes':
+            $Q->negate();
+        case 'includes':
+            $teams = $agents = array();
+            foreach ($value as $id => $ST) {
+                switch ($id[0]) {
+                case 'M':
+                    $agents[] = $thisstaff->getId();
+                    break;
+                case 's':
+                    $agents[] = (int) substr($id, 1);
+                    break;
+                case 'T':
+                    $teams = array_merge($thisstaff->getTeams());
+                    break;
+                case 't':
+                    $teams[] = (int) substr($id, 1);
+                    break;
+                }
+            }
+            $constraints = array();
+            if ($teams)
+                $constraints['team_id__in'] = $teams;
+            if ($agents)
+                $constraints['staff_id__in'] = $agents;
+            $Q->add(Q::any($constraints));
+        }
+        return $Q;
+    }
+
+    function describeSearchMethod($method) {
+        switch ($method) {
+        case 'assigned':
+            return __('assigned');
+        case '!assigned':
+            return __('unassigned');
+        default:
+            return parent::describeSearchMethod($method);
+        }
+    }
+}
+
+class TicketStateChoiceField extends ChoiceField {
+    function getChoices($verbose=false) {
+        return array(
+            'open' => __('Open'),
+            'closed' => __('Closed'),
+            'archived' => __('Archived'),
+            'deleted' => __('Deleted'),
+        );
+    }
+
+    function getSearchMethods() {
+        return array(
+            'includes' =>   __('is'),
+            '!includes' =>  __('is not'),
+        );
+    }
+
+    function getSearchQ($method, $value, $name=false) {
+        return parent::getSearchQ($method, $value, 'status__state');
+    }
+}
+
+class TicketFlagChoiceField extends ChoiceField {
+    function getChoices($verbose=false) {
+        return array(
+            'isanswered' =>   __('Answered'),
+            'isoverdue' =>    __('Overdue'),
+        );
+    }
+
+    function getSearchMethods() {
+        return array(
+            'includes' =>   __('is'),
+            '!includes' =>  __('is not'),
+        );
+    }
+
+    function getSearchQ($method, $value, $name=false) {
+        $Q = new Q();
+        if (isset($value['isanswered']))
+            $Q->add(array('isanswered' => 1));
+        if (isset($value['isoverdue']))
+            $Q->add(array('isoverdue' => 1));
+        if ($method == '!includes')
+            $Q->negate();
+        if ($Q->constraints)
+            return $Q;
+    }
+}
+
+class TicketSourceChoiceField extends ChoiceField {
+    function getChoices($verbose=false) {
+        return array(
+            'web' => __('Web'),
+            'email' => __('Email'),
+            'phone' => __('Phone'),
+            'api' => __('API'),
+            'other' => __('Other'),
+        );
+    }
+
+    function getSearchMethods() {
+        return array(
+            'includes' =>   __('is'),
+            '!includes' =>  __('is not'),
+        );
+    }
+
+    function getSearchQ($method, $value, $name=false) {
+        return parent::getSearchQ($method, $value, 'source');
+    }
+}
+
+class OpenClosedTicketStatusList extends TicketStatusList {
+    function getItems($criteria=array()) {
+        $rv = array();
+        $base = parent::getItems($criteria);
+        foreach ($base as $idx=>$S) {
+            if (in_array($S->state, array('open', 'closed')))
+                $rv[$idx] = $S;
+        }
+        return $rv;
+    }
+}
+class TicketStatusChoiceField extends SelectionField {
+    static $widget = 'ChoicesWidget';
+
+    function getList() {
+        return new OpenClosedTicketStatusList(
+            DynamicList::lookup(
+                array('type' => 'ticket-status'))
+        );
+    }
+
+    function getSearchMethods() {
+        return array(
+            'includes' =>   __('is'),
+            '!includes' =>  __('is not'),
+        );
+    }
+
+    function getSearchQ($method, $value, $name=false) {
+        $name = $name ?: $this->get('name');
+        switch ($method) {
+        case '!includes':
+            return Q::not(array("{$name}__in" => array_keys($value)));
+        case 'includes':
+            return new Q(array("{$name}__in" => array_keys($value)));
+        default:
+            return parent::getSearchQ($method, $value, $name);
+        }
+    }
+}
diff --git a/include/class.sequence.php b/include/class.sequence.php
index cc27801c596a8dd17acac088286e15bc7de6b4aa..912e7cd28e910f2ba16c6b7345d56c70d2c94166 100644
--- a/include/class.sequence.php
+++ b/include/class.sequence.php
@@ -205,7 +205,7 @@ class Sequence extends VerySimpleModel {
     }
 
     function __create($data) {
-        $instance = parent::create($data);
+        $instance = new static($data);
         $instance->save();
         return $instance;
     }
@@ -214,9 +214,6 @@ class Sequence extends VerySimpleModel {
 class RandomSequence extends Sequence {
     var $padding = '0';
 
-    // Override the ORM constructor and do nothing
-    function __construct() {}
-
     function __next($digits=6) {
         if ($digits < 6)
             $digits = 6;
@@ -228,7 +225,7 @@ class RandomSequence extends Sequence {
         return $this->next($format);
     }
 
-    function save() {
+    function save($refetch=false) {
         throw new RuntimeException('RandomSequence is not database-backed');
     }
 }
diff --git a/include/class.setup.php b/include/class.setup.php
index fe70fc10b10f8688e9eabbeab0944aa27e3379f5..bcdd24687dde9b92131c87f54dcda5be552eb486 100644
--- a/include/class.setup.php
+++ b/include/class.setup.php
@@ -14,10 +14,10 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-Class SetupWizard {
+class SetupWizard {
 
     //Mimimum requirements
-    var $prereq = array('php'   => '5.3',
+    var $prereq = array('php'   => '5.4',
                         'mysql' => '5.0');
 
     //Version info - same as the latest version.
@@ -28,7 +28,7 @@ Class SetupWizard {
     //Errors
     var $errors=array();
 
-    function SetupWizard(){
+    function __construct(){
         $this->errors=array();
         $this->version_verbose = sprintf(__('osTicket %s' /* <%s> is for the version */),
             THIS_VERSION);
@@ -47,6 +47,8 @@ Class SetupWizard {
         load SQL schema - assumes MySQL && existing connection
         */
     function load_sql($schema, $prefix, $abort=true, $debug=false) {
+        global $ost;
+
         # Strip comments and remarks
         $schema=preg_replace('%^\s*(#|--).*$%m', '', $schema);
         # Replace table prefix
@@ -62,8 +64,10 @@ Class SetupWizard {
         foreach($statements as $k=>$sql) {
             if(db_query($sql, false)) continue;
             $error = "[$sql] ".db_error();
-            if($abort)
-                    return $this->abort($error, $debug);
+            if ($abort)
+                return $this->abort($error, $debug);
+            elseif ($debug && $ost)
+                $ost->logDBError('DB Error #'.db_errno(), $error, false);
         }
 
         return true;
diff --git a/include/class.sla.php b/include/class.sla.php
index 070743a2bb6f0c8e22c3664be229c8817a401852..d961ebcba2f9e7de02a6eb8051415e46c396111e 100644
--- a/include/class.sla.php
+++ b/include/class.sla.php
@@ -3,7 +3,6 @@
     class.sla.php
 
     SLA
-
     Peter Rotich <peter@osticket.com>
     Copyright (c)  2006-2013 osTicket
     http://www.osticket.com
@@ -14,76 +13,60 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-class SLA {
-
-    var $id;
-
-    var $info;
-    var $config;
-
-    function SLA($id) {
-        $this->id=0;
-        $this->load($id);
-    }
-
-    function load($id=0) {
-
-        if(!$id && !($id=$this->getId()))
-            return false;
+class SLA extends VerySimpleModel
+implements TemplateVariable {
 
-        $sql='SELECT * FROM '.SLA_TABLE.' WHERE id='.db_input($id);
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
+    static $meta = array(
+        'table' => SLA_TABLE,
+        'pk' => array('id'),
+    );
 
-        $this->ht=db_fetch_array($res);
-        $this->id=$this->ht['id'];
-        return true;
-    }
+    const FLAG_ACTIVE       = 0x0001;
+    const FLAG_ESCALATE     = 0x0002;
+    const FLAG_NOALERTS     = 0x0004;
+    const FLAG_TRANSIENT    = 0x0008;
 
-    function reload() {
-        return $this->load();
-    }
+    var $_config;
 
     function getId() {
         return $this->id;
     }
 
     function getName() {
-        return $this->ht['name'];
+        return $this->getLocal('name');
     }
 
     function getGracePeriod() {
-        return $this->ht['grace_period'];
-    }
-
-    function getNotes() {
-        return $this->ht['notes'];
+        return $this->grace_period;
     }
 
-    function getHashtable() {
-        return array_merge($this->getConfig()->getInfo(), $this->ht);
+    function getInfo() {
+        $base = $this->ht;
+        $base['isactive'] = $this->flags & self::FLAG_ACTIVE;
+        $base['disable_overdue_alerts'] = $this->flags & self::FLAG_NOALERTS;
+        $base['enable_priority_escalation'] = $this->flags & self::FLAG_ESCALATE;
+        $base['transient'] = $this->flags & self::FLAG_TRANSIENT;
+        return $base;
     }
 
-    function getInfo() {
-        return $this->getHashtable();
+    function getCreateDate() {
+        return $this->created;
     }
 
-    function getConfig() {
-        if (!isset($this->config))
-            $this->config = new SlaConfig($this->getId());
-        return $this->config;
+    function getUpdateDate() {
+        return $this->updated;
     }
 
     function isActive() {
-        return ($this->ht['isactive']);
+        return $this->flags & self::FLAG_ACTIVE;
     }
 
     function isTransient() {
-        return $this->getConfig()->get('transient', false);
+        return $this->flags & self::FLAG_TRANSIENT;
     }
 
     function sendAlerts() {
-        return (!$this->ht['disable_overdue_alerts']);
+        return 0 === ($this->flags & self::FLAG_NOALERTS);
     }
 
     function alertOnOverdue() {
@@ -91,18 +74,80 @@ class SLA {
     }
 
     function priorityEscalation() {
-        return ($this->ht['enable_priority_escalation']);
+        return $this->flags && self::FLAG_ESCALATE;
     }
 
-    function update($vars,&$errors) {
+    function getTranslateTag($subtag) {
+        return _H(sprintf('sla.%s.%s', $subtag, $this->getId()));
+    }
 
-        if(!SLA::save($this->getId(),$vars,$errors))
+    function getLocal($subtag) {
+        $tag = $this->getTranslateTag($subtag);
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : $this->ht[$subtag];
+    }
+
+    static function getLocalById($id, $subtag, $default) {
+        $tag = _H(sprintf('sla.%s.%s', $subtag, $id));
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : $default;
+    }
+
+    // TemplateVariable interface
+    function asVar() {
+        return $this->getName();
+    }
+
+    static function getVarScope() {
+        return array(
+            'name' => __('SLA Plan'),
+            'graceperiod' => __("Grace Period (hrs)"),
+        );
+    }
+
+    function update($vars, &$errors) {
+
+        if (!$vars['grace_period'])
+            $errors['grace_period'] = __('Grace period required');
+        elseif (!is_numeric($vars['grace_period']))
+            $errors['grace_period'] = __('Numeric value required (in hours)');
+
+        if (!$vars['name'])
+            $errors['name'] = __('Name is required');
+        elseif (($sid=SLA::getIdByName($vars['name'])) && $sid!=$vars['id'])
+            $errors['name'] = __('Name already exists');
+
+        if ($errors)
             return false;
 
-        $this->reload();
-        $this->getConfig()->set('transient', isset($vars['transient']) ? 1 : 0);
+        $this->name = $vars['name'];
+        $this->grace_period = $vars['grace_period'];
+        $this->notes = Format::sanitize($vars['notes']);
+        $this->flags =
+              ($vars['isactive'] ? self::FLAG_ACTIVE : 0)
+            | (isset($vars['disable_overdue_alerts']) ? self::FLAG_NOALERTS : 0)
+            | (isset($vars['enable_priority_escalation']) ? self::FLAG_ESCALATE : 0)
+            | (isset($vars['transient']) ? self::FLAG_TRANSIENT : 0);
+
+        if ($this->save())
+            return true;
 
-        return true;
+        if (isset($this->id)) {
+            $errors['err']=sprintf(__('Unable to update %s.'), __('this SLA plan'))
+               .' '.__('Internal error occurred');
+        } else {
+            $errors['err']=sprintf(__('Unable to add %s.'), __('this SLA plan'))
+               .' '.__('Internal error occurred');
+        }
+
+        return false;
+    }
+
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+
+        return parent::save($refetch || $this->dirty);
     }
 
     function delete() {
@@ -111,6 +156,7 @@ class SLA {
         if(!$cfg || $cfg->getDefaultSLAId()==$this->getId())
             return false;
 
+        //TODO: Use ORM to delete & update
         $id=$this->getId();
         $sql='DELETE FROM '.SLA_TABLE.' WHERE id='.db_input($id).' LIMIT 1';
         if(db_query($sql) && ($num=db_affected_rows())) {
@@ -123,95 +169,49 @@ class SLA {
     }
 
     /** static functions **/
-    function create($vars,&$errors) {
-        if (($id = SLA::save(0,$vars,$errors)) && ($sla = self::lookup($id)))
-            $sla->getConfig()->set('transient',
-                isset($vars['transient']) ? 1 : 0);
-        return $id;
-    }
+    static function getSLAs($criteria=array()) {
 
-    function getSLAs() {
+       $slas = self::objects()
+           ->order_by('name')
+           ->values_flat('id', 'name', 'flags', 'grace_period');
 
-        $slas=array();
-
-        $sql='SELECT id, name, isactive, grace_period FROM '.SLA_TABLE.' ORDER BY name';
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while($row=db_fetch_array($res))
-                $slas[$row['id']] = sprintf(__('%s (%d hours - %s)'
+        $entries = array();
+        foreach ($slas as $row) {
+            $row[2] = $row[2] & self::FLAG_ACTIVE;
+            $entries[$row[0]] = sprintf(__('%s (%d hours - %s)'
                         /* Tokens are <name> (<#> hours - <Active|Disabled>) */),
-                        $row['name'],
-                        $row['grace_period'],
-                        $row['isactive']?__('Active'):__('Disabled'));
+                        self::getLocalById($row[0], 'name', $row[1]),
+                        $row[3],
+                        $row[2] ? __('Active') : __('Disabled'));
         }
 
-        return $slas;
+        return $entries;
     }
 
-
-    function getIdByName($name) {
-
-        $sql='SELECT id FROM '.SLA_TABLE.' WHERE name='.db_input($name);
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id)=db_fetch_row($res);
-
-        return $id;
+    static function getSLAName($id) {
+        $slas = static::getSLAs();
+        return @$slas[$id];
     }
 
-    function lookup($id) {
-        return ($id && is_numeric($id) && ($sla= new SLA($id)) && $sla->getId()==$id)?$sla:null;
-    }
-
-    function save($id,$vars,&$errors) {
-
-        if(!$vars['grace_period'])
-            $errors['grace_period']=__('Grace period required');
-        elseif(!is_numeric($vars['grace_period']))
-            $errors['grace_period']=__('Numeric value required (in hours)');
+    static function getIdByName($name) {
+        $row = static::objects()
+            ->filter(array('name'=>$name))
+            ->values_flat('id')
+            ->first();
 
-        if(!$vars['name'])
-            $errors['name']=__('Name is required');
-        elseif(($sid=SLA::getIdByName($vars['name'])) && $sid!=$id)
-            $errors['name']=__('Name already exists');
-
-        if($errors) return false;
-
-        $sql=' updated=NOW() '.
-             ',isactive='.db_input($vars['isactive']).
-             ',name='.db_input($vars['name']).
-             ',grace_period='.db_input($vars['grace_period']).
-             ',disable_overdue_alerts='.db_input(isset($vars['disable_overdue_alerts'])?1:0).
-             ',enable_priority_escalation='.db_input(isset($vars['enable_priority_escalation'])?1:0).
-             ',notes='.db_input(Format::sanitize($vars['notes']));
-
-        if($id) {
-            $sql='UPDATE '.SLA_TABLE.' SET '.$sql.' WHERE id='.db_input($id);
-            if(db_query($sql))
-                return true;
-
-            $errors['err']=sprintf(__('Unable to update %s.'), __('this SLA plan'))
-               .' '.__('Internal error occurred');
-        }else{
-            if (isset($vars['id']))
-                $sql .= ', id='.db_input($vars['id']);
-
-            $sql='INSERT INTO '.SLA_TABLE.' SET '.$sql.',created=NOW() ';
-            if(db_query($sql) && ($id=db_insert_id()))
-                return $id;
-
-            $errors['err']=sprintf(__('Unable to add %s.'), __('this SLA plan'))
-               .' '.__('Internal error occurred');
-        }
-
-        return false;
+        return $row ? $row[0] : 0;
     }
-}
 
-require_once(INCLUDE_DIR.'class.config.php');
-class SlaConfig extends Config {
-    var $table = CONFIG_TABLE;
+    static function create($vars=false, &$errors=array()) {
+        $sla = new static($vars);
+        $sla->created = SqlFunction::NOW();
+        return $sla;
+    }
 
-    function SlaConfig($id) {
-        parent::Config("sla.$id");
+    static function __create($vars, &$errors=array()) {
+        $sla = self::create($vars);
+        $sla->save();
+        return $sla;
     }
 }
 ?>
diff --git a/include/class.staff.php b/include/class.staff.php
index e937871c149e37d55d8d1d48485c218611a0d036..a5e56c22addbf69c3ec83dfa4def3f0eaf6750b8 100644
--- a/include/class.staff.php
+++ b/include/class.staff.php
@@ -17,73 +17,82 @@ include_once(INCLUDE_DIR.'class.ticket.php');
 include_once(INCLUDE_DIR.'class.dept.php');
 include_once(INCLUDE_DIR.'class.error.php');
 include_once(INCLUDE_DIR.'class.team.php');
-include_once(INCLUDE_DIR.'class.group.php');
+include_once(INCLUDE_DIR.'class.role.php');
 include_once(INCLUDE_DIR.'class.passwd.php');
 include_once(INCLUDE_DIR.'class.user.php');
 include_once(INCLUDE_DIR.'class.auth.php');
 
-class Staff extends AuthenticatedUser
-implements EmailContact {
-
-    var $ht;
-    var $id;
-
-    var $dept;
+class Staff extends VerySimpleModel
+implements AuthenticatedUser, EmailContact, TemplateVariable {
+
+    static $meta = array(
+        'table' => STAFF_TABLE,
+        'pk' => array('staff_id'),
+        'joins' => array(
+            'dept' => array(
+                'constraint' => array('dept_id' => 'Dept.id'),
+            ),
+            'role' => array(
+                'constraint' => array('role_id' => 'Role.id'),
+            ),
+            'dept_access' => array(
+                'reverse' => 'StaffDeptAccess.staff',
+            ),
+            'teams' => array(
+                'reverse' => 'TeamMember.staff',
+            ),
+        ),
+    );
+
+    var $authkey;
     var $departments;
-    var $group;
-    var $teams;
-    var $timezone;
-    var $stats;
+    var $stats = array();
+    var $_extra;
+    var $passwd_change;
+    var $_roles = null;
+    var $_teams = null;
+    var $_config = null;
+    var $_perm;
 
-    function Staff($var) {
-        $this->id =0;
-        return ($this->load($var));
-    }
+    function __onload() {
 
-    function load($var='') {
-
-        if(!$var && !($var=$this->getId()))
-            return false;
+        // WE have to patch info here to support upgrading from old versions.
+        $time = null;
+        if (isset($this->passwdreset) && $this->passwdreset)
+            $time=strtotime($this->passwdreset);
+        elseif (isset($this->added) && $this->added)
+            $time=strtotime($this->added);
 
-        $sql='SELECT staff.created as added, grp.*, staff.* '
-            .' FROM '.STAFF_TABLE.' staff '
-            .' LEFT JOIN '.GROUP_TABLE.' grp ON(grp.group_id=staff.group_id)
-               WHERE ';
-
-        if (is_numeric($var))
-            $sql .= 'staff_id='.db_input($var);
-        elseif (Validator::is_email($var))
-            $sql .= 'email='.db_input($var);
-        elseif (is_string($var))
-            $sql .= 'username='.db_input($var);
-        else
-            return null;
+        if ($time)
+            $this->passwd_change = time()-$time; //XXX: check timezone issues.
+    }
 
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return NULL;
+    function get($field, $default=false) {
 
+        // Autoload config if not loaded already
+        if (!isset($this->_config))
+            $this->getConfig();
 
-        $this->ht=db_fetch_array($res);
-        $this->id  = $this->ht['staff_id'];
-        $this->teams = $this->ht['teams'] = array();
-        $this->group = $this->dept = null;
-        $this->departments = $this->stats = array();
-        $this->config = new Config('staff.'.$this->id);
+        if (isset($this->_config[$field]))
+            return $this->_config[$field];
 
-        //WE have to patch info here to support upgrading from old versions.
-        if(($time=strtotime($this->ht['passwdreset']?$this->ht['passwdreset']:$this->ht['added'])))
-            $this->ht['passwd_change'] = time()-$time; //XXX: check timezone issues.
+        return parent::get($field, $default);
+    }
 
-        if($this->ht['timezone_id'])
-            $this->ht['tz_offset'] = Timezone::getOffsetById($this->ht['timezone_id']);
-        elseif($this->ht['timezone_offset'])
-            $this->ht['tz_offset'] = $this->ht['timezone_offset'];
+    function getConfig() {
 
-        return ($this->id);
-    }
+        if (!isset($this->_config) && $this->getId()) {
+            $_config = new Config('staff.'.$this->getId(),
+                    // Defaults
+                    array(
+                        'default_from_name' => '',
+                        'datetime_format'   => '',
+                        'thread_view_order' => '',
+                        ));
+            $this->_config = $_config->getInfo();
+        }
 
-    function reload() {
-        return $this->load();
+        return $this->_config;
     }
 
     function __toString() {
@@ -94,23 +103,77 @@ implements EmailContact {
         return $this->__toString();
     }
 
+    static function getVarScope() {
+      return array(
+        'dept' => array('class' => 'Dept', 'desc' => __('Department')),
+        'email' => __('Email Address'),
+        'name' => array(
+          'class' => 'PersonsName', 'desc' => __('Agent name'),
+        ),
+        'mobile' => __('Mobile Number'),
+        'phone' => __('Phone Number'),
+        'signature' => __('Signature'),
+        'timezone' => "Agent's configured timezone",
+        'username' => 'Access username',
+      );
+    }
+
+    function getVar($tag) {
+        switch ($tag) {
+        case 'mobile':
+            return Format::phone($this->ht['mobile']);
+        case 'phone':
+            return Format::phone($this->ht['phone']);
+        }
+    }
+
     function getHashtable() {
-        return $this->ht;
+        $base = $this->ht;
+        unset($base['teams']);
+        unset($base['dept_access']);
+
+        if ($this->getConfig())
+            $base += $this->getConfig();
+
+        return $base;
     }
 
     function getInfo() {
-        return $this->config->getInfo() + $this->getHashtable();
+        return $this->getHashtable();
     }
 
     // AuthenticatedUser implementation...
     // TODO: Move to an abstract class that extends Staff
-    function getRole() {
+    function getUserType() {
         return 'staff';
     }
 
     function getAuthBackend() {
-        list($authkey, ) = explode(':', $this->getAuthKey());
-        return StaffAuthenticationBackend::getBackend($authkey);
+        list($bk, ) = explode(':', $this->getAuthKey());
+
+        // If administering a user other than yourself, fallback to the
+        // agent's declared backend, if any
+        if (!$bk && $this->backend)
+            $bk = $this->backend;
+
+        return StaffAuthenticationBackend::getBackend($bk);
+    }
+
+    function setAuthKey($key) {
+        $this->authkey = $key;
+    }
+
+    function getAuthKey() {
+        return $this->authkey;
+    }
+
+    // logOut the user
+    function logOut() {
+
+        if ($bk = $this->getAuthBackend())
+            return $bk->signOut($this);
+
+        return false;
     }
 
     /*compares user password*/
@@ -125,10 +188,9 @@ implements EmailContact {
             return false;
 
         //Password is a MD5 hash: rehash it (if enabled) otherwise force passwd change.
-        $sql='UPDATE '.STAFF_TABLE.' SET passwd='.db_input(Passwd::hash($password))
-            .' WHERE staff_id='.db_input($this->getId());
+        $this->passwd = Passwd::hash($password);
 
-        if(!$autoupdate || !db_query($sql))
+        if(!$autoupdate || !$this->save())
             $this->forcePasswdRest();
 
         return true;
@@ -139,109 +201,174 @@ implements EmailContact {
     }
 
     function hasPassword() {
-        return (bool) $this->ht['passwd'];
+        return (bool) $this->passwd;
     }
 
     function forcePasswdRest() {
-        return db_query('UPDATE '.STAFF_TABLE.' SET change_passwd=1 WHERE staff_id='.db_input($this->getId()));
+        $this->change_passwd = 1;
+        return $this->save();
     }
 
     /* check if passwd reset is due. */
     function isPasswdResetDue() {
         global $cfg;
         return ($cfg && $cfg->getPasswdResetPeriod()
-                    && $this->ht['passwd_change']>($cfg->getPasswdResetPeriod()*30*24*60*60));
-    }
+                    && $this->passwd_change>($cfg->getPasswdResetPeriod()*30*24*60*60));
+    }
+
+    function setPassword($new, $current=false) {
+        // Allow the backend to update the password. This is the preferred
+        // method as it allows for integration with password policies and
+        // also allows for remotely updating the password where possible and
+        // supported.
+        if (!($bk = $this->getAuthBackend())
+            || !$bk instanceof AuthBackend
+        ) {
+            // Fallback to osTicket authentication token udpates
+            $bk = new osTicketAuthentication();
+        }
 
-    function isPasswdChangeDue() {
-        return $this->isPasswdResetDue();
+        // And now for the magic
+        if (!$bk->supportsPasswordChange()) {
+            throw new PasswordUpdateFailed(
+                __('Authentication backend does not support password updates'));
+        }
+        // Backend should throw PasswordUpdateFailed directly
+        $rv = $bk->setPassword($this, $new, $current);
+
+        // Successfully updated authentication tokens
+        $this->change_passwd = 0;
+        $this->cancelResetTokens();
+        $this->passwdreset = SqlFunction::NOW();
+
+        return $rv;
     }
 
-    function getTZoffset() {
-        return $this->ht['tz_offset'];
+    function canAccess($something) {
+        if ($something instanceof RestrictedAccess)
+            return $something->checkStaffPerm($this);
+
+        return true;
     }
 
-    function observeDaylight() {
-        return $this->ht['daylight_saving']?true:false;
+    function isPasswdChangeDue() {
+        return $this->isPasswdResetDue();
     }
 
     function getRefreshRate() {
-        return $this->ht['auto_refresh_rate'];
+        return $this->auto_refresh_rate;
     }
 
     function getPageLimit() {
-        return $this->ht['max_page_size'];
+        return $this->max_page_size;
     }
 
     function getId() {
-        return $this->id;
+        return $this->staff_id;
     }
     function getUserId() {
         return $this->getId();
     }
 
     function getEmail() {
-        return $this->ht['email'];
+        return $this->email;
+    }
+
+    function getAvatar($size=null) {
+        global $cfg;
+        $source = $cfg->getStaffAvatarSource();
+        $avatar = $source->getAvatar($this);
+        if (isset($size))
+            $avatar->setSize($size);
+        return $avatar;
     }
 
     function getUserName() {
-        return $this->ht['username'];
+        return $this->username;
     }
 
     function getPasswd() {
-        return $this->ht['passwd'];
+        return $this->passwd;
     }
 
     function getName() {
-        return new PersonsName(array('first' => $this->ht['firstname'], 'last' => $this->ht['lastname']));
+        return new AgentsName(array('first' => $this->ht['firstname'], 'last' => $this->ht['lastname']));
+    }
+
+    function getAvatarAndName() {
+        return $this->getAvatar().Format::htmlchars((string) $this->getName());
     }
 
     function getFirstName() {
-        return $this->ht['firstname'];
+        return $this->firstname;
     }
 
     function getLastName() {
-        return $this->ht['lastname'];
+        return $this->lastname;
     }
 
     function getSignature() {
-        return $this->ht['signature'];
+        return $this->signature;
     }
 
     function getDefaultSignatureType() {
-        return $this->ht['default_signature_type'];
+        return $this->default_signature_type;
+    }
+
+    function getReplyFromNameType() {
+        return $this->default_from_name;
     }
 
     function getDefaultPaperSize() {
-        return $this->ht['default_paper_size'];
+        return $this->default_paper_size;
     }
 
     function forcePasswdChange() {
-        return ($this->ht['change_passwd']);
+        return $this->change_passwd;
     }
 
     function getDepartments() {
+        // TODO: Cache this in the agent's session as it is unlikely to
+        //       change while logged in
+
+        if (!isset($this->departments)) {
+
+            // Departments the staff is "allowed" to access...
+            // based on the group they belong to + user's primary dept + user's managed depts.
+            $sql='SELECT DISTINCT d.id FROM '.STAFF_TABLE.' s '
+                .' LEFT JOIN '.STAFF_DEPT_TABLE.' g ON (s.staff_id=g.staff_id) '
+                .' INNER JOIN '.DEPT_TABLE.' d ON (LOCATE(CONCAT("/", s.dept_id, "/"), d.path) OR d.manager_id=s.staff_id OR LOCATE(CONCAT("/", g.dept_id, "/"), d.path)) '
+                .' WHERE s.staff_id='.db_input($this->getId());
+            $depts = array();
+            if (($res=db_query($sql)) && db_num_rows($res)) {
+                while(list($id)=db_fetch_row($res))
+                    $depts[] = $id;
+            }
 
-        if($this->departments)
-            return $this->departments;
-
-        //Departments the staff is "allowed" to access...
-        // based on the group they belong to + user's primary dept + user's managed depts.
-        $sql='SELECT DISTINCT d.dept_id FROM '.STAFF_TABLE.' s '
-            .' LEFT JOIN '.GROUP_DEPT_TABLE.' g ON(s.group_id=g.group_id) '
-            .' INNER JOIN '.DEPT_TABLE.' d ON(d.dept_id=s.dept_id OR d.manager_id=s.staff_id OR d.dept_id=g.dept_id) '
-            .' WHERE s.staff_id='.db_input($this->getId());
-
-        $depts = array();
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while(list($id)=db_fetch_row($res))
-                $depts[] = $id;
-        } else { //Neptune help us! (fallback)
-            $depts = array_merge($this->getGroup()->getDepartments(), array($this->getDeptId()));
-        }
+            /* ORM method — about 2.0ms slower
+            $q = Q::any(array(
+                'path__contains' => '/'.$this->dept_id.'/',
+                'manager_id' => $this->getId(),
+            ));
+            // Add in extended access
+            foreach ($this->dept_access->depts->values_flat('dept_id') as $row) {
+                // Skip primary dept
+                if ($row[0] == $this->dept_id)
+                    continue;
+                $q->add(new Q(array('path__contains'=>'/'.$row[0].'/')));
+            }
+
+            $dept_ids = Dept::objects()
+                ->filter($q)
+                ->distinct('id')
+                ->values_flat('id');
 
-        $this->departments = array_filter(array_unique($depts));
+            foreach ($dept_ids as $row)
+                $depts[] = $row[0];
+            */
 
+            $this->departments = $depts;
+        }
 
         return $this->departments;
     }
@@ -257,40 +384,89 @@ implements EmailContact {
                     ))?array_keys($depts):array();
     }
 
-    function getGroupId() {
-        return $this->ht['group_id'];
+    function getDeptId() {
+        return $this->dept_id;
     }
 
-    function getGroup() {
+    function getDept() {
+        return $this->dept;
+    }
 
-        if(!$this->group && $this->getGroupId())
-            $this->group = Group::lookup($this->getGroupId());
+    function setDepartmentId($dept_id, $eavesdrop=false) {
+        // Grant access to the current department
+        $old = $this->dept_id;
+        if ($eavesdrop) {
+            $da = StaffDeptAccess::create(array(
+                'dept_id' => $old,
+                'role_id' => $this->role_id,
+            ));
+            $da->setAlerts(true);
+            $this->dept_access->add($da);
+        }
 
-        return $this->group;
+        // Drop extended access to new department
+        $this->dept_id = $dept_id;
+        if ($da = $this->dept_access->findFirst(array(
+            'dept_id' => $dept_id))
+        ) {
+            $this->dept_access->remove($da);
+        }
     }
 
-    function getDeptId() {
-        return $this->ht['dept_id'];
+    function usePrimaryRoleOnAssignment() {
+        return $this->getExtraAttr('def_assn_role', true);
     }
 
-    function getDept() {
+    function getLanguage() {
+        return (isset($this->lang)) ? $this->lang : false;
+    }
 
-        if(!$this->dept && $this->getDeptId())
-            $this->dept= Dept::lookup($this->getDeptId());
+    function getTimezone() {
+        if (isset($this->timezone))
+            return $this->timezone;
+    }
 
-        return $this->dept;
+    function getLocale() {
+        //XXX: isset is required here to avoid possible crash when upgrading
+        // installation where locale column doesn't exist yet.
+        return isset($this->locale) ? $this->locale : 0;
     }
 
-    function getLanguage() {
-        static $cached = false;
-        if (!$cached) $cached = &$_SESSION['staff:lang'];
+    function getRole($dept=null) {
+        $deptId = is_object($dept) ? $dept->getId() : $dept;
+        if ($deptId && $deptId != $this->dept_id) {
+            if (isset($this->_roles[$deptId]))
+                return $this->_roles[$deptId];
+
+            if ($access = $this->dept_access->findFirst(array('dept_id' => $deptId)))
+                return $this->_roles[$deptId] = $access->role;
 
-        if (!$cached) {
-            $cached = $this->config->get('lang');
-            if (!$cached)
-                $cached = Internationalization::getDefaultLanguage();
+            if (!$this->usePrimaryRoleOnAssignment())
+                // View only access
+                return new Role(array());
+
+            // Fall through to primary role
         }
-        return $cached;
+        // For the primary department, use the primary role
+        return $this->role;
+    }
+
+    function hasPerm($perm, $global=true) {
+        if ($global)
+            return $this->getPermission()->has($perm);
+        if ($this->getRole()->hasPerm($perm))
+            return true;
+        foreach ($this->dept_access as $da)
+            if ($da->role->hasPerm($perm))
+                return true;
+        return false;
+    }
+
+    function canManageTickets() {
+        return $this->hasPerm(TicketModel::PERM_DELETE, false)
+                || $this->hasPerm(TicketModel::PERM_TRANSFER, false)
+                || $this->hasPerm(TicketModel::PERM_ASSIGN, false)
+                || $this->hasPerm(TicketModel::PERM_CLOSE, false);
     }
 
     function isManager() {
@@ -301,28 +477,24 @@ implements EmailContact {
         return TRUE;
     }
 
-    function isGroupActive() {
-        return ($this->ht['group_enabled']);
-    }
-
     function isactive() {
-        return ($this->ht['isactive']);
+        return $this->isactive;
     }
 
     function isVisible() {
-         return ($this->ht['isvisible']);
+         return $this->isvisible;
     }
 
     function onVacation() {
-        return ($this->ht['onvacation']);
+        return $this->onvacation;
     }
 
     function isAvailable() {
-        return ($this->isactive() && $this->isGroupActive() && !$this->onVacation());
+        return ($this->isactive() && !$this->onVacation());
     }
 
     function showAssignedOnly() {
-        return ($this->ht['assigned_only']);
+        return $this->assigned_only;
     }
 
     function isAccessLimited() {
@@ -330,7 +502,7 @@ implements EmailContact {
     }
 
     function isAdmin() {
-        return ($this->ht['isadmin']);
+        return $this->isadmin;
     }
 
     function isTeamMember($teamId) {
@@ -341,101 +513,97 @@ implements EmailContact {
         return ($deptId && in_array($deptId, $this->getDepts()) && !$this->isAccessLimited());
     }
 
-    function canCreateTickets() {
-        return ($this->ht['can_create_tickets']);
+    function showAssignedTickets() {
+        return $this->show_assigned_tickets;
     }
 
-    function canEditTickets() {
-        return ($this->ht['can_edit_tickets']);
-    }
+    function getTeams() {
 
-    function canDeleteTickets() {
-        return ($this->ht['can_delete_tickets']);
-    }
+        if (!isset($this->_teams)) {
+            $this->_teams = array();
+            foreach ($this->teams as $team)
+                 $this->_teams[] = $team->team_id;
+        }
 
-    function canCloseTickets() {
-        return ($this->ht['can_close_tickets']);
+        return $this->_teams;
     }
+    /* stats */
 
-    function canPostReply() {
-        return ($this->ht['can_post_ticket_reply']);
+    function resetStats() {
+        $this->stats = array();
     }
 
-    function canViewStaffStats() {
-        return ($this->ht['can_view_staff_stats']);
-    }
+    /* returns staff's quick stats - used on nav menu...etc && warnings */
+    function getTicketsStats() {
 
-    function canAssignTickets() {
-        return ($this->ht['can_assign_tickets']);
-    }
+        if(!$this->stats['tickets'])
+            $this->stats['tickets'] = Ticket::getStaffStats($this);
 
-    function canTransferTickets() {
-        return ($this->ht['can_transfer_tickets']);
+        return  $this->stats['tickets'];
     }
 
-    function canBanEmails() {
-        return ($this->ht['can_ban_emails']);
+    function getNumAssignedTickets() {
+        return ($stats=$this->getTicketsStats())?$stats['assigned']:0;
     }
 
-    function canManageTickets() {
-        return ($this->isAdmin()
-                 || $this->canDeleteTickets()
-                    || $this->canCloseTickets());
+    function getNumClosedTickets() {
+        return ($stats=$this->getTicketsStats())?$stats['closed']:0;
     }
 
-    function canManagePremade() {
-        return ($this->ht['can_manage_premade']);
-    }
+    function getTasksStats() {
 
-    function canManageCannedResponses() {
-        return $this->canManagePremade();
+        if (!$this->stats['tasks'])
+            $this->stats['tasks'] = Task::getStaffStats($this);
+
+        return  $this->stats['tasks'];
     }
 
-    function canManageFAQ() {
-        return ($this->ht['can_manage_faq']);
+    function getNumAssignedTasks() {
+        return ($stats=$this->getTasksStats()) ? $stats['assigned'] : 0;
     }
 
-    function canManageFAQs() {
-        return $this->canManageFAQ();
+    function getNumClosedTasks() {
+        return ($stats=$this->getTasksStats()) ? $stats['closed'] : 0;
     }
 
-    function showAssignedTickets() {
-        return ($this->ht['show_assigned_tickets']);
+    function getExtraAttr($attr=false, $default=null) {
+        if (!isset($this->_extra) && isset($this->extra))
+            $this->_extra = JsonDataParser::decode($this->extra);
+
+        return $attr
+            ? (isset($this->_extra[$attr]) ? $this->_extra[$attr] : $default)
+            : $this->_extra;
     }
 
-    function getTeams() {
+    function setExtraAttr($attr, $value, $commit=true) {
+        $this->getExtraAttr();
+        $this->_extra[$attr] = $value;
+        $this->extra = JsonDataEncoder::encode($this->_extra);
 
-        if(!$this->teams) {
-            $sql='SELECT team_id FROM '.TEAM_MEMBER_TABLE
-                .' WHERE staff_id='.db_input($this->getId());
-            if(($res=db_query($sql)) && db_num_rows($res))
-                while(list($id)=db_fetch_row($res))
-                    $this->teams[] = $id;
+        if ($commit) {
+            $this->save();
         }
-
-        return $this->teams;
     }
-    /* stats */
 
-    function resetStats() {
-        $this->stats = array();
+    function getPermission() {
+        if (!isset($this->_perm)) {
+            $this->_perm = new RolePermission($this->permissions);
+        }
+        return $this->_perm;
     }
 
-    /* returns staff's quick stats - used on nav menu...etc && warnings */
-    function getTicketsStats() {
-
-        if(!$this->stats['tickets'])
-            $this->stats['tickets'] = Ticket::getStaffStats($this);
-
-        return  $this->stats['tickets'];
+    function getPermissionInfo() {
+        return $this->getPermission()->getInfo();
     }
 
-    function getNumAssignedTickets() {
-        return ($stats=$this->getTicketsStats())?$stats['assigned']:0;
-    }
+    function onLogin($bk) {
+        // Update last apparent language preference
+        $this->setExtraAttr('browser_lang',
+            Internationalization::getCurrentLanguage(),
+            false);
 
-    function getNumClosedTickets() {
-        return ($stats=$this->getTicketsStats())?$stats['closed']:0;
+        $this->lastlogin = SqlFunction::NOW();
+        $this->save();
     }
 
     //Staff profile update...unfortunately we have to separate it from admin update to avoid potential issues
@@ -445,7 +613,7 @@ implements EmailContact {
         $vars['firstname']=Format::striptags($vars['firstname']);
         $vars['lastname']=Format::striptags($vars['lastname']);
 
-        if($this->getId()!=$vars['id'])
+        if (isset($this->staff_id) && $this->getId() != $vars['id'])
             $errors['err']=__('Internal error occurred');
 
         if(!$vars['firstname'])
@@ -458,7 +626,8 @@ implements EmailContact {
             $errors['email']=__('Valid email is required');
         elseif(Email::getIdByEmail($vars['email']))
             $errors['email']=__('Already in-use as system email');
-        elseif(($uid=Staff::getIdByEmail($vars['email'])) && $uid!=$this->getId())
+        elseif (($uid=static::getIdByEmail($vars['email']))
+                && (!isset($this->staff_id) || $uid!=$this->getId()))
             $errors['email']=__('Email already in-use by another agent');
 
         if($vars['phone'] && !Validator::is_phone($vars['phone']))
@@ -467,106 +636,87 @@ implements EmailContact {
         if($vars['mobile'] && !Validator::is_phone($vars['mobile']))
             $errors['mobile']=__('Valid phone number is required');
 
-        if($vars['passwd1'] || $vars['passwd2'] || $vars['cpasswd']) {
-
-            if(!$vars['passwd1'])
-                $errors['passwd1']=__('New password is required');
-            elseif($vars['passwd1'] && strlen($vars['passwd1'])<6)
-                $errors['passwd1']=__('Password must be at least 6 characters');
-            elseif($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2']))
-                $errors['passwd2']=__('Passwords do not match');
-
-            if (($rtoken = $_SESSION['_staff']['reset-token'])) {
-                $_config = new Config('pwreset');
-                if ($_config->get($rtoken) != $this->getId())
-                    $errors['err'] =
-                        __('Invalid reset token. Logout and try again');
-                elseif (!($ts = $_config->lastModified($rtoken))
-                        && ($cfg->getPwResetWindow() < (time() - strtotime($ts))))
-                    $errors['err'] =
-                        __('Invalid reset token. Logout and try again');
-            }
-            elseif(!$vars['cpasswd'])
-                $errors['cpasswd']=__('Current password is required');
-            elseif(!$this->cmp_passwd($vars['cpasswd']))
-                $errors['cpasswd']=__('Invalid current password!');
-            elseif(!strcasecmp($vars['passwd1'], $vars['cpasswd']))
-                $errors['passwd1']=__('New password MUST be different from the current password!');
-        }
-
-        if(!$vars['timezone_id'])
-            $errors['timezone_id']=__('Time zone selection is required');
-
         if($vars['default_signature_type']=='mine' && !$vars['signature'])
             $errors['default_signature_type'] = __("You don't have a signature");
 
-        if($errors) return false;
-
-        $this->config->set('lang', $vars['lang']);
-        $_SESSION['staff:lang'] = null;
-        TextDomain::configureForUser($this);
-
-        $sql='UPDATE '.STAFF_TABLE.' SET updated=NOW() '
-            .' ,firstname='.db_input($vars['firstname'])
-            .' ,lastname='.db_input($vars['lastname'])
-            .' ,email='.db_input($vars['email'])
-            .' ,phone="'.db_input(Format::phone($vars['phone']),false).'"'
-            .' ,phone_ext='.db_input($vars['phone_ext'])
-            .' ,mobile="'.db_input(Format::phone($vars['mobile']),false).'"'
-            .' ,signature='.db_input(Format::sanitize($vars['signature']))
-            .' ,timezone_id='.db_input($vars['timezone_id'])
-            .' ,daylight_saving='.db_input(isset($vars['daylight_saving'])?1:0)
-            .' ,show_assigned_tickets='.db_input(isset($vars['show_assigned_tickets'])?1:0)
-            .' ,max_page_size='.db_input($vars['max_page_size'])
-            .' ,auto_refresh_rate='.db_input($vars['auto_refresh_rate'])
-            .' ,default_signature_type='.db_input($vars['default_signature_type'])
-            .' ,default_paper_size='.db_input($vars['default_paper_size']);
-
-
-        if($vars['passwd1']) {
-            $sql.=' ,change_passwd=0, passwdreset=NOW(), passwd='.db_input(Passwd::hash($vars['passwd1']));
-            $info = array('password' => $vars['passwd1']);
-            Signal::send('auth.pwchange', $this, $info);
-            $this->cancelResetTokens();
-        }
-
-        $sql.=' WHERE staff_id='.db_input($this->getId());
-
-        //echo $sql;
-
-        return (db_query($sql));
-    }
-
-
-    function updateTeams($teams) {
-
-        if($teams) {
-            foreach($teams as $k=>$id) {
-                $sql='INSERT IGNORE INTO '.TEAM_MEMBER_TABLE.' SET updated=NOW() '
-                    .' ,staff_id='.db_input($this->getId()).', team_id='.db_input($id);
-                db_query($sql);
+        // Update the user's password if requested
+        if ($vars['passwd1']) {
+            try {
+                $this->setPassword($vars['passwd1'], $vars['cpasswd']);
+            }
+            catch (BadPassword $ex) {
+                $errors['passwd1'] = $ex->getMessage();
+            }
+            catch (PasswordUpdateFailed $ex) {
+                // TODO: Add a warning banner or crash the update
             }
         }
 
-        $sql='DELETE FROM '.TEAM_MEMBER_TABLE.' WHERE staff_id='.db_input($this->getId());
-        if($teams)
-            $sql.=' AND team_id NOT IN('.implode(',', db_input($teams)).')';
-
-        db_query($sql);
-
-        return true;
-    }
-
-    function update($vars, &$errors) {
-
-        if(!$this->save($this->getId(), $vars, $errors))
+        $this->firstname = $vars['firstname'];
+        $this->lastname = $vars['lastname'];
+        $this->email = $vars['email'];
+        $this->phone = Format::phone($vars['phone']);
+        $this->phone_ext = $vars['phone_ext'];
+        $this->mobile = Format::phone($vars['mobile']);
+        $this->signature = Format::sanitize($vars['signature']);
+        $this->timezone = $vars['timezone'];
+        $this->locale = $vars['locale'];
+        if (!$cfg->showAssignedTickets())
+            // Allow local unsetting if unset globally
+            $this->show_assigned_tickets = isset($vars['show_assigned_tickets']) ? 1 : 0;
+        $this->max_page_size = $vars['max_page_size'];
+        $this->auto_refresh_rate = $vars['auto_refresh_rate'];
+        $this->default_signature_type = $vars['default_signature_type'];
+        $this->default_paper_size = $vars['default_paper_size'];
+        $this->lang = $vars['lang'];
+        $this->onvacation = isset($vars['onvacation']) ? 1 : 0;
+
+        if (isset($vars['avatar_code']))
+          $this->setExtraAttr('avatar', $vars['avatar_code']);
+
+        if ($errors)
             return false;
 
-        $this->updateTeams($vars['teams']);
-        $this->reload();
-
-        Signal::send('model.modified', $this);
+        $_SESSION['::lang'] = null;
+        TextDomain::configureForUser($this);
 
+        // Update the config information
+        $_config = new Config('staff.'.$this->getId());
+        $_config->updateAll(array(
+                    'datetime_format' => $vars['datetime_format'],
+                    'default_from_name' => $vars['default_from_name'],
+                    'thread_view_order' => $vars['thread_view_order'],
+                    )
+                );
+        $this->_config = $_config->getInfo();
+
+        return $this->save();
+    }
+
+    function updateTeams($membership, &$errors) {
+        $dropped = array();
+        foreach ($this->teams as $TM)
+            $dropped[$TM->team_id] = 1;
+
+        reset($membership);
+        while(list(, list($team_id, $alerts)) = each($membership)) {
+            $member = $this->teams->findFirst(array('team_id' => $team_id));
+            if (!$member) {
+                $this->teams->add($member = new TeamMember(array(
+                    'team_id' => $team_id,
+                )));
+            }
+            $member->setAlerts($alerts);
+            if (!$errors)
+                $member->save();
+            unset($dropped[$member->team_id]);
+        }
+        if (!$errors && $dropped) {
+            $member = $this->teams
+                ->filter(array('team_id__in' => array_keys($dropped)))
+                ->delete();
+            $this->teams->reset();
+        }
         return true;
     }
 
@@ -574,101 +724,104 @@ implements EmailContact {
         global $thisstaff;
 
         if (!$thisstaff || $this->getId() == $thisstaff->getId())
-            return 0;
-
-        $sql='DELETE FROM '.STAFF_TABLE
-            .' WHERE staff_id = '.db_input($this->getId()).' LIMIT 1';
-        if(db_query($sql) && ($num=db_affected_rows())) {
-            // DO SOME HOUSE CLEANING
-            //Move remove any ticket assignments...TODO: send alert to Dept. manager?
-            db_query('UPDATE '.TICKET_TABLE.' SET staff_id=0 WHERE staff_id='.db_input($this->getId()));
-
-            //Update the poster and clear staff_id on ticket thread table.
-            db_query('UPDATE '.TICKET_THREAD_TABLE
-                    .' SET staff_id=0, poster= '.db_input($this->getName()->getOriginal())
-                    .' WHERE staff_id='.db_input($this->getId()));
-
-            //Cleanup Team membership table.
-            db_query('DELETE FROM '.TEAM_MEMBER_TABLE.' WHERE staff_id='.db_input($this->getId()));
+            return false;
 
-            // Destrory config settings
-            $this->config->destroy();
-        }
+        if (!parent::delete())
+            return false;
 
-        Signal::send('model.deleted', $this);
+        // DO SOME HOUSE CLEANING
+        //Move remove any ticket assignments...TODO: send alert to Dept. manager?
+        Ticket::objects()
+            ->filter(array('staff_id' => $this->getId()))
+            ->update(array('staff_id' => 0));
+
+        //Update the poster and clear staff_id on ticket thread table.
+        ThreadEntry::objects()
+            ->filter(array('staff_id' => $this->getId()))
+            ->update(array(
+                'staff_id' => 0,
+                'poster' => $this->getName()->getOriginal(),
+            ));
+
+        // Cleanup Team membership table.
+        TeamMember::objects()
+            ->filter(array('staff_id'=>$this->getId()))
+            ->delete();
+
+        // Cleanup staff dept access
+        StaffDeptAccess::objects()
+            ->filter(array('staff_id'=>$this->getId()))
+            ->delete();
 
-        return $num;
+        return true;
     }
 
     /**** Static functions ********/
-    function getStaffMembers($availableonly=false) {
+    static function lookup($var) {
+        if (is_array($var))
+            return parent::lookup($var);
+        elseif (is_numeric($var))
+            return parent::lookup(array('staff_id'=>$var));
+        elseif (Validator::is_email($var))
+            return parent::lookup(array('email'=>$var));
+        elseif (is_string($var))
+            return parent::lookup(array('username'=>$var));
+        else
+            return null;
+    }
+
+    static function getStaffMembers($criteria=array()) {
         global $cfg;
 
-        $sql = 'SELECT s.staff_id, s.firstname, s.lastname FROM '
-            .STAFF_TABLE.' s ';
+        $members = static::objects();
 
-        if($availableonly) {
-            $sql.=' INNER JOIN '.GROUP_TABLE.' g ON(g.group_id=s.group_id AND g.group_enabled=1) '
-                 .' WHERE s.isactive=1 AND s.onvacation=0';
+        if (isset($criteria['available'])) {
+            $members = $members->filter(array(
+                'onvacation' => 0,
+                'isactive' => 1,
+            ));
         }
 
-        switch ($cfg->getDefaultNameFormat()) {
+        switch ($cfg->getAgentNameFormat()) {
         case 'last':
         case 'lastfirst':
         case 'legal':
-            $sql .= ' ORDER BY s.lastname, s.firstname';
+            $members->order_by('lastname', 'firstname');
             break;
 
         default:
-            $sql .= ' ORDER BY s.firstname, s.lastname';
+            $members->order_by('firstname', 'lastname');
         }
 
         $users=array();
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while(list($id, $fname, $lname) = db_fetch_row($res))
-                $users[$id] = new PersonsName(
-                    array('first' => $fname, 'last' => $lname));
+        foreach ($members as $M) {
+            $users[$M->getId()] = $M->getName();
         }
 
         return $users;
     }
 
-    function getAvailableStaffMembers() {
-        return self::getStaffMembers(true);
-    }
-
-    function getIdByUsername($username) {
-
-        $sql='SELECT staff_id FROM '.STAFF_TABLE.' WHERE username='.db_input($username);
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id) = db_fetch_row($res);
-
-        return $id;
+    static function getAvailableStaffMembers() {
+        return self::getStaffMembers(array('available'=>true));
     }
-    function getIdByEmail($email) {
-
-        $sql='SELECT staff_id FROM '.STAFF_TABLE.' WHERE email='.db_input($email);
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id) = db_fetch_row($res);
 
-        return $id;
+    static function getIdByUsername($username) {
+        $row = static::objects()->filter(array('username' => $username))
+            ->values_flat('staff_id')->first();
+        return $row ? $row[0] : 0;
     }
 
-    function lookup($id) {
-        return ($id && ($staff= new Staff($id)) && $staff->getId()) ? $staff : null;
+    static function getIdByEmail($email) {
+        $row = static::objects()->filter(array('email' => $email))
+            ->values_flat('staff_id')->first();
+        return $row ? $row[0] : 0;
     }
 
 
-    function create($vars, &$errors) {
-        if(($id=self::save(0, $vars, $errors)) && ($staff=Staff::lookup($id))) {
-            if ($vars['teams'])
-                $staff->updateTeams($vars['teams']);
-            if ($vars['welcome_email'])
-                $staff->sendResetEmail('registration-staff', false);
-            Signal::send('model.created', $staff);
-        }
-
-        return $id;
+    static function create($vars=false) {
+        $staff = new static($vars);
+        $staff->created = SqlFunction::NOW();
+        return $staff;
     }
 
     function cancelResetTokens() {
@@ -683,11 +836,11 @@ implements EmailContact {
     function sendResetEmail($template='pwreset-staff', $log=true) {
         global $ost, $cfg;
 
-        $content = Page::lookup(Page::getIdByType($template));
+        $content = Page::lookupByType($template);
         $token = Misc::randCode(48); // 290-bits
 
         if (!$content)
-            return new Error(/* @trans */ 'Unable to retrieve password reset email template');
+            return new BaseError(/* @trans */ 'Unable to retrieve password reset email template');
 
         $vars = array(
             'url' => $ost->getConfig()->getBaseUrl(),
@@ -721,9 +874,10 @@ implements EmailContact {
                 $email->getEmail()
             ), false);
 
+        $lang = $this->lang ?: $this->getExtraAttr('browser_lang');
         $msg = $ost->replaceTemplateVariables(array(
-            'subj' => $content->getName(),
-            'body' => $content->getBody(),
+            'subj' => $content->getLocalName($lang),
+            'body' => $content->getLocalBody($lang),
         ), $vars);
 
         $_config = new Config('pwreset');
@@ -733,13 +887,80 @@ implements EmailContact {
             $msg['body']);
     }
 
-    function save($id, $vars, &$errors) {
+    static function importCsv($stream, $defaults=array(), $callback=false) {
+        require_once INCLUDE_DIR . 'class.import.php';
+
+        $importer = new CsvImporter($stream);
+        $imported = 0;
+        $fields = array(
+            'firstname' => new TextboxField(array(
+                'label' => __('First name'),
+            )),
+            'lastname' => new TextboxField(array(
+                'label' => __('Last name'),
+            )),
+            'email' => new TextboxField(array(
+                'label' => __('Email Address'),
+                'configuration' => array(
+                    'validator' => 'email',
+                ),
+            )),
+            'username' => new TextboxField(array(
+                'label' => __('Username'),
+                'validators' => function($self, $value) {
+                    if (!Validator::is_username($value))
+                        $self->addError('Not a valid username');
+                },
+            )),
+        );
+        $form = new SimpleForm($fields);
+
+        try {
+            db_autocommit(false);
+            $errors = array();
+            $records = $importer->importCsv($form->getFields(), $defaults);
+            foreach ($records as $data) {
+                if (!isset($data['email']) || !isset($data['username']))
+                    throw new ImportError('Both `username` and `email` fields are required');
+
+                if ($agent = self::lookup(array('username' => $data['username']))) {
+                    // TODO: Update the user
+                }
+                elseif ($agent = self::create($data, $errors)) {
+                    if ($callback)
+                        $callback($agent, $data);
+                    $agent->save();
+                }
+                else {
+                    throw new ImportError(sprintf(__('Unable to import (%s): %s'),
+                        $data['username'],
+                        print_r($errors, true)
+                    ));
+                }
+                $imported++;
+            }
+            db_autocommit(true);
+        }
+        catch (Exception $ex) {
+            db_rollback();
+            return $ex->getMessage();
+        }
+        return $imported;
+    }
+
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($refetch || $this->dirty);
+    }
+
+    function update($vars, &$errors) {
 
         $vars['username']=Format::striptags($vars['username']);
         $vars['firstname']=Format::striptags($vars['firstname']);
         $vars['lastname']=Format::striptags($vars['lastname']);
 
-        if($id && $id!=$vars['id'])
+        if (isset($this->staff_id) && $this->getId() != $vars['id'])
             $errors['err']=__('Internal Error');
 
         if(!$vars['firstname'])
@@ -750,14 +971,16 @@ implements EmailContact {
         $error = '';
         if(!$vars['username'] || !Validator::is_username($vars['username'], $error))
             $errors['username']=($error) ? $error : __('Username is required');
-        elseif(($uid=Staff::getIdByUsername($vars['username'])) && $uid!=$id)
+        elseif (($uid=static::getIdByUsername($vars['username']))
+                && (!isset($this->staff_id) || $uid!=$this->getId()))
             $errors['username']=__('Username already in use');
 
         if(!$vars['email'] || !Validator::is_valid_email($vars['email']))
             $errors['email']=__('Valid email is required');
         elseif(Email::getIdByEmail($vars['email']))
             $errors['email']=__('Already in use system email');
-        elseif(($uid=Staff::getIdByEmail($vars['email'])) && $uid!=$id)
+        elseif (($uid=static::getIdByEmail($vars['email']))
+                && (!isset($this->staff_id) || $uid!=$this->getId()))
             $errors['email']=__('Email already in use by another agent');
 
         if($vars['phone'] && !Validator::is_phone($vars['phone']))
@@ -766,37 +989,18 @@ implements EmailContact {
         if($vars['mobile'] && !Validator::is_phone($vars['mobile']))
             $errors['mobile']=__('Valid phone number is required');
 
-        if($vars['passwd1'] || $vars['passwd2'] || !$id) {
-            if($vars['passwd1'] && strcmp($vars['passwd1'], $vars['passwd2'])) {
-                $errors['passwd2']=__('Passwords do not match');
-            }
-            elseif ($vars['backend'] != 'local' || $vars['welcome_email']) {
-                // Password can be omitted
-            }
-            elseif(!$vars['passwd1'] && !$id) {
-                $errors['passwd1']=__('Temporary password is required');
-                $errors['temppasswd']=__('Required');
-            } elseif($vars['passwd1'] && strlen($vars['passwd1'])<6) {
-                $errors['passwd1']=__('Password must be at least 6 characters');
-            }
-        }
-
         if(!$vars['dept_id'])
             $errors['dept_id']=__('Department is required');
-
-        if(!$vars['group_id'])
-            $errors['group_id']=__('Group is required');
-
-        if(!$vars['timezone_id'])
-            $errors['timezone_id']=__('Time zone selection is required');
+        if(!$vars['role_id'])
+            $errors['role_id']=__('Role for primary department is required');
 
         // Ensure we will still have an administrator with access
-        if ($vars['isadmin'] !== '1' || $vars['isactive'] !== '1') {
+        if ($vars['isadmin'] !== '1' || $vars['islocked'] === '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) {
+                if ($count == 1 && $sid == $uid) {
                     $errors['isadmin'] = __(
                         'Cowardly refusing to remove or lock out the only active administrator'
                     );
@@ -804,56 +1008,469 @@ implements EmailContact {
             }
         }
 
-        if($errors) return false;
-
-
-        $sql='SET updated=NOW() '
-            .' ,isadmin='.db_input($vars['isadmin'])
-            .' ,isactive='.db_input($vars['isactive'])
-            .' ,isvisible='.db_input(isset($vars['isvisible'])?1:0)
-            .' ,onvacation='.db_input(isset($vars['onvacation'])?1:0)
-            .' ,assigned_only='.db_input(isset($vars['assigned_only'])?1:0)
-            .' ,dept_id='.db_input($vars['dept_id'])
-            .' ,group_id='.db_input($vars['group_id'])
-            .' ,timezone_id='.db_input($vars['timezone_id'])
-            .' ,daylight_saving='.db_input(isset($vars['daylight_saving'])?1:0)
-            .' ,username='.db_input($vars['username'])
-            .' ,firstname='.db_input($vars['firstname'])
-            .' ,lastname='.db_input($vars['lastname'])
-            .' ,email='.db_input($vars['email'])
-            .' ,backend='.db_input($vars['backend'])
-            .' ,phone="'.db_input(Format::phone($vars['phone']),false).'"'
-            .' ,phone_ext='.db_input($vars['phone_ext'])
-            .' ,mobile="'.db_input(Format::phone($vars['mobile']),false).'"'
-            .' ,signature='.db_input(Format::sanitize($vars['signature']))
-            .' ,notes='.db_input(Format::sanitize($vars['notes']));
-
-        if($vars['passwd1']) {
-            $sql.=' ,passwd='.db_input(Passwd::hash($vars['passwd1']));
-
-            if(isset($vars['change_passwd']))
-                $sql.=' ,change_passwd=1';
+        // Update some things for ::updateAccess to inspect
+        $this->setDepartmentId($vars['dept_id']);
+
+        // Format access update as [array(dept_id, role_id, alerts?)]
+        $access = array();
+        if (isset($vars['dept_access'])) {
+            foreach (@$vars['dept_access'] as $dept_id) {
+                $access[] = array($dept_id, $vars['dept_access_role'][$dept_id],
+                    @$vars['dept_access_alerts'][$dept_id]);
+            }
+        }
+        $this->updateAccess($access, $errors);
+        $this->setExtraAttr('def_assn_role',
+            isset($vars['assign_use_pri_role']), false);
+
+        // Format team membership as [array(team_id, alerts?)]
+        $teams = array();
+        if (isset($vars['teams'])) {
+            foreach (@$vars['teams'] as $team_id) {
+                $teams[] = array($team_id, @$vars['team_alerts'][$team_id]);
+            }
         }
-        elseif (!isset($vars['change_passwd']))
-            $sql .= ' ,change_passwd=0';
+        $this->updateTeams($teams, $errors);
+
+        // Update the local permissions
+        $this->updatePerms($vars['perms'], $errors);
+
+        $this->isadmin = $vars['isadmin'];
+        $this->isactive = isset($vars['islocked']) ? 0 : 1;
+        $this->isvisible = isset($vars['isvisible'])?1:0;
+        $this->onvacation = isset($vars['onvacation'])?1:0;
+        $this->assigned_only = isset($vars['assigned_only'])?1:0;
+        $this->role_id = $vars['role_id'];
+        $this->username = $vars['username'];
+        $this->firstname = $vars['firstname'];
+        $this->lastname = $vars['lastname'];
+        $this->email = $vars['email'];
+        $this->backend = $vars['backend'];
+        $this->phone = Format::phone($vars['phone']);
+        $this->phone_ext = $vars['phone_ext'];
+        $this->mobile = Format::phone($vars['mobile']);
+        $this->notes = Format::sanitize($vars['notes']);
+
+        if ($errors)
+            return false;
 
-        if($id) {
-            $sql='UPDATE '.STAFF_TABLE.' '.$sql.' WHERE staff_id='.db_input($id);
-            if(db_query($sql) && db_affected_rows())
-                return true;
+        if ($this->save()) {
+            if ($vars['welcome_email'])
+                $this->sendResetEmail('registration-staff', false);
+            return true;
+        }
 
+        if (isset($this->staff_id)) {
             $errors['err']=sprintf(__('Unable to update %s.'), __('this agent'))
                .' '.__('Internal error occurred');
         } else {
-            $sql='INSERT INTO '.STAFF_TABLE.' '.$sql.', created=NOW()';
-            if(db_query($sql) && ($uid=db_insert_id()))
-                return $uid;
-
             $errors['err']=sprintf(__('Unable to create %s.'), __('this agent'))
                .' '.__('Internal error occurred');
         }
-
         return false;
     }
+
+    /**
+     * Parameters:
+     * $access - (<array($dept_id, $role_id, $alerts)>) a list of the complete,
+     *      extended access for this agent. Any the agent currently has, which
+     *      is not listed will be removed.
+     * $errors - (<array>) list of error messages from the process, which will
+     *      be indexed by the dept_id number.
+     */
+    function updateAccess($access, &$errors) {
+        reset($access);
+        $dropped = array();
+        foreach ($this->dept_access as $DA)
+            $dropped[$DA->dept_id] = 1;
+        while (list(, list($dept_id, $role_id, $alerts)) = each($access)) {
+            unset($dropped[$dept_id]);
+            if (!$role_id || !Role::lookup($role_id))
+                $errors['dept_access'][$dept_id] = __('Select a valid role');
+            if (!$dept_id || !Dept::lookup($dept_id))
+                $errors['dept_access'][$dept_id] = __('Select a valid departent');
+            if ($dept_id == $this->getDeptId())
+                $errors['dept_access'][$dept_id] = __('Agent already has access to this department');
+            $da = $this->dept_access->findFirst(array('dept_id' => $dept_id));
+            if (!isset($da)) {
+                $da = new StaffDeptAccess(array(
+                    'dept_id' => $dept_id, 'role_id' => $role_id
+                ));
+                $this->dept_access->add($da);
+            }
+            else {
+                $da->role_id = $role_id;
+            }
+            $da->setAlerts($alerts);
+            if (!$errors)
+                $da->save();
+        }
+        if (!$errors && $dropped) {
+            $this->dept_access
+                ->filter(array('dept_id__in' => array_keys($dropped)))
+                ->delete();
+            $this->dept_access->reset();
+        }
+        return !$errors;
+    }
+
+    function updatePerms($vars, &$errors=array()) {
+        if (!$vars) {
+            $this->permissions = '';
+            return;
+        }
+        $permissions = $this->getPermission();
+        foreach (RolePermission::allPermissions() as $g => $perms) {
+            foreach ($perms as $k => $v) {
+                $permissions->set($k, in_array($k, $vars) ? 1 : 0);
+            }
+        }
+        $this->permissions = $permissions->toJson();
+        return true;
+    }
+
+}
+
+interface RestrictedAccess {
+    function checkStaffPerm($staff);
+}
+
+class StaffDeptAccess extends VerySimpleModel {
+    static $meta = array(
+        'table' => STAFF_DEPT_TABLE,
+        'pk' => array('staff_id', 'dept_id'),
+        'select_related' => array('dept', 'role'),
+        'joins' => array(
+            'dept' => array(
+                'constraint' => array('dept_id' => 'Dept.id'),
+                // FIXME: The ORM needs a way to support
+                //        staff__dept_access__dept performing a LEFT join b/c
+                //        staff__dept_access is LEFT
+                'null' => true,
+            ),
+            'staff' => array(
+                'constraint' => array('staff_id' => 'Staff.staff_id'),
+            ),
+            'role' => array(
+                'constraint' => array('role_id' => 'Role.id'),
+            ),
+        ),
+    );
+
+    const FLAG_ALERTS =     0x0001;
+
+    function isAlertsEnabled() {
+        return $this->flags & self::FLAG_ALERTS != 0;
+    }
+
+    function setFlag($flag, $value) {
+        if ($value)
+            $this->flags |= $flag;
+        else
+            $this->flags &= ~$flag;
+    }
+
+    function setAlerts($value) {
+        $this->setFlag(self::FLAG_ALERTS, $value);
+    }
+}
+
+/**
+ * This form is used to administratively change the password. The
+ * ChangePasswordForm is used for an agent to change their own password.
+ */
+class PasswordResetForm
+extends AbstractForm {
+    function buildFields() {
+        return array(
+            'welcome_email' => new BooleanField(array(
+                'default' => true,
+                'configuration' => array(
+                    'desc' => __('Send the agent a password reset email'),
+                ),
+            )),
+            'passwd1' => new PasswordField(array(
+                'placeholder' => __('New Password'),
+                'required' => true,
+                'configuration' => array(
+                    'classes' => 'span12',
+                ),
+                'visibility' => new VisibilityConstraint(
+                    new Q(array('welcome_email' => false)),
+                    VisibilityConstraint::HIDDEN
+                ),
+            )),
+            'passwd2' => new PasswordField(array(
+                'placeholder' => __('Confirm Password'),
+                'required' => true,
+                'configuration' => array(
+                    'classes' => 'span12',
+                ),
+                'visibility' => new VisibilityConstraint(
+                    new Q(array('welcome_email' => false)),
+                    VisibilityConstraint::HIDDEN
+                ),
+            )),
+            'change_passwd' => new BooleanField(array(
+                'default' => true,
+                'configuration' => array(
+                    'desc' => __('Require password change at next login'),
+                    'classes' => 'form footer',
+                ),
+                'visibility' => new VisibilityConstraint(
+                    new Q(array('welcome_email' => false)),
+                    VisibilityConstraint::HIDDEN
+                ),
+            )),
+        );
+    }
+
+    function validate($clean) {
+        if ($clean['passwd1'] != $clean['passwd2'])
+            $this->getField('passwd1')->addError(__('Passwords do not match'));
+    }
+}
+
+class PasswordChangeForm
+extends AbstractForm {
+    function buildFields() {
+        $fields = array(
+            'current' => new PasswordField(array(
+                'placeholder' => __('Current Password'),
+                'required' => true,
+                'configuration' => array(
+                    'autofocus' => true,
+                ),
+            )),
+            'passwd1' => new PasswordField(array(
+                'label' => __('Enter a new password'),
+                'placeholder' => __('New Password'),
+                'required' => true,
+            )),
+            'passwd2' => new PasswordField(array(
+                'placeholder' => __('Confirm Password'),
+                'required' => true,
+            )),
+        );
+
+        // When using the password reset system, the current password is not
+        // required for agents.
+        if (isset($_SESSION['_staff']['reset-token'])) {
+            unset($fields['current']);
+            $fields['passwd1']->set('configuration', array('autofocus' => true));
+        }
+        else {
+            $fields['passwd1']->set('layout',
+                new GridFluidCell(12, array('style' => 'padding-top: 20px'))
+            );
+        }
+        return $fields;
+    }
+
+    function getInstructions() {
+        return __('Confirm your current password and enter a new password to continue');
+    }
+
+    function validate($clean) {
+        if ($clean['passwd1'] != $clean['passwd2'])
+            $this->getField('passwd1')->addError(__('Passwords do not match'));
+    }
+}
+
+class ResetAgentPermissionsForm
+extends AbstractForm {
+    function buildFields() {
+        $permissions = array();
+        foreach (RolePermission::allPermissions() as $g => $perms) {
+            foreach ($perms as $k => $v) {
+                if (!$v['primary'])
+                    continue;
+                $permissions[$g][$k] = "{$v['title']} — {$v['desc']}";
+            }
+        }
+        return array(
+            'clone' => new ChoiceField(array(
+                'default' => 0,
+                'choices' =>
+                    array(0 => '— '.__('Clone an existing agent').' —')
+                    + Staff::getStaffMembers(),
+                'configuration' => array(
+                    'classes' => 'span12',
+                ),
+            )),
+            'perms' => new ChoiceField(array(
+                'choices' => $permissions,
+                'widget' => 'TabbedBoxChoicesWidget',
+                'configuration' => array(
+                    'multiple' => true,
+                ),
+            )),
+        );
+    }
+
+    function getClean() {
+        $clean = parent::getClean();
+        // Index permissions as ['ticket.edit' => 1]
+        $clean['perms'] = array_keys($clean['perms']);
+        return $clean;
+    }
+
+    function render($staff=true, $title=false, $options=array()) {
+        return parent::render($staff, $title, $options + array('template' => 'dynamic-form-simple.tmpl.php'));
+    }
+}
+
+class ChangeDepartmentForm
+extends AbstractForm {
+    function buildFields() {
+        return array(
+            'dept_id' => new ChoiceField(array(
+                'default' => 0,
+                'required' => true,
+                'label' => __('Primary Department'),
+                'choices' =>
+                    array(0 => '— '.__('Primary Department').' —')
+                    + Dept::getDepartments(),
+                'configuration' => array(
+                    'classes' => 'span12',
+                ),
+            )),
+            'role_id' => new ChoiceField(array(
+                'default' => 0,
+                'required' => true,
+                'label' => __('Primary Role'),
+                'choices' =>
+                    array(0 => '— '.__('Corresponding Role').' —')
+                    + Role::getRoles(),
+                'configuration' => array(
+                    'classes' => 'span12',
+                ),
+            )),
+            'eavesdrop' => new BooleanField(array(
+                'configuration' => array(
+                    'desc' => __('Maintain access to current primary department'),
+                    'classes' => 'form footer',
+                ),
+            )),
+            // alerts?
+        );
+    }
+
+    function getInstructions() {
+        return __('Change the primary department and primary role of the selected agents');
+    }
+
+    function getClean() {
+        $clean = parent::getClean();
+        $clean['eavesdrop'] = $clean['eavesdrop'] ? 1 : 0;
+        return $clean;
+    }
+
+    function render($staff=true, $title=false, $options=array()) {
+        return parent::render($staff, $title, $options + array('template' => 'dynamic-form-simple.tmpl.php'));
+    }
+}
+
+class StaffQuickAddForm
+extends AbstractForm {
+    static $layout = 'GridFormLayout';
+
+    function buildFields() {
+        global $cfg;
+
+        return array(
+            'firstname' => new TextboxField(array(
+                'required' => true,
+                'configuration' => array(
+                    'placeholder' => __("First Name"),
+                    'autofocus' => true,
+                ),
+                'layout' => new GridFluidCell(6),
+            )),
+            'lastname' => new TextboxField(array(
+                'required' => true,
+                'configuration' => array(
+                    'placeholder' => __("Last Name"),
+                ),
+                'layout' => new GridFluidCell(6),
+            )),
+            'email' => new TextboxField(array(
+                'required' => true,
+                'configuration' => array(
+                    'validator' => 'email',
+                    'placeholder' => __('Email Address — e.g. me@mycompany.com'),
+                    'length' => 128,
+                  ),
+            )),
+            'dept_id' => new ChoiceField(array(
+                'label' => __('Department'),
+                'required' => true,
+                'choices' => Dept::getDepartments(),
+                'default' => $cfg->getDefaultDeptId(),
+                'layout' => new GridFluidCell(6),
+            )),
+            'role_id' => new ChoiceField(array(
+                'label' => __('Primary Role'),
+                'required' => true,
+                'choices' =>
+                    array(0 => __('Select Role'))
+                    + Role::getRoles(),
+                'layout' => new GridFluidCell(6),
+            )),
+            'isadmin' => new BooleanField(array(
+                'label' => __('Account Type'),
+                'configuration' => array(
+                    'desc' => __('Agent has access to the admin panel'),
+                ),
+                'layout' => new GridFluidCell(6),
+            )),
+            'welcome_email' => new BooleanField(array(
+                'configuration' => array(
+                    'desc' => __('Send a welcome email with login information'),
+                ),
+                'default' => true,
+                'layout' => new GridFluidCell(12, array('style' => 'padding-top: 50px')),
+            )),
+            'passwd1' => new PasswordField(array(
+                'required' => true,
+                'configuration' => array(
+                    'placeholder' => __("Temporary Password"),
+                ),
+                'visibility' => new VisibilityConstraint(
+                    new Q(array('welcome_email' => false))
+                ),
+                'layout' => new GridFluidCell(6),
+            )),
+            'passwd2' => new PasswordField(array(
+                'required' => true,
+                'configuration' => array(
+                    'placeholder' => __("Confirm Password"),
+                ),
+                'visibility' => new VisibilityConstraint(
+                    new Q(array('welcome_email' => false))
+                ),
+                'layout' => new GridFluidCell(6),
+            )),
+            // TODO: Add role_id drop-down
+        );
+    }
+
+    function getClean() {
+        $clean = parent::getClean();
+        list($clean['username'],) = preg_split('/[^\w.-]/u', $clean['email'], 2);
+        if (mb_strlen($clean['username']) < 3 || Staff::lookup($clean['username']))
+            $clean['username'] = mb_strtolower($clean['firstname']);
+        $clean['perms'] = array(
+            User::PERM_CREATE,
+            User::PERM_EDIT,
+            User::PERM_DELETE,
+            User::PERM_MANAGE,
+            User::PERM_DIRECTORY,
+            Organization::PERM_CREATE,
+            Organization::PERM_EDIT,
+            Organization::PERM_DELETE,
+            FAQ::PERM_MANAGE,
+        );
+        return $clean;
+    }
 }
-?>
diff --git a/include/class.task.php b/include/class.task.php
new file mode 100644
index 0000000000000000000000000000000000000000..5350d9b606d33f949213d78d3589a047f1169e40
--- /dev/null
+++ b/include/class.task.php
@@ -0,0 +1,1561 @@
+<?php
+/*********************************************************************
+    class.task.php
+
+    Task
+
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  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:
+**********************************************************************/
+
+include_once INCLUDE_DIR.'class.role.php';
+
+
+class TaskModel extends VerySimpleModel {
+    static $meta = array(
+        'table' => TASK_TABLE,
+        'pk' => array('id'),
+        'joins' => array(
+            'dept' => array(
+                'constraint' => array('dept_id' => 'Dept.id'),
+            ),
+            'lock' => array(
+                'constraint' => array('lock_id' => 'Lock.lock_id'),
+                'null' => true,
+            ),
+            'staff' => array(
+                'constraint' => array('staff_id' => 'Staff.staff_id'),
+                'null' => true,
+            ),
+            'team' => array(
+                'constraint' => array('team_id' => 'Team.team_id'),
+                'null' => true,
+            ),
+            'thread' => array(
+                'constraint' => array(
+                    'id'  => 'TaskThread.object_id',
+                    "'A'" => 'TaskThread.object_type',
+                ),
+                'list' => false,
+                'null' => false,
+            ),
+            'cdata' => array(
+                'constraint' => array('id' => 'TaskCData.task_id'),
+                'list' => false,
+            ),
+            'entries' => array(
+                'constraint' => array(
+                    "'A'" => 'DynamicFormEntry.object_type',
+                    'id' => 'DynamicFormEntry.object_id',
+                ),
+                'list' => true,
+            ),
+
+            'ticket' => array(
+                'constraint' => array(
+                    'object_type' => "'T'",
+                    'object_id' => 'Ticket.ticket_id',
+                ),
+                'null' => true,
+            ),
+        ),
+    );
+
+    const PERM_CREATE   = 'task.create';
+    const PERM_EDIT     = 'task.edit';
+    const PERM_ASSIGN   = 'task.assign';
+    const PERM_TRANSFER = 'task.transfer';
+    const PERM_REPLY    = 'task.reply';
+    const PERM_CLOSE    = 'task.close';
+    const PERM_DELETE   = 'task.delete';
+
+    static protected $perms = array(
+            self::PERM_CREATE    => array(
+                'title' =>
+                /* @trans */ 'Create',
+                'desc'  =>
+                /* @trans */ 'Ability to create tasks'),
+            self::PERM_EDIT      => array(
+                'title' =>
+                /* @trans */ 'Edit',
+                'desc'  =>
+                /* @trans */ 'Ability to edit tasks'),
+            self::PERM_ASSIGN    => array(
+                'title' =>
+                /* @trans */ 'Assign',
+                'desc'  =>
+                /* @trans */ 'Ability to assign tasks to agents or teams'),
+            self::PERM_TRANSFER  => array(
+                'title' =>
+                /* @trans */ 'Transfer',
+                'desc'  =>
+                /* @trans */ 'Ability to transfer tasks between departments'),
+            self::PERM_REPLY => array(
+                'title' =>
+                /* @trans */ 'Post Reply',
+                'desc'  =>
+                /* @trans */ 'Ability to post task update'),
+            self::PERM_CLOSE     => array(
+                'title' =>
+                /* @trans */ 'Close',
+                'desc'  =>
+                /* @trans */ 'Ability to close tasks'),
+            self::PERM_DELETE    => array(
+                'title' =>
+                /* @trans */ 'Delete',
+                'desc'  =>
+                /* @trans */ 'Ability to delete tasks'),
+            );
+
+    const ISOPEN    = 0x0001;
+    const ISOVERDUE = 0x0002;
+
+
+    protected function hasFlag($flag) {
+        return ($this->get('flags') & $flag) !== 0;
+    }
+
+    protected function clearFlag($flag) {
+        return $this->set('flags', $this->get('flags') & ~$flag);
+    }
+
+    protected function setFlag($flag) {
+        return $this->set('flags', $this->get('flags') | $flag);
+    }
+
+    function getId() {
+        return $this->id;
+    }
+
+    function getNumber() {
+        return $this->number;
+    }
+
+    function getStaffId() {
+        return $this->staff_id;
+    }
+
+    function getStaff() {
+        return $this->staff;
+    }
+
+    function getTeamId() {
+        return $this->team_id;
+    }
+
+    function getTeam() {
+        return $this->team;
+    }
+
+    function getDeptId() {
+        return $this->dept_id;
+    }
+
+    function getDept() {
+        return $this->dept;
+    }
+
+    function getCreateDate() {
+        return $this->created;
+    }
+
+    function getDueDate() {
+        return $this->duedate;
+    }
+
+    function getCloseDate() {
+        return $this->isClosed() ? $this->closed : '';
+    }
+
+    function isOpen() {
+        return $this->hasFlag(self::ISOPEN);
+    }
+
+    function isClosed() {
+        return !$this->isOpen();
+    }
+
+    function isCloseable() {
+
+        if ($this->isClosed())
+            return true;
+
+        $warning = null;
+        if ($this->getMissingRequiredFields()) {
+            $warning = sprintf(
+                    __( '%1$s is missing data on %2$s one or more required fields %3$s and cannot be closed'),
+                    __('This task'),
+                    '', '');
+        }
+
+        return $warning ?: true;
+    }
+
+    protected function close() {
+        return $this->clearFlag(self::ISOPEN);
+    }
+
+    protected function reopen() {
+        return $this->setFlag(self::ISOPEN);
+    }
+
+    function isAssigned() {
+        return ($this->isOpen() && ($this->getStaffId() || $this->getTeamId()));
+    }
+
+    function isOverdue() {
+        return $this->hasFlag(self::ISOVERDUE);
+    }
+
+    static function getPermissions() {
+        return self::$perms;
+    }
+
+}
+
+RolePermission::register(/* @trans */ 'Tasks', TaskModel::getPermissions());
+
+
+class Task extends TaskModel implements RestrictedAccess, Threadable {
+    var $form;
+    var $entry;
+
+    var $_thread;
+    var $_entries;
+    var $_answers;
+
+    var $lastrespondent;
+
+    function __onload() {
+        $this->loadDynamicData();
+    }
+
+    function loadDynamicData() {
+        if (!isset($this->_answers)) {
+            $this->_answers = array();
+            foreach (DynamicFormEntryAnswer::objects()
+                ->filter(array(
+                    'entry__object_id' => $this->getId(),
+                    'entry__object_type' => ObjectModel::OBJECT_TYPE_TASK
+                )) as $answer
+            ) {
+                $tag = mb_strtolower($answer->field->name)
+                    ?: 'field.' . $answer->field->id;
+                    $this->_answers[$tag] = $answer;
+            }
+        }
+        return $this->_answers;
+    }
+
+    function getStatus() {
+        return $this->isOpen() ? __('Open') : __('Completed');
+    }
+
+    function getTitle() {
+        return $this->__cdata('title', ObjectModel::OBJECT_TYPE_TASK);
+    }
+
+    function checkStaffPerm($staff, $perm=null) {
+
+        // Must be a valid staff
+        if (!$staff instanceof Staff && !($staff=Staff::lookup($staff)))
+            return false;
+
+        // Check access based on department or assignment
+        if (!$staff->canAccessDept($this->getDeptId())
+                && $this->isOpen()
+                && $staff->getId() != $this->getStaffId()
+                && !$staff->isTeamMember($this->getTeamId()))
+            return false;
+
+        // At this point staff has access unless a specific permission is
+        // requested
+        if ($perm === null)
+            return true;
+
+        // Permission check requested -- get role.
+        if (!($role=$staff->getRole($this->getDeptId())))
+            return false;
+
+        // Check permission based on the effective role
+        return $role->hasPerm($perm);
+    }
+
+    function getAssignee() {
+
+        if (!$this->isOpen() || !$this->isAssigned())
+            return false;
+
+        if ($this->staff)
+            return $this->staff;
+
+        if ($this->team)
+            return $this->team;
+
+        return null;
+    }
+
+    function getAssigneeId() {
+
+        if (!($assignee=$this->getAssignee()))
+            return null;
+
+        $id = '';
+        if ($assignee instanceof Staff)
+            $id = 's'.$assignee->getId();
+        elseif ($assignee instanceof Team)
+            $id = 't'.$assignee->getId();
+
+        return $id;
+    }
+
+    function getAssignees() {
+
+        $assignees=array();
+        if ($this->staff)
+            $assignees[] = $this->staff->getName();
+
+        //Add team assignment
+        if ($this->team)
+            $assignees[] = $this->team->getName();
+
+        return $assignees;
+    }
+
+    function getAssigned($glue='/') {
+        $assignees = $this->getAssignees();
+
+        return $assignees ? implode($glue, $assignees):'';
+    }
+
+    function getLastRespondent() {
+
+        if (!isset($this->lastrespondent)) {
+            $this->lastrespondent = Staff::objects()
+                ->filter(array(
+                'staff_id' => static::objects()
+                    ->filter(array(
+                        'thread__entries__type' => 'R',
+                        'thread__entries__staff_id__gt' => 0
+                    ))
+                    ->values_flat('thread__entries__staff_id')
+                    ->order_by('-thread__entries__id')
+                    ->limit(1)
+                ))
+                ->first()
+                ?: false;
+        }
+
+        return $this->lastrespondent;
+    }
+
+    function getDynamicFields($criteria=array()) {
+
+        $fields = DynamicFormField::objects()->filter(array(
+                    'id__in' => $this->entries
+                    ->filter($criteria)
+                ->values_flat('answers__field_id')));
+
+        return ($fields && count($fields)) ? $fields : array();
+    }
+
+    function getMissingRequiredFields() {
+
+        return $this->getDynamicFields(array(
+                    'answers__field__flags__hasbit' => DynamicFormField::FLAG_ENABLED,
+                    'answers__field__flags__hasbit' => DynamicFormField::FLAG_CLOSE_REQUIRED,
+                    'answers__value__isnull' => true,
+                    ));
+    }
+
+    function getParticipants() {
+        $participants = array();
+        foreach ($this->getThread()->collaborators as $c)
+            $participants[] = $c->getName();
+
+        return $participants ? implode(', ', $participants) : ' ';
+    }
+
+    function getThreadId() {
+        return $this->thread->getId();
+    }
+
+    function getThread() {
+        return $this->thread;
+    }
+
+    function getThreadEntry($id) {
+        return $this->getThread()->getEntry($id);
+    }
+
+    function getThreadEntries($type=false) {
+        $thread = $this->getThread()->getEntries();
+        if ($type && is_array($type))
+            $thread->filter(array('type__in' => $type));
+        return $thread;
+    }
+
+    function postThreadEntry($type, $vars, $options=array()) {
+        $errors = array();
+        $poster = isset($options['poster']) ? $options['poster'] : null;
+        $alert = isset($options['alert']) ? $options['alert'] : true;
+        switch ($type) {
+        case 'N':
+        default:
+            return $this->postNote($vars, $errors, $poster, $alert);
+        }
+    }
+
+    function getForm() {
+        if (!isset($this->form)) {
+            // Look for the entry first
+            if ($this->form = DynamicFormEntry::lookup(
+                        array('object_type' => ObjectModel::OBJECT_TYPE_TASK))) {
+                return $this->form;
+            }
+            // Make sure the form is in the database
+            elseif (!($this->form = DynamicForm::lookup(
+                            array('type' => ObjectModel::OBJECT_TYPE_TASK)))) {
+                $this->__loadDefaultForm();
+                return $this->getForm();
+            }
+            // Create an entry to be saved later
+            $this->form = $this->form->instanciate();
+            $this->form->object_type = ObjectModel::OBJECT_TYPE_TASK;
+        }
+
+        return $this->form;
+    }
+
+    function getAssignmentForm($source=null, $options=array()) {
+        $prompt = $assignee = '';
+        // Possible assignees
+        $assignees = array();
+        switch (strtolower($options['target'])) {
+            case 'agents':
+                $dept = $this->getDept();
+                foreach ($dept->getAssignees() as $member)
+                    $assignees['s'.$member->getId()] = $member;
+
+                if (!$source && $this->isOpen() && $this->staff)
+                    $assignee = sprintf('s%d', $this->staff->getId());
+                $prompt = __('Select an Agent');
+                break;
+            case 'teams':
+                if (($teams = Team::getActiveTeams()))
+                    foreach ($teams as $id => $name)
+                        $assignees['t'.$id] = $name;
+
+                if (!$source && $this->isOpen() && $this->team)
+                    $assignee = sprintf('t%d', $this->team->getId());
+                $prompt = __('Select a Team');
+                break;
+        }
+
+        // Default to current assignee if source is not set
+        if (!$source)
+            $source = array('assignee' => array($assignee));
+
+        $form = AssignmentForm::instantiate($source, $options);
+
+        if ($assignees)
+            $form->setAssignees($assignees);
+
+        if ($prompt && ($f=$form->getField('assignee')))
+            $f->configure('prompt', $prompt);
+
+
+        return $form;
+    }
+
+    function getClaimForm($source=null, $options=array()) {
+        global $thisstaff;
+
+        $id = sprintf('s%d', $thisstaff->getId());
+        if(!$source)
+            $source = array('assignee' => array($id));
+
+        $form = ClaimForm::instantiate($source, $options);
+        $form->setAssignees(array($id => $thisstaff->getName()));
+
+        return $form;
+
+    }
+
+
+    function getTransferForm($source=null) {
+
+        if (!$source)
+            $source = array('dept' => array($this->getDeptId()));
+
+        return TransferForm::instantiate($source);
+    }
+
+    function addDynamicData($data) {
+
+        $tf = TaskForm::getInstance($this->id, true);
+        foreach ($tf->getFields() as $f)
+            if (isset($data[$f->get('name')]))
+                $tf->setAnswer($f->get('name'), $data[$f->get('name')]);
+
+        $tf->save();
+
+        return $tf;
+    }
+
+    function getDynamicData($create=true) {
+        if (!isset($this->_entries)) {
+            $this->_entries = DynamicFormEntry::forObject($this->id,
+                    ObjectModel::OBJECT_TYPE_TASK)->all();
+            if (!$this->_entries && $create) {
+                $f = TaskForm::getInstance($this->id, true);
+                $f->save();
+                $this->_entries[] = $f;
+            }
+        }
+
+        return $this->_entries ?: array();
+    }
+
+    function setStatus($status, $comments='', &$errors=array()) {
+        global $thisstaff;
+
+        $ecb = null;
+        switch($status) {
+        case 'open':
+            if ($this->isOpen())
+                return false;
+
+            $this->reopen();
+            $this->closed = null;
+
+            $ecb = function ($t) {
+                $t->logEvent('reopened', false, null, 'closed');
+            };
+            break;
+        case 'closed':
+            if ($this->isClosed())
+                return false;
+
+            // Check if task is closeable
+            $closeable = $this->isCloseable();
+            if ($closeable !== true)
+                $errors['err'] = $closeable ?: sprintf(__('%s cannot be closed'), __('This task'));
+
+            if ($errors)
+                return false;
+
+            $this->close();
+            $this->closed = SqlFunction::NOW();
+            $ecb = function($t) {
+                $t->logEvent('closed');
+            };
+            break;
+        default:
+            return false;
+        }
+
+        if (!$this->save(true))
+            return false;
+
+        // Log events via callback
+        if ($ecb) $ecb($this);
+
+        if ($comments) {
+            $errors = array();
+            $this->postNote(array(
+                        'note' => $comments,
+                        'title' => sprintf(
+                            __('Status changed to %s'),
+                            $this->getStatus())
+                        ),
+                    $errors,
+                    $thisstaff);
+        }
+
+        return true;
+    }
+
+    function to_json() {
+
+        $info = array(
+                'id'  => $this->getId(),
+                'title' => $this->getTitle()
+                );
+
+        return JsonDataEncoder::encode($info);
+    }
+
+    function __cdata($field, $ftype=null) {
+
+        foreach ($this->getDynamicData() as $e) {
+            // Make sure the form type matches
+            if (!$e->form
+                    || ($ftype && $ftype != $e->form->get('type')))
+                continue;
+
+            // Get the named field and return the answer
+            if ($a = $e->getAnswer($field))
+                return $a;
+        }
+
+        return null;
+    }
+
+    function __toString() {
+        return (string) $this->getTitle();
+    }
+
+    /* util routines */
+
+    function logEvent($state, $data=null, $user=null, $annul=null) {
+        $this->getThread()->getEvents()->log($this, $state, $data, $user, $annul);
+    }
+
+    function claim(ClaimForm $form, &$errors) {
+        global $thisstaff;
+
+        $dept = $this->getDept();
+        $assignee = $form->getAssignee();
+        if (!($assignee instanceof Staff)
+                || !$thisstaff
+                || $thisstaff->getId() != $assignee->getId()) {
+            $errors['err'] = __('Unknown assignee');
+        } elseif (!$assignee->isAvailable()) {
+            $errors['err'] = __('Agent is unavailable for assignment');
+        } elseif ($dept->assignMembersOnly() && !$dept->isMember($assignee)) {
+            $errors['err'] = __('Permission denied');
+        }
+
+        if ($errors)
+            return false;
+
+        return $this->assignToStaff($assignee, $form->getComments(), false);
+    }
+
+    function assignToStaff($staff, $note, $alert=true) {
+
+        if(!is_object($staff) && !($staff = Staff::lookup($staff)))
+            return false;
+
+        if (!$staff->isAvailable())
+            return false;
+
+        $this->staff_id = $staff->getId();
+
+        if (!$this->save())
+            return false;
+
+        $this->onAssignment($staff, $note, $alert);
+
+        global $thisstaff;
+        $data = array();
+        if ($thisstaff && $staff->getId() == $thisstaff->getId())
+            $data['claim'] = true;
+        else
+            $data['staff'] = $staff->getId();
+
+        $this->logEvent('assigned', $data);
+
+        return true;
+    }
+
+
+    function assign(AssignmentForm $form, &$errors, $alert=true) {
+        global $thisstaff;
+
+        $evd = array();
+        $assignee = $form->getAssignee();
+        if ($assignee instanceof Staff) {
+            if ($this->getStaffId() == $assignee->getId()) {
+                $errors['assignee'] = sprintf(__('%s already assigned to %s'),
+                        __('Task'),
+                        __('the agent')
+                        );
+            } elseif(!$assignee->isAvailable()) {
+                $errors['assignee'] = __('Agent is unavailable for assignment');
+            } else {
+                $this->staff_id = $assignee->getId();
+                if ($thisstaff && $thisstaff->getId() == $assignee->getId())
+                    $evd['claim'] = true;
+                else
+                    $evd['staff'] = array($assignee->getId(), $assignee->getName());
+            }
+        } elseif ($assignee instanceof Team) {
+            if ($this->getTeamId() == $assignee->getId()) {
+                $errors['assignee'] = sprintf(__('%s already assigned to %s'),
+                        __('Task'),
+                        __('the team')
+                        );
+            } else {
+                $this->team_id = $assignee->getId();
+                $evd = array('team' => $assignee->getId());
+            }
+        } else {
+            $errors['assignee'] = __('Unknown assignee');
+        }
+
+        if ($errors || !$this->save(true))
+            return false;
+
+        $this->logEvent('assigned', $evd);
+
+        $this->onAssignment($assignee,
+                $form->getField('comments')->getClean(),
+                $alert);
+
+        return true;
+    }
+
+    function onAssignment($assignee, $comments='', $alert=true) {
+        global $thisstaff, $cfg;
+
+        if (!is_object($assignee))
+            return false;
+
+        $assigner = $thisstaff ?: __('SYSTEM (Auto Assignment)');
+
+        //Assignment completed... post internal note.
+        $note = null;
+        if ($comments) {
+
+            $title = sprintf(__('Task assigned to %s'),
+                    (string) $assignee);
+
+            $errors = array();
+            $note = $this->postNote(
+                    array('note' => $comments, 'title' => $title),
+                    $errors,
+                    $assigner,
+                    false);
+        }
+
+        // Send alerts out if enabled.
+        if (!$alert || !$cfg->alertONTaskAssignment())
+            return false;
+
+        if (!($dept=$this->getDept())
+            || !($tpl = $dept->getTemplate())
+            || !($email = $dept->getAlertEmail())
+        ) {
+            return true;
+        }
+
+        // Recipients
+        $recipients = array();
+        if ($assignee instanceof Staff) {
+            if ($cfg->alertStaffONTaskAssignment())
+                $recipients[] = $assignee;
+        } elseif (($assignee instanceof Team) && $assignee->alertsEnabled()) {
+            if ($cfg->alertTeamMembersONTaskAssignment() && ($members=$assignee->getMembers()))
+                $recipients = array_merge($recipients, $members);
+            elseif ($cfg->alertTeamLeadONTaskAssignment() && ($lead=$assignee->getTeamLead()))
+                $recipients[] = $lead;
+        }
+
+        if ($recipients
+            && ($msg=$tpl->getTaskAssignmentAlertMsgTemplate())) {
+
+            $msg = $this->replaceVars($msg->asArray(),
+                array('comments' => $comments,
+                      'assignee' => $assignee,
+                      'assigner' => $assigner
+                )
+            );
+            // Send the alerts.
+            $sentlist = array();
+            $options = $note instanceof ThreadEntry
+                ? array('thread' => $note)
+                : array();
+
+            foreach ($recipients as $k => $staff) {
+                if (!is_object($staff)
+                    || !$staff->isAvailable()
+                    || in_array($staff->getEmail(), $sentlist)) {
+                    continue;
+                }
+
+                $alert = $this->replaceVars($msg, array('recipient' => $staff));
+                $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
+                $sentlist[] = $staff->getEmail();
+            }
+        }
+
+        return true;
+    }
+
+    function transfer(TransferForm $form, &$errors, $alert=true) {
+        global $thisstaff, $cfg;
+
+        $cdept = $this->getDept();
+        $dept = $form->getDept();
+        if (!$dept || !($dept instanceof Dept))
+            $errors['dept'] = __('Department selection required');
+        elseif ($dept->getid() == $this->getDeptId())
+            $errors['dept'] = __('Task already in the department');
+        else
+            $this->dept_id = $dept->getId();
+
+        if ($errors || !$this->save(true))
+            return false;
+
+        // Log transfer event
+        $this->logEvent('transferred');
+
+        // Post internal note if any
+        $note = $form->getField('comments')->getClean();
+        if ($note) {
+            $title = sprintf(__('%1$s transferred from %2$s to %3$s'),
+                    __('Task'),
+                   $cdept->getName(),
+                    $dept->getName());
+
+            $_errors = array();
+            $note = $this->postNote(
+                    array('note' => $note, 'title' => $title),
+                    $_errors, $thisstaff, false);
+        }
+
+        // Send alerts if requested && enabled.
+        if (!$alert || !$cfg->alertONTaskTransfer())
+            return true;
+
+        if (($email = $dept->getAlertEmail())
+             && ($tpl = $dept->getTemplate())
+             && ($msg=$tpl->getTaskTransferAlertMsgTemplate())) {
+
+            $msg = $this->replaceVars($msg->asArray(),
+                array('comments' => $note, 'staff' => $thisstaff));
+            // Recipients
+            $recipients = array();
+            // Assigned staff or team... if any
+            if ($this->isAssigned() && $cfg->alertAssignedONTaskTransfer()) {
+                if($this->getStaffId())
+                    $recipients[] = $this->getStaff();
+                elseif ($this->getTeamId()
+                    && ($team=$this->getTeam())
+                    && ($members=$team->getMembers())
+                ) {
+                    $recipients = array_merge($recipients, $members);
+                }
+            } elseif ($cfg->alertDeptMembersONTaskTransfer() && !$this->isAssigned()) {
+                // Only alerts dept members if the task is NOT assigned.
+                if ($members = $dept->getMembersForAlerts()->all())
+                    $recipients = array_merge($recipients, $members);
+            }
+
+            // Always alert dept manager??
+            if ($cfg->alertDeptManagerONTaskTransfer()
+                && ($manager=$dept->getManager())) {
+                $recipients[] = $manager;
+            }
+
+            $sentlist = $options = array();
+            if ($note instanceof ThreadEntry) {
+                $options += array('thread'=>$note);
+            }
+
+            foreach ($recipients as $k=>$staff) {
+                if (!is_object($staff)
+                    || !$staff->isAvailable()
+                    || in_array($staff->getEmail(), $sentlist)
+                ) {
+                    continue;
+                }
+                $alert = $this->replaceVars($msg, array('recipient' => $staff));
+                $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
+                $sentlist[] = $staff->getEmail();
+            }
+        }
+
+        return true;
+    }
+
+    function postNote($vars, &$errors, $poster='', $alert=true) {
+        global $cfg, $thisstaff;
+
+        $vars['staffId'] = 0;
+        $vars['poster'] = 'SYSTEM';
+        if ($poster && is_object($poster)) {
+            $vars['staffId'] = $poster->getId();
+            $vars['poster'] = $poster->getName();
+        } elseif ($poster) { //string
+            $vars['poster'] = $poster;
+        }
+
+        if (!($note=$this->getThread()->addNote($vars, $errors)))
+            return null;
+
+        $assignee = $this->getStaff();
+
+        if (isset($vars['task:status']))
+            $this->setStatus($vars['task:status']);
+
+        $this->onActivity(array(
+            'activity' => $note->getActivity(),
+            'threadentry' => $note,
+            'assignee' => $assignee
+        ), $alert);
+
+        return $note;
+    }
+
+    /* public */
+    function postReply($vars, &$errors, $alert = true) {
+        global $thisstaff, $cfg;
+
+
+        if (!$vars['poster'] && $thisstaff)
+            $vars['poster'] = $thisstaff;
+
+        if (!$vars['staffId'] && $thisstaff)
+            $vars['staffId'] = $thisstaff->getId();
+
+        if (!$vars['ip_address'] && $_SERVER['REMOTE_ADDR'])
+            $vars['ip_address'] = $_SERVER['REMOTE_ADDR'];
+
+        if (!($response = $this->getThread()->addResponse($vars, $errors)))
+            return null;
+
+        $assignee = $this->getStaff();
+
+        if (isset($vars['task:status']))
+            $this->setStatus($vars['task:status']);
+
+        /*
+        // TODO: add auto claim setting for tasks.
+        // Claim on response bypasses the department assignment restrictions
+        if ($thisstaff
+            && $this->isOpen()
+            && !$this->getStaffId()
+            && $cfg->autoClaimTasks)
+        ) {
+            $this->staff_id = $thisstaff->getId();
+        }
+        */
+
+        $this->lastrespondent = $response->staff;
+        $this->save();
+
+        // Send activity alert to agents
+        $activity = $vars['activity'] ?: $response->getActivity();
+        $this->onActivity( array(
+                    'activity' => $activity,
+                    'threadentry' => $response,
+                    'assignee' => $assignee,
+                    ));
+        // Send alert to collaborators
+        if ($alert && $vars['emailcollab']) {
+            $signature = '';
+            $this->notifyCollaborators($response,
+                array('signature' => $signature)
+            );
+        }
+
+        return $response;
+    }
+
+    function pdfExport($options=array()) {
+        global $thisstaff;
+
+        require_once(INCLUDE_DIR.'class.pdf.php');
+        if (!isset($options['psize'])) {
+            if ($_SESSION['PAPER_SIZE'])
+                $psize = $_SESSION['PAPER_SIZE'];
+            elseif (!$thisstaff || !($psize = $thisstaff->getDefaultPaperSize()))
+                $psize = 'Letter';
+
+            $options['psize'] = $psize;
+        }
+
+        $pdf = new Task2PDF($this, $options);
+        $name = 'Task-'.$this->getNumber().'.pdf';
+        Http::download($name, 'application/pdf', $pdf->Output($name, 'S'));
+        //Remember what the user selected - for autoselect on the next print.
+        $_SESSION['PAPER_SIZE'] = $options['psize'];
+        exit;
+    }
+
+    /* util routines */
+    function replaceVars($input, $vars = array()) {
+        global $ost;
+
+        return $ost->replaceTemplateVariables($input,
+                array_merge($vars, array('task' => $this)));
+    }
+
+    function asVar() {
+       return $this->getNumber();
+    }
+
+    function getVar($tag) {
+        global $cfg;
+
+        if ($tag && is_callable(array($this, 'get'.ucfirst($tag))))
+            return call_user_func(array($this, 'get'.ucfirst($tag)));
+
+        switch(mb_strtolower($tag)) {
+        case 'phone':
+        case 'phone_number':
+            return $this->getPhoneNumber();
+            break;
+        case 'staff_link':
+            return sprintf('%s/scp/tasks.php?id=%d', $cfg->getBaseUrl(), $this->getId());
+            break;
+        case 'create_date':
+            return new FormattedDate($this->getCreateDate());
+            break;
+         case 'due_date':
+            if ($due = $this->getEstDueDate())
+                return new FormattedDate($due);
+            break;
+        case 'close_date':
+            if ($this->isClosed())
+                return new FormattedDate($this->getCloseDate());
+            break;
+        case 'last_update':
+            return new FormattedDate($this->last_update);
+        default:
+            if (isset($this->_answers[$tag]))
+                // The answer object is retrieved here which will
+                // automatically invoke the toString() method when the
+                // answer is coerced into text
+                return $this->_answers[$tag];
+        }
+        return false;
+    }
+
+    static function getVarScope() {
+        $base = array(
+            'assigned' => __('Assigned agent and/or team'),
+            'close_date' => array(
+                'class' => 'FormattedDate', 'desc' => __('Date Closed'),
+            ),
+            'create_date' => array(
+                'class' => 'FormattedDate', 'desc' => __('Date created'),
+            ),
+            'dept' => array(
+                'class' => 'Dept', 'desc' => __('Department'),
+            ),
+            'due_date' => array(
+                'class' => 'FormattedDate', 'desc' => __('Due Date'),
+            ),
+            'number' => __('Task number'),
+            'recipients' => array(
+                'class' => 'UserList', 'desc' => __('List of all recipient names'),
+            ),
+            'status' => __('Status'),
+            'staff' => array(
+                'class' => 'Staff', 'desc' => __('Assigned/closing agent'),
+            ),
+            'subject' => 'Subject',
+            'team' => array(
+                'class' => 'Team', 'desc' => __('Assigned/closing team'),
+            ),
+            'thread' => array(
+                'class' => 'TaskThread', 'desc' => __('Task Thread'),
+            ),
+            'last_update' => array(
+                'class' => 'FormattedDate', 'desc' => __('Time of last update'),
+            ),
+        );
+
+        $extra = VariableReplacer::compileFormScope(TaskForm::getInstance());
+        return $base + $extra;
+    }
+
+    function onActivity($vars, $alert=true) {
+        global $cfg, $thisstaff;
+
+        if (!$alert // Check if alert is enabled
+            || !$cfg->alertONTaskActivity()
+            || !($dept=$this->getDept())
+            || !($email=$cfg->getAlertEmail())
+            || !($tpl = $dept->getTemplate())
+            || !($msg=$tpl->getTaskActivityAlertMsgTemplate())
+        ) {
+            return;
+        }
+
+        // Alert recipients
+        $recipients = array();
+        //Last respondent.
+        if ($cfg->alertLastRespondentONTaskActivity())
+            $recipients[] = $this->getLastRespondent();
+
+        // Assigned staff / team
+        if ($cfg->alertAssignedONTaskActivity()) {
+            if (isset($vars['assignee'])
+                    && $vars['assignee'] instanceof Staff)
+                 $recipients[] = $vars['assignee'];
+            elseif ($this->isOpen() && ($assignee = $this->getStaff()))
+                $recipients[] = $assignee;
+
+            if ($team = $this->getTeam())
+                $recipients = array_merge($recipients, $team->getMembers());
+        }
+
+        // Dept manager
+        if ($cfg->alertDeptManagerONTaskActivity() && $dept && $dept->getManagerId())
+            $recipients[] = $dept->getManager();
+
+        $options = array();
+        $staffId = $thisstaff ? $thisstaff->getId() : 0;
+        if ($vars['threadentry'] && $vars['threadentry'] instanceof ThreadEntry) {
+            $options = array('thread' => $vars['threadentry']);
+
+            // Activity details
+            if (!$vars['message'])
+                $vars['message'] = $vars['threadentry'];
+
+            // Staff doing the activity
+            $staffId = $vars['threadentry']->getStaffId() ?: $staffId;
+        }
+
+        $msg = $this->replaceVars($msg->asArray(),
+                array(
+                    'note' => $vars['threadentry'], // For compatibility
+                    'activity' => $vars['activity'],
+                    'message' => $vars['message']));
+
+        $isClosed = $this->isClosed();
+        $sentlist=array();
+        foreach ($recipients as $k=>$staff) {
+            if (!is_object($staff)
+                // Don't bother vacationing staff.
+                || !$staff->isAvailable()
+                // No need to alert the poster!
+                || $staffId == $staff->getId()
+                // No duplicates.
+                || isset($sentlist[$staff->getEmail()])
+                // Make sure staff has access to task
+                || ($isClosed && !$this->checkStaffPerm($staff))
+            ) {
+                continue;
+            }
+            $alert = $this->replaceVars($msg, array('recipient' => $staff));
+            $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
+            $sentlist[$staff->getEmail()] = 1;
+        }
+
+    }
+
+    /*
+     * Notify collaborators on response or new message
+     *
+     */
+    function  notifyCollaborators($entry, $vars = array()) {
+        global $cfg;
+
+        if (!$entry instanceof ThreadEntry
+            || !($recipients=$this->getThread()->getParticipants())
+            || !($dept=$this->getDept())
+            || !($tpl=$dept->getTemplate())
+            || !($msg=$tpl->getTaskActivityNoticeMsgTemplate())
+            || !($email=$dept->getEmail())
+        ) {
+            return;
+        }
+
+        // Who posted the entry?
+        $skip = array();
+        if ($entry instanceof MessageThreadEntry) {
+            $poster = $entry->getUser();
+            // Skip the person who sent in the message
+            $skip[$entry->getUserId()] = 1;
+            // Skip all the other recipients of the message
+            foreach ($entry->getAllEmailRecipients() as $R) {
+                foreach ($recipients as $R2) {
+                    if (0 === strcasecmp($R2->getEmail(), $R->mailbox.'@'.$R->host)) {
+                        $skip[$R2->getUserId()] = true;
+                        break;
+                    }
+                }
+            }
+        } else {
+            $poster = $entry->getStaff();
+        }
+
+        $vars = array_merge($vars, array(
+            'message' => (string) $entry,
+            'poster' => $poster ?: _S('A collaborator'),
+            )
+        );
+
+        $msg = $this->replaceVars($msg->asArray(), $vars);
+
+        $attachments = $cfg->emailAttachments()?$entry->getAttachments():array();
+        $options = array('thread' => $entry);
+
+        foreach ($recipients as $recipient) {
+            // Skip folks who have already been included on this part of
+            // the conversation
+            if (isset($skip[$recipient->getUserId()]))
+                continue;
+            $notice = $this->replaceVars($msg, array('recipient' => $recipient));
+            $email->send($recipient, $notice['subj'], $notice['body'], $attachments,
+                $options);
+        }
+    }
+
+    function update($forms, $vars, &$errors) {
+        global $thisstaff;
+
+
+        if (!$forms || !$this->checkStaffPerm($thisstaff, Task::PERM_EDIT))
+            return false;
+
+
+        foreach ($forms as $form) {
+            $form->setSource($vars);
+            if (!$form->isValid(function($f) {
+                return $f->isVisibleToStaff() && $f->isEditableToStaff();
+            }, array('mode'=>'edit'))) {
+                $errors = array_merge($errors, $form->errors());
+            }
+        }
+
+        if ($errors)
+            return false;
+
+        // Update dynamic meta-data
+        $changes = array();
+        foreach ($forms as $f) {
+            $changes += $f->getChanges();
+            $f->save();
+        }
+
+
+        if ($vars['note']) {
+            $_errors = array();
+            $this->postNote(array(
+                        'note' => $vars['note'],
+                        'title' => __('Task Update'),
+                        ),
+                    $_errors,
+                    $thisstaff);
+        }
+
+        if ($changes)
+            $this->logEvent('edited', array('fields' => $changes));
+
+        Signal::send('model.updated', $this);
+        return $this->save();
+    }
+
+    /* static routines */
+    static function lookupIdByNumber($number) {
+
+        if (($task = self::lookup(array('number' => $number))))
+            return $task->getId();
+
+    }
+
+    static function isNumberUnique($number) {
+        return !self::lookupIdByNumber($number);
+    }
+
+    static function create($vars=false) {
+        global $thisstaff, $cfg;
+
+        if (!is_array($vars)
+                || !$thisstaff
+                || !$thisstaff->hasPerm(Task::PERM_CREATE, false))
+            return null;
+
+        $task = new static(array(
+            'flags' => self::ISOPEN,
+            'object_id' => $vars['object_id'],
+            'object_type' => $vars['object_type'],
+            'number' => $cfg->getNewTaskNumber(),
+            'created' => new SqlFunction('NOW'),
+            'updated' => new SqlFunction('NOW'),
+        ));
+
+        if ($vars['internal_formdata']['dept_id'])
+            $task->dept_id = $vars['internal_formdata']['dept_id'];
+        if ($vars['internal_formdata']['duedate'])
+            $task->duedate = $vars['internal_formdata']['duedate'];
+
+        if (!$task->save(true))
+            return false;
+
+        // Add dynamic data
+        $task->addDynamicData($vars['default_formdata']);
+
+        // Create a thread + message.
+        $thread = TaskThread::create($task);
+        $thread->addDescription($vars);
+
+
+        $task->logEvent('created', null, $thisstaff);
+
+        // Get role for the dept
+        $role = $thisstaff->getRole($task->dept_id);
+        // Assignment
+        $assignee = $vars['internal_formdata']['assignee'];
+        if ($assignee
+                // skip assignment if the user doesn't have perm.
+                && $role->hasPerm(Task::PERM_ASSIGN)) {
+            $_errors = array();
+            $assigneeId = sprintf('%s%d',
+                    ($assignee  instanceof Staff) ? 's' : 't',
+                    $assignee->getId());
+
+            $form = AssignmentForm::instantiate(array('assignee' => $assigneeId));
+
+            $task->assign($form, $_errors);
+        }
+
+        Signal::send('task.created', $task);
+
+        return $task;
+    }
+
+    function delete($comments='') {
+        global $ost, $thisstaff;
+
+        $thread = $this->getThread();
+
+        if (!parent::delete())
+            return false;
+
+        $thread->delete();
+
+        Draft::deleteForNamespace('task.%.' . $this->getId());
+
+        foreach (DynamicFormEntry::forObject($this->getId(), ObjectModel::OBJECT_TYPE_TASK) as $form)
+            $form->delete();
+
+        // Log delete
+        $log = sprintf(__('Task #%1$s deleted by %2$s'),
+                $this->getNumber(),
+                $thisstaff ? $thisstaff->getName() : __('SYSTEM'));
+
+        if ($comments)
+            $log .= sprintf('<hr>%s', $comments);
+
+        $ost->logDebug(
+                sprintf( __('Task #%s deleted'), $this->getNumber()),
+                $log);
+
+        return true;
+
+    }
+
+    static function __loadDefaultForm() {
+
+        require_once INCLUDE_DIR.'class.i18n.php';
+
+        $i18n = new Internationalization();
+        $tpl = $i18n->getTemplate('form.yaml');
+        foreach ($tpl->getData() as $f) {
+            if ($f['type'] == ObjectModel::OBJECT_TYPE_TASK) {
+                $form = DynamicForm::create($f);
+                $form->save();
+                break;
+            }
+        }
+    }
+
+    /* Quick staff's stats */
+    static function getStaffStats($staff) {
+        global $cfg;
+
+        /* Unknown or invalid staff */
+        if (!$staff
+                || (!is_object($staff) && !($staff=Staff::lookup($staff)))
+                || !$staff->isStaff())
+            return null;
+
+        $where = array('(task.staff_id='.db_input($staff->getId())
+                    .sprintf(' AND task.flags & %d != 0 ', TaskModel::ISOPEN)
+                    .') ');
+        $where2 = '';
+
+        if(($teams=$staff->getTeams()))
+            $where[] = ' ( task.team_id IN('.implode(',', db_input(array_filter($teams)))
+                        .') AND '
+                        .sprintf('task.flags & %d != 0 ', TaskModel::ISOPEN)
+                        .')';
+
+        if(!$staff->showAssignedOnly() && ($depts=$staff->getDepts())) //Staff with limited access just see Assigned tasks.
+            $where[] = 'task.dept_id IN('.implode(',', db_input($depts)).') ';
+
+        $where = implode(' OR ', $where);
+        if ($where) $where = 'AND ( '.$where.' ) ';
+
+        $sql =  'SELECT \'open\', count(task.id ) AS tasks '
+                .'FROM ' . TASK_TABLE . ' task '
+                . sprintf(' WHERE task.flags & %d != 0 ', TaskModel::ISOPEN)
+                . $where . $where2
+
+                .'UNION SELECT \'overdue\', count( task.id ) AS tasks '
+                .'FROM ' . TASK_TABLE . ' task '
+                . sprintf(' WHERE task.flags & %d != 0 ', TaskModel::ISOPEN)
+                . sprintf(' AND task.flags & %d != 0 ', TaskModel::ISOVERDUE)
+                . $where
+
+                .'UNION SELECT \'assigned\', count( task.id ) AS tasks '
+                .'FROM ' . TASK_TABLE . ' task '
+                . sprintf(' WHERE task.flags & %d != 0 ', TaskModel::ISOPEN)
+                .'AND task.staff_id = ' . db_input($staff->getId()) . ' '
+                . $where
+
+                .'UNION SELECT \'closed\', count( task.id ) AS tasks '
+                .'FROM ' . TASK_TABLE . ' task '
+                . sprintf(' WHERE task.flags & %d = 0 ', TaskModel::ISOPEN)
+                . $where;
+
+        $res = db_query($sql);
+        $stats = array();
+        while ($row = db_fetch_row($res))
+            $stats[$row[0]] = $row[1];
+
+        return $stats;
+    }
+
+    static function getAgentActions($agent, $options=array()) {
+        if (!$agent)
+            return;
+
+        require STAFFINC_DIR.'templates/tasks-actions.tmpl.php';
+    }
+}
+
+
+class TaskCData extends VerySimpleModel {
+    static $meta = array(
+        'pk' => array('task_id'),
+        'table' => TASK_CDATA_TABLE,
+        'joins' => array(
+            'task' => array(
+                'constraint' => array('task_id' => 'TaskModel.task_id'),
+            ),
+        ),
+    );
+}
+
+
+class TaskForm extends DynamicForm {
+    static $instance;
+    static $defaultForm;
+    static $internalForm;
+
+    static $forms;
+
+    static $cdata = array(
+            'table' => TASK_CDATA_TABLE,
+            'object_id' => 'task_id',
+            'object_type' => 'A',
+        );
+
+    static function objects() {
+        $os = parent::objects();
+        return $os->filter(array('type'=>ObjectModel::OBJECT_TYPE_TASK));
+    }
+
+    static function getDefaultForm() {
+        if (!isset(static::$defaultForm)) {
+            if (($o = static::objects()) && $o[0])
+                static::$defaultForm = $o[0];
+        }
+
+        return static::$defaultForm;
+    }
+
+    static function getInstance($object_id=0, $new=false) {
+        if ($new || !isset(static::$instance))
+            static::$instance = static::getDefaultForm()->instanciate();
+
+        static::$instance->object_type = ObjectModel::OBJECT_TYPE_TASK;
+
+        if ($object_id)
+            static::$instance->object_id = $object_id;
+
+        return static::$instance;
+    }
+
+    static function getInternalForm($source=null, $options=array()) {
+        if (!isset(static::$internalForm))
+            static::$internalForm = new TaskInternalForm($source, $options);
+
+        return static::$internalForm;
+    }
+}
+
+class TaskInternalForm
+extends AbstractForm {
+    static $layout = 'GridFormLayout';
+
+    function buildFields() {
+
+        $fields = array(
+                'dept_id' => new DepartmentField(array(
+                    'id'=>1,
+                    'label' => __('Department'),
+                    'required' => true,
+                    'layout' => new GridFluidCell(6),
+                    )),
+                'assignee' => new AssigneeField(array(
+                    'id'=>2,
+                    'label' => __('Assignee'),
+                    'required' => false,
+                    'layout' => new GridFluidCell(6),
+                    )),
+                'duedate'  =>  new DatetimeField(array(
+                    'id' => 3,
+                    'label' => __('Due Date'),
+                    'required' => false,
+                    'configuration' => array(
+                        'min' => Misc::gmtime(),
+                        'time' => true,
+                        'gmt' => true,
+                        'future' => true,
+                        ),
+                    )),
+
+            );
+
+        $mode = @$this->options['mode'];
+        if ($mode && $mode == 'edit') {
+            unset($fields['dept_id']);
+            unset($fields['assignee']);
+        }
+
+        return $fields;
+    }
+}
+
+// Task thread class
+class TaskThread extends ObjectThread {
+
+    function addDescription($vars, &$errors=array()) {
+
+        $vars['threadId'] = $this->getId();
+        $vars['message'] = $vars['description'];
+        unset($vars['description']);
+
+        return MessageThreadEntry::create($vars, $errors);
+    }
+
+    static function create($task=false) {
+        assert($task !== false);
+
+        $id = is_object($task) ? $task->getId() : $task;
+        $thread = parent::create(array(
+                    'object_id' => $id,
+                    'object_type' => ObjectModel::OBJECT_TYPE_TASK
+                    ));
+        if ($thread->save())
+            return $thread;
+    }
+
+}
+?>
diff --git a/include/class.team.php b/include/class.team.php
index 1ae093c9427b4e6d587abc146d8ca3de04311094..d6963eb9213272959dc5fe0da0d319b59376a784 100644
--- a/include/class.team.php
+++ b/include/class.team.php
@@ -14,42 +14,28 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-class Team {
-
-    var $id;
-    var $ht;
-
-    var $members;
-
-    function Team($id) {
-
-        return $this->load($id);
-    }
-
-    function load($id=0) {
-
-        if(!$id && !($id=$this->getId()))
-            return false;
-
-        $sql='SELECT team.*,count(member.staff_id) as members '
-            .' FROM '.TEAM_TABLE.' team '
-            .' LEFT JOIN '.TEAM_MEMBER_TABLE.' member USING(team_id) '
-            .' WHERE team.team_id='.db_input($id)
-            .' GROUP BY team.team_id ';
-
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
-
-        $this->ht=db_fetch_array($res);
-        $this->id=$this->ht['team_id'];
-        $this->members=array();
-
-        return $this->id;
-    }
-
-    function reload() {
-        return $this->load($this->getId());
-    }
+class Team extends VerySimpleModel
+implements TemplateVariable {
+
+    static $meta = array(
+        'table' => TEAM_TABLE,
+        'pk' => array('team_id'),
+        'joins' => array(
+            'lead' => array(
+                'null' => true,
+                'constraint' => array('lead_id' => 'Staff.staff_id'),
+            ),
+            'members' => array(
+                'list' => true,
+                'reverse' => 'TeamMember.team',
+            ),
+        ),
+    );
+
+    const FLAG_ENABLED  = 0x0001;
+    const FLAG_NOALERTS = 0x0002;
+
+    var $_members;
 
     function asVar() {
         return $this->__toString();
@@ -59,50 +45,60 @@ class Team {
         return (string) $this->getName();
     }
 
+    static function getVarScope() {
+        return array(
+            'name' => __('Team Name'),
+            'lead' => array(
+                'class' => 'Staff', 'desc' => __('Team Lead'),
+            ),
+            'members' => array(
+                'class' => 'UserList', 'desc' => __('Team Members'),
+            ),
+        );
+    }
+
+    function getVar($tag) {
+        switch ($tag) {
+        case 'members':
+            return new UserList($this->getMembers()->all());
+        }
+    }
+
     function getId() {
-        return $this->id;
+        return $this->team_id;
     }
 
     function getName() {
-        return $this->ht['name'];
+        return $this->name;
+    }
+    function getLocalName() {
+        return $this->getLocal('name');
     }
 
     function getNumMembers() {
-        return $this->ht['members'];
+        return $this->members->count();
     }
 
     function getMembers() {
-
-        if(!$this->members && $this->getNumMembers()) {
-            $sql='SELECT m.staff_id FROM '.TEAM_MEMBER_TABLE.' m '
-                .'LEFT JOIN '.STAFF_TABLE.' s USING(staff_id) '
-                .'WHERE m.team_id='.db_input($this->getId()).' AND s.staff_id IS NOT NULL '
-                .'ORDER BY s.lastname, s.firstname';
-            if(($res=db_query($sql)) && db_num_rows($res)) {
-                while(list($id)=db_fetch_row($res))
-                    if(($staff= Staff::lookup($id)))
-                        $this->members[]= $staff;
-            }
+        if (!isset($this->_members)) {
+            $this->_members = array();
+            foreach ($this->members as $m)
+                $this->_members[] = $m->staff;
         }
-
-        return $this->members;
+        return $this->_members;
     }
 
     function hasMember($staff) {
-        return db_count(
-             'SELECT COUNT(*) FROM '.TEAM_MEMBER_TABLE
-            .' WHERE team_id='.db_input($this->getId())
-            .'   AND staff_id='.db_input($staff->getId())) !== 0;
+        return $this->members
+            ->filter(array('staff_id'=>$staff->getId()))
+            ->count() !== 0;
     }
 
     function getLeadId() {
-        return $this->ht['lead_id'];
+        return $this->lead_id;
     }
 
     function getTeamLead() {
-        if(!$this->lead && $this->getLeadId())
-            $this->lead=Staff::lookup($this->getLeadId());
-
         return $this->lead;
     }
 
@@ -111,7 +107,11 @@ class Team {
     }
 
     function getHashtable() {
-        return $this->ht;
+        $base = $this->ht;
+        $base['isenabled'] = $this->isEnabled();
+        $base['noalerts'] = !$this->alertsEnabled();
+        unset($base['members']);
+        return $base;
     }
 
     function getInfo() {
@@ -119,7 +119,7 @@ class Team {
     }
 
     function isEnabled() {
-        return ($this->ht['isenabled']);
+        return $this->flags & self::FLAG_ENABLED;
     }
 
     function isActive() {
@@ -127,144 +127,255 @@ class Team {
     }
 
     function alertsEnabled() {
-        return !$this->ht['noalerts'];
+        return ($this->flags & self::FLAG_NOALERTS) == 0;
     }
 
-    function update($vars, &$errors) {
+    function getTranslateTag($subtag) {
+        return _H(sprintf('team.%s.%s', $subtag, $this->getId()));
+    }
+    function getLocal($subtag) {
+        $tag = $this->getTranslateTag($subtag);
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : $this->ht[$subtag];
+    }
+    static function getLocalById($id, $subtag, $default) {
+        $tag = _H(sprintf('team.%s.%s', $subtag, $id));
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : $default;
+    }
 
-        //reset team lead if they're being deleted
-        if($this->getLeadId()==$vars['lead_id']
-                && $vars['remove'] && in_array($this->getLeadId(), $vars['remove']))
-            $vars['lead_id']=0;
+    function update($vars, &$errors=array()) {
 
-        //Save the changes...
-        if(!Team::save($this->getId(), $vars, $errors))
+        if (!$vars['name']) {
+            $errors['name']=__('Team name is required');
+        } elseif(($tid=self::getIdByName($vars['name'])) && $tid!=$vars['id']) {
+            $errors['name']=__('Team name already exists');
+        }
+
+        // Reset team lead if they're getting removed
+        if (isset($this->lead_id)
+                && $this->lead_id == $vars['lead_id']
+                && $vars['remove']
+                && in_array($this->lead_id, $vars['remove']))
+            $vars['lead_id'] =0 ;
+
+        $this->flags =
+              ($vars['isenabled'] ? self::FLAG_ENABLED : 0)
+            | (isset($vars['noalerts']) ? self::FLAG_NOALERTS : 0);
+        $this->lead_id = $vars['lead_id'] ?: 0;
+        $this->name = Format::striptags($vars['name']);
+        $this->notes = Format::sanitize($vars['notes']);
+
+        // Format access update as [array(staff_id, alerts?)]
+        $access = array();
+        if (isset($vars['members'])) {
+            foreach (@$vars['members'] as $staff_id) {
+                $access[] = array($staff_id, @$vars['member_alerts'][$staff_id]);
+            }
+        }
+        $this->updateMembers($access, $errors);
+
+        if ($errors)
             return false;
 
-        //Delete staff marked for removal...
-        if($vars['remove']) {
-            $sql='DELETE FROM '.TEAM_MEMBER_TABLE
-                .' WHERE team_id='.db_input($this->getId())
-                .' AND staff_id IN ('
-                    .implode(',', db_input($vars['remove']))
-                .')';
-            db_query($sql);
+        if ($this->save())
+            return $this->members->saveAll();
+
+        if (isset($this->team_id)) {
+            $errors['err']=sprintf(__('Unable to update %s.'), __('this team'))
+               .' '.__('Internal error occurred');
+        } else {
+            $errors['err']=sprintf(__('Unable to create %s.'), __('this team'))
+               .' '.__('Internal error occurred');
         }
 
-        //Reload.
-        $this->reload();
+        return false;
+    }
+
+    function updateMembers($access, &$errors) {
+      reset($access);
+      $dropped = array();
+      foreach ($this->members as $member)
+          $dropped[$member->staff_id] = 1;
+      while (list(, list($staff_id, $alerts)) = each($access)) {
+          unset($dropped[$staff_id]);
+          if (!$staff_id || !Staff::lookup($staff_id))
+              $errors['members'][$staff_id] = __('No such agent');
+          $member = $this->members->findFirst(array('staff_id' => $staff_id));
+          if (!isset($member)) {
+              $member = new TeamMember(array('staff_id' => $staff_id));
+              $this->members->add($member);
+          }
+          $member->setAlerts($alerts);
+      }
+      if (!$errors && $dropped) {
+          $this->members
+              ->filter(array('staff_id__in' => array_keys($dropped)))
+              ->delete();
+          $this->members->reset();
+      }
+      return !$errors;
+    }
 
-        return true;
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+
+        return parent::save($refetch || $this->dirty);
     }
 
     function delete() {
         global $thisstaff;
 
-        if(!$thisstaff || !($id=$this->getId()))
+        if (!$thisstaff || !($id=$this->getId()))
             return false;
 
         # Remove the team
-        $res = db_query(
-            'DELETE FROM '.TEAM_TABLE.' WHERE team_id='.db_input($id)
-          .' LIMIT 1');
-        if (db_affected_rows($res) != 1)
+        if (!parent::delete())
             return false;
 
         # Remove members of this team
-        db_query('DELETE FROM '.TEAM_MEMBER_TABLE
-               .' WHERE team_id='.db_input($id));
+        $this->members->delete();
 
         # Reset ticket ownership for tickets owned by this team
-        db_query('UPDATE '.TICKET_TABLE.' SET team_id=0 WHERE team_id='
-            .db_input($id));
+        Ticket::objects()
+            ->filter(array('team_id' => $id))
+            ->update(array('team_id' => 0));
 
         return true;
     }
 
     /* ----------- Static function ------------------*/
-    function lookup($id) {
-        return ($id && is_numeric($id) && ($team= new Team($id)) && $team->getId()==$id)?$team:null;
-    }
+    static function getIdByName($name) {
 
+        $row = self::objects()
+            ->filter(array('name'=>trim($name)))
+            ->values_flat('team_id')
+            ->first();
 
-    function getIdbyName($name) {
+        return $row ? $row[0] : 0;
+    }
 
-        $sql='SELECT team_id FROM '.TEAM_TABLE.' WHERE name='.db_input($name);
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id)=db_fetch_row($res);
+    static function getTeams($criteria=array()) {
+        static $teams = null;
+        if (!$teams || $criteria) {
+            $teams = array();
+            $query = static::objects()
+                ->values_flat('team_id', 'name', 'flags')
+                ->order_by('name');
+
+            if (isset($criteria['active']) && $criteria['active']) {
+                $query->annotate(array('members_count'=>SqlAggregate::COUNT('members')))
+                ->filter(array(
+                    'flags__hasbit'=>self::FLAG_ENABLED,
+                    'members__staff__isactive'=>1,
+                    'members__staff__onvacation'=>0,
+                ))
+                ->filter(array('members_count__gt'=>0));
+            }
 
-        return $id;
-    }
+            $items = array();
+            foreach ($query as $row) {
+                //TODO: Fix enabled - flags is a bit field.
+                list($id, $name, $flags) = $row;
+                $enabled = $flags & self::FLAG_ENABLED;
+                $items[$id] = sprintf('%s%s',
+                    self::getLocalById($id, 'name', $name),
+                    ($enabled || isset($criteria['active']))
+                        ? '' : ' ' . __('(disabled)'));
+            }
 
-    function getTeams( $availableOnly=false ) {
+            //TODO: sort if $criteria['localize'];
+            if ($criteria)
+                return $items;
 
-        $teams=array();
-        $sql='SELECT team_id, name FROM '.TEAM_TABLE;
-        if($availableOnly) {
-            //Make sure the members are active...TODO: include group check!!
-            $sql='SELECT t.team_id, t.name, count(m.staff_id) as members '
-                .' FROM '.TEAM_TABLE.' t '
-                .' LEFT JOIN '.TEAM_MEMBER_TABLE.' m ON(m.team_id=t.team_id) '
-                .' INNER JOIN '.STAFF_TABLE.' s ON(s.staff_id=m.staff_id AND s.isactive=1 AND onvacation=0) '
-                .' INNER JOIN '.GROUP_TABLE.' g ON(g.group_id=s.group_id AND g.group_enabled=1) '
-                .' WHERE t.isenabled=1 '
-                .' GROUP BY t.team_id '
-                .' HAVING members>0'
-                .' ORDER by t.name ';
-        }
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while(list($id, $name)=db_fetch_row($res))
-                $teams[$id] = $name;
+            $teams = $items;
         }
 
         return $teams;
     }
 
-    function getActiveTeams() {
-        return self::getTeams(true);
-    }
+    static function getActiveTeams() {
+        static $teams = null;
 
-    function create($vars, &$errors) {
-        return self::save(0, $vars, $errors);
+        if (!isset($teams))
+            $teams = self::getTeams(array('active'=>true));
+
+        return $teams;
     }
 
-    function save($id, $vars, &$errors) {
-        if($id && $vars['id']!=$id)
-            $errors['err']=__('Missing or invalid team');
+    static function create($vars=false) {
+        $team = new static($vars);
+        $team->created = SqlFunction::NOW();
+        return $team;
+    }
 
-        $vars['name'] = Format::striptags($vars['name']);
-        if(!$vars['name']) {
-            $errors['name']=__('Team name is required');
-        } elseif(strlen($vars['name'])<3) {
-            $errors['name']=__('Team name must be at least 3 chars.');
-        } elseif(($tid=Team::getIdByName($vars['name'])) && $tid!=$id) {
-            $errors['name']=__('Team name already exists');
-        }
+    static function __create($vars, &$errors) {
+        return self::create($vars)->save();
+    }
 
-        if($errors) return false;
+}
 
-        $sql='SET updated=NOW(),isenabled='.db_input($vars['isenabled']).
-             ',name='.db_input($vars['name']).
-             ',noalerts='.db_input(isset($vars['noalerts'])?$vars['noalerts']:0).
-             ',notes='.db_input(Format::sanitize($vars['notes']));
+class TeamMember extends VerySimpleModel {
+    static $meta = array(
+        'table' => TEAM_MEMBER_TABLE,
+        'pk' => array('team_id', 'staff_id'),
+        'select_related' => array('staff'),
+        'joins' => array(
+            'team' => array(
+                'constraint' => array('team_id' => 'Team.team_id'),
+            ),
+            'staff' => array(
+                'constraint' => array('staff_id' => 'Staff.staff_id'),
+            ),
+        ),
+    );
+
+    const FLAG_ALERTS = 0x0001;
+
+    function isAlertsEnabled() {
+        return $this->flags & self::FLAG_ALERTS != 0;
+    }
 
-        if($id) {
-            $sql='UPDATE '.TEAM_TABLE.' '.$sql.',lead_id='.db_input($vars['lead_id']).' WHERE team_id='.db_input($id);
-            if(db_query($sql) && db_affected_rows())
-                return true;
+    function setFlag($flag, $value) {
+        if ($value)
+            $this->flags |= $flag;
+        else
+            $this->flags &= ~$flag;
+    }
 
-            $errors['err']=sprintf(__('Unable to update %s.'), __('this team'))
-               .' '.__('Internal error occurred');
-        } else {
-            $sql='INSERT INTO '.TEAM_TABLE.' '.$sql.',created=NOW()';
-            if(db_query($sql) && ($id=db_insert_id()))
-                return $id;
+    function setAlerts($value) {
+        $this->setFlag(self::FLAG_ALERTS, $value);
+    }
+}
 
-            $errors['err']=sprintf(__('Unable to create %s.'), __('this team'))
-               .' '.__('Internal error occurred');
-        }
+class TeamQuickAddForm
+extends AbstractForm {
+    function buildFields() {
+        return array(
+            'name' => new TextboxField(array(
+                'required' => true,
+                'configuration' => array(
+                    'placeholder' => __('Name'),
+                    'classes' => 'span12',
+                    'autofocus' => true,
+                    'length' => 128,
+                ),
+            )),
+            'lead_id' => new ChoiceField(array(
+                'label' => __('Optionally select a leader for the team'),
+                'default' => 0,
+                'choices' =>
+                    array(0 => '— '.__('None').' —')
+                    + Staff::getStaffMembers(),
+                'configuration' => array(
+                    'classes' => 'span12',
+                ),
+            )),
+        );
+    }
 
-        return false;
+    function render($staff=true, $title=false, $options=array()) {
+        return parent::render($staff, $title, $options + array('template' => 'dynamic-form-simple.tmpl.php'));
     }
 }
-?>
diff --git a/include/class.template.php b/include/class.template.php
index 00a81c066c486a9fe9f95c1fea17b3bae57c683e..36cf856028f5f3b6527c81fc65204e8cb53dccaf 100644
--- a/include/class.template.php
+++ b/include/class.template.php
@@ -23,65 +23,166 @@ class EmailTemplateGroup {
     var $_templates;
     static $all_groups = array(
         'sys' => /* @trans */ 'System Management Templates',
-        'ticket.user' => /* @trans */ 'End-User Email Templates',
-        'ticket.staff' => /* @trans */ 'Agent Email Templates',
+        'a.ticket.user' => /* @trans */ 'Ticket End-User Email Templates',
+        'b.ticket.staff' => /* @trans */ 'Ticket Agent Email Templates',
+        'c.task' => /* @trans */ 'Task Email Templates',
     );
     static $all_names=array(
         'ticket.autoresp'=>array(
-            'group'=>'ticket.user',
+            'group'=>'a.ticket.user',
             'name'=>/* @trans */ 'New Ticket Auto-response',
-            'desc'=>/* @trans */ 'Autoresponse sent to user, if enabled, on new ticket.'),
+            'desc'=>/* @trans */ 'Autoresponse sent to user, if enabled, on new ticket.',
+            'context' => array(
+                'ticket', 'signature', 'message', 'recipient'
+            ),
+        ),
         'ticket.autoreply'=>array(
-            'group'=>'ticket.user',
+            'group'=>'a.ticket.user',
             'name'=>/* @trans */ 'New Ticket Auto-reply',
-            'desc'=>/* @trans */ 'Canned Auto-reply sent to user on new ticket, based on filter matches. Overwrites "normal" auto-response.'),
+            'desc'=>/* @trans */ 'Canned Auto-reply sent to user on new ticket, based on filter matches. Overwrites "normal" auto-response.',
+            'context' => array(
+                'ticket', 'signature', 'response', 'recipient',
+            ),
+        ),
         'message.autoresp'=>array(
-            'group'=>'ticket.user',
+            'group'=>'a.ticket.user',
             'name'=>/* @trans */ 'New Message Auto-response',
-            'desc'=>/* @trans */ 'Confirmation sent to user when a new message is appended to an existing ticket.'),
+            'desc'=>/* @trans */ 'Confirmation sent to user when a new message is appended to an existing ticket.',
+            'context' => array(
+                'ticket', 'signature', 'recipient',
+            ),
+        ),
         'ticket.notice'=>array(
-            'group'=>'ticket.user',
+            'group'=>'a.ticket.user',
             'name'=>/* @trans */ 'New Ticket Notice',
-            'desc'=>/* @trans */ 'Notice sent to user, if enabled, on new ticket created by an agent on their behalf (e.g phone calls).'),
+            'desc'=>/* @trans */ 'Notice sent to user, if enabled, on new ticket created by an agent on their behalf (e.g phone calls).',
+            'context' => array(
+                'ticket', 'signature', 'recipient', 'staff', 'message',
+            ),
+        ),
         'ticket.overlimit'=>array(
-            'group'=>'ticket.user',
+            'group'=>'a.ticket.user',
             'name'=>/* @trans */ 'Over Limit Notice',
-            'desc'=>/* @trans */ 'A one-time notice sent, if enabled, when user has reached the maximum allowed open tickets.'),
+            'desc'=>/* @trans */ 'A one-time notice sent, if enabled, when user has reached the maximum allowed open tickets.',
+            'context' => array(
+                'ticket', 'signature',
+            ),
+        ),
         'ticket.reply'=>array(
-            'group'=>'ticket.user',
+            'group'=>'a.ticket.user',
             'name'=>/* @trans */ 'Response/Reply Template',
-            'desc'=>/* @trans */ 'Template used on ticket response/reply'),
+            'desc'=>/* @trans */ 'Template used on ticket response/reply',
+            'context' => array(
+                'ticket', 'signature', 'response', 'staff', 'poster', 'recipient',
+            ),
+        ),
         'ticket.activity.notice'=>array(
-            'group'=>'ticket.user',
+            'group'=>'a.ticket.user',
             'name'=>/* @trans */ 'New Activity Notice',
-            'desc'=>/* @trans */ 'Template used to notify collaborators on ticket activity (e.g CC on reply)'),
+            'desc'=>/* @trans */ 'Template used to notify collaborators on ticket activity (e.g CC on reply)',
+            'context' => array(
+                'ticket', 'signature', 'message', 'poster', 'recipient',
+            ),
+        ),
         'ticket.alert'=>array(
-            'group'=>'ticket.staff',
+            'group'=>'b.ticket.staff',
             'name'=>/* @trans */ 'New Ticket Alert',
-            'desc'=>/* @trans */ 'Alert sent to agents, if enabled, on new ticket.'),
+            'desc'=>/* @trans */ 'Alert sent to agents, if enabled, on new ticket.',
+            'context' => array(
+                'ticket', 'recipient', 'message',
+            ),
+        ),
         'message.alert'=>array(
-            'group'=>'ticket.staff',
+            'group'=>'b.ticket.staff',
             'name'=>/* @trans */ 'New Message Alert',
-            'desc'=>/* @trans */ 'Alert sent to agents, if enabled, when user replies to an existing ticket.'),
+            'desc'=>/* @trans */ 'Alert sent to agents, if enabled, when user replies to an existing ticket.',
+            'context' => array(
+                'ticket', 'recipient', 'message', 'poster',
+            ),
+        ),
         'note.alert'=>array(
-            'group'=>'ticket.staff',
+            'group'=>'b.ticket.staff',
             'name'=>/* @trans */ 'Internal Activity Alert',
-            'desc'=>/* @trans */ 'Alert sent out to Agents when internal activity such as an internal note or an agent reply is appended to a ticket.'),
+            'desc'=>/* @trans */ 'Alert sent out to Agents when internal activity such as an internal note or an agent reply is appended to a ticket.',
+            'context' => array(
+                'ticket', 'recipient', 'note', 'comments', 'activity',
+            ),
+        ),
         'assigned.alert'=>array(
-            'group'=>'ticket.staff',
+            'group'=>'b.ticket.staff',
             'name'=>/* @trans */ 'Ticket Assignment Alert',
-            'desc'=>/* @trans */ 'Alert sent to agents on ticket assignment.'),
+            'desc'=>/* @trans */ 'Alert sent to agents on ticket assignment.',
+            'context' => array(
+                'ticket', 'recipient', 'comments', 'assignee', 'assigner',
+            ),
+        ),
         'transfer.alert'=>array(
-            'group'=>'ticket.staff',
+            'group'=>'b.ticket.staff',
             'name'=>/* @trans */ 'Ticket Transfer Alert',
-            'desc'=>/* @trans */ 'Alert sent to agents on ticket transfer.'),
+            'desc'=>/* @trans */ 'Alert sent to agents on ticket transfer.',
+            'context' => array(
+                'ticket', 'recipient', 'comments', 'staff',
+            ),
+        ),
         'ticket.overdue'=>array(
-            'group'=>'ticket.staff',
+            'group'=>'b.ticket.staff',
             'name'=>/* @trans */ 'Overdue Ticket Alert',
-            'desc'=>/* @trans */ 'Alert sent to agents on stale or overdue tickets.'),
-        );
+            'desc'=>/* @trans */ 'Alert sent to agents on stale or overdue tickets.',
+            'context' => array(
+                'ticket', 'recipient', 'comments',
+            ),
+        ),
+        'task.alert' => array(
+            'group'=>'c.task',
+            'name'=>/* @trans */ 'New Task Alert',
+            'desc'=>/* @trans */ 'Alert sent to agents, if enabled, on new task.',
+            'context' => array(
+                'task', 'recipient', 'message',
+            ),
+        ),
+        'task.activity.notice' => array(
+            'group'=>'c.task',
+            'name'=>/* @trans */ 'New Activity Notice',
+            'desc'=>/* @trans */ 'Template used to notify collaborators on task activity.',
+            'context' => array(
+                'task', 'signature', 'message', 'poster', 'recipient',
+            ),
+        ),
+        'task.activity.alert'=>array(
+            'group'=>'c.task',
+            'name'=>/* @trans */ 'New Activity Alert',
+            'desc'=>/* @trans */ 'Alert sent to selected agents, if enabled, on new activity.',
+            'context' => array(
+                'task', 'recipient', 'note', 'comments', 'activity',
+            ),
+        ),
+        'task.assignment.alert' => array(
+            'group'=>'c.task',
+            'name'=>/* @trans */ 'Task Assignment Alert',
+            'desc'=>/* @trans */ 'Alert sent to agents on task assignment.',
+            'context' => array(
+                'task', 'recipient', 'comments', 'assignee', 'assigner',
+            ),
+        ),
+        'task.transfer.alert'=>array(
+            'group'=>'c.task',
+            'name'=>/* @trans */ 'Task Transfer Alert',
+            'desc'=>/* @trans */ 'Alert sent to agents on task transfer.',
+            'context' => array(
+                'task', 'recipient', 'note', 'comments', 'activity',
+            ),
+        ),
+        'task.overdue.alert'=>array(
+            'group'=>'c.task',
+            'name'=>/* @trans */ 'Overdue Task Alert',
+            'desc'=>/* @trans */ 'Alert sent to agents on stale or overdue task.',
+            'context' => array(
+                'task', 'recipient', 'comments',
+            ),
+        ),
+    );
 
-    function EmailTemplateGroup($id){
+    function __construct($id){
         $this->id=0;
         $this->load($id);
     }
@@ -157,7 +258,7 @@ class EmailTemplateGroup {
         return (db_query($sql) && db_affected_rows());
     }
 
-    function getTemplateDescription($name) {
+    static function getTemplateDescription($name) {
         return static::$all_names[$name];
     }
 
@@ -248,6 +349,31 @@ class EmailTemplateGroup {
         return $this->getMsgTemplate('ticket.overdue');
     }
 
+    /* Tasks templates */
+    function getNewTaskAlertMsgTemplate() {
+        return $this->getMsgTemplate('task.alert');
+    }
+
+    function  getTaskActivityAlertMsgTemplate() {
+        return $this->getMsgTemplate('task.activity.alert');
+    }
+
+    function  getTaskActivityNoticeMsgTemplate() {
+        return $this->getMsgTemplate('task.activity.notice');
+    }
+
+    function getTaskTransferAlertMsgTemplate() {
+        return $this->getMsgTemplate('task.transfer.alert');
+    }
+
+    function getTaskAssignmentAlertMsgTemplate() {
+        return $this->getMsgTemplate('task.assignment.alert');
+    }
+
+    function getTaskOverdueAlertMsgTemplate() {
+        return $this->getMsgTemplate('task.overdue.alert');
+    }
+
     function update($vars,&$errors) {
         if(!$vars['isactive'] && $this->isInUse())
             $errors['isactive']=__('In-use template set cannot be disabled!');
@@ -382,7 +508,7 @@ class EmailTemplate {
     var $ht;
     var $_group;
 
-    function EmailTemplate($id, $group=null){
+    function __construct($id, $group=null){
         $this->id=0;
         if ($id) $this->load($id);
         if ($group) $this->_group = $group;
@@ -401,7 +527,7 @@ class EmailTemplate {
 
         $this->ht=db_fetch_array($res);
         $this->id=$this->ht['id'];
-        $this->attachments = new GenericAttachments($this->id, 'T');
+        $this->attachments = GenericAttachments::forIdAndType($this->id, 'T');
 
         return true;
     }
@@ -456,6 +582,22 @@ class EmailTemplate {
         return $this->getGroup()->getTemplateDescription($this->ht['code_name']);
     }
 
+    function getInvalidVariableUsage() {
+        $context = VariableReplacer::getContextForRoot($this->ht['code_name']);
+        $invalid = array();
+        foreach (array($this->getSubject(), $this->getBody()) as $B) {
+            $variables = array();
+            if (!preg_match_all('`%\{([^}]*)\}`', $B, $variables, PREG_SET_ORDER))
+                continue;
+            foreach ($variables as $V) {
+                if (!isset($context[$V[1]])) {
+                    $invalid[] = $V[0];
+                }
+            }
+        }
+        return $invalid;
+    }
+
     function update($vars, &$errors) {
 
         if(!$this->save($this->getId(),$vars,$errors))
@@ -464,12 +606,10 @@ class EmailTemplate {
         $this->reload();
 
         // Inline images (attached to the draft)
-        if (isset($vars['draft_id']) && $vars['draft_id']) {
-            if ($draft = Draft::lookup($vars['draft_id'])) {
-                $this->attachments->deleteInlines();
-                $this->attachments->upload($draft->getAttachmentIds($this->getBody()), true);
-            }
-        }
+        $keepers = Draft::getAttachmentIds($this->getBody());
+        // Just keep the IDs only
+        $keepers = array_map(function($i) { return $i['id']; }, $keepers);
+        $this->attachments->keepOnlyFileIds($keepers, true);
 
         return true;
     }
diff --git a/include/class.thread.php b/include/class.thread.php
index 9f38f47f4f177d1ba3178ddc54fbb39cb2312da8..b9adbef211bb43b9e047f6c793c6405fa1986a0d 100644
--- a/include/class.thread.php
+++ b/include/class.thread.php
@@ -2,7 +2,7 @@
 /*********************************************************************
     class.thread.php
 
-    Ticket thread
+    Thread of things!
     XXX: Please DO NOT add any ticket related logic! use ticket class.
 
     Peter Rotich <peter@osticket.com>
@@ -16,273 +16,636 @@
 **********************************************************************/
 include_once(INCLUDE_DIR.'class.ticket.php');
 include_once(INCLUDE_DIR.'class.draft.php');
+include_once(INCLUDE_DIR.'class.role.php');
 
 //Ticket thread.
-class Thread {
-
-    var $id; // same as ticket ID.
-    var $ticket;
-
-    function Thread($ticket) {
+class Thread extends VerySimpleModel {
+    static $meta = array(
+        'table' => THREAD_TABLE,
+        'pk' => array('id'),
+        'joins' => array(
+            'ticket' => array(
+                'constraint' => array(
+                    'object_type' => "'T'",
+                    'object_id' => 'TicketModel.ticket_id',
+                ),
+            ),
+            'task' => array(
+                'constraint' => array(
+                    'object_type' => "'A'",
+                    'object_id' => 'Task.id',
+                ),
+            ),
+            'collaborators' => array(
+                'reverse' => 'Collaborator.thread',
+            ),
+            'entries' => array(
+                'reverse' => 'ThreadEntry.thread',
+            ),
+            'events' => array(
+                'reverse' => 'ThreadEvent.thread',
+                'broker' => 'ThreadEvents',
+            ),
+        ),
+    );
 
-        $this->ticket = $ticket;
+    const MODE_STAFF = 1;
+    const MODE_CLIENT = 2;
 
-        $this->id = 0;
+    var $_object;
+    var $_entries;
+    var $_collaborators; // Cache for collabs
+    var $_participants;
 
-        $this->load();
+    function getId() {
+        return $this->id;
     }
 
-    function load() {
-
-        if(!$this->getTicketId())
-            return null;
+    function getObjectId() {
+        return $this->object_id;
+    }
 
-        $sql='SELECT ticket.ticket_id as id '
-            .' ,count(DISTINCT attach.attach_id) as attachments '
-            ." ,count(DISTINCT CASE WHEN thread.thread_type = 'M' THEN thread.id ELSE NULL END) as messages "
-            ." ,count(DISTINCT CASE WHEN thread.thread_type = 'R' THEN thread.id ELSE NULL END) as responses "
-            ." ,count(DISTINCT CASE WHEN thread.thread_type = 'N' THEN thread.id ELSE NULL END) as notes "
-            .' FROM '.TICKET_TABLE.' ticket '
-            .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON ('
-                .'ticket.ticket_id=attach.ticket_id) '
-            .' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON ('
-                .'ticket.ticket_id=thread.ticket_id) '
-            .' WHERE ticket.ticket_id='.db_input($this->getTicketId())
-            .' GROUP BY ticket.ticket_id';
-
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
+    function getObjectType() {
+        return $this->object_type;
+    }
 
-        $this->ht = db_fetch_array($res);
+    function getObject() {
 
-        $this->id = $this->ht['id'];
+        if (!$this->_object)
+            $this->_object = ObjectModel::lookup(
+                    $this->getObjectId(), $this->getObjectType());
 
-        return true;
+        return $this->_object;
     }
 
-    function getId() {
-        return $this->id;
+    function getNumAttachments() {
+        return Attachment::objects()->filter(array(
+            'thread_entry__thread' => $this
+        ))->count();
+    }
+
+    function getNumEntries() {
+        return $this->entries->count();
+    }
+
+    function getEntries($criteria=false) {
+        if (!isset($this->_entries)) {
+            $this->_entries = $this->entries->annotate(array(
+                'has_attachments' => SqlAggregate::COUNT(SqlCase::N()
+                    ->when(array('attachments__inline'=>0), 1)
+                    ->otherwise(null)
+                ),
+            ));
+            $this->_entries->exclude(array('flags__hasbit'=>ThreadEntry::FLAG_HIDDEN));
+            if ($criteria)
+                $this->_entries->filter($criteria);
+        }
+        return $this->_entries;
     }
 
-    function getTicketId() {
-        return $this->getTicket()?$this->getTicket()->getId():0;
+    // Collaborators
+    function getNumCollaborators() {
+        return $this->collaborators->count();
     }
 
-    function getTicket() {
-        return $this->ticket;
-    }
+    function getNumActiveCollaborators() {
 
-    function getNumAttachments() {
-        return $this->ht['attachments'];
-    }
+        if (!isset($this->ht['active_collaborators']))
+            $this->ht['active_collaborators'] = count($this->getActiveCollaborators());
 
-    function getNumMessages() {
-        return $this->ht['messages'];
+        return $this->ht['active_collaborators'];
     }
 
-    function getNumResponses() {
-        return $this->ht['responses'];
+    function getActiveCollaborators() {
+        return $this->getCollaborators(array('isactive'=>1));
     }
 
-    function getNumNotes() {
-        return $this->ht['notes'];
-    }
+    function getCollaborators($criteria=array()) {
 
-    function getCount() {
-        return $this->getNumMessages() + $this->getNumResponses();
-    }
+        if ($this->_collaborators && !$criteria)
+            return $this->_collaborators;
 
-    function getMessages() {
-        return $this->getEntries('M');
+        $collaborators = $this->collaborators
+            ->filter(array('thread_id' => $this->getId()));
+
+        if (isset($criteria['isactive']))
+            $collaborators->filter(array('isactive' => $criteria['isactive']));
+
+        // TODO: sort by name of the user
+        $collaborators->order_by('user__name');
+
+        if (!$criteria)
+            $this->_collaborators = $collaborators;
+
+        return $collaborators;
     }
 
-    function getResponses() {
-        return $this->getEntries('R');
+    function addCollaborator($user, $vars, &$errors, $event=true) {
+
+        if (!$user)
+            return null;
+
+        $vars = array_merge(array(
+                'threadId' => $this->getId(),
+                'userId' => $user->getId()), $vars);
+        if (!($c=Collaborator::add($vars, $errors)))
+            return null;
+
+        $this->_collaborators = null;
+
+        if ($event)
+            $this->getEvents()->log($this->getObject(),
+                'collab',
+                array('add' => array($user->getId() => array(
+                        'name' => $user->getName()->getOriginal(),
+                        'src' => @$vars['source'],
+                    ))
+                )
+            );
+
+        return $c;
     }
 
-    function getNotes() {
-        return $this->getEntries('N');
-    }
-
-    function getEntries($type, $order='ASC') {
-
-        if(!$order || !in_array($order, array('DESC','ASC')))
-            $order='ASC';
-
-        $sql='SELECT thread.*
-               , COALESCE(user.name,
-                    IF(staff.staff_id,
-                        CONCAT_WS(" ", staff.firstname, staff.lastname),
-                        NULL)) as name '
-            .' ,count(DISTINCT attach.attach_id) as attachments '
-            .' FROM '.TICKET_THREAD_TABLE.' thread '
-            .' LEFT JOIN '.USER_TABLE.' user
-                ON (thread.user_id=user.id) '
-            .' LEFT JOIN '.STAFF_TABLE.' staff
-                ON (thread.staff_id=staff.staff_id) '
-            .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach
-                ON (thread.ticket_id=attach.ticket_id
-                        AND thread.id=attach.ref_id) '
-            .' WHERE  thread.ticket_id='.db_input($this->getTicketId());
-
-        if($type && is_array($type))
-            $sql.=' AND thread.thread_type IN('.implode(',', db_input($type)).')';
-        elseif($type)
-            $sql.=' AND thread.thread_type='.db_input($type);
-
-        $sql.=' GROUP BY thread.id '
-             .' ORDER BY thread.created '.$order;
-
-        $entries = array();
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while($rec=db_fetch_array($res)) {
-                $rec['body'] = ThreadBody::fromFormattedText($rec['body'], $rec['format']);
-                $entries[] = $rec;
+    function updateCollaborators($vars, &$errors) {
+        global $thisstaff;
+
+        if (!$thisstaff) return;
+
+        //Deletes
+        if($vars['del'] && ($ids=array_filter($vars['del']))) {
+            $collabs = array();
+            foreach ($ids as $k => $cid) {
+                if (($c=Collaborator::lookup($cid))
+                        && $c->getThreadId() == $this->getId()
+                        && $c->delete())
+                     $collabs[] = $c;
             }
+            $this->getEvents()->log($this->getObject(), 'collab', array(
+                'del' => array($c->user_id => array('name' => $c->getName()->getOriginal()))
+            ));
         }
 
-        return $entries;
-    }
+        //statuses
+        $cids = null;
+        if($vars['cid'] && ($cids=array_filter($vars['cid']))) {
+            $this->collaborators->filter(array(
+                'thread_id' => $this->getId(),
+                'id__in' => $cids
+            ))->update(array(
+                'updated' => SqlFunction::NOW(),
+                'isactive' => 1,
+            ));
+        }
 
-    function getEntry($id) {
-        return ThreadEntry::lookup($id, $this->getTicketId());
+        if ($cids) {
+            $this->collaborators->filter(array(
+                'thread_id' => $this->getId(),
+                Q::not(array('id__in' => $cids))
+            ))->update(array(
+                'updated' => SqlFunction::NOW(),
+                'isactive' => 0,
+            ));
+        }
+
+        unset($this->ht['active_collaborators']);
+        $this->_collaborators = null;
+
+        return true;
     }
 
-    function addNote($vars, &$errors) {
 
-        //Add ticket Id.
-        $vars['ticketId'] = $this->getTicketId();
+    //UserList of participants (collaborators)
+    function getParticipants() {
+
+        if (!isset($this->_participants)) {
+            $list = new UserList();
+            if ($collabs = $this->getActiveCollaborators()) {
+                foreach ($collabs as $c)
+                    $list->add($c);
+            }
+
+            $this->_participants = $list;
+        }
 
-        return Note::create($vars, $errors);
+        return $this->_participants;
     }
 
-    function addMessage($vars, &$errors) {
 
-        $vars['ticketId'] = $this->getTicketId();
-        $vars['staffId'] = 0;
+    // Render thread
+    function render($type=false, $options=array()) {
 
-        return Message::create($vars, $errors);
-    }
+        $mode = $options['mode'] ?: self::MODE_STAFF;
 
-    function addResponse($vars, &$errors) {
+        // Register thread actions prior to rendering the thread.
+        if (!class_exists('tea_showemailheaders'))
+            include_once INCLUDE_DIR . 'class.thread_actions.php';
 
-        $vars['ticketId'] = $this->getTicketId();
-        $vars['userId'] = 0;
+        $entries = $this->getEntries();
+        if ($type && is_array($type))
+            $entries->filter(array('type__in' => $type));
 
-        return Response::create($vars, $errors);
-    }
+        if ($options['sort'] && !strcasecmp($options['sort'], 'DESC'))
+            $entries->order_by('-id');
 
-    function deleteAttachments() {
+        // Precache all the attachments on this thread
+        AttachmentFile::objects()->filter(array(
+            'attachments__thread_entry__thread__id' => $this->id
+        ))->all();
 
-        $deleted=0;
-        // Clear reference table
-        $res=db_query('DELETE FROM '.TICKET_ATTACHMENT_TABLE.' WHERE ticket_id='.db_input($this->getTicketId()));
-        if ($res && db_affected_rows())
-            $deleted = AttachmentFile::deleteOrphans();
+        $events = $this->getEvents();
+        $inc = ($mode == self::MODE_STAFF) ? STAFFINC_DIR : CLIENTINC_DIR;
+        include $inc . 'templates/thread-entries.tmpl.php';
+    }
 
-        return $deleted;
+    function getEntry($id) {
+        return ThreadEntry::lookup($id, $this->getId());
     }
 
-    function delete() {
+    function getEvents() {
+        return $this->events;
+    }
 
-        $sql = 'UPDATE '.TICKET_EMAIL_INFO_TABLE.' mid
-            INNER JOIN '.TICKET_THREAD_TABLE.' thread ON (thread.id = mid.thread_id)
-            SET mid.headers = null WHERE thread.ticket_id = '
-            .db_input($this->getTicketId());
-        db_query($sql);
+    /**
+     * postEmail
+     *
+     * After some security and sanity checks, attaches the body and subject
+     * of the message in reply to this thread item
+     *
+     * Parameters:
+     * mailinfo - (array) of information about the email, with at least the
+     *          following keys
+     *      - mid - (string) email message-id
+     *      - name - (string) personal name of email originator
+     *      - email - (string<email>) originating email address
+     *      - subject - (string) email subject line (decoded)
+     *      - body - (string) email message body (decoded)
+     */
+    function postEmail($mailinfo, $entry=null) {
+        // +==================+===================+=============+
+        // | Orig Thread-Type | Reply Thread-Type | Requires    |
+        // +==================+===================+=============+
+        // | *                | Message (M)       | From: Owner |
+        // | *                | Note (N)          | From: Staff |
+        // | Response (R)     | Message (M)       |             |
+        // | Message (M)      | Response (R)      | From: Staff |
+        // +------------------+-------------------+-------------+
 
-        $res=db_query('DELETE FROM '.TICKET_THREAD_TABLE.' WHERE ticket_id='.db_input($this->getTicketId()));
-        if(!$res || !db_affected_rows())
+        if (!$object = $this->getObject()) {
+            // How should someone find this thread?
             return false;
+        }
+        elseif ($object instanceof Ticket && (
+               !$mailinfo['staffId']
+            && $object->isClosed()
+            && !$object->isReopenable()
+        )) {
+            // Ticket is closed, not reopenable, and email was not submitted
+            // by an agent. Email cannot be submitted
+            return false;
+        }
 
-        $this->deleteAttachments();
-        $this->removeCollaborators();
+        $vars = array(
+            'mid' =>    $mailinfo['mid'],
+            'header' => $mailinfo['header'],
+            'poster' => $mailinfo['name'],
+            'origin' => 'Email',
+            'source' => 'Email',
+            'ip' =>     '',
+            'reply_to' => $entry,
+            'recipients' => $mailinfo['recipients'],
+            'to-email-id' => $mailinfo['to-email-id'],
+        );
 
-        return true;
-    }
+        // XXX: Is this necessary?
+        if ($object instanceof Ticket)
+            $vars['ticketId'] = $object->getId();
+        if ($object instanceof Task)
+            $vars['taskId'] = $object->getId();
 
-    function removeCollaborators() {
-        $sql='DELETE FROM '.TICKET_COLLABORATOR_TABLE
-            .' WHERE ticket_id='.db_input($this->getTicketId());
+        $errors = array();
 
-        return  (db_query($sql) && db_affected_rows());
-    }
+        if (isset($mailinfo['attachments']))
+            $vars['attachments'] = $mailinfo['attachments'];
 
-    /* static */
-    function lookup($ticket) {
+        $body = $mailinfo['message'];
 
-        return ($ticket
-                && is_object($ticket)
-                && ($thread = new Thread($ticket))
-                && $thread->getId()
-                )?$thread:null;
-    }
+        // Attempt to determine the user posting the entry and the
+        // corresponding entry type by the information determined by the
+        // mail parser (via the In-Reply-To header)
+        switch ($mailinfo['userClass']) {
+        case 'C': # Thread collaborator
+            $vars['flags'] = ThreadEntry::FLAG_COLLABORATOR;
+        case 'U': # Ticket owner
+            $vars['thread-type'] = 'M';
+            $vars['userId'] = $mailinfo['userId'];
+            break;
 
-    function getVar($name) {
-        switch ($name) {
-        case 'original':
-            return Message::firstByTicketId($this->ticket->getId())
-                ->getBody();
+        case 'A': # System administrator
+        case 'S': # Staff member (agent)
+            $vars['thread-type'] = 'N';
+            $vars['staffId'] = $mailinfo['staffId'];
+            if ($vars['staffId'])
+                $vars['poster'] = Staff::lookup($mailinfo['staffId']);
             break;
-        case 'last_message':
-        case 'lastmessage':
-            return $this->ticket->getLastMessage()->getBody();
+
+        // The user type was not identified by the mail parsing system. It
+        // is likely that the In-Reply-To and References headers were not
+        // properly brokered by the user's mail client. Use the old logic to
+        // determine the post type.
+        default:
+            // Disambiguate if the user happens also to be a staff member of
+            // the system. The current ticket owner should _always_ post
+            // messages instead of notes or responses
+            if ($object instanceof Ticket
+                && strcasecmp($mailinfo['email'], $object->getEmail()) == 0
+            ) {
+                $vars['thread-type'] = 'M';
+                $vars['userId'] = $object->getUserId();
+            }
+            // Consider collaborator role (disambiguate staff members as
+            // collaborators). Normally, the block above should match based
+            // on the Referenced message-id header
+            elseif ($C = $this->collaborators->filter(array(
+                'user__emails__address' => $mailinfo['email']
+            ))->first()) {
+                $vars['thread-type'] = 'M';
+                // XXX: There's no way that mailinfo[userId] would be set
+                $vars['userId'] = $mailinfo['userId'] ?: $C->getUserId();
+                $vars['flags'] = ThreadEntry::FLAG_COLLABORATOR;
+            }
+            // Don't process the email -- it came FROM this system
+            elseif (Email::getIdByEmail($mailinfo['email'])) {
+                return false;
+            }
+        }
+
+        // Ensure we record the name of the person posting
+        $vars['poster'] = $vars['poster']
+            ?: $mailinfo['name'] ?: $mailinfo['email'];
+
+        // TODO: Consider security constraints
+        if (!$vars['thread-type']) {
+            //XXX: Are we potentially leaking the email address to
+            // collaborators?
+            // Try not to destroy the format of the body
+            $header = sprintf(
+                _S('Received From: %1$s <%2$s>') . "\n\n",
+                $mailinfo['name'], $mailinfo['email']);
+            if ($body instanceof HtmlThreadEntryBody)
+                $header = nl2br(Format::htmlchars($header));
+            // Add the banner to the top of the message
+            if ($body instanceof ThreadEntryBody)
+                $body->prepend($header);
+            $vars['userId'] = 0; //Unknown user! //XXX: Assume ticket owner?
+            $vars['thread-type'] = 'M';
+        }
+
+        switch ($vars['thread-type']) {
+        case 'M':
+            $vars['message'] = $body;
+
+            if ($object instanceof Threadable)
+                return $object->postThreadEntry('M', $vars);
+            elseif ($this instanceof ObjectThread)
+                return $this->addMessage($vars, $errors);
+            break;
+
+        case 'N':
+            $vars['note'] = $body;
+
+            if ($object instanceof Threadable)
+                return $object->postThreadEntry('N', $vars);
+            elseif ($this instanceof ObjectThread)
+                return $this->addNote($vars, $errors);
             break;
         }
+
+        throw new Exception('Unable to continue thread via email.');
+
+        // Currently impossible, but indicate that this thread object could
+        // not append the incoming email.
+        return false;
     }
-}
 
+    function deleteAttachments() {
+        $deleted = Attachment::objects()->filter(array(
+            'thread_entry__thread' => $this,
+        ))->delete();
+
+        if ($deleted)
+            AttachmentFile::deleteOrphans();
+
+        return $deleted;
+    }
 
-class ThreadEntry {
+    function removeCollaborators() {
+        return Collaborator::objects()
+            ->filter(array('thread_id'=>$this->getId()))
+            ->delete();
+    }
 
-    var $id;
-    var $ht;
+    /**
+     * Function: lookupByEmailHeaders
+     *
+     * Attempt to locate a thread by the email headers. It should be
+     * considered a secondary lookup to ThreadEntry::lookupByEmailHeaders(),
+     * which should find an actual thread entry, which should be possible
+     * for all email communcation which is associated with a thread entry.
+     * The only time where this is useful is for threads which triggered
+     * email communication without a thread entry, for instance, like
+     * tickets created without an initial message.
+     */
+    function lookupByEmailHeaders(&$mailinfo) {
+        $possibles = array();
+        foreach (array('mid', 'in-reply-to', 'references') as $header) {
+            $matches = array();
+            if (!isset($mailinfo[$header]) || !$mailinfo[$header])
+                continue;
+            // Header may have multiple entries (usually separated by
+            // spaces ( )
+            elseif (!preg_match_all('/<([^>@]+@[^>]+)>/', $mailinfo[$header],
+                        $matches))
+                continue;
 
-    var $staff;
-    var $ticket;
+            // The References header will have the most recent message-id
+            // (parent) on the far right.
+            // @see rfc 1036, section 2.2.5
+            // @see http://www.jwz.org/doc/threading.html
+            $possibles = array_merge($possibles, array_reverse($matches[1]));
+        }
 
-    var $attachments;
+        // Add the message id if it is embedded in the body
+        $match = array();
+        if (preg_match('`(?:class="mid-|Ref-Mid: )([^"\s]*)(?:$|")`',
+                $mailinfo['message'], $match)
+            && !in_array($match[1], $possibles)
+        ) {
+            $possibles[] = $match[1];
+        }
 
+        foreach ($possibles as $mid) {
+            // Attempt to detect the ticket and user ids from the
+            // message-id header. If the message originated from
+            // osTicket, the Mailer class can break it apart. If it came
+            // from this help desk, the 'loopback' property will be set
+            // to true.
+            $mid_info = Mailer::decodeMessageId($mid);
+            if (!$mid_info || !$mid_info['loopback'])
+                continue;
+            if (isset($mid_info['uid'])
+                && @$mid_info['threadId']
+                && ($t = Thread::lookup($mid_info['threadId']))
+            ) {
+                if (@$mid_info['userId']) {
+                    $mailinfo['userId'] = $mid_info['userId'];
+                }
+                elseif (@$mid_info['staffId']) {
+                    $mailinfo['staffId'] = $mid_info['staffId'];
+                }
+                // ThreadEntry was positively identified
+                return $t;
+            }
+        }
 
-    function ThreadEntry($id, $type='', $ticketId=0) {
-        $this->load($id, $type, $ticketId);
+        return null;
     }
 
-    function load($id=0, $type='', $ticketId=0) {
+    function delete() {
 
-        if(!$id && !($id=$this->getId()))
+        //Self delete
+        if (!parent::delete())
             return false;
 
-        $sql='SELECT thread.*'
-            .' ,count(DISTINCT attach.attach_id) as attachments '
-            .' FROM '.TICKET_THREAD_TABLE.' thread '
-            .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach
-                ON (thread.ticket_id=attach.ticket_id
-                        AND thread.id=attach.ref_id) '
-            .' WHERE  thread.id='.db_input($id);
+        // Clear email meta data (header..etc)
+        ThreadEntryEmailInfo::objects()
+            ->filter(array('thread_entry__thread' => $this))
+            ->update(array('headers' => null));
+
+        // Mass delete entries
+        $this->deleteAttachments();
+        $this->removeCollaborators();
+
+        $this->entries->delete();
+
+        // Null out the events
+        $this->events->update(array('thread_id' => 0));
+
+        return true;
+    }
+
+    static function create($vars=false) {
+        $inst = new static($vars);
+        $inst->created = SqlFunction::NOW();
+        return $inst;
+    }
+}
+
+class ThreadEntryEmailInfo extends VerySimpleModel {
+    static $meta = array(
+        'table' => THREAD_ENTRY_EMAIL_TABLE,
+        'pk' => array('id'),
+        'joins' => array(
+            'thread_entry' => array(
+                'constraint' => array('thread_entry_id' => 'ThreadEntry.id'),
+            ),
+        ),
+    );
+}
 
-        if($type)
-            $sql.=' AND thread.thread_type='.db_input($type);
+class ThreadEntry extends VerySimpleModel
+implements TemplateVariable {
+    static $meta = array(
+        'table' => THREAD_ENTRY_TABLE,
+        'pk' => array('id'),
+        'select_related' => array('staff', 'user', 'email_info'),
+        'ordering' => array('created', 'id'),
+        'joins' => array(
+            'thread' => array(
+                'constraint' => array('thread_id' => 'Thread.id'),
+            ),
+            'parent' => array(
+                'constraint' => array('pid' => 'ThreadEntry.id'),
+                'null' => true,
+            ),
+            'children' => array(
+                'reverse' => 'ThreadEntry.parent',
+            ),
+            'email_info' => array(
+                'reverse' => 'ThreadEntryEmailInfo.thread_entry',
+                'list' => false,
+            ),
+            'attachments' => array(
+                'reverse' => 'Attachment.thread_entry',
+                'null' => true,
+            ),
+            'staff' => array(
+                'constraint' => array('staff_id' => 'Staff.staff_id'),
+                'null' => true,
+            ),
+            'user' => array(
+                'constraint' => array('user_id' => 'User.id'),
+                'null' => true,
+            ),
+        ),
+    );
 
-        if($ticketId)
-            $sql.=' AND thread.ticket_id='.db_input($ticketId);
+    const FLAG_ORIGINAL_MESSAGE         = 0x0001;
+    const FLAG_EDITED                   = 0x0002;
+    const FLAG_HIDDEN                   = 0x0004;
+    const FLAG_GUARDED                  = 0x0008;   // No replace on edit
+    const FLAG_RESENT                   = 0x0010;
+
+    const FLAG_COLLABORATOR             = 0x0020;   // Message from collaborator
+    const FLAG_BALANCED                 = 0x0040;   // HTML does not need to be balanced on ::display()
+    const FLAG_SYSTEM                   = 0x0080;   // Entry is a system note.
+
+    const PERM_EDIT     = 'thread.edit';
+
+    var $_headers;
+    var $_thread;
+    var $_actions;
+    var $is_autoreply;
+    var $is_bounce;
+
+    static protected $perms = array(
+        self::PERM_EDIT => array(
+            'title' => /* @trans */ 'Edit Thread',
+            'desc'  => /* @trans */ 'Ability to edit thread items of other agents',
+        ),
+    );
 
-        $sql.=' GROUP BY thread.id ';
+    function postEmail($mailinfo) {
+        global $ost;
 
-        if(!($res=db_query($sql)) || !db_num_rows($res))
+        if (!($thread = $this->getThread()))
+            // Kind of hard to continue a discussion without a thread ...
             return false;
 
-        $this->ht = db_fetch_array($res);
-        $this->id = $this->ht['id'];
+        elseif ($this->getEmailMessageId() == $mailinfo['mid'])
+            // Reporting success so the email can be moved or deleted.
+            return true;
 
-        $this->staff = $this->ticket = null;
-        $this->attachments = array();
+        // Mail sent by this system will have a predictable message-id
+        // If this incoming mail matches the code, then it very likely
+        // originated from this system and looped
+        $info = Mailer::decodeMessageId($mailinfo['mid']);
+        if ($info && $info['loopback']) {
+            // This mail was sent by this system. It was received due to
+            // some kind of mail delivery loop. It should not be considered
+            // a response to an existing thread entry
+            if ($ost)
+                $ost->log(LOG_ERR, _S('Email loop detected'), sprintf(
+                _S('It appears as though &lt;%s&gt; is being used as a forwarded or fetched email account and is also being used as a user / system account. Please correct the loop or seek technical assistance.'),
+                $mailinfo['email']),
 
-        return true;
-    }
+                // This is quite intentional -- don't continue the loop
+                false,
+                // Force the message, even if logging is disabled
+                true);
+            return $this;
+        }
 
-    function reload() {
-        return $this->load();
+        return $thread->postEmail($mailinfo, $this);
     }
 
     function getId() {
@@ -290,49 +653,48 @@ class ThreadEntry {
     }
 
     function getPid() {
-        return $this->ht['pid'];
+        return $this->get('pid', 0);
     }
 
     function getParent() {
-        if ($this->getPid())
-            return ThreadEntry::lookup($this->getPid());
+        return $this->parent;
     }
 
     function getType() {
-        return $this->ht['thread_type'];
+        return $this->type;
     }
 
     function getSource() {
-        return $this->ht['source'];
+        return $this->source;
     }
 
     function getPoster() {
-        return $this->ht['poster'];
+        return $this->poster;
     }
 
     function getTitle() {
-        return $this->ht['title'];
+        return $this->title;
     }
 
     function getBody() {
-        return ThreadBody::fromFormattedText($this->ht['body'], $this->ht['format']);
+        return ThreadEntryBody::fromFormattedText($this->body, $this->format,
+            array('balanced' => $this->hasFlag(self::FLAG_BALANCED))
+        );
     }
 
     function setBody($body) {
         global $cfg;
 
-        if (!$body instanceof ThreadBody) {
-            if ($cfg->isHtmlThreadEnabled())
-                $body = new HtmlThreadBody($body);
+        if (!$body instanceof ThreadEntryBody) {
+            if ($cfg->isRichTextEnabled())
+                $body = new HtmlThreadEntryBody($body);
             else
-                $body = new TextThreadBody($body);
+                $body = new TextThreadEntryBody($body);
         }
 
-        $sql='UPDATE '.TICKET_THREAD_TABLE.' SET updated=NOW()'
-            .',format='.db_input($body->getType())
-            .',body='.db_input((string) $body)
-            .' WHERE id='.db_input($this->getId());
-        return db_query($sql) && db_affected_rows();
+        $this->format = $body->getType();
+        $this->body = (string) $body;
+        return $this->save();
     }
 
     function getMessage() {
@@ -340,49 +702,31 @@ class ThreadEntry {
     }
 
     function getCreateDate() {
-        return $this->ht['created'];
+        return $this->created;
     }
 
     function getUpdateDate() {
-        return $this->ht['updated'];
+        return $this->updated;
     }
 
     function getNumAttachments() {
-        return $this->ht['attachments'];
-    }
-
-    function getTicketId() {
-        return $this->ht['ticket_id'];
-    }
-
-    function _deferEmailInfo() {
-        if (isset($this->ht['email_mid']))
-            return;
-
-        // Don't do this more than once
-        $this->ht['email_mid'] = false;
-
-        $sql = 'SELECT email_mid, headers FROM '.TICKET_EMAIL_INFO_TABLE
-            .' WHERE thread_id='.db_input($this->getId());
-        if (!($res = db_query($sql)))
-            return;
-
-        list($this->ht['email_mid'], $this->ht['headers']) = db_fetch_row($res);
+        return $this->attachments->count();
     }
 
     function getEmailMessageId() {
-        $this->_deferEmailInfo();
-        return $this->ht['email_mid'];
+        if ($this->email_info)
+            return $this->email_info->mid;
     }
 
     function getEmailHeaderArray() {
         require_once(INCLUDE_DIR.'class.mailparse.php');
 
-        $this->_deferEmailInfo();
-        if (!isset($this->ht['@headers']))
-            $this->ht['@headers'] = Mail_Parse::splitHeaders($this->ht['headers']);
-
-        return $this->ht['@headers'];
+        if (!isset($this->_headers) && $this->email_info
+            && isset($this->email_info->headers)
+        ) {
+            $this->_headers = Mail_Parse::splitHeaders($this->email_info->headers);
+        }
+        return $this->_headers;
     }
 
     function getEmailReferences($include_mid=true) {
@@ -422,6 +766,21 @@ class ThreadEntry {
         return $recipients;
     }
 
+    /**
+     * Recurse through the ancestry of this thread entry to find the first
+     * thread entry which cites a email Message-ID field.
+     *
+     * Returns:
+     * <ThreadEntry> or null if neither this thread entry nor any of its
+     * ancestry contains an email header with an email Message-ID header.
+     */
+    function findOriginalEmailMessage() {
+        $P = $this;
+        while (!$P->getEmailMessageId()
+            && ($P = $P->getParent()));
+        return $P;
+    }
+
     function getUIDFromEmailReference($ref) {
 
         $info = unpack('Vtid/Vuid',
@@ -432,50 +791,58 @@ class ThreadEntry {
 
     }
 
-    function getTicket() {
+    function getThreadId() {
+        return $this->thread_id;
+    }
+
+    function getThread() {
 
-        if(!$this->ticket && $this->getTicketId())
-            $this->ticket = Ticket::lookup($this->getTicketId());
+        if (!isset($this->_thread) && $this->thread_id)
+            // TODO: Consider typing the thread based on its type field
+            $this->_thread = ObjectThread::lookup($this->getThreadId());
 
-        return $this->ticket;
+        return $this->_thread;
     }
 
     function getStaffId() {
-        return $this->ht['staff_id'];
+        return isset($this->staff_id) ? $this->staff_id : 0;
     }
 
     function getStaff() {
-
-        if(!$this->staff && $this->getStaffId())
-            $this->staff = Staff::lookup($this->getStaffId());
-
         return $this->staff;
     }
 
     function getUserId() {
-        return $this->ht['user_id'];
+        return isset($this->user_id) ? $this->user_id : 0;
     }
 
     function getUser() {
+        return $this->user;
+    }
 
-        if (!isset($this->user)) {
-            if (!($ticket = $this->getTicket()))
-                return null;
+    function getEditor() {
+        static $types = array(
+            'U' => 'User',
+            'S' => 'Staff',
+        );
+        if (!isset($types[$this->editor_type]))
+            return null;
 
-            if ($ticket->getOwnerId() == $this->getUserId())
-                $this->user = new TicketOwner(
-                    User::lookup($this->getUserId()), $ticket);
-            else
-                $this->user = Collaborator::lookup(array(
-                    'userId'=>$this->getUserId(), 'ticketId'=>$this->getTicketId()));
-        }
+        return $types[$this->editor_type]::lookup($this->editor);
+    }
 
-        return $this->user;
+    function getName() {
+        if ($this->staff_id)
+            return $this->staff->getName();
+        if ($this->user_id)
+            return $this->user->getName();
+
+        return $this->poster;
     }
 
     function getEmailHeader() {
-        $this->_deferEmailInfo();
-        return $this->ht['headers'];
+        if ($this->email_info)
+            return $this->email_info->headers;
     }
 
     function isAutoReply() {
@@ -500,10 +867,24 @@ class ThreadEntry {
         return ($this->isAutoReply() || $this->isBounce());
     }
 
-    //Web uploads - caller is expected to format, validate and set any errors.
-    function uploadFiles($files) {
-
-        if(!$files || !is_array($files))
+    function hasFlag($flag) {
+        return ($this->get('flags', 0) & $flag) != 0;
+    }
+    function clearFlag($flag) {
+        return $this->set('flags', $this->get('flags') & ~$flag);
+    }
+    function setFlag($flag) {
+        return $this->set('flags', $this->get('flags') | $flag);
+    }
+
+    function isSystem() {
+        return $this->hasFlag(self::FLAG_SYSTEM);
+    }
+
+    //Web uploads - caller is expected to format, validate and set any errors.
+    function uploadFiles($files) {
+
+        if(!$files || !is_array($files))
             return false;
 
         $uploaded=array();
@@ -512,9 +893,9 @@ class ThreadEntry {
                 continue;
 
             if(!$file['error']
-                    && ($id=AttachmentFile::upload($file))
-                    && $this->saveAttachment($id))
-                $uploaded[]=$id;
+                    && ($F=AttachmentFile::upload($file))
+                    && $this->saveAttachment($F))
+                $uploaded[]= $F->getId();
             else {
                 if(!$file['error'])
                     $error = sprintf(__('Unable to upload file - %s'),$file['name']);
@@ -527,7 +908,7 @@ class ThreadEntry {
                  XXX: We're doing it here because it will eventually become a thread post comment (hint: comments coming!)
                  XXX: logNote must watch for possible loops
                */
-                $this->getTicket()->logNote(__('File Upload Error'), $error, 'SYSTEM', false);
+                $this->getThread()->getObject()->logNote(__('File Upload Error'), $error, 'SYSTEM', false);
             }
 
         }
@@ -554,258 +935,110 @@ class ThreadEntry {
         if(!$attachment || !is_array($attachment))
             return null;
 
-        $id=0;
-        if ($attachment['error'] || !($id=$this->saveAttachment($attachment))) {
+        $A=null;
+        if ($attachment['error'] || !($A=$this->saveAttachment($attachment))) {
             $error = $attachment['error'];
-
             if(!$error)
-                $error = sprintf(_S('Unable to import attachment - %s'),$attachment['name']);
-
-            $this->getTicket()->logNote(_S('File Import Error'), $error,
-                _S('SYSTEM'), false);
+                $error = sprintf(_S('Unable to import attachment - %s'),
+                        $attachment['name']);
+            //FIXME: logComment here
+            $this->getThread()->getObject()->logNote(
+                    _S('File Import Error'), $error, _S('SYSTEM'), false);
         }
 
-        return $id;
+        return $A;
     }
 
    /*
     Save attachment to the DB.
     @file is a mixed var - can be ID or file hashtable.
     */
-    function saveAttachment(&$file) {
+    function saveAttachment(&$file, $name=false) {
+
+        $inline = is_array($file) && @$file['inline'];
 
         if (is_numeric($file))
             $fileId = $file;
+        elseif ($file instanceof AttachmentFile)
+            $fileId = $file->getId();
+        elseif ($F = AttachmentFile::create($file))
+            $fileId = $F->getId();
         elseif (is_array($file) && isset($file['id']))
             $fileId = $file['id'];
-        elseif (!($fileId = AttachmentFile::save($file)))
-            return 0;
-
-        $inline = is_array($file) && @$file['inline'];
+        else
+            return false;
 
-        // TODO: Add a unique index to TICKET_ATTACHMENT_TABLE (file_id,
-        // ref_id), and remove this block
-        if ($id = db_result(db_query('SELECT attach_id FROM '.TICKET_ATTACHMENT_TABLE
-                .' WHERE file_id='.db_input($fileId).' AND ref_id='
-                .db_input($this->getId()))))
-            return $id;
+        $att = new Attachment(array(
+            'type' => 'H',
+            'object_id' => $this->getId(),
+            'file_id' => $fileId,
+            'inline' => $inline ? 1 : 0,
+        ));
 
-        $sql ='INSERT IGNORE INTO '.TICKET_ATTACHMENT_TABLE.' SET created=NOW() '
-             .' ,file_id='.db_input($fileId)
-             .' ,ticket_id='.db_input($this->getTicketId())
-             .' ,inline='.db_input($inline ? 1 : 0)
-             .' ,ref_id='.db_input($this->getId());
+        // Record varying file names in the attachment record
+        if (is_array($file) && isset($file['name'])) {
+            $filename = $file['name'];
+        }
+        elseif (is_string($name)) {
+            $filename = $name;
+        }
+        if ($filename) {
+            // This should be a noop since the ORM caches on PK
+            $F = $F ?: AttachmentFile::lookup($fileId);
+            // XXX: This is not Unicode safe
+            if ($F && 0 !== strcasecmp($F->name, $filename))
+                $att->name = $filename;
+        }
 
-        return (db_query($sql) && ($id=db_insert_id()))?$id:0;
+        if (!$att->save())
+            return false;
+        return $att;
     }
 
     function saveAttachments($files) {
-        $ids=array();
-        foreach($files as $file)
-           if(($id=$this->saveAttachment($file)))
-               $ids[] = $id;
+        $attachments = array();
+        foreach ($files as $name=>$file) {
+           if (($A = $this->saveAttachment($file, $name)))
+               $attachments[] = $A;
+        }
 
-        return $ids;
+        return $attachments;
     }
 
     function getAttachments() {
-
-        if($this->attachments)
-            return $this->attachments;
-
-        //XXX: inner join the file table instead?
-        $sql='SELECT a.attach_id, f.id as file_id, f.size, lower(f.`key`) as file_hash, f.`signature` as file_sig, f.name, a.inline '
-            .' FROM '.FILE_TABLE.' f '
-            .' INNER JOIN '.TICKET_ATTACHMENT_TABLE.' a ON(f.id=a.file_id) '
-            .' WHERE a.ticket_id='.db_input($this->getTicketId())
-            .' AND a.ref_id='.db_input($this->getId());
-
-        $this->attachments = array();
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            while($rec=db_fetch_array($res)) {
-                $rec['download_url'] = AttachmentFile::generateDownloadUrl(
-                    $rec['file_id'], $rec['file_hash'], $rec['file_sig']);
-                $this->attachments[] = $rec;
-            }
-        }
-
         return $this->attachments;
     }
 
     function getAttachmentUrls() {
         $json = array();
-        foreach ($this->getAttachments() as $att) {
-            $json[$att['file_hash']] = array(
-                'download_url' => $att['download_url'],
-                'filename' => $att['name'],
+        foreach ($this->attachments as $att) {
+            $json[$att->file->getKey()] = array(
+                'download_url' => $att->file->getDownloadUrl(),
+                'filename' => $att->getFilename(),
             );
         }
+
         return $json;
     }
 
-    function getAttachmentsLinks($file='attachment.php', $target='', $separator=' ') {
+    function getAttachmentsLinks($file='attachment.php', $target='_blank', $separator=' ') {
+        // TODO: Move this to the respective UI templates
 
         $str='';
-        foreach($this->getAttachments() as $attachment ) {
-            if ($attachment['inline'])
-                continue;
+        foreach ($this->attachments as $att ) {
+            if ($att->inline) continue;
             $size = '';
-            if($attachment['size'])
-                $size=sprintf('<em>(%s)</em>', Format::file_size($attachment['size']));
+            if ($att->file->size)
+                $size=sprintf('<em>(%s)</em>', Format::file_size($att->file->size));
 
-            $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);
+            $str .= sprintf(
+                '<a class="Icon file no-pjax" href="%s" target="%s">%s</a>%s&nbsp;%s',
+                $att->file->getDownloadUrl(), $target,
+                Format::htmlchars($att->file->name), $size, $separator);
         }
 
         return $str;
     }
-    /**
-     * postEmail
-     *
-     * After some security and sanity checks, attaches the body and subject
-     * of the message in reply to this thread item
-     *
-     * Parameters:
-     * mailinfo - (array) of information about the email, with at least the
-     *          following keys
-     *      - mid - (string) email message-id
-     *      - name - (string) personal name of email originator
-     *      - email - (string<email>) originating email address
-     *      - subject - (string) email subject line (decoded)
-     *      - body - (string) email message body (decoded)
-     */
-    function postEmail($mailinfo) {
-        global $ost;
-
-        // +==================+===================+=============+
-        // | Orig Thread-Type | Reply Thread-Type | Requires    |
-        // +==================+===================+=============+
-        // | *                | Message (M)       | From: Owner |
-        // | *                | Note (N)          | From: Staff |
-        // | Response (R)     | Message (M)       |             |
-        // | Message (M)      | Response (R)      | From: Staff |
-        // +------------------+-------------------+-------------+
-
-        if (!$ticket = $this->getTicket())
-            // Kind of hard to continue a discussion without a ticket ...
-            return false;
-
-        // Make sure the email is NOT already fetched... (undeleted emails)
-        elseif ($this->getEmailMessageId() == $mailinfo['mid'])
-            // Reporting success so the email can be moved or deleted.
-            return true;
-
-        // Mail sent by this system will have a message-id format of
-        // <code-random-mailbox@domain.tld>
-        // where code is a predictable string based on the SECRET_SALT of
-        // this osTicket installation. If this incoming mail matches the
-        // code, then it very likely originated from this system and looped
-        $msgId_info = Mailer::decodeMessageId($mailinfo['mid']);
-        if ($msgId_info['loopback']) {
-            // This mail was sent by this system. It was received due to
-            // some kind of mail delivery loop. It should not be considered
-            // a response to an existing thread entry
-            if ($ost) $ost->log(LOG_ERR, _S('Email loop detected'), sprintf(
-                _S('It appears as though &lt;%s&gt; is being used as a forwarded or fetched email account and is also being used as a user / system account. Please correct the loop or seek technical assistance.'),
-                $mailinfo['email']),
-
-                // This is quite intentional -- don't continue the loop
-                false,
-                // Force the message, even if logging is disabled
-                true);
-            return true;
-        }
-
-        $vars = array(
-            'mid' =>    $mailinfo['mid'],
-            'header' => $mailinfo['header'],
-            'ticketId' => $ticket->getId(),
-            'poster' => $mailinfo['name'],
-            'origin' => 'Email',
-            'source' => 'Email',
-            'ip' =>     '',
-            'reply_to' => $this,
-            'recipients' => $mailinfo['recipients'],
-            'to-email-id' => $mailinfo['to-email-id'],
-        );
-        $errors = array();
-
-        if (isset($mailinfo['attachments']))
-            $vars['attachments'] = $mailinfo['attachments'];
-
-        $body = $mailinfo['message'];
-
-        // Disambiguate if the user happens also to be a staff member of the
-        // system. The current ticket owner should _always_ post messages
-        // instead of notes or responses
-        if ($mailinfo['userId']
-                || strcasecmp($mailinfo['email'], $ticket->getEmail()) == 0) {
-            $vars['message'] = $body;
-            $vars['userId'] = $mailinfo['userId'] ?: $ticket->getUserId();
-            return $ticket->postMessage($vars, 'Email');
-        }
-        // Consider collaborator role (disambiguate staff members as
-        // collaborators)
-        elseif (($E = UserEmail::lookup($mailinfo['email']))
-            && ($C = Collaborator::lookup(array(
-                'ticketId' => $ticket->getId(), 'userId' => $E->user_id
-            )))
-        ) {
-            $vars['userId'] = $C->getUserId();
-            $vars['message'] = $body;
-            return $ticket->postMessage($vars, 'Email');
-        }
-        elseif ($mailinfo['staffId']
-                || ($mailinfo['staffId'] = Staff::getIdByEmail($mailinfo['email']))) {
-            $vars['staffId'] = $mailinfo['staffId'];
-            $poster = Staff::lookup($mailinfo['staffId']);
-            $vars['note'] = $body;
-            return $ticket->postNote($vars, $errors, $poster);
-        }
-        elseif (Email::getIdByEmail($mailinfo['email'])) {
-            // Don't process the email -- it came FROM this system
-            return true;
-        }
-        // Support the mail parsing system declaring a thread-type
-        elseif (isset($mailinfo['thread-type'])) {
-            switch ($mailinfo['thread-type']) {
-            case 'N':
-                $vars['note'] = $body;
-                $poster = $mailinfo['email'];
-                return $ticket->postNote($vars, $errors, $poster);
-            }
-        }
-        // TODO: Consider security constraints
-        else {
-            //XXX: Are we potentially leaking the email address to
-            // collaborators?
-            $header = sprintf("Received From: %s <%s>\n\n", $mailinfo['name'],
-                $mailinfo['email']);
-            if ($body instanceof HtmlThreadBody)
-                $header = nl2br(Format::htmlchars($header));
-            // Add the banner to the top of the message
-            if ($body instanceof ThreadBody)
-                $body->prepend($header);
-
-            $vars['message'] = $body;
-            $vars['userId'] = 0; //Unknown user! //XXX: Assume ticket owner?
-            return $ticket->postMessage($vars, 'Email');
-        }
-        // Currently impossible, but indicate that this thread object could
-        // not append the incoming email.
-        return false;
-    }
-
-    /* Returns file names with id as key */
-    function getFiles() {
-
-        $files = array();
-        foreach($this->getAttachments() as $attachment)
-            $files[$attachment['file_id']] = $attachment['name'];
-
-        return $files;
-    }
-
 
     /* save email info
      * TODO: Refactor it to include outgoing emails on responses.
@@ -827,12 +1060,23 @@ class ThreadEntry {
 
     /* static */
     function logEmailHeaders($id, $mid, $header=false) {
-        $sql='INSERT INTO '.TICKET_EMAIL_INFO_TABLE
-            .' SET thread_id='.db_input($id)
-            .', email_mid='.db_input($mid); //TODO: change it to message_id.
+
+        if (!$id || !$mid)
+            return false;
+
+        $this->email_info = new ThreadEntryEmailInfo(array(
+            'thread_entry_id' => $id,
+            'mid' => $mid,
+        ));
+
         if ($header)
-            $sql .= ', headers='.db_input($header);
-        return db_query($sql)?db_insert_id():0;
+            $this->email_info->headers = trim($header);
+
+        return $this->email_info->save();
+    }
+
+    function getActivity() {
+        return new ThreadActivity('', '');
     }
 
     /* variables */
@@ -841,44 +1085,39 @@ class ThreadEntry {
         return (string) $this->getBody();
     }
 
+    // TemplateVariable interface
     function asVar() {
         return (string) $this->getBody()->display('email');
     }
 
     function getVar($tag) {
-        global $cfg;
-
-        if($tag && is_callable(array($this, 'get'.ucfirst($tag))))
-            return call_user_func(array($this, 'get'.ucfirst($tag)));
-
         switch(strtolower($tag)) {
             case 'create_date':
-                return Format::date(
-                        $cfg->getDateTimeFormat(),
-                        Misc::db2gmtime($this->getCreateDate()),
-                        $cfg->getTZOffset(),
-                        $cfg->observeDaylightSaving());
-                break;
+                return new FormattedDate($this->getCreateDate());
             case 'update_date':
-                return Format::date(
-                        $cfg->getDateTimeFormat(),
-                        Misc::db2gmtime($this->getUpdateDate()),
-                        $cfg->getTZOffset(),
-                        $cfg->observeDaylightSaving());
-                break;
+                return new FormattedDate($this->getUpdateDate());
+            case 'files':
+                throw new OOBContent(OOBContent::FILES, $this->attachments->all());
         }
-
-        return false;
     }
 
-    /* static calls */
-
-    function lookup($id, $tid=0, $type='') {
-        return ($id
-                && is_numeric($id)
-                && ($e = new ThreadEntry($id, $type, $tid))
-                && $e->getId()==$id
-                )?$e:null;
+    static function getVarScope() {
+        return array(
+          'files' => __('Attached files'),
+          'body' => __('Message body'),
+          'create_date' => array(
+              'class' => 'FormattedDate', 'desc' => __('Date created'),
+          ),
+          'ip_address' => __('IP address of remote user, for web submissions'),
+          'poster' => __('Submitter of the thread item'),
+          'staff' => array(
+            'class' => 'Staff', 'desc' => __('Agent posting the note or response'),
+          ),
+          'title' => __('Subject, if any'),
+          'user' => array(
+            'class' => 'User', 'desc' => __('User posting the message'),
+          ),
+        );
     }
 
     /**
@@ -897,22 +1136,23 @@ class ThreadEntry {
     function lookupByEmailHeaders(&$mailinfo, &$seen=false) {
         // Search for messages using the References header, then the
         // in-reply-to header
-        $search = 'SELECT thread_id, email_mid FROM '.TICKET_EMAIL_INFO_TABLE
-               . ' WHERE email_mid=%s ORDER BY thread_id DESC';
-
-        if (list($id, $mid) = db_fetch_row(db_query(
-                sprintf($search, db_input($mailinfo['mid']))))) {
+        if ($entry = ThreadEntry::objects()
+            ->filter(array('email_info__mid' => $mailinfo['mid']))
+            ->order_by(false)
+            ->first()
+        ) {
             $seen = true;
-            return ThreadEntry::lookup($id);
+            return $entry;
         }
 
-        foreach (array('in-reply-to', 'references') as $header) {
+        $possibles = array();
+        foreach (array('mid', 'in-reply-to', 'references') as $header) {
             $matches = array();
             if (!isset($mailinfo[$header]) || !$mailinfo[$header])
                 continue;
             // Header may have multiple entries (usually separated by
             // spaces ( )
-            elseif (!preg_match_all('/<[^>@]+@[^>]+>/', $mailinfo[$header],
+            elseif (!preg_match_all('/<([^>@]+@[^>]+)>/', $mailinfo[$header],
                         $matches))
                 continue;
 
@@ -920,60 +1160,82 @@ class ThreadEntry {
             // (parent) on the far right.
             // @see rfc 1036, section 2.2.5
             // @see http://www.jwz.org/doc/threading.html
-            $thread = null;
-            foreach (array_reverse($matches[0]) as $mid) {
-                //Try to determine if it's a reply to a tagged email.
-                $ref = null;
-                if (strpos($mid, '+')) {
-                    list($left, $right) = explode('@',$mid);
-                    list($left, $ref) = explode('+', $left);
-                    $mid = "$left@$right";
+            $possibles = array_merge($possibles, array_reverse($matches[1]));
+        }
+
+        // Add the message id if it is embedded in the body
+        $match = array();
+        if (preg_match('`(?:class="mid-|Ref-Mid: )([^"\s]*)(?:$|")`',
+                (string) $mailinfo['message'], $match)
+            && !in_array($match[1], $possibles)
+        ) {
+            $possibles[] = $match[1];
+        }
+
+        $thread = null;
+        foreach ($possibles as $mid) {
+            // Attempt to detect the ticket and user ids from the
+            // message-id header. If the message originated from
+            // osTicket, the Mailer class can break it apart. If it came
+            // from this help desk, the 'loopback' property will be set
+            // to true.
+            $mid_info = Mailer::decodeMessageId($mid);
+            if (!$mid_info || !$mid_info['loopback'])
+                continue;
+            if (isset($mid_info['uid'])
+                && @$mid_info['entryId']
+                && ($t = ThreadEntry::lookup($mid_info['entryId']))
+                && ($t->thread_id == $mid_info['threadId'])
+            ) {
+                if (@$mid_info['userId']) {
+                    $mailinfo['userId'] = $mid_info['userId'];
                 }
-                $res = db_query(sprintf($search, db_input($mid)));
-                while (list($id) = db_fetch_row($res)) {
-                    if (!($t = ThreadEntry::lookup($id)))
-                        continue;
-                    // Capture the first match thread item
-                    if (!$thread)
-                        $thread = $t;
-                    // We found a match  - see if we can ID the user.
-                    // XXX: Check access of ref is enough?
-                    if ($ref && ($uid = $t->getUIDFromEmailReference($ref))) {
-                        if ($ref[0] =='s') //staff
-                            $mailinfo['staffId'] = $uid;
-                        else // user or collaborator.
-                            $mailinfo['userId'] = $uid;
-
-                        // Best possible case — found the thread and the
-                        // user
-                        return $t;
-                    }
+                elseif (@$mid_info['staffId']) {
+                    $mailinfo['staffId'] = $mid_info['staffId'];
                 }
-                // Attempt to detect the ticket and user ids from the
-                // message-id header. If the message originated from
-                // osTicket, the Mailer class can break it apart. If it came
-                // from this help desk, the 'loopback' property will be set
-                // to true.
-                $mid_info = Mailer::decodeMessageId($mid);
-                if ($mid_info['loopback'] && isset($mid_info['uid'])
-                    && @$mid_info['threadId']
-                    && ($t = ThreadEntry::lookup($mid_info['threadId']))
-                ) {
-                    if (@$mid_info['userId']) {
-                        $mailinfo['userId'] = $mid_info['userId'];
-                    }
-                    elseif (@$mid_info['staffId']) {
-                        $mailinfo['staffId'] = $mid_info['staffId'];
-                    }
-                    // ThreadEntry was positively identified
+
+                // Capture the user type
+                if (@$mid_info['userClass'])
+                    $mailinfo['userClass'] = $mid_info['userClass'];
+
+
+                // ThreadEntry was positively identified
+                return $t;
+            }
+
+            // Try to determine if it's a reply to a tagged email.
+            // (Deprecated)
+            $ref = null;
+            if (strpos($mid, '+')) {
+                list($left, $right) = explode('@',$mid);
+                list($left, $ref) = explode('+', $left);
+                $mid = "$left@$right";
+            }
+            $entries = ThreadEntry::objects()
+                ->filter(array('email_info__mid' => $mid))
+                ->order_by(false);
+            foreach ($entries as $t) {
+                // Capture the first match thread item
+                if (!$thread)
+                    $thread = $t;
+                // We found a match  - see if we can ID the user.
+                // XXX: Check access of ref is enough?
+                if ($ref && ($uid = $t->getUIDFromEmailReference($ref))) {
+                    if ($ref[0] =='s') //staff
+                        $mailinfo['staffId'] = $uid;
+                    else // user or collaborator.
+                        $mailinfo['userId'] = $uid;
+
+                    // Best possible case — found the thread and the
+                    // user
                     return $t;
                 }
             }
-            // Second best case — found a thread but couldn't identify the
-            // user from the header. Return the first thread entry matched
-            if ($thread)
-                return $thread;
         }
+        // Second best case — found a thread but couldn't identify the
+        // user from the header. Return the first thread entry matched
+        if ($thread)
+            return $thread;
 
         // Search for ticket by the [#123456] in the subject line
         // This is the last resort -  emails must match to avoid message
@@ -993,29 +1255,26 @@ class ThreadEntry {
             //We have a valid ticket and user
             if ($ticket->getUserId() == $user->getId() //owner
                     ||  ($c = Collaborator::lookup( // check if collaborator
-                            array('userId' => $user->getId(),
-                                  'ticketId' => $ticket->getId())))) {
+                            array('user_id' => $user->getId(),
+                                  'thread_id' => $ticket->getThreadId())))) {
 
                 $mailinfo['userId'] = $user->getId();
                 return $ticket->getLastMessage();
             }
         }
 
-        // Search for the message-id token in the body
-        if (preg_match('`(?:class="mid-|Ref-Mid: )([^"\s]*)(?:$|")`',
-                $mailinfo['message'], $match))
-            if ($thread = ThreadEntry::lookupByRefMessageId($match[1],
-                    $mailinfo['email']))
-                return $thread;
-
         return null;
     }
 
     /**
      * Find a thread entry from a message-id created from the
-     * ::asMessageId() method
+     * ::asMessageId() method.
+     *
+     * *DEPRECATED* use Mailer::decodeMessageId() instead
      */
     function lookupByRefMessageId($mid, $from) {
+        global $ost;
+
         $mid = trim($mid, '<>');
         list($ver, $ids, $mails) = explode('$', $mid, 3);
 
@@ -1027,56 +1286,44 @@ class ThreadEntry {
         if (!$ids || !$ids['thread'])
             return false;
 
-        $thread = ThreadEntry::lookup($ids['thread']);
-        if (!$thread)
+        $entry = ThreadEntry::lookup($ids['thread']);
+        if (!$entry)
             return false;
 
-        if (0 === strcasecmp($thread->asMessageId($from, $ver), $mid))
-            return $thread;
-    }
-
-    /**
-     * Get an email message-id that can be used to represent this thread
-     * entry. The same message-id can be passed to ::lookupByRefMessageId()
-     * to find this thread entry
-     *
-     * Formats:
-     * Initial (version <null>)
-     * <$:b32(thread-id)$:md5(to-addr.ticket-num.ticket-id)@:md5(url)>
-     *      thread-id - thread-id, little-endian INT, packed
-     *      :b32() - base32 encoded
-     *      to-addr - individual email recipient
-     *      ticket-num - external ticket number
-     *      ticket-id - internal ticket id
-     *      :md5() - last 10 hex chars of MD5 sum
-     *      url - helpdesk URL
-     */
-    function asMessageId($to, $version=false) {
-        global $ost;
-
+        // Compute the value to be compared from $mails (which used to be in
+        // ThreadEntry::asMessageId() (#nolint)
         $domain = md5($ost->getConfig()->getURL());
-        $ticket = $this->getTicket();
-        return sprintf('$%s$%s@%s',
-            base64_encode(pack('V', $this->getId())),
-            substr(md5($to . $ticket->getNumber() . $ticket->getId()), -10),
+        $ticket = $entry->getThread()->getObject();
+        if (!$ticket instanceof Ticket)
+            return false;
+
+        $check = sprintf('%s@%s',
+            substr(md5($from . $ticket->getNumber() . $ticket->getId()), -10),
             substr($domain, -10)
         );
+
+        if ($check != $mails)
+            return false;
+
+        return $entry;
     }
 
     //new entry ... we're trusting the caller to check validity of the data.
-    function create($vars) {
+    static function create($vars=false) {
         global $cfg;
 
+        assert(is_array($vars));
+
         //Must have...
-        if(!$vars['ticketId'] || !$vars['type'] || !in_array($vars['type'], array('M','R','N')))
+        if (!$vars['threadId'] || !$vars['type'])
             return false;
 
 
-        if (!$vars['body'] instanceof ThreadBody) {
-            if ($cfg->isHtmlThreadEnabled())
-                $vars['body'] = new HtmlThreadBody($vars['body']);
+        if (!$vars['body'] instanceof ThreadEntryBody) {
+            if ($cfg->isRichTextEnabled())
+                $vars['body'] = new HtmlThreadEntryBody($vars['body']);
             else
-                $vars['body'] = new TextThreadBody($vars['body']);
+                $vars['body'] = new TextThreadEntryBody($vars['body']);
         }
 
         // Drop stripped images
@@ -1092,7 +1339,7 @@ class ThreadEntry {
         }
 
         // Handle extracted embedded images (<img src="data:base64,..." />).
-        // The extraction has already been performed in the ThreadBody
+        // The extraction has already been performed in the ThreadEntryBody
         // class. Here they should simply be added to the attachments list
         if ($atts = $vars['body']->getEmbeddedHtmlImages()) {
             if (!is_array($vars['attachments']))
@@ -1109,35 +1356,45 @@ class ThreadEntry {
         if ($poster && is_object($poster))
             $poster = (string) $poster;
 
-        $sql=' INSERT INTO '.TICKET_THREAD_TABLE.' SET created=NOW() '
-            .' ,thread_type='.db_input($vars['type'])
-            .' ,ticket_id='.db_input($vars['ticketId'])
-            .' ,title='.db_input(Format::sanitize($vars['title'], true))
-            .' ,format='.db_input($vars['body']->getType())
-            .' ,staff_id='.db_input($vars['staffId'])
-            .' ,user_id='.db_input($vars['userId'])
-            .' ,poster='.db_input($poster)
-            .' ,source='.db_input($vars['source']);
+        $entry = new static(array(
+            'created' => SqlFunction::NOW(),
+            'type' => $vars['type'],
+            'thread_id' => $vars['threadId'],
+            'title' => Format::sanitize($vars['title'], true),
+            'format' => $vars['body']->getType(),
+            'staff_id' => $vars['staffId'],
+            'user_id' => $vars['userId'],
+            'poster' => $poster,
+            'source' => $vars['source'],
+            'flags' => $vars['flags'] ?: 0,
+        ));
+
+        if ($entry->format == 'html')
+            // The current codebase properly balances html
+            $entry->flags |= self::FLAG_BALANCED;
+
+        // Flag system messages
+        if (!($vars['staffId'] || $vars['userId']))
+            $entry->flags |= self::FLAG_SYSTEM;
 
         if (!isset($vars['attachments']) || !$vars['attachments'])
             // Otherwise, body will be configured in a block below (after
             // inline attachments are saved and updated in the database)
-            $sql.=' ,body='.db_input($body);
+            $entry->body = $body;
 
-        if(isset($vars['pid']))
-            $sql.=' ,pid='.db_input($vars['pid']);
+        if (isset($vars['pid']))
+            $entry->pid = $vars['pid'];
         // Check if 'reply_to' is in the $vars as the previous ThreadEntry
         // instance. If the body of the previous message is found in the new
         // body, strip it out.
         elseif (isset($vars['reply_to'])
                 && $vars['reply_to'] instanceof ThreadEntry)
-            $sql.=' ,pid='.db_input($vars['reply_to']->getId());
+            $entry->pid = $vars['reply_to']->getId();
 
-        if($vars['ip_address'])
-            $sql.=' ,ip_address='.db_input($vars['ip_address']);
+        if ($vars['ip_address'])
+            $entry->ip_address = $vars['ip_address'];
 
-        //echo $sql;
-        if(!db_query($sql) || !($entry=self::lookup(db_insert_id(), $vars['ticketId'])))
+        if (!$entry->save())
             return false;
 
         /************* ATTACHMENTS *****************/
@@ -1170,188 +1427,621 @@ class ThreadEntry {
                         'src="cid:'.$a['key'].'"', $body);
                 }
             }
-            $sql = 'UPDATE '.TICKET_THREAD_TABLE.' SET body='.db_input($body)
-                .' WHERE `id`='.db_input($entry->getId());
-            if (!db_query($sql) || !db_affected_rows())
-                return false;
 
-            // Set the $entry here for search indexing
-            $entry->ht['body'] = $body;
+            $entry->body = $body;
+            if (!$entry->save())
+                return false;
         }
 
-        // Email message id
+        // Save mail message id, if available
         $entry->saveEmailInfo($vars);
 
         // Inline images (attached to the draft)
         $entry->saveAttachments(Draft::getAttachmentIds($body));
 
-        Signal::send('model.created', $entry);
+        Signal::send('threadentry.created', $entry);
 
         return $entry;
     }
 
-    function add($vars) {
-        return ($entry=self::create($vars))?$entry->getId():0;
-    }
-}
-
-/* Message - Ticket thread entry of type message */
-class Message extends ThreadEntry {
-
-    function Message($id, $ticketId=0) {
-        parent::ThreadEntry($id, 'M', $ticketId);
+    static function add($vars, &$errors=array()) {
+        return self::create($vars);
     }
 
-    function getSubject() {
-        return $this->getTitle();
+    // Extensible thread entry actions ------------------------
+    /**
+     * getActions
+     *
+     * Retrieve a list of possible actions. This list is shown to the agent
+     * via drop-down list at the top-right of the thread entry when rendered
+     * in the UI.
+     */
+    function getActions() {
+        if (!isset($this->_actions)) {
+            $this->_actions = array();
+
+            foreach (self::$action_registry as $group=>$list) {
+                $T = array();
+                $this->_actions[__($group)] = &$T;
+                foreach ($list as $id=>$action) {
+                    $A = new $action($this);
+                    if ($A->isVisible()) {
+                        $T[$id] = $A;
+                    }
+                }
+                unset($T);
+            }
+        }
+        return $this->_actions;
     }
 
-    function create($vars, &$errors) {
-        return self::lookup(self::add($vars, $errors));
+    function hasActions() {
+        foreach ($this->getActions() as $group => $list) {
+            if (count($list))
+                return true;
+        }
+        return false;
     }
 
-    function add($vars, &$errors) {
-
-        if(!$vars || !is_array($vars) || !$vars['ticketId'])
-            $errors['err'] = __('Missing or invalid data');
-        elseif(!$vars['message'])
-            $errors['message'] = __('Message content is required');
-
-        if($errors) return false;
-
-        $vars['type'] = 'M';
-        $vars['body'] = $vars['message'];
-
-        if (!$vars['poster']
-                && $vars['userId']
-                && ($user = User::lookup($vars['userId'])))
-            $vars['poster'] = (string) $user->getName();
+    function triggerAction($name) {
+        foreach ($this->getActions() as $group=>$list) {
+            foreach ($list as $id=>$action) {
+                if (0 === strcasecmp($id, $name)) {
+                    if (!$action->isEnabled())
+                        return false;
 
-        return ThreadEntry::add($vars);
+                    $action->trigger();
+                    return true;
+                }
+            }
+        }
+        return false;
     }
 
-    function lookup($id, $tid=0, $type='M') {
+    static $action_registry = array();
 
-        return ($id
-                && is_numeric($id)
-                && ($m = new Message($id, $tid))
-                && $m->getId()==$id
-                )?$m:null;
-    }
+    static function registerAction($group, $action) {
+        if (!isset(self::$action_registry[$group]))
+            self::$action_registry[$group] = array();
 
-    function lastByTicketId($ticketId) {
-        return self::byTicketId($ticketId);
+        self::$action_registry[$group][$action::getId()] = $action;
     }
 
-    function firstByTicketId($ticketId) {
-        return self::byTicketId($ticketId, false);
+    static function getPermissions() {
+        return self::$perms;
     }
+}
 
-    function byTicketId($ticketId, $last=true) {
-
-        $sql=' SELECT thread.id FROM '.TICKET_THREAD_TABLE.' thread '
-            .' WHERE thread_type=\'M\' AND thread.ticket_id = '.db_input($ticketId)
-            .sprintf(' ORDER BY thread.id %s LIMIT 1', $last ? 'DESC' : 'ASC');
+RolePermission::register(/* @trans */ 'Tickets', ThreadEntry::getPermissions());
+
+class ThreadEvent extends VerySimpleModel {
+    static $meta = array(
+        'table' => THREAD_EVENT_TABLE,
+        'pk' => array('id'),
+        'joins' => array(
+            // Originator of activity
+            'agent' => array(
+                'constraint' => array(
+                    'uid' => 'Staff.staff_id',
+                ),
+                'null' => true,
+            ),
+            // Agent assignee
+            'staff' => array(
+                'constraint' => array(
+                    'staff_id' => 'Staff.staff_id',
+                ),
+                'null' => true,
+            ),
+            'team' => array(
+                'constraint' => array(
+                    'team_id' => 'Team.team_id',
+                ),
+                'null' => true,
+            ),
+            'thread' => array(
+                'constraint' => array('thread_id' => 'Thread.id'),
+            ),
+            'user' => array(
+                'constraint' => array(
+                    'uid' => 'User.id',
+                ),
+                'null' => true,
+            ),
+            'dept' => array(
+                'constraint' => array(
+                    'dept_id' => 'Dept.id',
+                ),
+                'null' => true,
+            ),
+        ),
+    );
 
-        if (($res = db_query($sql)) && ($id = db_result($res)))
-            return Message::lookup($id);
+    // Valid events for database storage
+    const ASSIGNED  = 'assigned';
+    const CLOSED    = 'closed';
+    const CREATED   = 'created';
+    const COLLAB    = 'collab';
+    const EDITED    = 'edited';
+    const ERROR     = 'error';
+    const OVERDUE   = 'overdue';
+    const REOPENED  = 'reopened';
+    const STATUS    = 'status';
+    const TRANSFERRED = 'transferred';
+    const VIEWED    = 'viewed';
+
+    const MODE_STAFF = 1;
+    const MODE_CLIENT = 2;
+
+    var $_data;
+
+    function getAvatar($size=null) {
+        if ($this->uid && $this->uid_type == 'S')
+            return $this->agent ? $this->agent->getAvatar($size) : '';
+        if ($this->uid && $this->uid_type == 'U')
+            return $this->user ? $this->user->getAvatar($size) : '';
+    }
+
+    function getUserName() {
+        if ($this->uid && $this->uid_type == 'S')
+            return $this->agent ? $this->agent->getName() : $this->username;
+        if ($this->uid && $this->uid_type == 'U')
+            return $this->user ? $this->user->getName() : $this->username;
+        return $this->username;
+    }
+
+    function getIcon() {
+        $icons = array(
+            'assigned'  => 'hand-right',
+            'collab'    => 'group',
+            'created'   => 'magic',
+            'overdue'   => 'time',
+            'transferred' => 'share-alt',
+            'edited'    => 'pencil',
+            'closed'    => 'thumbs-up-alt',
+            'reopened'  => 'rotate-right',
+            'resent'    => 'reply-all icon-flip-horizontal',
+        );
+        return @$icons[$this->state] ?: 'chevron-sign-right';
+    }
 
-        return null;
+    function getDescription($mode=self::MODE_STAFF) {
+        // Abstract description
+        return $this->template(sprintf(
+            __('%s by {somebody} {timestamp}'),
+            $this->state
+        ));
     }
-}
 
-/* Response - Ticket thread entry of type response */
-class Response extends ThreadEntry {
+    function template($description) {
+        global $thisstaff, $cfg;
 
-    function Response($id, $ticketId=0) {
-        parent::ThreadEntry($id, 'R', $ticketId);
-    }
+        $self = $this;
+        return preg_replace_callback('/\{(<(?P<type>([^>]+))>)?(?P<key>[^}.]+)(\.(?P<data>[^}]+))?\}/',
+            function ($m) use ($self, $thisstaff, $cfg) {
+                switch ($m['key']) {
+                case 'assignees':
+                    $assignees = array();
+                    if ($S = $self->staff) {
+                        $avatar = '';
+                        if ($cfg->isAvatarsEnabled())
+                            $avatar = $S->getAvatar();
+                        $assignees[] =
+                            $avatar.$S->getName();
+                    }
+                    if ($T = $self->team) {
+                        $assignees[] = $T->getLocalName();
+                    }
+                    return implode('/', $assignees);
+                case 'somebody':
+                    $name = $self->getUserName();
+                    if ($cfg->isAvatarsEnabled()
+                            && ($avatar = $self->getAvatar()))
+                        $name = $avatar.$name;
+                    return $name;
+                case 'timestamp':
+                    $timeFormat = null;
+                    if ($thisstaff
+                            && !strcasecmp($thisstaff->datetime_format,
+                                'relative')) {
+                        $timeFormat = function ($timestamp) {
+                            return Format::relativeTime(Misc::db2gmtime($timestamp));
+                        };
+                    }
 
-    function getSubject() {
-        return $this->getTitle();
+                    return sprintf('<time %s datetime="%s"
+                            data-toggle="tooltip" title="%s">%s</time>',
+                        $timeFormat ? 'class="relative"' : '',
+                        date(DateTime::W3C, Misc::db2gmtime($self->timestamp)),
+                        Format::daydatetime($self->timestamp),
+                        $timeFormat ? $timeFormat($self->timestamp) :
+                        Format::datetime($self->timestamp)
+                    );
+                case 'agent':
+                    $name = $self->agent->getName();
+                    if ($cfg->isAvatarsEnabled()
+                            && ($avatar = $self->getAvatar()))
+                        $name = $avatar.$name;
+                    return $name;
+                case 'dept':
+                    if ($dept = $self->getDept())
+                        return $dept->getLocalName();
+                    return __('None');
+                case 'data':
+                    $val = $self->getData($m['data']);
+                    if (is_array($val))
+                        list($val, $fallback) = $val;
+                    if ($m['type'] && class_exists($m['type']))
+                        $val = $m['type']::lookup($val);
+                    if (!$val && $fallback)
+                        $val = $fallback;
+                    return Format::htmlchars((string) $val);
+                }
+                return $m[0];
+            },
+            $description
+        );
     }
 
-    function getRespondent() {
-        return $this->getStaff();
+    function getDept() {
+        return $this->dept;
     }
 
-    function create($vars, &$errors) {
-        return self::lookup(self::add($vars, $errors));
+    function getData($key=false) {
+        if (!isset($this->_data))
+            $this->_data = JsonDataParser::decode($this->data);
+        return ($key) ? @$this->_data[$key] : $this->_data;
     }
 
-    function add($vars, &$errors) {
-
-        if(!$vars || !is_array($vars) || !$vars['ticketId'])
-            $errors['err'] = __('Missing or invalid data');
-        elseif(!$vars['response'])
-            $errors['response'] = __('Response content is required');
-
-        if($errors) return false;
+    function render($mode) {
+        $inc = ($mode == self::MODE_STAFF) ? STAFFINC_DIR : CLIENTINC_DIR;
+        $event = $this->getTypedEvent();
+        include $inc . 'templates/thread-event.tmpl.php';
+    }
 
-        $vars['type'] = 'R';
-        $vars['body'] = $vars['response'];
-        if(!$vars['pid'] && $vars['msgId'])
-            $vars['pid'] = $vars['msgId'];
+    static function create($ht=false, $user=false) {
+        $inst = new static($ht);
+        $inst->timestamp = SqlFunction::NOW();
 
-        if (!$vars['poster']
-                && $vars['staffId']
-                && ($staff = Staff::lookup($vars['staffId'])))
-            $vars['poster'] = (string) $staff->getName();
+        global $thisstaff, $thisclient;
+        $user = is_object($user) ? $user : $thisstaff ?: $thisclient;
+        if ($user instanceof Staff) {
+            $inst->uid_type = 'S';
+            $inst->uid = $user->getId();
+        }
+        elseif ($user instanceof User) {
+            $inst->uid_type = 'U';
+            $inst->uid = $user->getId();
+        }
 
-        return ThreadEntry::add($vars);
+        return $inst;
     }
 
-
-    function lookup($id, $tid=0, $type='R') {
-
-        return ($id
-                && is_numeric($id)
-                && ($r = new Response($id, $tid))
-                && $r->getId()==$id
-                )?$r:null;
+    static function forTicket($ticket, $state, $user=false) {
+        $inst = self::create(array(
+            'staff_id' => $ticket->getStaffId(),
+            'team_id' => $ticket->getTeamId(),
+            'dept_id' => $ticket->getDeptId(),
+            'topic_id' => $ticket->getTopicId(),
+        ), $user);
+        return $inst;
     }
-}
 
-/* Note - Ticket thread entry of type note (Internal Note) */
-class Note extends ThreadEntry {
+    function getTypedEvent() {
+        static $subclasses;
 
-    function Note($id, $ticketId=0) {
-        parent::ThreadEntry($id, 'N', $ticketId);
+        if (!isset($subclasses)) {
+            $parent = get_class($this);
+            $subclasses = array();
+            foreach (get_declared_classes() as $class) {
+                if (is_subclass_of($class, $parent))
+                    $subclasses[$class::$state] = $class;
+            }
+        }
+        if (!($class = $subclasses[$this->state]))
+            return $this;
+        return new $class($this->ht);
     }
+}
 
-    /* static */
-    function create($vars, &$errors) {
-        return self::lookup(self::add($vars, $errors));
+class ThreadEvents extends InstrumentedList {
+    function annul($event) {
+        $this->queryset
+            ->filter(array('state' => $event))
+            ->update(array('annulled' => 1));
     }
 
-    function add($vars, &$errors) {
-
-        //Check required params.
-        if(!$vars || !is_array($vars) || !$vars['ticketId'])
-            $errors['err'] = __('Missing or invalid data');
-        elseif(!$vars['note'])
-            $errors['note'] = __('Note content is required');
+    /**
+     * Add an event to the thread activity log.
+     *
+     * Parameters:
+     * $object - Object to log activity for
+     * $state - State name of the activity (one of 'created', 'edited',
+     *      'deleted', 'closed', 'reopened', 'error', 'collab', 'resent',
+     *      'assigned', 'transferred')
+     * $data - (array?) Details about the state change
+     * $user - (string|User|Staff) user triggering the state change
+     * $annul - (state) a corresponding state change that is annulled by
+     *      this event
+     */
+    function log($object, $state, $data=null, $user=null, $annul=null) {
+        global $thisstaff, $thisclient;
+
+        if ($object instanceof Ticket)
+            // TODO: Use $object->createEvent() (nolint)
+            $event = ThreadEvent::forTicket($object, $state, $user);
+        else
+            $event = ThreadEvent::create(false, $user);
+
+        # Annul previous entries if requested (for instance, reopening a
+        # ticket will annul an 'closed' entry). This will be useful to
+        # easily prevent repeated statistics.
+        if ($annul) {
+            $this->annul($annul);
+        }
 
-        if($errors) return false;
+        $username = $user;
+        $user = is_object($user) ? $user : $thisclient ?: $thisstaff;
+        if (!is_string($username)) {
+            if ($user instanceof Staff) {
+                $username = $user->getUserName();
+            }
+            // XXX: Use $user here
+            elseif ($thisclient) {
+                if ($thisclient->hasAccount)
+                    $username = $thisclient->getAccount()->getUserName();
+                if (!$username)
+                    $username = $thisclient->getEmail();
+            }
+            else {
+                # XXX: Security Violation ?
+                $username = 'SYSTEM';
+            }
+        }
+        $event->username = $username;
+        $event->state = $state;
+
+        if ($data) {
+            if (is_array($data))
+                $data = JsonDataEncoder::encode($data);
+            if (!is_string($data))
+                throw new InvalidArgumentException('Data must be string or array');
+            $event->data = $data;
+        }
 
-        //TODO: use array_intersect_key  when we move to php 5 to extract just what we need.
-        $vars['type'] = 'N';
-        $vars['body'] = $vars['note'];
+        $this->add($event);
+
+        // Save event immediately
+        return $event->save();
+    }
+}
+
+class AssignmentEvent extends ThreadEvent {
+    static $icon = 'hand-right';
+    static $state = 'assigned';
 
-        return ThreadEntry::add($vars);
+    function getDescription($mode=self::MODE_STAFF) {
+        $data = $this->getData();
+        switch (true) {
+        case !is_array($data):
+        default:
+            $desc = __('Assignee changed by <b>{somebody}</b> to <strong>{assignees}</strong> {timestamp}');
+            break;
+        case isset($data['staff']):
+            $desc = __('<b>{somebody}</b> assigned this to <strong>{<Staff>data.staff}</strong> {timestamp}');
+            break;
+        case isset($data['team']):
+            $desc = __('<b>{somebody}</b> assigned this to <strong>{<Team>data.team}</strong> {timestamp}');
+            break;
+        case isset($data['claim']):
+            $desc = __('<b>{somebody}</b> claimed this {timestamp}');
+            break;
+        }
+        return $this->template($desc);
     }
+}
 
-    function lookup($id, $tid=0, $type='N') {
+class CloseEvent extends ThreadEvent {
+    static $icon = 'thumbs-up-alt';
+    static $state = 'closed';
 
-        return ($id
-                && is_numeric($id)
-                && ($n = new Note($id, $tid))
-                && $n->getId()==$id
-                )?$n:null;
+    function getDescription($mode=self::MODE_STAFF) {
+        if ($this->getData('status'))
+            return $this->template(__('Closed by <b>{somebody}</b> with status of {<TicketStatus>data.status} {timestamp}'));
+        else
+            return $this->template(__('Closed by <b>{somebody}</b> {timestamp}'));
     }
 }
 
-class ThreadBody /* extends SplString */ {
+class CollaboratorEvent extends ThreadEvent {
+    static $icon = 'group';
+    static $state = 'collab';
+
+    function getDescription($mode=self::MODE_STAFF) {
+        $data = $this->getData();
+        switch (true) {
+        case isset($data['org']):
+            $desc = __('Collaborators for {<Organization>data.org} organization added');
+            break;
+        case isset($data['del']):
+            $base = __('<b>{somebody}</b> removed <strong>%s</strong> from the collaborators {timestamp}');
+            $collabs = array();
+            $users = User::objects()->filter(array('id__in' => array_keys($data['del'])));
+            foreach ($data['del'] as $id=>$c) {
+                $U = false;
+                foreach ($users as $user) {
+                    if ($user->id == $id) {
+                        $U = $user;
+                        break;
+                    }
+                }
+                $collabs[] = Format::htmlchars($U ? $U->getName() : @$c['name'] ?: $c);
+            }
+            $desc = sprintf($base, implode(', ', $collabs));
+            break;
+        case isset($data['add']):
+            $base = __('<b>{somebody}</b> added <strong>%s</strong> as collaborators {timestamp}');
+            $collabs = array();
+            if ($data['add']) {
+                $users = User::objects()->filter(array('id__in' => array_keys($data['add'])));
+                foreach ($data['add'] as $id=>$c) {
+                    $U = false;
+                    foreach ($users as $user) {
+                        if ($user->id == $id) {
+                            $U = $user;
+                            break;
+                        }
+                    }
+                    $c = sprintf("%s %s",
+                        Format::htmlchars($U ? $U->getName() : @$c['name'] ?: $c),
+                        $c['src'] ? sprintf(__('via %s'
+                            /* e.g. "Added collab "Me <me@company.me>" via Email (to)" */
+                            ), $c['src']) : ''
+                    );
+                    $collabs[] = $c;
+                }
+            }
+            $desc = $collabs
+                ? sprintf($base, implode(', ', $collabs))
+                : 'somebody';
+            break;
+        }
+        return $this->template($desc);
+    }
+}
+
+class CreationEvent extends ThreadEvent {
+    static $icon = 'magic';
+    static $state = 'created';
+
+    function getDescription($mode=self::MODE_STAFF) {
+        return $this->template(__('Created by <b>{somebody}</b> {timestamp}'));
+    }
+}
+
+class EditEvent extends ThreadEvent {
+    static $icon = 'pencil';
+    static $state = 'edited';
+
+    function getDescription($mode=self::MODE_STAFF) {
+        $data = $this->getData();
+        switch (true) {
+        case isset($data['owner']):
+            $desc = __('<b>{somebody}</b> changed ownership to {<User>data.owner} {timestamp}');
+            break;
+        case isset($data['status']):
+            $desc = __('<b>{somebody}</b> changed the status to <strong>{<TicketStatus>data.status}</strong> {timestamp}');
+            break;
+        case isset($data['fields']):
+            $fields = $changes = array();
+            foreach (DynamicFormField::objects()->filter(array(
+                'id__in' => array_keys($data['fields'])
+            )) as $F) {
+                $fields[$F->id] = $F;
+            }
+            foreach ($data['fields'] as $id=>$f) {
+                $field = $fields[$id];
+                if ($mode == self::MODE_CLIENT && !$field->isVisibleToUsers())
+                    continue;
+                list($old, $new) = $f;
+                $impl = $field->getImpl($field);
+                $before = $impl->to_php($old);
+                $after = $impl->to_php($new);
+                $changes[] = sprintf('<strong>%s</strong> %s',
+                    $field->getLocal('label'), $impl->whatChanged($before, $after));
+            }
+            // Fallthrough to other editable fields
+        case isset($data['topic_id']):
+        case isset($data['sla_id']):
+        case isset($data['source']):
+        case isset($data['user_id']):
+        case isset($data['duedate']):
+            $base = __('Updated by <b>{somebody}</b> {timestamp} — %s');
+            foreach (array(
+                'topic_id' => array(__('Help Topic'), array('Topic', 'getTopicName')),
+                'sla_id' => array(__('SLA'), array('SLA', 'getSLAName')),
+                'duedate' => array(__('Duedate'), array('Format', 'date')),
+                'user_id' => array(__('Ticket Owner'), array('User', 'getNameById')),
+                'source' => array(__('Source'), null)
+            ) as $f => $info) {
+                if (isset($data[$f])) {
+                    list($name, $desc) = $info;
+                    list($old, $new) = $data[$f];
+                    if ($desc && is_callable($desc)) {
+                        $new = call_user_func($desc, $new);
+                        if ($old)
+                            $old = call_user_func($desc, $old);
+                    }
+                    if ($old and $new) {
+                        $changes[] = sprintf(
+                            __('<strong>%1$s</strong> changed from <strong>%2$s</strong> to <strong>%3$s</strong>'),
+                            Format::htmlchars($name), Format::htmlchars($old), Format::htmlchars($new)
+                        );
+                    }
+                    elseif ($new) {
+                        $changes[] = sprintf(
+                            __('<strong>%1$s</strong> set to <strong>%2$s</strong>'),
+                            Format::htmlchars($name), Format::htmlchars($new)
+                        );
+                    }
+                    else {
+                        $changes[] = sprintf(
+                            __('unset <strong>%1$s</strong>'),
+                            Format::htmlchars($name)
+                        );
+                    }
+                }
+            }
+            $desc = $changes
+                ? sprintf($base, implode(', ', $changes)) : '';
+            break;
+        }
+
+        return $this->template($desc);
+    }
+}
+
+class OverdueEvent extends ThreadEvent {
+    static $icon = 'time';
+    static $state = 'overdue';
+
+    function getDescription($mode=self::MODE_STAFF) {
+        return $this->template(__('Flagged as overdue by the system {timestamp}'));
+    }
+}
+
+class ReopenEvent extends ThreadEvent {
+    static $icon = 'rotate-right';
+    static $state = 'reopened';
+
+    function getDescription($mode=self::MODE_STAFF) {
+        return $this->template(__('Reopened by <b>{somebody}</b> {timestamp}'));
+    }
+}
+
+class ResendEvent extends ThreadEvent {
+    static $icon = 'reply-all icon-flip-horizontal';
+    static $state = 'resent';
+
+    function getDescription($mode=self::MODE_STAFF) {
+        return $this->template(__('<b>{somebody}</b> resent <strong><a href="#thread-entry-{data.entry}">a previous response</a></strong> {timestamp}'));
+    }
+}
+
+class TransferEvent extends ThreadEvent {
+    static $icon = 'share-alt';
+    static $state = 'transferred';
+
+    function getDescription($mode=self::MODE_STAFF) {
+        return $this->template(__('<b>{somebody}</b> transferred this to <strong>{dept}</strong> {timestamp}'));
+    }
+}
+
+class ViewEvent extends ThreadEvent {
+    static $state = 'viewed';
+}
+
+class ThreadEntryBody /* extends SplString */ {
 
     static $types = array('text', 'html');
 
@@ -1366,7 +2056,7 @@ class ThreadBody /* extends SplString */ {
     function __construct($body, $type='text', $options=array()) {
         $type = strtolower($type);
         if (!in_array($type, static::$types))
-            throw new Exception("$type: Unsupported ThreadBody type");
+            throw new Exception("$type: Unsupported ThreadEntryBody type");
         $this->body = (string) $body;
         if (strlen($this->body) > 250000) {
             $max_packet = db_get_variable('max_allowed_packet', 'global');
@@ -1389,10 +2079,10 @@ class ThreadBody /* extends SplString */ {
         $conv = $this->type . ':' . strtolower($type);
         switch ($conv) {
         case 'text:html':
-            return new ThreadBody(sprintf('<pre>%s</pre>',
+            return new ThreadEntryBody(sprintf('<pre>%s</pre>',
                 Format::htmlchars($this->body)), $type);
         case 'html:text':
-            return new ThreadBody(Format::html2text((string) $this), $type);
+            return new ThreadEntryBody(Format::html2text((string) $this), $type);
         }
     }
 
@@ -1473,14 +2163,14 @@ class ThreadBody /* extends SplString */ {
         return Format::searchable($this->body);
     }
 
-    static function fromFormattedText($text, $format=false) {
+    static function fromFormattedText($text, $format=false, $options=array()) {
         switch ($format) {
         case 'text':
-            return new TextThreadBody($text);
+            return new TextThreadEntryBody($text);
         case 'html':
-            return new HtmlThreadBody($text, array('strip-embedded'=>false));
+            return new HtmlThreadEntryBody($text, array('strip-embedded'=>false) + $options);
         default:
-            return new ThreadBody($text);
+            return new ThreadEntryBody($text);
         }
     }
 
@@ -1492,7 +2182,7 @@ class ThreadBody /* extends SplString */ {
     }
 }
 
-class TextThreadBody extends ThreadBody {
+class TextThreadEntryBody extends ThreadEntryBody {
     function __construct($body, $options=array()) {
         parent::__construct($body, 'text', $options);
     }
@@ -1501,6 +2191,10 @@ class TextThreadBody extends ThreadBody {
         return  Format::stripEmptyLines(parent::getClean());
     }
 
+    function prepend($what) {
+        $this->body = $what . "\n\n" . $this->body;
+    }
+
     function display($output=false) {
         if ($this->isEmpty())
             return '(empty)';
@@ -1520,7 +2214,7 @@ class TextThreadBody extends ThreadBody {
         }
     }
 }
-class HtmlThreadBody extends ThreadBody {
+class HtmlThreadEntryBody extends ThreadEntryBody {
     function __construct($body, $options=array()) {
         if (!isset($options['strip-embedded']) || $options['strip-embedded'])
             $body = $this->extractEmbeddedHtmlImages($body);
@@ -1556,6 +2250,10 @@ class HtmlThreadBody extends ThreadBody {
         return Format::searchable($body);
     }
 
+    function prepend($what) {
+        $this->body = sprintf('<div>%s<br/><br/></div>%s', $what, $this->body);
+    }
+
     function display($output=false) {
         if ($this->isEmpty())
             return '(empty)';
@@ -1566,8 +2264,452 @@ class HtmlThreadBody extends ThreadBody {
         case 'pdf':
             return Format::clickableurls($this->body);
         default:
-            return Format::display($this->body);
+            return Format::display($this->body, true, !$this->options['balanced']);
+        }
+    }
+}
+
+
+/* Message - Ticket thread entry of type message */
+class MessageThreadEntry extends ThreadEntry {
+
+    const ENTRY_TYPE = 'M';
+
+    function getSubject() {
+        return $this->getTitle();
+    }
+
+    static function add($vars, &$errors=array()) {
+
+        if (!$vars || !is_array($vars) || !$vars['threadId'])
+            $errors['err'] = __('Missing or invalid data');
+        elseif (!$vars['message'])
+            $errors['message'] = __('Message content is required');
+
+        if ($errors) return false;
+
+        $vars['type'] = self::ENTRY_TYPE;
+        $vars['body'] = $vars['message'];
+
+        if (!$vars['poster']
+                && $vars['userId']
+                && ($user = User::lookup($vars['userId'])))
+            $vars['poster'] = (string) $user->getName();
+
+        return parent::add($vars);
+    }
+
+    static function getVarScope() {
+        $base = parent::getVarScope();
+        unset($base['staff']);
+        return $base;
+    }
+}
+
+/* thread entry of type response */
+class ResponseThreadEntry extends ThreadEntry {
+
+    const ENTRY_TYPE = 'R';
+
+    function getActivity() {
+        return new ThreadActivity(
+                _S('New Response'),
+                _S('New response posted'));
+    }
+
+    function getSubject() {
+        return $this->getTitle();
+    }
+
+    function getRespondent() {
+        return $this->getStaff();
+    }
+
+    static function add($vars, &$errors=array()) {
+
+        if (!$vars || !is_array($vars) || !$vars['threadId'])
+            $errors['err'] = __('Missing or invalid data');
+        elseif (!$vars['response'])
+            $errors['response'] = __('Response content is required');
+
+        if ($errors) return false;
+
+        $vars['type'] = self::ENTRY_TYPE;
+        $vars['body'] = $vars['response'];
+        if (!$vars['pid'] && $vars['msgId'])
+            $vars['pid'] = $vars['msgId'];
+
+        if (!$vars['poster']
+                && $vars['staffId']
+                && ($staff = Staff::lookup($vars['staffId'])))
+            $vars['poster'] = (string) $staff->getName();
+
+        return parent::add($vars);
+    }
+
+    static function getVarScope() {
+        $base = parent::getVarScope();
+        unset($base['user']);
+        return $base;
+    }
+}
+
+/* Thread entry of type note (Internal Note) */
+class NoteThreadEntry extends ThreadEntry {
+    const ENTRY_TYPE = 'N';
+
+    function getMessage() {
+        return $this->getBody();
+    }
+
+    function getActivity() {
+        return new ThreadActivity(
+                _S('New Internal Note'),
+                _S('New internal note posted'));
+    }
+
+    static function add($vars, &$errors=array()) {
+
+        //Check required params.
+        if (!$vars || !is_array($vars) || !$vars['threadId'])
+            $errors['err'] = __('Missing or invalid data');
+        elseif (!$vars['note'])
+            $errors['note'] = __('Note content is required');
+
+        if ($errors) return false;
+
+        //TODO: use array_intersect_key  when we move to php 5 to extract just what we need.
+        $vars['type'] = self::ENTRY_TYPE;
+        $vars['body'] = $vars['note'];
+
+        return parent::add($vars);
+    }
+
+    static function getVarScope() {
+        $base = parent::getVarScope();
+        unset($base['user']);
+        return $base;
+    }
+}
+
+// Object specific thread utils.
+class ObjectThread extends Thread
+implements TemplateVariable {
+    static $types = array(
+        ObjectModel::OBJECT_TYPE_TASK => 'TaskThread',
+        ObjectModel::OBJECT_TYPE_TICKET => 'TicketThread',
+    );
+
+    var $counts;
+
+    function getCounts() {
+        if (!isset($this->counts) && $this->getId()) {
+            $this->counts = array();
+
+            $stuff = $this->entries
+                ->values_flat('type')
+                ->annotate(array(
+                    'count' => SqlAggregate::COUNT('id')
+                ));
+
+            foreach ($stuff as $row) {
+                list($type, $count) = $row;
+                $this->counts[$type] = $count;
+            }
         }
+        return $this->counts;
+    }
+
+    function getNumMessages() {
+        $this->getCounts();
+        return $this->counts[MessageThreadEntry::ENTRY_TYPE];
+    }
+
+    function getNumResponses() {
+        $this->getCounts();
+        return $this->counts[ResponseThreadEntry::ENTRY_TYPE];
+    }
+
+    function getNumNotes() {
+        $this->getCounts();
+        return $this->counts[NoteThreadEntry::ENTRY_TYPE];
+    }
+
+
+    function getLastMessage($criteria=false) {
+        $entries = clone $this->getEntries();
+        $entries->filter(array(
+            'type' => MessageThreadEntry::ENTRY_TYPE
+        ));
+
+        if ($criteria)
+            $entries->filter($criteria);
+
+        $entries->order_by('-id');
+
+        return $entries->first();
+    }
+
+    function getLastEmailMessage($criteria=array()) {
+
+        $criteria += array(
+                'source' => 'Email',
+                'email_info__headers__isnull' => false);
+
+        return $this->getLastMessage($criteria);
+    }
+
+    function getLastEmailMessageByUser($user) {
+
+        $uid = is_numeric($user) ? $user : 0;
+        if (!$uid && ($user instanceof EmailContact))
+            $uid = $user->getUserId();
+
+        return $uid
+                ? $this->getLastEmailMessage(array('user_id' => $uid))
+                : null;
+    }
+
+    function getEntry($criteria) {
+        // XXX: PUNT
+        if (is_numeric($criteria))
+            return parent::getEntry($criteria);
+
+        $entries = clone $this->getEntries();
+        $entries->filter($criteria);
+        return $entries->first();
+    }
+
+    function getMessages() {
+        $entries = clone $this->getEntries();
+        return $entries->filter(array(
+            'type' => MessageThreadEntry::ENTRY_TYPE
+        ));
+    }
+
+    function getResponses() {
+        $entries = clone $this->getEntries();
+        return $entries->filter(array(
+            'type' => ResponseThreadEntry::ENTRY_TYPE
+        ));
+    }
+
+    function getNotes() {
+        $entries = clone $this->getEntries();
+        return $entries->filter(array(
+            'type' => NoteThreadEntry::ENTRY_TYPE
+        ));
+    }
+
+    function addNote($vars, &$errors) {
+
+        //Add ticket Id.
+        $vars['threadId'] = $this->getId();
+        return NoteThreadEntry::add($vars, $errors);
+    }
+
+    function addMessage($vars, &$errors) {
+
+        $vars['threadId'] = $this->getId();
+        $vars['staffId'] = 0;
+
+        if (!($message = MessageThreadEntry::add($vars, $errors)))
+            return $message;
+
+        $this->lastmessage = SqlFunction::NOW();
+        $this->save(true);
+        return $message;
+    }
+
+    function addResponse($vars, &$errors) {
+
+        $vars['threadId'] = $this->getId();
+        $vars['userId'] = 0;
+
+        if (!($resp = ResponseThreadEntry::add($vars, $errors)))
+            return $resp;
+
+        $this->lastresponse = SqlFunction::NOW();
+        $this->save(true);
+        return $resp;
+    }
+
+    function getVar($name) {
+        switch ($name) {
+        case 'original':
+            $entry = $this->entries->filter(array(
+                'type' => MessageThreadEntry::ENTRY_TYPE,
+                'flags__hasbit' => ThreadEntry::FLAG_ORIGINAL_MESSAGE,
+                ))
+                ->order_by('id')
+                ->first();
+            if ($entry)
+                return $entry->getBody();
+
+            break;
+        case 'last_message':
+        case 'lastmessage':
+            $entry = $this->getLastMessage();
+            if ($entry)
+                return $entry->getBody();
+
+            break;
+        }
+    }
+
+    static function getVarScope() {
+      return array(
+        'original' => array('class' => 'MessageThreadEntry', 'desc' => __('Original Message')),
+        'lastmessage' => array('class' => 'MessageThreadEntry', 'desc' => __('Last Message')),
+      );
+    }
+
+    static function lookup($criteria, $type=false) {
+        if (!$type)
+            return parent::lookup($criteria);
+
+        $class = false;
+        if (isset(self::$types[$type]))
+            $class = self::$types[$type];
+        if (!class_exists($class))
+            $class = get_called_class();
+
+        return $class::lookup($criteria);
+    }
+}
+
+// Ticket thread class
+class TicketThread extends ObjectThread {
+    static function create($ticket=false) {
+        assert($ticket !== false);
+
+        $id = is_object($ticket) ? $ticket->getId() : $ticket;
+        $thread = parent::create(array(
+                    'object_id' => $id,
+                    'object_type' => ObjectModel::OBJECT_TYPE_TICKET
+                    ));
+        if ($thread->save())
+            return $thread;
+    }
+}
+
+/**
+ * Class: ThreadEntryAction
+ *
+ * Defines a simple action to be performed on a thread entry item, such as
+ * viewing the raw email headers used to generate the message, resend the
+ * confirmation emails, etc.
+ */
+abstract class ThreadEntryAction {
+    static $name;               // Friendly, translatable name
+    static $id;                 // Unique identifier used for plumbing
+    static $icon = 'cog';
+
+    var $entry;
+
+    function getName() {
+        $class = get_class($this);
+        return __($class::$name);
+    }
+
+    static function getId() {
+        return static::$id;
+    }
+
+    function getIcon() {
+        $class = get_class($this);
+        return 'icon-' . $class::$icon;
+    }
+
+    function __construct(ThreadEntry $thread) {
+        $this->entry = $thread;
+    }
+
+    abstract function trigger();
+
+    function isEnabled() {
+        return $this->isVisible();
+    }
+    function isVisible() {
+        return true;
+    }
+
+    /**
+     * getJsStub
+     *
+     * Retrieves a small JavaScript snippet to insert into the rendered page
+     * which should, via an AJAX callback, trigger this action to be
+     * performed. The URL for this sort of activity is already provided for
+     * you via the ::getAjaxUrl() method in this class.
+     */
+    abstract function getJsStub();
+
+    /**
+     * getAjaxUrl
+     *
+     * Generate a URL to be used as an AJAX callback. The URL can be used to
+     * trigger this thread entry action via the callback.
+     *
+     * Parameters:
+     * $dialog - (bool) used in conjunction with `$.dialog()` javascript
+     *      function which assumes the `ajax.php/` should be replace a leading
+     *      `#` in the url
+     */
+    function getAjaxUrl($dialog=false) {
+        return sprintf('%stickets/%d/thread/%d/%s',
+            $dialog ? '#' : 'ajax.php/',
+            $this->entry->getThread()->getObjectId(),
+            $this->entry->getId(),
+            static::getId()
+        );
     }
 }
+
+interface Threadable {
+    function getThreadId();
+    function getThread();
+    function postThreadEntry($type, $vars, $options=array());
+}
+
+/**
+ * ThreadActivity
+ *
+ * Object to thread activity
+ *
+ */
+class ThreadActivity implements TemplateVariable {
+    var $title;
+    var $desc;
+
+    function __construct($title, $desc) {
+        $this->title = $title;
+        $this->desc = $desc;
+    }
+
+    function getTitle() {
+        return $this->title;
+    }
+
+    function getDescription() {
+        return $this->desc;
+    }
+    function asVar() {
+        return (string) $this->getTitle();
+    }
+
+    function getVar($tag) {
+        if ($tag && is_callable(array($this, 'get'.ucfirst($tag))))
+            return call_user_func(array($this, 'get'.ucfirst($tag)));
+
+        return false;
+    }
+
+    static function getVarScope() {
+        return array(
+          'title' => __('Activity Title'),
+          'description' => __('Activity Description'),
+        );
+    }
+}
+
 ?>
diff --git a/include/class.thread_actions.php b/include/class.thread_actions.php
new file mode 100644
index 0000000000000000000000000000000000000000..428793399807d245f86cf0592041606784cff557
--- /dev/null
+++ b/include/class.thread_actions.php
@@ -0,0 +1,361 @@
+<?php
+/*********************************************************************
+    class.thread_actions.php
+
+    Actions for thread entries. This serves as a simple repository for
+    drop-down actions which can be triggered on the ticket-view page for an
+    object's thread.
+
+    Jared Hancock <jared@osticket.com>
+    Peter Rotich <peter@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:
+**********************************************************************/
+include_once(INCLUDE_DIR.'class.thread.php');
+
+class TEA_ShowEmailHeaders extends ThreadEntryAction {
+    static $id = 'view_headers';
+    static $name = /* trans */ 'View Email Headers';
+    static $icon = 'envelope';
+
+    function isVisible() {
+        global $thisstaff;
+
+        if (!$this->entry->getEmailHeader())
+            return false;
+
+        return $thisstaff && $thisstaff->isAdmin();
+    }
+
+    function getJsStub() {
+        return sprintf("$.dialog('%s');",
+            $this->getAjaxUrl()
+        );
+    }
+
+    function trigger() {
+        switch ($_SERVER['REQUEST_METHOD']) {
+        case 'GET':
+            return $this->trigger__get();
+        }
+    }
+
+    private function trigger__get() {
+        $headers = $this->entry->getEmailHeader();
+
+        include STAFFINC_DIR . 'templates/thread-email-headers.tmpl.php';
+    }
+}
+ThreadEntry::registerAction(/* trans */ 'E-Mail', 'TEA_ShowEmailHeaders');
+
+class TEA_EditThreadEntry extends ThreadEntryAction {
+    static $id = 'edit';
+    static $name = /* trans */ 'Edit';
+    static $icon = 'pencil';
+
+    function isVisible() {
+        // Can't edit system posts
+        return ($this->entry->staff_id || $this->entry->user_id)
+            && $this->entry->type != 'R' && $this->isEnabled();
+    }
+
+    function isEnabled() {
+        global $thisstaff;
+
+        $T = $this->entry->getThread()->getObject();
+        // You can edit your own posts or posts by your department members
+        // if your a manager, or everyone's if your an admin
+        return $thisstaff && (
+            $thisstaff->getId() == $this->entry->staff_id
+            || ($T instanceof Ticket
+                && $T->getDept()->getManagerId() == $thisstaff->getId()
+            )
+            || ($T instanceof Ticket
+                && $thisstaff->getRole($T->getDeptId())->hasPerm(ThreadEntry::PERM_EDIT)
+            )
+        );
+    }
+
+    function getJsStub() {
+        return sprintf(<<<JS
+var url = '%s';
+$.dialog(url, [201], function(xhr, resp) {
+  var json = JSON.parse(resp);
+  if (!json || !json.thread_id)
+    return;
+  $('#thread-entry-'+json.thread_id)
+    .attr('id', 'thread-entry-' + json.new_id)
+    .html(json.entry)
+    .find('.thread-body')
+    .delay(500)
+    .effect('highlight');
+}, {size:'large'});
+JS
+        , $this->getAjaxUrl());
+    }
+
+
+    function trigger() {
+        switch ($_SERVER['REQUEST_METHOD']) {
+        case 'GET':
+            return $this->trigger__get();
+        case 'POST':
+            return $this->trigger__post();
+        }
+    }
+
+    protected function trigger__get() {
+        global $cfg, $thisstaff;
+
+        $poster = $this->entry->getStaff();
+
+        include STAFFINC_DIR . 'templates/thread-entry-edit.tmpl.php';
+    }
+
+    function updateEntry($guard=false) {
+        global $thisstaff;
+
+        $old = $this->entry;
+        $new = ThreadEntryBody::fromFormattedText($_POST['body'], $old->format);
+
+        if ($new->getClean() == $old->body)
+            // No update was performed
+            return $old;
+
+        $entry = ThreadEntry::create(array(
+            // Copy most information from the old entry
+            'poster' => $old->poster,
+            'userId' => $old->user_id,
+            'staffId' => $old->staff_id,
+            'type' => $old->type,
+            'threadId' => $old->thread_id,
+
+            // Connect the new entry to be a child of the previous
+            'pid' => $old->id,
+
+            // Add in new stuff
+            'title' => Format::htmlchars($_POST['title']),
+            'body' => $new,
+            'ip_address' => $_SERVER['REMOTE_ADDR'],
+        ));
+
+        if (!$entry)
+            return false;
+
+        // Note, anything that points to the $old entry as PID should remain
+        // that way for email header lookups and such to remain consistent
+
+        if ($old->flags & ThreadEntry::FLAG_EDITED
+            // If editing another person's edit, make a new entry
+            and ($old->editor == $thisstaff->getId() && $old->editor_type == 'S')
+            and !($old->flags & ThreadEntry::FLAG_GUARDED)
+        ) {
+            // Replace previous edit --------------------------
+            $original = $old->getParent();
+            // Link the new entry to the old id
+            $entry->pid = $old->pid;
+            // Drop the previous edit, and base this edit off the original
+            $old->delete();
+            $old = $original;
+        }
+
+        // Mark the new entry as edited (but not hidden nor guarded)
+        $entry->flags = ($old->flags & ~(ThreadEntry::FLAG_HIDDEN | ThreadEntry::FLAG_GUARDED))
+            | ThreadEntry::FLAG_EDITED;
+
+        // Guard against deletes on future edit if requested. This is done
+        // if an email was triggered by the last edit. In such a case, it
+        // should not be replaced by a subsequent edit.
+        if ($guard)
+            $entry->flags |= ThreadEntry::FLAG_GUARDED;
+
+        // Log the editor
+        $entry->editor = $thisstaff->getId();
+        $entry->editor_type = 'S';
+
+        // Sort in the same place in the thread
+        $entry->created = $old->created;
+        $entry->updated = SqlFunction::NOW();
+        $entry->save(true);
+
+        // Hide the old entry from the object thread
+        $old->flags |= ThreadEntry::FLAG_HIDDEN;
+        $old->save();
+
+        return $entry;
+    }
+
+    protected function trigger__post() {
+        global $thisstaff;
+
+        if (!($entry = $this->updateEntry()))
+            return $this->trigger__get();
+
+        ob_start();
+        include STAFFINC_DIR . 'templates/thread-entry.tmpl.php';
+        $content = ob_get_clean();
+
+        Http::response('201', JsonDataEncoder::encode(array(
+            'thread_id' => $this->entry->id, # This is the old id!
+            'new_id' => $entry->id,
+            'entry' => $content,
+        )));
+    }
+}
+ThreadEntry::registerAction(/* trans */ 'Manage', 'TEA_EditThreadEntry');
+
+class TEA_OrigThreadEntry extends ThreadEntryAction {
+    static $id = 'previous';
+    static $name = /* trans */ 'View History';
+    static $icon = 'copy';
+
+    function isVisible() {
+        // Can't edit system posts
+        return $this->entry->flags & ThreadEntry::FLAG_EDITED;
+    }
+
+    function getJsStub() {
+        return sprintf("$.dialog('%s');",
+            $this->getAjaxUrl()
+        );
+    }
+
+    function trigger() {
+        switch ($_SERVER['REQUEST_METHOD']) {
+        case 'GET':
+            return $this->trigger__get();
+        }
+    }
+
+    private function trigger__get() {
+        global $thisstaff;
+
+        if (!$this->entry->getParent())
+            Http::response(404, 'No history for this entry');
+        $entry = $this->entry;
+        include STAFFINC_DIR . 'templates/thread-entry-view.tmpl.php';
+    }
+}
+ThreadEntry::registerAction(/* trans */ 'Manage', 'TEA_OrigThreadEntry');
+
+class TEA_EditAndResendThreadEntry extends TEA_EditThreadEntry {
+    static $id = 'edit_resend';
+    static $name = /* trans */ 'Edit and Resend';
+    static $icon = 'reply-all';
+
+    function isVisible() {
+        // Can only resend replies
+        return $this->entry->staff_id && $this->entry->type == 'R'
+            && $this->isEnabled();
+    }
+
+    protected function trigger__post() {
+        $resend = @$_POST['commit'] == 'resend';
+
+        if (!($entry = $this->updateEntry($resend)))
+            return $this->trigger__get();
+
+        if ($resend)
+            $this->resend($entry);
+
+        ob_start();
+        include STAFFINC_DIR . 'templates/thread-entry.tmpl.php';
+        $content = ob_get_clean();
+
+        Http::response('201', JsonDataEncoder::encode(array(
+            'thread_id' => $this->entry->id, # This is the old id!
+            'new_id' => $entry->id,
+            'entry' => $content,
+        )));
+    }
+
+    function resend($response) {
+        global $cfg, $thisstaff;
+
+        if (!($object = $response->getThread()->getObject()))
+            return false;
+
+        $vars = $_POST;
+        $dept = $object->getDept();
+        $poster = $response->getStaff();
+
+        if ($thisstaff && $vars['signature'] == 'mine')
+            $signature = $thisstaff->getSignature();
+        elseif ($poster && $vars['signature'] == 'theirs')
+            $signature = $poster->getSignature();
+        elseif ($vars['signature'] == 'dept' && $dept && $dept->isPublic())
+            $signature = $dept->getSignature();
+        else
+            $signature = '';
+
+        $variables = array(
+            'response' => $response,
+            'signature' => $signature,
+            'staff' => $response->getStaff(),
+            'poster' => $response->getStaff());
+        $options = array('thread' => $response);
+
+        // Resend response to collabs
+        if (($object instanceof Ticket)
+                && ($email=$dept->getEmail())
+                && ($tpl = $dept->getTemplate())
+                && ($msg=$tpl->getReplyMsgTemplate())) {
+
+            $msg = $object->replaceVars($msg->asArray(),
+                $variables + array('recipient' => $object->getOwner()));
+
+            $attachments = $cfg->emailAttachments()
+                ? $response->getAttachments() : array();
+            $email->send($object->getOwner(), $msg['subj'], $msg['body'],
+                $attachments, $options);
+        }
+        // TODO: Add an option to the dialog
+        $object->notifyCollaborators($response, array('signature' => $signature));
+
+        // Log an event that the item was resent
+        $object->logEvent('resent', array('entry' => $response->id));
+
+        // Flag the entry as resent
+        $response->flags |= ThreadEntry::FLAG_RESENT;
+        $response->save();
+    }
+}
+ThreadEntry::registerAction(/* trans */ 'Manage', 'TEA_EditAndResendThreadEntry');
+
+class TEA_ResendThreadEntry extends TEA_EditAndResendThreadEntry {
+    static $id = 'resend';
+    static $name = /* trans */ 'Resend';
+    static $icon = 'reply-all';
+
+    function isVisible() {
+        // Can only resend replies
+        return $this->entry->staff_id && $this->entry->type == 'R'
+            && !parent::isEnabled();
+    }
+    function isEnabled() {
+        return true;
+    }
+
+    protected function trigger__get() {
+        global $cfg, $thisstaff;
+
+        $poster = $this->entry->getStaff();
+
+        include STAFFINC_DIR . 'templates/thread-entry-resend.tmpl.php';
+    }
+
+    protected function trigger__post() {
+        $resend = @$_POST['commit'] == 'resend';
+
+        if (@$_POST['commit'] == 'resend')
+            $this->resend($this->entry);
+
+        Http::response('201', 'Okee dokey');
+    }
+}
+ThreadEntry::registerAction(/* trans */ 'Manage', 'TEA_ResendThreadEntry');
diff --git a/include/class.ticket.php b/include/class.ticket.php
index 1b4c2badf0737366918a18c23524daf4d6452b98..453d40dc5ef607a098536b649d400a2cbe632500 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -22,6 +22,7 @@ include_once(INCLUDE_DIR.'class.dept.php');
 include_once(INCLUDE_DIR.'class.topic.php');
 include_once(INCLUDE_DIR.'class.lock.php');
 include_once(INCLUDE_DIR.'class.file.php');
+include_once(INCLUDE_DIR.'class.export.php');
 include_once(INCLUDE_DIR.'class.attachment.php');
 include_once(INCLUDE_DIR.'class.banlist.php');
 include_once(INCLUDE_DIR.'class.template.php');
@@ -32,100 +33,259 @@ include_once(INCLUDE_DIR.'class.canned.php');
 require_once(INCLUDE_DIR.'class.dynamic_forms.php');
 require_once(INCLUDE_DIR.'class.user.php');
 require_once(INCLUDE_DIR.'class.collaborator.php');
+require_once(INCLUDE_DIR.'class.task.php');
+require_once(INCLUDE_DIR.'class.faq.php');
+
+class TicketModel extends VerySimpleModel {
+    static $meta = array(
+        'table' => TICKET_TABLE,
+        'pk' => array('ticket_id'),
+        'joins' => array(
+            'user' => array(
+                'constraint' => array('user_id' => 'User.id')
+            ),
+            'status' => array(
+                'constraint' => array('status_id' => 'TicketStatus.id')
+            ),
+            'lock' => array(
+                'constraint' => array('lock_id' => 'Lock.lock_id'),
+                'null' => true,
+            ),
+            'dept' => array(
+                'constraint' => array('dept_id' => 'Dept.id'),
+            ),
+            'sla' => array(
+                'constraint' => array('sla_id' => 'Sla.id'),
+                'null' => true,
+            ),
+            'staff' => array(
+                'constraint' => array('staff_id' => 'Staff.staff_id'),
+                'null' => true,
+            ),
+            'tasks' => array(
+                'reverse' => 'Task.ticket',
+            ),
+            'team' => array(
+                'constraint' => array('team_id' => 'Team.team_id'),
+                'null' => true,
+            ),
+            'topic' => array(
+                'constraint' => array('topic_id' => 'Topic.topic_id'),
+                'null' => true,
+            ),
+            'thread' => array(
+                'reverse' => 'TicketThread.ticket',
+                'list' => false,
+                'null' => true,
+            ),
+            'cdata' => array(
+                'reverse' => 'TicketCData.ticket',
+                'list' => false,
+            ),
+            'entries' => array(
+                'constraint' => array(
+                    "'T'" => 'DynamicFormEntry.object_type',
+                    'ticket_id' => 'DynamicFormEntry.object_id',
+                ),
+                'list' => true,
+            ),
+        )
+    );
+
+    const PERM_CREATE   = 'ticket.create';
+    const PERM_EDIT     = 'ticket.edit';
+    const PERM_ASSIGN   = 'ticket.assign';
+    const PERM_TRANSFER = 'ticket.transfer';
+    const PERM_REPLY    = 'ticket.reply';
+    const PERM_CLOSE    = 'ticket.close';
+    const PERM_DELETE   = 'ticket.delete';
+
+
+    static protected $perms = array(
+            self::PERM_CREATE => array(
+                'title' =>
+                /* @trans */ 'Create',
+                'desc'  =>
+                /* @trans */ 'Ability to open tickets on behalf of users'),
+            self::PERM_EDIT => array(
+                'title' =>
+                /* @trans */ 'Edit',
+                'desc'  =>
+                /* @trans */ 'Ability to edit tickets'),
+            self::PERM_ASSIGN => array(
+                'title' =>
+                /* @trans */ 'Assign',
+                'desc'  =>
+                /* @trans */ 'Ability to assign tickets to agents or teams'),
+            self::PERM_TRANSFER => array(
+                'title' =>
+                /* @trans */ 'Transfer',
+                'desc'  =>
+                /* @trans */ 'Ability to transfer tickets between departments'),
+            self::PERM_REPLY => array(
+                'title' =>
+                /* @trans */ 'Post Reply',
+                'desc'  =>
+                /* @trans */ 'Ability to post a ticket reply'),
+            self::PERM_CLOSE => array(
+                'title' =>
+                /* @trans */ 'Close',
+                'desc'  =>
+                /* @trans */ 'Ability to close tickets'),
+            self::PERM_DELETE => array(
+                'title' =>
+                /* @trans */ 'Delete',
+                'desc'  =>
+                /* @trans */ 'Ability to delete tickets'),
+            );
 
+    // Ticket Sources
+    static protected $sources =  array(
+            'Phone' =>
+            /* @trans */ 'Phone',
+            'Email' =>
+            /* @trans */ 'Email',
+
+            'Web' =>
+            /* @trans */ 'Web',
+            'API' =>
+            /* @trans */ 'API',
+            'Other' =>
+            /* @trans */ 'Other',
+            );
 
-class Ticket {
-
-    var $id;
-    var $number;
+    function getId() {
+        return $this->ticket_id;
+    }
 
-    var $ht;
+    function getEffectiveDate() {
+         return Format::datetime(max(
+             strtotime($this->thread->lastmessage),
+             strtotime($this->closed),
+             strtotime($this->reopened),
+             strtotime($this->created)
+         ));
+    }
 
-    var $lastMsgId;
+    static function registerCustomData(DynamicForm $form) {
+        if (!isset(static::$meta['joins']['cdata+'.$form->id])) {
+            $cdata_class = <<<EOF
+class DynamicForm{$form->id} extends DynamicForm {
+    static function getInstance() {
+        static \$instance;
+        if (!isset(\$instance))
+            \$instance = static::lookup({$form->id});
+        return \$instance;
+    }
+}
+class TicketCdataForm{$form->id}
+extends VerySimpleModel {
+    static \$meta = array(
+        'view' => true,
+        'pk' => array('ticket_id'),
+        'joins' => array(
+            'ticket' => array(
+                'constraint' => array('ticket_id' => 'TicketModel.ticket_id'),
+            ),
+        )
+    );
+    static function getQuery(\$compiler) {
+        return '('.DynamicForm{$form->id}::getCrossTabQuery('T', 'ticket_id').')';
+    }
+}
+EOF;
+            eval($cdata_class);
+            $join = array(
+                'constraint' => array('ticket_id' => 'TicketCdataForm'.$form->id.'.ticket_id'),
+                'list' => true,
+            );
+            // This may be necessary if the model has already been inspected
+            if (static::$meta instanceof ModelMeta)
+                static::$meta->addJoin('cdata+'.$form->id, $join);
+            else {
+                static::$meta['joins']['cdata+'.$form->id] = array(
+                    'constraint' => array('ticket_id' => 'TicketCdataForm'.$form->id.'.ticket_id'),
+                    'list' => true,
+                );
+            }
+        }
+    }
 
-    var $status;
-    var $dept;  //Dept obj
-    var $sla;   // SLA obj
-    var $staff; //Staff obj
-    var $client; //Client Obj
-    var $team;  //Team obj
-    var $topic; //Topic obj
-    var $tlock; //TicketLock obj
+    static function getPermissions() {
+        return self::$perms;
+    }
 
-    var $thread; //Thread obj.
+    static function getSources() {
+        static $translated = false;
+        if (!$translated) {
+            foreach (static::$sources as $k=>$v)
+                static::$sources[$k] = __($v);
+        }
 
-    function Ticket($id) {
-        $this->id = 0;
-        $this->load($id);
+        return static::$sources;
     }
+}
 
-    function load($id=0) {
-
-        if(!$id && !($id=$this->getId()))
-            return false;
+RolePermission::register(/* @trans */ 'Tickets', TicketModel::getPermissions(), true);
+
+class TicketCData extends VerySimpleModel {
+    static $meta = array(
+        'table' => TICKET_CDATA_TABLE,
+        'pk' => array('ticket_id'),
+        'joins' => array(
+            'ticket' => array(
+                'constraint' => array('ticket_id' => 'TicketModel.ticket_id'),
+            ),
+            ':priority' => array(
+                'constraint' => array('priority' => 'Priority.priority_id'),
+                'null' => true,
+            ),
+        ),
+    );
+}
 
-        $sql='SELECT  ticket.*, lock_id, dept_name '
-            .' ,IF(sla.id IS NULL, NULL, '
-                .'DATE_ADD(ticket.created, INTERVAL sla.grace_period HOUR)) as sla_duedate '
-            .' ,count(distinct attach.attach_id) as attachments'
-            .' FROM '.TICKET_TABLE.' ticket '
-            .' LEFT JOIN '.DEPT_TABLE.' dept ON (ticket.dept_id=dept.dept_id) '
-            .' LEFT JOIN '.SLA_TABLE.' sla ON (ticket.sla_id=sla.id AND sla.isactive=1) '
-            .' LEFT JOIN '.TICKET_LOCK_TABLE.' tlock
-                ON ( ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW()) '
-            .' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach
-                ON ( ticket.ticket_id=attach.ticket_id) '
-            .' WHERE ticket.ticket_id='.db_input($id)
-            .' GROUP BY ticket.ticket_id';
-
-        //echo $sql;
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
+class Ticket extends TicketModel
+implements RestrictedAccess, Threadable {
 
+    static $meta = array(
+        'select_related' => array('topic', 'staff', 'user', 'team', 'dept', 'sla', 'thread',
+            'user__default_email'),
+    );
 
-        $this->ht = db_fetch_array($res);
+    var $lastMsgId;
+    var $last_message;
 
-        $this->id       = $this->ht['ticket_id'];
-        $this->number   = $this->ht['number'];
-        $this->_answers = array();
+    var $owner;     // TicketOwner
+    var $_user;      // EndUser
+    var $_answers;
+    var $collaborators;
+    var $active_collaborators;
+    var $recipients;
+    var $lastrespondent;
 
+    function __onload() {
         $this->loadDynamicData();
-
-        //Reset the sub classes (initiated ondemand)...good for reloads.
-        $this->status= null;
-        $this->staff = null;
-        $this->client = null;
-        $this->team  = null;
-        $this->dept = null;
-        $this->sla = null;
-        $this->tlock = null;
-        $this->stats = null;
-        $this->topic = null;
-        $this->thread = null;
-        $this->collaborators = null;
-
-        return true;
     }
 
-    function loadDynamicData() {
-        if (!$this->_answers) {
-            foreach (DynamicFormEntry::forTicket($this->getId(), true) as $form) {
-                foreach ($form->getAnswers() as $answer) {
-                    $tag = mb_strtolower($answer->getField()->get('name'))
-                        ?: 'field.' . $answer->getField()->get('id');
-                        $this->_answers[$tag] = $answer;
-                }
+    function loadDynamicData($force=false) {
+        if (!isset($this->_answers) || $force) {
+            $this->_answers = array();
+            foreach (DynamicFormEntryAnswer::objects()
+                ->filter(array(
+                    'entry__object_id' => $this->getId(),
+                    'entry__object_type' => 'T'
+                )) as $answer
+            ) {
+                $tag = mb_strtolower($answer->field->name)
+                    ?: 'field.' . $answer->field->id;
+                    $this->_answers[$tag] = $answer;
             }
         }
         return $this->_answers;
     }
 
-    function reload() {
-        return $this->load();
-    }
-
     function hasState($state) {
-        return  (strcasecmp($this->getState(), $state)==0);
+        return  strcasecmp($this->getState(), $state) == 0;
     }
 
     function isOpen() {
@@ -133,7 +293,7 @@ class Ticket {
     }
 
     function isReopened() {
-        return ($this->getReopenDate());
+        return null !== $this->getReopenDate();
     }
 
     function isReopenable() {
@@ -144,6 +304,25 @@ class Ticket {
          return $this->hasState('closed');
     }
 
+    function isCloseable() {
+
+        if ($this->isClosed())
+            return true;
+
+        $warning = null;
+        if ($this->getMissingRequiredFields()) {
+            $warning = sprintf(
+                    __( '%1$s is missing data on %2$s one or more required fields %3$s and cannot be closed'),
+                    __('This ticket'),
+                    '', '');
+        } elseif (($num=$this->getNumOpenTasks())) {
+            $warning = sprintf(__('%1$s has %2$d open tasks and cannot be closed'),
+                    __('This ticket'), $num);
+        }
+
+        return $warning ?: true;
+    }
+
     function isArchived() {
          return $this->hasState('archived');
     }
@@ -153,96 +332,107 @@ class Ticket {
     }
 
     function isAssigned() {
-        return ($this->isOpen() && ($this->getStaffId() || $this->getTeamId()));
+        return $this->isOpen() && ($this->getStaffId() || $this->getTeamId());
     }
 
     function isOverdue() {
-        return ($this->ht['isoverdue']);
+        return $this->ht['isoverdue'];
     }
 
     function isAnswered() {
-       return ($this->ht['isanswered']);
+       return $this->ht['isanswered'];
     }
 
     function isLocked() {
-        return ($this->getLockId());
+        return null !== $this->getLock();
     }
 
-    function checkStaffAccess($staff) {
+    function checkStaffPerm($staff, $perm=null) {
+        // Must be a valid staff
+        if (!$staff instanceof Staff && !($staff=Staff::lookup($staff)))
+            return false;
 
-        if(!is_object($staff) && !($staff=Staff::lookup($staff)))
+        // Check access based on department or assignment
+        if (($staff->showAssignedOnly()
+            || !$staff->canAccessDept($this->getDeptId()))
+            // only open tickets can be considered assigned
+            && $this->isOpen()
+            && $staff->getId() != $this->getStaffId()
+            && !$staff->isTeamMember($this->getTeamId())
+        ) {
             return false;
+        }
 
-        // Staff has access to the department.
-        if (!$staff->showAssignedOnly()
-                && $staff->canAccessDept($this->getDeptId()))
+        // At this point staff has view access unless a specific permission is
+        // requested
+        if ($perm === null)
             return true;
 
-        // Only consider assignment if the ticket is open
-        if (!$this->isOpen())
+        // Permission check requested -- get role.
+        if (!($role=$staff->getRole($this->getDeptId())))
             return false;
 
-        // Check ticket access based on direct or team assignment
-        if ($staff->getId() == $this->getStaffId()
-                || ($this->getTeamId()
-                    && $staff->isTeamMember($this->getTeamId())
-        ))
-            return true;
-
-        // No access bro!
-        return false;
+        // Check permission based on the effective role
+        return $role->hasPerm($perm);
     }
 
     function checkUserAccess($user) {
-
         if (!$user || !($user instanceof EndUser))
             return false;
 
-        //Ticket Owner
+        // Ticket Owner
         if ($user->getId() == $this->getUserId())
             return true;
 
-        //Collaborator?
+        // Organization
+        if ($user->canSeeOrgTickets()
+            && ($U = $this->getUser())
+            && ($U->getOrgId() == $user->getOrgId())
+        ) {
+            // The owner of this ticket is in the same organization as the
+            // user in question, and the organization is configured to allow
+            // the user in question to see other tickets in the
+            // organization.
+            return true;
+        }
+
+        // Collaborator?
         // 1) If the user was authorized via this ticket.
         if ($user->getTicketId() == $this->getId()
-                && !strcasecmp($user->getRole(), 'collaborator'))
+            && !strcasecmp($user->getUserType(), 'collaborator')
+        ) {
             return true;
-
+        }
         // 2) Query the database to check for expanded access...
         if (Collaborator::lookup(array(
-                        'userId' => $user->getId(),
-                        'ticketId' => $this->getId())))
+            'user_id' => $user->getId(),
+            'thread_id' => $this->getThreadId()))
+        ) {
             return true;
-
+        }
         return false;
     }
 
-    //Getters
-    function getId() {
-        return  $this->id;
-    }
-
+    // Getters
     function getNumber() {
         return $this->number;
     }
 
     function getOwnerId() {
-        return $this->ht['user_id'];
+        return $this->user_id;
     }
 
     function getOwner() {
-
-        if (!isset($this->owner)
-                && ($u=User::lookup($this->getOwnerId())))
-            $this->owner = new TicketOwner(new EndUser($u), $this);
-
+        if (!isset($this->owner)) {
+            $this->owner = new TicketOwner(new EndUser($this->user), $this);
+        }
         return $this->owner;
     }
 
-    function getEmail(){
-        if ($o = $this->getOwner())
+    function getEmail() {
+        if ($o = $this->getOwner()) {
             return $o->getEmail();
-
+        }
         return null;
     }
 
@@ -251,14 +441,16 @@ class Ticket {
         return $this->getEmail();
     }
 
-    function getAuthToken() {
+    // Deprecated
+    function getOldAuthToken() {
         # XXX: Support variable email address (for CCs)
         return md5($this->getId() . strtolower($this->getEmail()) . SECRET_SALT);
     }
 
     function getName(){
-        if ($o = $this->getOwner())
+        if ($o = $this->getOwner()) {
             return $o->getName();
+        }
         return null;
     }
 
@@ -268,15 +460,12 @@ class Ticket {
 
     /* Help topic title  - NOT object -> $topic */
     function getHelpTopic() {
-
-        if(!$this->ht['helptopic'] && ($topic=$this->getTopic()))
-            $this->ht['helptopic'] = $topic->getFullName();
-
-        return $this->ht['helptopic'];
+        if ($this->topic)
+            return $this->topic->getFullName();
     }
 
     function getCreateDate() {
-        return $this->ht['created'];
+        return $this->created;
     }
 
     function getOpenDate() {
@@ -284,73 +473,94 @@ class Ticket {
     }
 
     function getReopenDate() {
-        return $this->ht['reopened'];
+        return $this->reopened;
     }
 
     function getUpdateDate() {
-        return $this->ht['updated'];
+        return $this->updated;
+    }
+
+    function getEffectiveDate() {
+        return $this->lastupdate;
     }
 
     function getDueDate() {
-        return $this->ht['duedate'];
+        return $this->duedate;
     }
 
     function getSLADueDate() {
-        return $this->ht['sla_duedate'];
+        if ($sla = $this->getSLA()) {
+            $dt = new DateTime($this->getCreateDate());
+
+            return $dt
+                ->add(new DateInterval('PT' . $sla->getGracePeriod() . 'H'))
+                ->format('Y-m-d H:i:s');
+        }
     }
 
-    function getEstDueDate() {
+    function updateEstDueDate() {
+        $this->est_duedate = $this->getEstDueDate();
+        $this->save();
+    }
 
-        //Real due date
-        if(($duedate=$this->getDueDate()))
+    function getEstDueDate() {
+        // Real due date
+        if ($duedate = $this->getDueDate()) {
             return $duedate;
-
-        //return sla due date (If ANY)
+        }
+        // return sla due date (If ANY)
         return $this->getSLADueDate();
     }
 
     function getCloseDate() {
-        return $this->ht['closed'];
+        return $this->closed;
     }
 
     function getStatusId() {
-        return $this->ht['status_id'];
+        return $this->status_id;
     }
 
-    function getStatus() {
-
-        if (!$this->status && $this->getStatusId())
-            $this->status = TicketStatus::lookup($this->getStatusId());
+    /**
+     * setStatusId
+     *
+     * Forceably set the ticket status ID to the received status ID. No
+     * checks are made. Use ::setStatus() to change the ticket status
+     */
+    // XXX: Use ::setStatus to change the status. This can be used as a
+    //      fallback if the logic in ::setStatus fails.
+    function setStatusId($id) {
+        $this->status_id = $id;
+        return $this->save();
+    }
 
+    function getStatus() {
         return $this->status;
     }
 
     function getState() {
-
-        if (!$this->getStatus())
+        if (!$this->getStatus()) {
             return '';
-
+        }
         return $this->getStatus()->getState();
     }
 
     function getDeptId() {
-       return $this->ht['dept_id'];
+       return $this->dept_id;
     }
 
     function getDeptName() {
-
-        if(!$this->ht['dept_name'] && ($dept = $this->getDept()))
-            $this->ht['dept_name'] = $dept->getName();
-
-       return $this->ht['dept_name'];
+        if ($this->dept instanceof Dept)
+            return $this->dept->getFullName();
     }
 
     function getPriorityId() {
         global $cfg;
 
         if (($a = $this->_answers['priority'])
-                && ($b = $a->getValue()))
+            && ($b = $a->getValue())
+        ) {
             return $b->getId();
+        }
         return $cfg->getDefaultPriorityId();
     }
 
@@ -365,11 +575,11 @@ class Ticket {
     }
 
     function getSource() {
-        return $this->ht['source'];
+        return $this->source;
     }
 
     function getIP() {
-        return $this->ht['ip_address'];
+        return $this->ip_address;
     }
 
     function getHashtable() {
@@ -377,42 +587,36 @@ class Ticket {
     }
 
     function getUpdateInfo() {
-        global $cfg;
-
-        $info=array('source'    =>  $this->getSource(),
-                    'topicId'   =>  $this->getTopicId(),
-                    'slaId' =>  $this->getSLAId(),
-                    'user_id' => $this->getOwnerId(),
-                    'duedate'   =>  $this->getDueDate()
-                        ? Format::userdate($cfg->getDateFormat(),
-                            Misc::db2gmtime($this->getDueDate()))
-                        :'',
-                    'time'  =>  $this->getDueDate()?(Format::userdate('G:i', Misc::db2gmtime($this->getDueDate()))):'',
-                    );
-
-        return $info;
-    }
-
-    function getLockId() {
-        return $this->ht['lock_id'];
+        return array(
+            'source'    => $this->getSource(),
+            'topicId'   => $this->getTopicId(),
+            'slaId'     => $this->getSLAId(),
+            'user_id'   => $this->getOwnerId(),
+            'duedate'   => $this->getDueDate()
+                ? Format::date($this->getDueDate())
+                : '',
+            'time'      => $this->getDueDate()?(Format::date($this->getDueDate(), true, 'HH:mm')):'',
+        );
     }
 
     function getLock() {
-
-        if(!$this->tlock && $this->getLockId())
-            $this->tlock= TicketLock::lookup($this->getLockId(), $this->getId());
-
-        return $this->tlock;
+        $lock = $this->lock;
+        if ($lock && !$lock->isExpired())
+            return $lock;
     }
 
-    function acquireLock($staffId, $lockTime) {
+    function acquireLock($staffId, $lockTime=null) {
+        global $cfg;
+
+        if (!isset($lockTime))
+            $lockTime = $cfg->getLockTime();
 
-        if(!$staffId or !$lockTime) //Lockig disabled?
+        if (!$staffId or !$lockTime) //Lockig disabled?
             return null;
 
-        //Check if the ticket is already locked.
-        if(($lock=$this->getLock()) && !$lock->isExpired()) {
-            if($lock->getStaffId()!=$staffId) //someone else locked the ticket.
+        // Check if the ticket is already locked.
+        if (($lock = $this->getLock()) && !$lock->isExpired()) {
+            if ($lock->getStaffId() != $staffId) //someone else locked the ticket.
                 return null;
 
             //Lock already exits...renew it
@@ -420,21 +624,35 @@ class Ticket {
 
             return $lock;
         }
-        //No lock on the ticket or it is expired
-        $this->tlock = null; //clear crap
-        $this->ht['lock_id'] = TicketLock::acquire($this->getId(), $staffId, $lockTime); //Create a new lock..
-        //load and return the newly created lock if any!
-        return $this->getLock();
+        // No lock on the ticket or it is expired
+        $this->lock = Lock::acquire($staffId, $lockTime); //Create a new lock..
+
+        if ($this->lock) {
+            $this->save();
+        }
+
+        // load and return the newly created lock if any!
+        return $this->lock;
+    }
+
+    function releaseLock($staffId=false) {
+        if (!($lock = $this->getLock()))
+            return false;
+
+        if ($staffId && $lock->staff_id != $staffId)
+            return false;
+
+        if (!$lock->delete())
+            return false;
+
+        $this->lock = null;
+        return $this->save();
     }
 
     function getDept() {
         global $cfg;
 
-        if(!$this->dept)
-            if(!($this->dept = Dept::lookup($this->getDeptId())))
-                $this->dept = $cfg->getDefaultDept();
-
-        return $this->dept;
+        return $this->dept ?: $cfg->getDefaultDept();
     }
 
     function getUserId() {
@@ -442,55 +660,63 @@ class Ticket {
     }
 
     function getUser() {
-
-        if(!isset($this->user) && $this->getOwner())
-            $this->user = new EndUser($this->getOwner());
-
-        return $this->user;
+        if (!isset($this->_user) && $this->user) {
+            $this->_user = new EndUser($this->user);
+        }
+        return $this->_user;
     }
 
     function getStaffId() {
-        return $this->ht['staff_id'];
+        return $this->staff_id;
     }
 
     function getStaff() {
-
-        if(!$this->staff && $this->getStaffId())
-            $this->staff= Staff::lookup($this->getStaffId());
-
         return $this->staff;
     }
 
     function getTeamId() {
-        return $this->ht['team_id'];
+        return $this->team_id;
     }
 
     function getTeam() {
+        return $this->team;
+    }
 
-        if(!$this->team && $this->getTeamId())
-            $this->team = Team::lookup($this->getTeamId());
+    function getAssigneeId() {
 
-        return $this->team;
+        if (!($assignee=$this->getAssignee()))
+            return null;
+
+        $id = '';
+        if ($assignee instanceof Staff)
+            $id = 's'.$assignee->getId();
+        elseif ($assignee instanceof Team)
+            $id = 't'.$assignee->getId();
+
+        return $id;
     }
 
     function getAssignee() {
 
-        if($staff=$this->getStaff())
-            return $staff->getName();
+        if (!$this->isOpen() || !$this->isAssigned())
+            return false;
+
+        if ($this->staff)
+            return $this->staff;
 
-        if($team=$this->getTeam())
-            return $team->getName();
+        if ($this->team)
+            return $this->team;
 
-        return '';
+        return null;
     }
 
     function getAssignees() {
 
-        $assignees=array();
-        if($staff=$this->getStaff())
+        $assignees = array();
+        if ($staff = $this->getStaff())
             $assignees[] = $staff->getName();
 
-        if($team=$this->getTeam())
+        if ($team = $this->getTeam())
             $assignees[] = $team->getName();
 
         return $assignees;
@@ -498,54 +724,49 @@ class Ticket {
 
     function getAssigned($glue='/') {
         $assignees = $this->getAssignees();
-        return $assignees?implode($glue, $assignees):'';
+        return $assignees ? implode($glue, $assignees) : '';
     }
 
     function getTopicId() {
-        return $this->ht['topic_id'];
+        return $this->topic_id;
     }
 
     function getTopic() {
-
-        if(!$this->topic && $this->getTopicId())
-            $this->topic = Topic::lookup($this->getTopicId());
-
         return $this->topic;
     }
 
 
     function getSLAId() {
-        return $this->ht['sla_id'];
+        return $this->sla_id;
     }
 
     function getSLA() {
-
-        if(!$this->sla && $this->getSLAId())
-            $this->sla = SLA::lookup($this->getSLAId());
-
         return $this->sla;
     }
 
     function getLastRespondent() {
-
-        $sql ='SELECT  resp.staff_id '
-             .' FROM '.TICKET_THREAD_TABLE.' resp '
-             .' LEFT JOIN '.STAFF_TABLE. ' USING(staff_id) '
-             .' WHERE  resp.ticket_id='.db_input($this->getId()).' AND resp.staff_id>0 '
-             .'   AND  resp.thread_type="R"'
-             .' ORDER BY resp.created DESC LIMIT 1';
-
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return null;
-
-        list($id)=db_fetch_row($res);
-
-        return Staff::lookup($id);
-
+        if (!isset($this->lastrespondent)) {
+            if (!$this->thread || !$this->thread->entries)
+                return $this->lastrespondent = false;
+            $this->lastrespondent = Staff::objects()
+                ->filter(array(
+                'staff_id' => $this->thread->entries
+                    ->filter(array(
+                        'type' => 'R',
+                        'staff_id__gt' => 0,
+                    ))
+                    ->values_flat('staff_id')
+                    ->order_by('-id')
+                    ->limit(1)
+                ))
+                ->first()
+                ?: false;
+        }
+        return $this->lastrespondent;
     }
 
     function getLastMessageDate() {
-        return $this->ht['lastmessage'];
+        return $this->thread->lastmessage;
     }
 
     function getLastMsgDate() {
@@ -553,40 +774,51 @@ class Ticket {
     }
 
     function getLastResponseDate() {
-        return $this->ht['lastresponse'];
+        return $this->thread->lastresponse;
     }
 
     function getLastRespDate() {
         return $this->getLastResponseDate();
     }
 
-
     function getLastMsgId() {
         return $this->lastMsgId;
     }
 
     function getLastMessage() {
         if (!isset($this->last_message)) {
-            if($this->getLastMsgId())
-                $this->last_message =  Message::lookup(
-                    $this->getLastMsgId(), $this->getId());
+            if ($this->getLastMsgId())
+                $this->last_message = MessageThreadEntry::lookup(
+                    $this->getLastMsgId(), $this->getThreadId());
 
             if (!$this->last_message)
-                $this->last_message = Message::lastByTicketId($this->getId());
+                $this->last_message = $this->getThread()->getLastMessage();
         }
         return $this->last_message;
     }
 
-    function getThread() {
+    function getNumTasks() {
+        // FIXME: Implement this after merging Tasks
+        return count($this->tasks);
+    }
+
+    function getNumOpenTasks() {
+        return count($this->tasks->filter(array(
+                        'flags__hasbit' => TaskModel::ISOPEN)));
+    }
 
-        if(!$this->thread)
-            $this->thread = Thread::lookup($this);
 
+    function getThreadId() {
+        if ($this->thread)
+            return $this->thread->id;
+    }
+
+    function getThread() {
         return $this->thread;
     }
 
     function getThreadCount() {
-        return $this->getNumMessages() + $this->getNumResponses();
+        return $this->getClientThread()->count();
     }
 
     function getNumMessages() {
@@ -602,15 +834,15 @@ class Ticket {
     }
 
     function getMessages() {
-        return $this->getThreadEntries('M');
+        return $this->getThreadEntries(array('M'));
     }
 
     function getResponses() {
-        return $this->getThreadEntries('R');
+        return $this->getThreadEntries(array('R'));
     }
 
     function getNotes() {
-        return $this->getThreadEntries('N');
+        return $this->getThreadEntries(array('N'));
     }
 
     function getClientThread() {
@@ -621,69 +853,135 @@ class Ticket {
         return $this->getThread()->getEntry($id);
     }
 
-    function getThreadEntries($type, $order='') {
-        return $this->getThread()->getEntries($type, $order);
+    function getThreadEntries($type=false) {
+        $entries = $this->getThread()->getEntries();
+        if ($type && is_array($type))
+            $entries->filter(array('type__in' => $type));
+
+        return $entries;
     }
 
-    //Collaborators
-    function getNumCollaborators() {
-        return count($this->getCollaborators());
+    //UserList of recipients  (owner + collaborators)
+    function getRecipients() {
+        if (!isset($this->recipients)) {
+            $list = new UserList();
+            $list->add($this->getOwner());
+            if ($collabs = $this->getThread()->getActiveCollaborators()) {
+                foreach ($collabs as $c)
+                    $list->add($c);
+            }
+            $this->recipients = $list;
+        }
+        return $this->recipients;
     }
 
-    function getNumActiveCollaborators() {
+    function getAssignmentForm($source=null, $options=array()) {
 
-        if (!isset($this->ht['active_collaborators']))
-            $this->ht['active_collaborators'] = count($this->getActiveCollaborators());
+        $prompt = $assignee = '';
+        // Possible assignees
+        $assignees = array();
+        switch (strtolower($options['target'])) {
+            case 'agents':
+                $dept = $this->getDept();
+                foreach ($dept->getAssignees() as $member)
+                    $assignees['s'.$member->getId()] = $member;
 
-        return $this->ht['active_collaborators'];
-    }
+                if (!$source && $this->isOpen() && $this->staff)
+                    $assignee = sprintf('s%d', $this->staff->getId());
+                $prompt = __('Select an Agent');
+                break;
+            case 'teams':
+                if (($teams = Team::getActiveTeams()))
+                    foreach ($teams as $id => $name)
+                        $assignees['t'.$id] = $name;
+
+                if (!$source && $this->isOpen() && $this->team)
+                    $assignee = sprintf('t%d', $this->team->getId());
+                $prompt = __('Select a Team');
+                break;
+        }
+
+        // Default to current assignee if source is not set
+        if (!$source)
+            $source = array('assignee' => array($assignee));
+
+        $form = AssignmentForm::instantiate($source, $options);
 
-    function getActiveCollaborators() {
-        return $this->getCollaborators(array('isactive'=>1));
+        if ($assignees)
+            $form->setAssignees($assignees);
+
+        if ($prompt && ($f=$form->getField('assignee')))
+            $f->configure('prompt', $prompt);
+
+
+        return $form;
     }
 
+    function getClaimForm($source=null, $options=array()) {
+        global $thisstaff;
 
-    function getCollaborators($criteria=array()) {
+        $id = sprintf('s%d', $thisstaff->getId());
+        if(!$source)
+            $source = array('assignee' => array($id));
 
-        if ($criteria)
-            return Collaborator::forTicket($this->getId(), $criteria);
+        $form = ClaimForm::instantiate($source, $options);
+        $form->setAssignees(array($id => $thisstaff->getName()));
 
-        if (!isset($this->collaborators))
-            $this->collaborators = Collaborator::forTicket($this->getId());
+        return $form;
 
-        return $this->collaborators;
     }
 
-    //UserList of recipients  (owner + collaborators)
-    function getRecipients() {
+    function getTransferForm($source=null) {
 
-        if (!isset($this->recipients)) {
-            $list = new UserList();
-            $list->add($this->getOwner());
-            if ($collabs = $this->getActiveCollaborators()) {
-                foreach ($collabs as $c)
-                    $list->add($c);
+        if (!$source)
+            $source = array('dept' => array($this->getDeptId()));
+
+        return TransferForm::instantiate($source);
+    }
+
+    function getDynamicFields($criteria=array()) {
+
+        $fields = DynamicFormField::objects()->filter(array(
+                    'id__in' => $this->entries
+                    ->filter($criteria)
+                ->values_flat('answers__field_id')));
+
+        return ($fields && count($fields)) ? $fields : array();
+    }
+
+    function hasClientEditableFields() {
+        $forms = DynamicFormEntry::forTicket($this->getId());
+        foreach ($forms as $form) {
+            foreach ($form->getFields() as $field) {
+                if ($field->isEditableToUsers())
+                    return true;
             }
-            $this->recipients = $list;
         }
-
-        return $this->recipients;
     }
 
+    function getMissingRequiredFields() {
+
+        return $this->getDynamicFields(array(
+                    'answers__field__flags__hasbit' => DynamicFormField::FLAG_ENABLED,
+                    'answers__field__flags__hasbit' => DynamicFormField::FLAG_CLOSE_REQUIRED,
+                    'answers__value__isnull' => true,
+                    ));
+    }
 
-    function addCollaborator($user, $vars, &$errors) {
+    function getMissingRequiredField() {
+        $fields = $this->getMissingRequiredFields();
+        return $fields ? $fields[0] : null;
+    }
 
-        if (!$user || $user->getId()==$this->getOwnerId())
-            return null;
+    function addCollaborator($user, $vars, &$errors, $event=true) {
 
-        $vars = array_merge(array(
-                'ticketId' => $this->getId(),
-                'userId' => $user->getId()), $vars);
-        if (!($c=Collaborator::add($vars, $errors)))
+        if (!$user || $user->getId() == $this->getOwnerId())
             return null;
 
-        $this->collaborators = null;
-        $this->recipients = null;
+        if ($c = $this->getThread()->addCollaborator($user, $vars, $errors, $event)) {
+            $this->collaborators = null;
+            $this->recipients = null;
+        }
 
         return $c;
     }
@@ -700,38 +998,86 @@ class Ticket {
             foreach ($ids as $k => $cid) {
                 if (($c=Collaborator::lookup($cid))
                         && $c->getTicketId() == $this->getId()
-                        && $c->remove())
-                     $collabs[] = $c;
+                        && $c->delete())
+                     $collabs[] = (string) $c;
             }
 
-            $this->logNote(_S('Collaborators Removed'),
-                    implode("<br>", $collabs), $thisstaff, false);
+            $this->logEvent('collab', array('del' => $collabs));
         }
 
         //statuses
         $cids = null;
         if($vars['cid'] && ($cids=array_filter($vars['cid']))) {
-            $sql='UPDATE '.TICKET_COLLABORATOR_TABLE
-                .' SET updated=NOW(), isactive=1 '
-                .' WHERE ticket_id='.db_input($this->getId())
-                .' AND id IN('.implode(',', db_input($cids)).')';
-            db_query($sql);
+            $this->getThread()->collaborators->filter(array(
+                'thread_id' => $this->getThreadId(),
+                'id__in' => $cids
+            ))->update(array(
+                'updated' => SqlFunction::NOW(),
+                'isactive' => 1,
+            ));
+        }
+
+        if ($cids) {
+            $this->getThread()->collaborators->filter(array(
+                'thread_id' => $this->getThreadId(),
+                Q::not(array('id__in' => $cids))
+            ))->update(array(
+                'updated' => SqlFunction::NOW(),
+                'isactive' => 0,
+            ));
+        }
+
+        unset($this->active_collaborators);
+        $this->collaborators = null;
+
+        return true;
+    }
+
+    function getAuthToken($user, $algo=1) {
+
+        //Format: // <user type><algo id used>x<pack of uid & tid><hash of the algo>
+        $authtoken = sprintf('%s%dx%s',
+                ($user->getId() == $this->getOwnerId() ? 'o' : 'c'),
+                $algo,
+                Base32::encode(pack('VV',$user->getId(), $this->getId())));
+
+        switch($algo) {
+            case 1:
+                $authtoken .= substr(base64_encode(
+                            md5($user->getId().$this->getCreateDate().$this->getId().SECRET_SALT, true)), 8);
+                break;
+            default:
+                return null;
         }
 
-        $sql='UPDATE '.TICKET_COLLABORATOR_TABLE
-            .' SET updated=NOW(), isactive=0 '
-            .' WHERE ticket_id='.db_input($this->getId());
-        if($cids)
-            $sql.=' AND id NOT IN('.implode(',', db_input($cids)).')';
+        return $authtoken;
+    }
 
-        db_query($sql);
+    function sendAccessLink($user) {
+        global $ost;
 
-        unset($this->ht['active_collaborators']);
-        $this->collaborators = null;
+        if (!($email = $ost->getConfig()->getDefaultEmail())
+            || !($content = Page::lookupByType('access-link')))
+            return;
 
-        return true;
+        $vars = array(
+            'url' => $ost->getConfig()->getBaseUrl(),
+            'ticket' => $this,
+            'user' => $user,
+            'recipient' => $user,
+        );
+
+        $lang = $user->getLanguage(UserAccount::LANG_MAILOUTS);
+        $msg = $ost->replaceTemplateVariables(array(
+            'subj' => $content->getLocalName($lang),
+            'body' => $content->getLocalBody($lang),
+        ), $vars);
+
+        $email->send($user, Format::striptags($msg['subj']),
+            $msg['body']);
     }
 
+
     /* -------------------- Setters --------------------- */
     function setLastMsgId($msgid) {
         return $this->lastMsgId=$msgid;
@@ -743,41 +1089,33 @@ class Ticket {
 
     //DeptId can NOT be 0. No orphans please!
     function setDeptId($deptId) {
-
-        //Make sure it's a valid department//
-        if(!($dept=Dept::lookup($deptId)) || $dept->getId()==$this->getDeptId())
+        // Make sure it's a valid department
+        if ($deptId == $this->getDeptId() || !($dept=Dept::lookup($deptId))) {
             return false;
-
-
-        $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), dept_id='.db_input($deptId)
-            .' WHERE ticket_id='.db_input($this->getId());
-
-        return (db_query($sql) && db_affected_rows());
+        }
+        $this->dept = $dept;
+        return $this->save();
     }
 
-    //Set staff ID...assign/unassign/release (id can be 0)
+    // Set staff ID...assign/unassign/release (id can be 0)
     function setStaffId($staffId) {
-
-        if(!is_numeric($staffId)) return false;
-
-        $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), staff_id='.db_input($staffId)
-            .' WHERE ticket_id='.db_input($this->getId());
-
-        if (!db_query($sql)  || !db_affected_rows())
+        if (!is_numeric($staffId))
             return false;
 
-        $this->staff = null;
-        $this->ht['staff_id'] = $staffId;
-
-        return true;
+        $this->staff = Staff::lookup($staffId);
+        return $this->save();
     }
 
     function setSLAId($slaId) {
-        if ($slaId == $this->getSLAId()) return true;
-        return db_query(
-             'UPDATE '.TICKET_TABLE.' SET sla_id='.db_input($slaId)
-            .' WHERE ticket_id='.db_input($this->getId()))
-            && db_affected_rows();
+        if ($slaId == $this->getSLAId())
+            return true;
+
+        $sla = null;
+        if ($slaId && !($sla = Sla::lookup($slaId)))
+            return false;
+
+        $this->sla = $sla;
+        return $this->save();
     }
     /**
      * Selects the appropriate service-level-agreement plan for this ticket.
@@ -812,121 +1150,132 @@ class Ticket {
 
     //Set team ID...assign/unassign/release (id can be 0)
     function setTeamId($teamId) {
+        if (!is_numeric($teamId))
+            return false;
 
-        if(!is_numeric($teamId)) return false;
-
-        $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW(), team_id='.db_input($teamId)
-            .' WHERE ticket_id='.db_input($this->getId());
-
-        return (db_query($sql)  && db_affected_rows());
+        $this->team = Team::lookup($teamId);
+        return $this->save();
     }
 
     //Status helper.
-    function setStatus($status, $comments='', $set_closing_agent=true) {
+
+    function setStatus($status, $comments='', &$errors=array(), $set_closing_agent=true) {
         global $thisstaff;
 
+        if ($thisstaff && !($role = $thisstaff->getRole($this->getDeptId())))
+            return false;
+
         if ($status && is_numeric($status))
             $status = TicketStatus::lookup($status);
 
         if (!$status || !$status instanceof TicketStatus)
             return false;
 
-        // XXX: intercept deleted status and do hard delete
-        if (!strcasecmp($status->getState(), 'deleted'))
-            return $this->delete($comments);
+        // Double check permissions (when changing status)
+        if ($role && $this->getStatusId()) {
+            switch ($status->getState()) {
+            case 'closed':
+                if (!($role->hasPerm(TicketModel::PERM_CLOSE)))
+                    return false;
+                break;
+            case 'deleted':
+                // XXX: intercept deleted status and do hard delete
+                if ($role->hasPerm(TicketModel::PERM_DELETE))
+                    return $this->delete($comments);
+                // Agent doesn't have permission to delete  tickets
+                return false;
+                break;
+            }
+        }
 
+        $hadStatus = $this->getStatusId();
         if ($this->getStatusId() == $status->getId())
             return true;
 
-        $sql = 'UPDATE '.TICKET_TABLE.' SET updated=NOW() '.
-               ' ,status_id='.db_input($status->getId());
-
+        // Perform checks on the *new* status, _before_ the status changes
         $ecb = null;
-        switch($status->getState()) {
+        switch ($status->getState()) {
             case 'closed':
-                $sql.=', closed=NOW(), duedate=NULL ';
+                // Check if ticket is closeable
+                $closeable = $this->isCloseable();
+                if ($closeable !== true)
+                    $errors['err'] = $closeable ?: sprintf(__('%s cannot be closed'), __('This ticket'));
+
+                if ($errors)
+                    return false;
+
+                $this->closed = $this->lastupdate = SqlFunction::NOW();
+                $this->duedate = null;
                 if ($thisstaff && $set_closing_agent)
-                    $sql.=', staff_id='.db_input($thisstaff->getId());
-                $this->clearOverdue();
+                    $this->staff = $thisstaff;
+                $this->clearOverdue(false);
 
-                $ecb = function($t) {
-                    $t->reload();
-                    $t->logEvent('closed');
+                $ecb = function($t) use ($status) {
+                    $t->logEvent('closed', array('status' => array($status->getId(), $status->getName())));
                     $t->deleteDrafts();
                 };
                 break;
             case 'open':
                 // TODO: check current status if it allows for reopening
                 if ($this->isClosed()) {
-                    $sql .= ',closed=NULL, reopened=NOW() ';
+                    $this->closed = $this->lastupdate = $this->reopened = SqlFunction::NOW();
                     $ecb = function ($t) {
-                        $t->logEvent('reopened', 'closed');
+                        $t->logEvent('reopened', false, null, 'closed');
                     };
                 }
 
                 // If the ticket is not open then clear answered flag
                 if (!$this->isOpen())
-                    $sql .= ', isanswered = 0 ';
+                    $this->isanswered = 0;
                 break;
             default:
                 return false;
 
         }
 
-        $sql.=' WHERE ticket_id='.db_input($this->getId());
-
-        if (!db_query($sql) || !db_affected_rows())
+        $this->status = $status;
+        if (!$this->save())
             return false;
 
         // 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 %1$s to %2$s by %3$s'),
-                    $this->getStatus(),
-                    $status,
-                    $thisstaff ?: 'SYSTEM');
-
+        if ($hadStatus) {
             $alert = false;
-            if (($comments = ThreadBody::clean($comments))) {
-                $note .= sprintf('<hr>%s', $comments);
+            if ($comments = ThreadEntryBody::clean($comments)) {
                 // Send out alerts if comments are included
                 $alert = true;
+                $this->logNote(__('Status Changed'), $comments, $thisstaff, $alert);
             }
-
-            $this->logNote(__('Status Changed'), $note, $thisstaff, $alert);
         }
         // Log events via callback
-        if ($ecb) $ecb($this);
+        if ($ecb)
+            $ecb($this);
+        elseif ($hadStatus)
+            // Don't log the initial status change
+            $this->logEvent('edited', array('status' => $status->getId()));
 
         return true;
     }
 
     function setState($state, $alerts=false) {
-
-        switch(strtolower($state)) {
-            case 'open':
-                return $this->setStatus('open');
-                break;
-            case 'closed':
-                return $this->setStatus('closed');
-                break;
-            case 'answered':
-                return $this->setAnsweredState(1);
-                break;
-            case 'unanswered':
-                return $this->setAnsweredState(0);
-                break;
-            case 'overdue':
-                return $this->markOverdue();
-                break;
-            case 'notdue':
-                return $this->clearOverdue();
-                break;
-            case 'unassined':
-                return $this->unassign();
-        }
-
+        switch (strtolower($state)) {
+        case 'open':
+            return $this->setStatus('open');
+        case 'closed':
+            return $this->setStatus('closed');
+        case 'answered':
+            return $this->setAnsweredState(1);
+        case 'unanswered':
+            return $this->setAnsweredState(0);
+        case 'overdue':
+            return $this->markOverdue();
+        case 'notdue':
+            return $this->clearOverdue();
+        case 'unassined':
+            return $this->unassign();
+        }
+        // FIXME: Throw and excception and add test cases
         return false;
     }
 
@@ -934,11 +1283,8 @@ class Ticket {
 
 
     function setAnsweredState($isanswered) {
-
-        $sql='UPDATE '.TICKET_TABLE.' SET isanswered='.db_input($isanswered)
-            .' WHERE ticket_id='.db_input($this->getId());
-
-        return (db_query($sql) && db_affected_rows());
+        $this->isanswered = $isanswered;
+        return $this->save();
     }
 
     function reopen() {
@@ -953,7 +1299,7 @@ class Ticket {
         if (!($status=$this->getStatus()->getReopenStatus()))
             $status = $cfg->getDefaultTicketStatusId();
 
-        return $status ? $this->setStatus($status, 'Reopened') : false;
+        return $status ? $this->setStatus($status) : false;
     }
 
     function onNewTicket($message, $autorespond=true, $alertstaff=true) {
@@ -961,86 +1307,107 @@ class Ticket {
 
         //Log stuff here...
 
-        if(!$autorespond && !$alertstaff) return true; //No alerts to send.
+        if (!$autorespond && !$alertstaff)
+            return true; //No alerts to send.
 
         /* ------ SEND OUT NEW TICKET AUTORESP && ALERTS ----------*/
 
-        $this->reload(); //get the new goodies.
         if(!$cfg
-                || !($dept=$this->getDept())
-                || !($tpl = $dept->getTemplate())
-                || !($email=$dept->getAutoRespEmail())) {
-                return false;  //bail out...missing stuff.
+            || !($dept=$this->getDept())
+            || !($tpl = $dept->getTemplate())
+            || !($email=$dept->getAutoRespEmail())
+        ) {
+            return false;  //bail out...missing stuff.
         }
 
-        $options = array(
-            'inreplyto'=>$message->getEmailMessageId(),
-            'references'=>$message->getEmailReferences(),
-            'thread'=>$message);
+        $options = array();
+        if (($message instanceof ThreadEntry)
+                && $message->getEmailMessageId()) {
+            $options += array(
+                'inreplyto'=>$message->getEmailMessageId(),
+                'references'=>$message->getEmailReferences(),
+                'thread'=>$message
+            );
+        }
+        else {
+            $options += array(
+                'thread' => $this->getThread(),
+            );
+        }
 
         //Send auto response - if enabled.
-        if($autorespond
-                && $cfg->autoRespONNewTicket()
-                && $dept->autoRespONNewTicket()
-                &&  ($msg=$tpl->getAutoRespMsgTemplate())) {
-
-            $msg = $this->replaceVars($msg->asArray(),
-                    array('message' => $message,
-                          'recipient' => $this->getOwner(),
-                          'signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')
-                    );
-
+        if ($autorespond
+            && $cfg->autoRespONNewTicket()
+            && $dept->autoRespONNewTicket()
+            && ($msg = $tpl->getAutoRespMsgTemplate())
+        ) {
+            $msg = $this->replaceVars(
+                $msg->asArray(),
+                array('message' => $message,
+                      'recipient' => $this->getOwner(),
+                      'signature' => ($dept && $dept->isPublic())?$dept->getSignature():''
+                )
+            );
             $email->sendAutoReply($this->getOwner(), $msg['subj'], $msg['body'],
                 null, $options);
         }
 
-        //Send alert to out sleepy & idle staff.
+        // Send alert to out sleepy & idle staff.
         if ($alertstaff
-                && $cfg->alertONNewTicket()
-                && ($email=$dept->getAlertEmail())
-                && ($msg=$tpl->getNewTicketAlertMsgTemplate())) {
-
+            && $cfg->alertONNewTicket()
+            && ($email=$dept->getAlertEmail())
+            && ($msg=$tpl->getNewTicketAlertMsgTemplate())
+        ) {
             $msg = $this->replaceVars($msg->asArray(), array('message' => $message));
-
-            $recipients=$sentlist=array();
-            //Exclude the auto responding email just incase it's from staff member.
-            if ($message->isAutoReply())
+            $recipients = $sentlist = array();
+            // Exclude the auto responding email just incase it's from staff member.
+            if ($message instanceof ThreadEntry && $message->isAutoReply())
                 $sentlist[] = $this->getEmail();
 
-            //Alert admin??
-            if($cfg->alertAdminONNewTicket()) {
-                $alert = $this->replaceVars($msg, array('recipient' => 'Admin'));
-                $email->sendAlert($cfg->getAdminEmail(), $alert['subj'], $alert['body'], null, $options);
-                $sentlist[]=$cfg->getAdminEmail();
+            // Only alerts dept members if the ticket is NOT assigned.
+            if ($cfg->alertDeptMembersONNewTicket() && !$this->isAssigned()) {
+                if (($members = $dept->getMembersForAlerts()))
+                    $recipients = array_merge($recipients, $members->all());
             }
 
-            //Only alerts dept members if the ticket is NOT assigned.
-            if($cfg->alertDeptMembersONNewTicket() && !$this->isAssigned()) {
-                if(($members=$dept->getMembersForAlerts()))
-                    $recipients=array_merge($recipients, $members);
+            if ($cfg->alertDeptManagerONNewTicket() && $dept &&
+                    ($manager=$dept->getManager())) {
+                $recipients[] = $manager;
             }
 
-            if($cfg->alertDeptManagerONNewTicket() && $dept && ($manager=$dept->getManager()))
-                $recipients[]= $manager;
-
             // Account manager
             if ($cfg->alertAcctManagerONNewMessage()
-                    && ($org = $this->getOwner()->getOrganization())
-                    && ($acct_manager = $org->getAccountManager())) {
+                && ($org = $this->getOwner()->getOrganization())
+                && ($acct_manager = $org->getAccountManager())
+            ) {
                 if ($acct_manager instanceof Team)
                     $recipients = array_merge($recipients, $acct_manager->getMembers());
                 else
                     $recipients[] = $acct_manager;
             }
 
-            foreach( $recipients as $k=>$staff) {
-                if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
+            foreach ($recipients as $k=>$staff) {
+                if (!is_object($staff)
+                    || !$staff->isAvailable()
+                    || in_array($staff->getEmail(), $sentlist)
+                ) {
+                    continue;
+                }
                 $alert = $this->replaceVars($msg, array('recipient' => $staff));
                 $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
                 $sentlist[] = $staff->getEmail();
             }
-        }
 
+            // Alert admin ONLY if not already a staff??
+            if ($cfg->alertAdminONNewTicket()
+                    && !in_array($cfg->getAdminEmail(), $sentlist)) {
+                $options += array('utype'=>'A');
+                $alert = $this->replaceVars($msg, array('recipient' => 'Admin'));
+                $email->sendAlert($cfg->getAdminEmail(), $alert['subj'],
+                        $alert['body'], null, $options);
+            }
+
+        }
         return true;
     }
 
@@ -1053,24 +1420,26 @@ class Ticket {
         $ost->logWarning(sprintf(_S('Maximum Open Tickets Limit (%s)'),$this->getEmail()),
             $msg);
 
-        if(!$sendNotice || !$cfg->sendOverLimitNotice())
+        if (!$sendNotice || !$cfg->sendOverLimitNotice())
             return true;
 
         //Send notice to user.
-        if(($dept = $this->getDept())
+        if (($dept = $this->getDept())
             && ($tpl=$dept->getTemplate())
             && ($msg=$tpl->getOverlimitMsgTemplate())
-            && ($email=$dept->getAutoRespEmail())) {
-
-            $msg = $this->replaceVars($msg->asArray(),
-                        array('signature' => ($dept && $dept->isPublic())?$dept->getSignature():''));
+            && ($email=$dept->getAutoRespEmail())
+        ) {
+            $msg = $this->replaceVars(
+                $msg->asArray(),
+                array('signature' => ($dept && $dept->isPublic())?$dept->getSignature():'')
+            );
 
             $email->sendAutoReply($this->getOwner(), $msg['subj'], $msg['body']);
         }
 
         $user = $this->getOwner();
 
-        //Alert admin...this might be spammy (no option to disable)...but it is helpful..I think.
+        // Alert admin...this might be spammy (no option to disable)...but it is helpful..I think.
         $alert=sprintf(__('Maximum open tickets reached for %s.'), $this->getEmail())."\n"
               .sprintf(__('Open tickets: %d'), $user->getNumOpenTickets())."\n"
               .sprintf(__('Max allowed: %d'), $cfg->getMaxOpenTickets())
@@ -1082,13 +1451,15 @@ class Ticket {
     }
 
     function onResponse($response, $options=array()) {
-        db_query('UPDATE '.TICKET_TABLE.' SET isanswered=1, lastresponse=NOW(), updated=NOW() WHERE ticket_id='.db_input($this->getId()));
-        $this->reload();
-        $vars = array_merge($options,
-                array(
-                    'activity' => _S('New Response'),
-                    'threadentry' => $response));
+        $this->isanswered = 1;
+        $this->save();
 
+        $vars = array_merge($options,
+            array(
+                'activity' => _S('New Response'),
+                'threadentry' => $response
+            )
+        );
         $this->onActivity($vars);
     }
 
@@ -1101,16 +1472,17 @@ class Ticket {
         global $cfg;
 
         if (!$entry instanceof ThreadEntry
-                || !($recipients=$this->getRecipients())
-                || !($dept=$this->getDept())
-                || !($tpl=$dept->getTemplate())
-                || !($msg=$tpl->getActivityNoticeMsgTemplate())
-                || !($email=$dept->getEmail()))
+            || !($recipients=$this->getRecipients())
+            || !($dept=$this->getDept())
+            || !($tpl=$dept->getTemplate())
+            || !($msg=$tpl->getActivityNoticeMsgTemplate())
+            || !($email=$dept->getEmail())
+        ) {
             return;
-
-        //Who posted the entry?
+        }
+        // Who posted the entry?
         $skip = array();
-        if ($entry instanceof Message) {
+        if ($entry instanceof MessageThreadEntry) {
             $poster = $entry->getUser();
             // Skip the person who sent in the message
             $skip[$entry->getUserId()] = 1;
@@ -1130,16 +1502,19 @@ class Ticket {
         }
 
         $vars = array_merge($vars, array(
-                    'message' => (string) $entry,
-                    'poster' => $poster ?: _S('A collaborator'),
-                    )
-                );
+            'message' => (string) $entry,
+            'poster' => $poster ?: _S('A collaborator'),
+            )
+        );
 
         $msg = $this->replaceVars($msg->asArray(), $vars);
 
         $attachments = $cfg->emailAttachments()?$entry->getAttachments():array();
-        $options = array('inreplyto' => $entry->getEmailMessageId(),
-                         'thread' => $entry);
+        $options = array('thread' => $entry);
+
+        if ($vars['from_name'])
+            $options += array('from_name' => $vars['from_name']);
+
         foreach ($recipients as $recipient) {
             // Skip folks who have already been included on this part of
             // the conversation
@@ -1149,14 +1524,14 @@ class Ticket {
             $email->send($recipient, $notice['subj'], $notice['body'], $attachments,
                 $options);
         }
-
-        return;
     }
 
     function onMessage($message, $autorespond=true) {
         global $cfg;
 
-        db_query('UPDATE '.TICKET_TABLE.' SET isanswered=0,lastmessage=NOW() WHERE ticket_id='.db_input($this->getId()));
+        $this->isanswered = 0;
+        $this->lastupdate = SqlFunction::NOW();
+        $this->save();
 
 
         // Reopen if closed AND reopenable
@@ -1165,59 +1540,72 @@ class Ticket {
         // confused with autorespond on new message setting
         if ($autorespond && $this->isClosed() && $this->isReopenable()) {
             $this->reopen();
-
-            // Auto-assign to closing staff or last respondent
-            // If the ticket is closed and auto-claim is not enabled then put the
-            // ticket back to unassigned pool.
-            if (!$cfg->autoClaimTickets()) {
-                $this->setStaffId(0);
-            }
-            elseif (!($staff = $this->getStaff()) || !$staff->isAvailable()) {
-                // Ticket has no assigned staff -  if auto-claim is enabled then
-                // try assigning it to the last respondent (if available)
-                // otherwise leave the ticket unassigned.
-                if (($lastrep = $this->getLastRespondent())
-                    && $lastrep->isAvailable()
-                ) {
-                    $this->setStaffId($lastrep->getId()); //direct assignment;
-                }
-                else {
-                    // unassign - last respondent is not available.
-                    $this->setStaffId(0);
-                }
-            }
+            // Auto-assign to closing staff or the last respondent if the
+            // agent is available and has access. Otherwise, put the ticket back
+            // to unassigned pool.
+            $dept = $this->getDept();
+            $staff = $this->getStaff() ?: $this->getLastRespondent();
+            $autoclaim = ($cfg->autoClaimTickets() && !$dept->disableAutoClaim());
+            if ($autoclaim
+                    && $staff
+                    // Is agent on vacation ?
+                    && $staff->isAvailable()
+                    // Does the agent have access to dept?
+                    && $staff->canAccessDept($dept->getId()))
+                $this->setStaffId($staff->getId());
+            else
+                $this->setStaffId(0); // Clear assignment
         }
 
-       /**********   double check auto-response  ************/
-        if (!($user = $message->getUser()))
-            $autorespond=false;
-        elseif ($autorespond && (Email::getIdByEmail($user->getEmail())))
-            $autorespond=false;
-        elseif ($autorespond && ($dept=$this->getDept()))
-            $autorespond=$dept->autoRespONNewMessage();
+        if (!$autorespond)
+            return;
+
+        // Figure out the user
+        if ($this->getOwnerId() == $message->getUserId())
+            $user = new TicketOwner(
+                    User::lookup($message->getUserId()), $this);
+        else
+            $user = Collaborator::lookup(array(
+                    'user_id' => $message->getUserId(),
+                    'thread_id' => $this->getThreadId()));
 
+        /**********   double check auto-response  ************/
+        if (!$user)
+            $autorespond = false;
+        elseif ((Email::getIdByEmail($user->getEmail())))
+            $autorespond = false;
+        elseif (($dept=$this->getDept()))
+            $autorespond = $dept->autoRespONNewMessage();
 
-        if(!$autorespond
-                || !$cfg->autoRespONNewMessage()
-                || !$message) return;  //no autoresp or alerts.
+        if (!$autorespond
+            || !$cfg->autoRespONNewMessage()
+            || !$message
+        ) {
+            return;  //no autoresp or alerts.
+        }
 
-        $this->reload();
         $dept = $this->getDept();
         $email = $dept->getAutoRespEmail();
 
-        //If enabled...send confirmation to user. ( New Message AutoResponse)
-        if($email
-                && ($tpl=$dept->getTemplate())
-                && ($msg=$tpl->getNewMessageAutorepMsgTemplate())) {
-
+        // If enabled...send confirmation to user. ( New Message AutoResponse)
+        if ($email
+            && ($tpl=$dept->getTemplate())
+            && ($msg=$tpl->getNewMessageAutorepMsgTemplate())
+        ) {
             $msg = $this->replaceVars($msg->asArray(),
-                            array(
-                                'recipient' => $user,
-                                'signature' => ($dept && $dept->isPublic())?$dept->getSignature():''));
+                array(
+                    'recipient' => $user,
+                    'signature' => ($dept && $dept->isPublic())?$dept->getSignature():''
+                )
+            );
+            $options = array('thread' => $message);
+            if ($message->getEmailMessageId()) {
+                $options += array(
+                        'inreplyto' => $message->getEmailMessageId(),
+                        'references' => $message->getEmailReferences()
+                        );
+            }
 
-            $options = array(
-                'inreplyto'=>$message->getEmailMessageId(),
-                'thread'=>$message);
             $email->sendAutoReply($user, $msg['subj'], $msg['body'],
                 null, $options);
         }
@@ -1229,15 +1617,17 @@ class Ticket {
         //TODO: do some shit
 
         if (!$alert // Check if alert is enabled
-                || !$cfg->alertONNewActivity()
-                || !($dept=$this->getDept())
-                || !($email=$cfg->getAlertEmail())
-                || !($tpl = $dept->getTemplate())
-                || !($msg=$tpl->getNoteAlertMsgTemplate()))
+            || !$cfg->alertONNewActivity()
+            || !($dept=$this->getDept())
+            || !($email=$cfg->getAlertEmail())
+            || !($tpl = $dept->getTemplate())
+            || !($msg=$tpl->getNoteAlertMsgTemplate())
+        ) {
             return;
+        }
 
         // Alert recipients
-        $recipients=array();
+        $recipients = array();
 
         //Last respondent.
         if ($cfg->alertLastRespondentONNewActivity())
@@ -1245,7 +1635,6 @@ class Ticket {
 
         // Assigned staff / team
         if ($cfg->alertAssignedONNewActivity()) {
-
             if (isset($vars['assignee'])
                     && $vars['assignee'] instanceof Staff)
                  $recipients[] = $vars['assignee'];
@@ -1263,10 +1652,7 @@ class Ticket {
         $options = array();
         $staffId = $thisstaff ? $thisstaff->getId() : 0;
         if ($vars['threadentry'] && $vars['threadentry'] instanceof ThreadEntry) {
-            $options = array(
-                'inreplyto' => $vars['threadentry']->getEmailMessageId(),
-                'references' => $vars['threadentry']->getEmailReferences(),
-                'thread' => $vars['threadentry']);
+            $options = array('thread' => $vars['threadentry']);
 
             // Activity details
             if (!$vars['comments'])
@@ -1286,52 +1672,66 @@ class Ticket {
         $sentlist=array();
         foreach ($recipients as $k=>$staff) {
             if (!is_object($staff)
-                    // Don't bother vacationing staff.
-                    || !$staff->isAvailable()
-                    // No need to alert the poster!
-                    || $staffId == $staff->getId()
-                    // No duplicates.
-                    || isset($sentlist[$staff->getEmail()])
-                    // Make sure staff has access to ticket
-                    || ($isClosed && !$this->checkStaffAccess($staff))
-                    )
+                // Don't bother vacationing staff.
+                || !$staff->isAvailable()
+                // No need to alert the poster!
+                || $staffId == $staff->getId()
+                // No duplicates.
+                || isset($sentlist[$staff->getEmail()])
+                // Make sure staff has access to ticket
+                || ($isClosed && !$this->checkStaffPerm($staff))
+            ) {
                 continue;
+            }
             $alert = $this->replaceVars($msg, array('recipient' => $staff));
             $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
             $sentlist[$staff->getEmail()] = 1;
         }
-
     }
 
     function onAssign($assignee, $comments, $alert=true) {
         global $cfg, $thisstaff;
 
-        if($this->isClosed()) $this->reopen(); //Assigned tickets must be open - otherwise why assign?
-
-        //Assignee must be an object of type Staff or Team
-        if(!$assignee || !is_object($assignee)) return false;
+        if ($this->isClosed())
+            $this->reopen(); //Assigned tickets must be open - otherwise why assign?
 
-        $this->reload();
+        // Assignee must be an object of type Staff or Team
+        if (!$assignee || !is_object($assignee))
+            return false;
 
+        $user_comments = (bool) $comments;
         $comments = $comments ?: _S('Ticket assignment');
         $assigner = $thisstaff ?: _S('SYSTEM (Auto Assignment)');
 
         //Log an internal note - no alerts on the internal note.
-        $note = $this->logNote(
-            sprintf(_S('Ticket Assigned to %s'), $assignee->getName()),
-            $comments, $assigner, false);
+        if ($user_comments) {
+            if ($assignee instanceof Staff
+                    && $thisstaff
+                    // self assignment
+                    && $assignee->getId() == $thisstaff->getId())
+                $title = sprintf(_S('Ticket claimed by %s'),
+                    $thisstaff->getName());
+            else
+                $title = sprintf(_S('Ticket Assigned to %s'),
+                        $assignee->getName());
+
+            $note = $this->logNote($title, $comments, $assigner, false);
+        }
 
-        //See if we need to send alerts
-        if(!$alert || !$cfg->alertONAssignment()) return true; //No alerts!
+        // See if we need to send alerts
+        if (!$alert || !$cfg->alertONAssignment())
+            return true; //No alerts!
 
         $dept = $this->getDept();
-        if(!$dept
-                || !($tpl = $dept->getTemplate())
-                || !($email = $dept->getAlertEmail()))
+        if (!$dept
+            || !($tpl = $dept->getTemplate())
+            || !($email = $dept->getAlertEmail())
+        ) {
             return true;
+        }
 
-        //recipients
-        $recipients=array();
+        // Recipients
+        $recipients = array();
         if ($assignee instanceof Staff) {
             if ($cfg->alertStaffONAssignment())
                 $recipients[] = $assignee;
@@ -1342,85 +1742,99 @@ class Ticket {
                 $recipients[] = $lead;
         }
 
-        //Get the message template
+        // Get the message template
         if ($recipients
-                && ($msg=$tpl->getAssignedAlertMsgTemplate())) {
-
+            && ($msg=$tpl->getAssignedAlertMsgTemplate())
+        ) {
             $msg = $this->replaceVars($msg->asArray(),
-                        array('comments' => $comments,
-                              'assignee' => $assignee,
-                              'assigner' => $assigner
-                              ));
-
-            //Send the alerts.
-            $sentlist=array();
-            $options = array(
-                'inreplyto'=>$note->getEmailMessageId(),
-                'references'=>$note->getEmailReferences(),
-                'thread'=>$note);
-            foreach( $recipients as $k=>$staff) {
-                if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
+                array('comments' => $comments,
+                      'assignee' => $assignee,
+                      'assigner' => $assigner
+                )
+            );
+            // Send the alerts.
+            $sentlist = array();
+            $options = $note instanceof ThreadEntry
+                ? array('thread'=>$note)
+                : array();
+            foreach ($recipients as $k=>$staff) {
+                if (!is_object($staff)
+                    || !$staff->isAvailable()
+                    || in_array($staff->getEmail(), $sentlist)
+                ) {
+                    continue;
+                }
                 $alert = $this->replaceVars($msg, array('recipient' => $staff));
                 $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
                 $sentlist[] = $staff->getEmail();
             }
         }
-
         return true;
     }
 
    function onOverdue($whine=true, $comments="") {
         global $cfg;
 
-        if($whine && ($sla=$this->getSLA()) && !$sla->alertOnOverdue())
+        if ($whine && ($sla = $this->getSLA()) && !$sla->alertOnOverdue())
             $whine = false;
 
-        //check if we need to send alerts.
-        if(!$whine
-                || !$cfg->alertONOverdueTicket()
-                || !($dept = $this->getDept()))
+        // Check if we need to send alerts.
+        if (!$whine
+            || !$cfg->alertONOverdueTicket()
+            || !($dept = $this->getDept())
+        ) {
             return true;
-
-        //Get the message template
-        if(($tpl = $dept->getTemplate())
-                && ($msg=$tpl->getOverdueAlertMsgTemplate())
-                && ($email = $dept->getAlertEmail())) {
-
+        }
+        // Get the message template
+        if (($tpl = $dept->getTemplate())
+            && ($msg=$tpl->getOverdueAlertMsgTemplate())
+            && ($email = $dept->getAlertEmail())
+        ) {
             $msg = $this->replaceVars($msg->asArray(),
-                array('comments' => $comments));
-
-            //recipients
-            $recipients=array();
-            //Assigned staff or team... if any
-            if($this->isAssigned() && $cfg->alertAssignedONOverdueTicket()) {
-                if($this->getStaffId())
+                array('comments' => $comments)
+            );
+            // Recipients
+            $recipients = array();
+            // Assigned staff or team... if any
+            if ($this->isAssigned() && $cfg->alertAssignedONOverdueTicket()) {
+                if ($this->getStaffId()) {
                     $recipients[]=$this->getStaff();
-                elseif($this->getTeamId() && ($team=$this->getTeam()) && ($members=$team->getMembers()))
+                }
+                elseif ($this->getTeamId()
+                    && ($team = $this->getTeam())
+                    && ($members = $team->getMembers())
+                ) {
                     $recipients=array_merge($recipients, $members);
-            } elseif($cfg->alertDeptMembersONOverdueTicket() && !$this->isAssigned()) {
-                //Only alerts dept members if the ticket is NOT assigned.
-                if ($members = $dept->getMembersForAlerts())
+                }
+            }
+            elseif ($cfg->alertDeptMembersONOverdueTicket() && !$this->isAssigned()) {
+                // Only alerts dept members if the ticket is NOT assigned.
+                if ($members = $dept->getMembersForAlerts()->all())
                     $recipients = array_merge($recipients, $members);
             }
-            //Always alert dept manager??
-            if($cfg->alertDeptManagerONOverdueTicket() && $dept && ($manager=$dept->getManager()))
+            // Always alert dept manager??
+            if ($cfg->alertDeptManagerONOverdueTicket()
+                && $dept && ($manager=$dept->getManager())
+            ) {
                 $recipients[]= $manager;
-
-            $sentlist=array();
-            foreach( $recipients as $k=>$staff) {
-                if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
+            }
+            $sentlist = array();
+            foreach ($recipients as $k=>$staff) {
+                if (!is_object($staff)
+                    || !$staff->isAvailable()
+                    || in_array($staff->getEmail(), $sentlist)
+                ) {
+                    continue;
+                }
                 $alert = $this->replaceVars($msg, array('recipient' => $staff));
                 $email->sendAlert($staff, $alert['subj'], $alert['body'], null);
                 $sentlist[] = $staff->getEmail();
             }
-
         }
-
         return true;
-
     }
 
-    //ticket obj as variable = ticket number.
+    // TemplateVariable interface
     function asVar() {
        return $this->getNumber();
     }
@@ -1428,65 +1842,100 @@ class Ticket {
     function getVar($tag) {
         global $cfg;
 
-        if($tag && is_callable(array($this, 'get'.ucfirst($tag))))
-            return call_user_func(array($this, 'get'.ucfirst($tag)));
-
         switch(mb_strtolower($tag)) {
-            case 'phone':
-            case 'phone_number':
-                return $this->getPhoneNumber();
-                break;
-            case 'auth_token':
-                return $this->getAuthToken();
-                break;
-            case 'client_link':
-                return sprintf('%s/view.php?t=%s',
-                        $cfg->getBaseUrl(), $this->getNumber());
-                break;
-            case 'staff_link':
-                return sprintf('%s/scp/tickets.php?id=%d', $cfg->getBaseUrl(), $this->getId());
-                break;
-            case 'create_date':
-                return Format::date(
-                        $cfg->getDateTimeFormat(),
-                        Misc::db2gmtime($this->getCreateDate()),
-                        $cfg->getTZOffset(),
-                        $cfg->observeDaylightSaving());
-                break;
-             case 'due_date':
-                $duedate ='';
-                if($this->getEstDueDate())
-                    $duedate = Format::date(
-                            $cfg->getDateTimeFormat(),
-                            Misc::db2gmtime($this->getEstDueDate()),
-                            $cfg->getTZOffset(),
-                            $cfg->observeDaylightSaving());
-
-                return $duedate;
-                break;
-            case 'close_date':
-                $closedate ='';
-                if($this->isClosed())
-                    $closedate = Format::date(
-                            $cfg->getDateTimeFormat(),
-                            Misc::db2gmtime($this->getCloseDate()),
-                            $cfg->getTZOffset(),
-                            $cfg->observeDaylightSaving());
-
-                return $closedate;
-                break;
-            case 'user':
-                return $this->getOwner();
-                break;
-            default:
-                if (isset($this->_answers[$tag]))
-                    // The answer object is retrieved here which will
-                    // automatically invoke the toString() method when the
-                    // answer is coerced into text
-                    return $this->_answers[$tag];
-        }
+        case 'phone':
+        case 'phone_number':
+            return $this->getPhoneNumber();
+            break;
+        case 'auth_token':
+            return $this->getOldAuthToken();
+            break;
+        case 'client_link':
+            return sprintf('%s/view.php?t=%s',
+                    $cfg->getBaseUrl(), $this->getNumber());
+            break;
+        case 'staff_link':
+            return sprintf('%s/scp/tickets.php?id=%d', $cfg->getBaseUrl(), $this->getId());
+            break;
+        case 'create_date':
+            return new FormattedDate($this->getCreateDate());
+            break;
+         case 'due_date':
+            if ($due = $this->getEstDueDate())
+                return new FormattedDate($due);
+            break;
+        case 'close_date':
+            if ($this->isClosed())
+                return new FormattedDate($this->getCloseDate());
+            break;
+        case 'last_update':
+            return new FormattedDate($this->last_update);
+        case 'user':
+            return $this->getOwner();
+        default:
+            if (isset($this->_answers[$tag]))
+                // The answer object is retrieved here which will
+                // automatically invoke the toString() method when the
+                // answer is coerced into text
+                return $this->_answers[$tag];
+        }
+    }
+
+    static function getVarScope() {
+        $base = array(
+            'assigned' => __('Assigned agent and/or team'),
+            'close_date' => array(
+                'class' => 'FormattedDate', 'desc' => __('Date Closed'),
+            ),
+            'create_date' => array(
+                'class' => 'FormattedDate', 'desc' => __('Date created'),
+            ),
+            'dept' => array(
+                'class' => 'Dept', 'desc' => __('Department'),
+            ),
+            'due_date' => array(
+                'class' => 'FormattedDate', 'desc' => __('Due Date'),
+            ),
+            'email' => __('Default email address of ticket owner'),
+            'name' => array(
+                'class' => 'PersonsName', 'desc' => __('Name of ticket owner'),
+            ),
+            'number' => __('Ticket number'),
+            'phone' => __('Phone number of ticket owner'),
+            'priority' => array(
+                'class' => 'Priority', 'desc' => __('Priority'),
+            ),
+            'recipients' => array(
+                'class' => 'UserList', 'desc' => __('List of all recipient names'),
+            ),
+            'source' => __('Source'),
+            'status' => array(
+                'class' => 'TicketStatus', 'desc' => __('Status'),
+            ),
+            'staff' => array(
+                'class' => 'Staff', 'desc' => __('Assigned/closing agent'),
+            ),
+            'subject' => 'Subject',
+            'team' => array(
+                'class' => 'Team', 'desc' => __('Assigned/closing team'),
+            ),
+            'thread' => array(
+                'class' => 'TicketThread', 'desc' => __('Ticket Thread'),
+            ),
+            'topic' => array(
+                'class' => 'Topic', 'desc' => __('Help topic'),
+            ),
+            // XXX: Isn't lastreponse and lastmessage more useful
+            'last_update' => array(
+                'class' => 'FormattedDate', 'desc' => __('Time of last update'),
+            ),
+            'user' => array(
+                'class' => 'User', 'desc' => __('Ticket owner'),
+            ),
+        );
 
-        return false;
+        $extra = VariableReplacer::compileFormScope(TicketForm::getInstance());
+        return $base + $extra;
     }
 
     //Replace base variables.
@@ -1494,7 +1943,6 @@ class Ticket {
         global $ost;
 
         $vars = array_merge($vars, array('ticket' => $this));
-
         return $ost->replaceTemplateVariables($input, $vars);
     }
 
@@ -1507,16 +1955,13 @@ class Ticket {
     }
 
     function markOverdue($whine=true) {
-
         global $cfg;
 
-        if($this->isOverdue())
+        if ($this->isOverdue())
             return true;
 
-        $sql='UPDATE '.TICKET_TABLE.' SET isoverdue=1, updated=NOW() '
-            .' WHERE ticket_id='.db_input($this->getId());
-
-        if(!db_query($sql) || !db_affected_rows())
+        $this->isoverdue = 1;
+        if (!$this->save())
             return false;
 
         $this->logEvent('overdue');
@@ -1525,103 +1970,131 @@ class Ticket {
         return true;
     }
 
-    function clearOverdue() {
-
-        if(!$this->isOverdue())
+    function clearOverdue($save=true) {
+        if (!$this->isOverdue())
             return true;
 
         //NOTE: Previously logged overdue event is NOT annuled.
 
-        $sql='UPDATE '.TICKET_TABLE.' SET isoverdue=0, updated=NOW() ';
-
-        //clear due date if it's in the past
-        if($this->getDueDate() && Misc::db2gmtime($this->getDueDate()) <= Misc::gmtime())
-            $sql.=', duedate=NULL';
+        $this->isoverdue = 0;
 
-        //Clear SLA if est. due date is in the past
-        if($this->getSLADueDate() && Misc::db2gmtime($this->getSLADueDate()) <= Misc::gmtime())
-            $sql.=', sla_id=0 ';
+        // clear due date if it's in the past
+        if ($this->getDueDate() && Misc::db2gmtime($this->getDueDate()) <= Misc::gmtime())
+            $this->duedate = null;
 
-        $sql.=' WHERE ticket_id='.db_input($this->getId());
+        // Clear SLA if est. due date is in the past
+        if ($this->getSLADueDate() && Misc::db2gmtime($this->getSLADueDate()) <= Misc::gmtime())
+            $this->sla = null;
 
-        return (db_query($sql) && db_affected_rows());
+        return $save ? $this->save() : true;
     }
 
-    //Dept Tranfer...with alert.. done by staff
-    function transfer($deptId, $comments, $alert = true) {
-
-        global $cfg, $thisstaff;
+    //Dept Transfer...with alert.. done by staff
+    function transfer(TransferForm $form, &$errors, $alert=true) {
+        global $thisstaff, $cfg;
 
-        if(!$thisstaff || !$thisstaff->canTransferTickets())
+        // Check if staff can do the transfer
+        if (!$this->checkStaffPerm($thisstaff, Ticket::PERM_TRANSFER))
             return false;
 
-        $currentDept = $this->getDeptName(); //Current department
+        $cdept = $this->getDept(); // Current department
+        $dept = $form->getDept(); // Target department
+        if (!$dept || !($dept instanceof Dept))
+            $errors['dept'] = __('Department selection required');
+        elseif ($dept->getid() == $this->getDeptId())
+            $errors['dept'] = sprintf(
+                    __('%s already in the department'), __('Ticket'));
+        else {
+            $this->dept_id = $dept->getId();
+
+            // Make sure the new department allows assignment to the
+            // currently assigned agent (if any)
+            if ($this->isAssigned()
+                && ($staff=$this->getStaff())
+                && $dept->assignMembersOnly()
+                && !$dept->isMember($staff)
+            ) {
+                $this->staff_id = 0;
+            }
+        }
 
-        if(!$deptId || !$this->setDeptId($deptId))
+        if ($errors || !$this->save(true))
             return false;
 
         // Reopen ticket if closed
-        if($this->isClosed()) $this->reopen();
-
-        $this->reload();
-        $dept = $this->getDept();
+        if ($this->isClosed())
+            $this->reopen();
 
         // Set SLA of the new department
-        if(!$this->getSLAId() || $this->getSLA()->isTransient())
-            $this->selectSLAId();
+        if (!$this->getSLAId() || $this->getSLA()->isTransient())
+            if (($slaId=$this->getDept()->getSLAId()))
+                $this->selectSLAId($slaId);
 
-        // Make sure the new department allows assignment to the
-        // currently assigned agent (if any)
-        if ($this->isAssigned()
-                && ($staff=$this->getStaff())
-                && $dept->assignMembersOnly()
-                && !$dept->isMember($staff)) {
-            $this->setStaffId(0);
-        }
+        // Log transfer event
+        $this->logEvent('transferred');
 
-        /*** log the transfer comments as internal note - with alerts disabled - ***/
-        $title=sprintf(_S('Ticket transferred from %1$s to %2$s'),
-            $currentDept, $this->getDeptName());
-        $comments=$comments?$comments:$title;
-        $note = $this->logNote($title, $comments, $thisstaff, false);
+        // Post internal note if any
+        $note = null;
+        $comments = $form->getField('comments')->getClean();
+        if ($comments) {
+            $title = sprintf(__('%1$s transferred from %2$s to %3$s'),
+                    __('Ticket'),
+                   $cdept->getName(),
+                    $dept->getName());
 
-        $this->logEvent('transferred');
+            $_errors = array();
+            $note = $this->postNote(
+                    array('note' => $comments, 'title' => $title),
+                    $_errors, $thisstaff, false);
+        }
 
         //Send out alerts if enabled AND requested
         if (!$alert || !$cfg->alertONTransfer())
             return true; //no alerts!!
 
          if (($email = $dept->getAlertEmail())
-                     && ($tpl = $dept->getTemplate())
-                     && ($msg=$tpl->getTransferAlertMsgTemplate())) {
-
+             && ($tpl = $dept->getTemplate())
+             && ($msg=$tpl->getTransferAlertMsgTemplate())
+         ) {
             $msg = $this->replaceVars($msg->asArray(),
-                array('comments' => $comments, 'staff' => $thisstaff));
-            //recipients
-            $recipients=array();
-            //Assigned staff or team... if any
+                array('comments' => $note, 'staff' => $thisstaff));
+            // Recipients
+            $recipients = array();
+            // Assigned staff or team... if any
             if($this->isAssigned() && $cfg->alertAssignedONTransfer()) {
                 if($this->getStaffId())
-                    $recipients[]=$this->getStaff();
-                elseif($this->getTeamId() && ($team=$this->getTeam()) && ($members=$team->getMembers()))
+                    $recipients[] = $this->getStaff();
+                elseif ($this->getTeamId()
+                    && ($team=$this->getTeam())
+                    && ($members=$team->getMembers())
+                ) {
                     $recipients = array_merge($recipients, $members);
-            } elseif($cfg->alertDeptMembersONTransfer() && !$this->isAssigned()) {
-                //Only alerts dept members if the ticket is NOT assigned.
-                if(($members=$dept->getMembersForAlerts()))
+                }
+            }
+            elseif ($cfg->alertDeptMembersONTransfer() && !$this->isAssigned()) {
+                // Only alerts dept members if the ticket is NOT assigned.
+                if ($members = $dept->getMembersForAlerts()->all())
                     $recipients = array_merge($recipients, $members);
             }
 
-            //Always alert dept manager??
-            if($cfg->alertDeptManagerONTransfer() && $dept && ($manager=$dept->getManager()))
-                $recipients[]= $manager;
-
-            $sentlist=array();
-            $options = array(
-                'inreplyto'=>$note->getEmailMessageId(),
-                'references'=>$note->getEmailReferences(),
-                'thread'=>$note);
-            foreach( $recipients as $k=>$staff) {
-                if(!is_object($staff) || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
+            // Always alert dept manager??
+            if ($cfg->alertDeptManagerONTransfer()
+                && $dept
+                && ($manager=$dept->getManager())
+            ) {
+                $recipients[] = $manager;
+            }
+            $sentlist = $options = array();
+            if ($note) {
+                $options += array('thread'=>$note);
+            }
+            foreach ($recipients as $k=>$staff) {
+                if (!is_object($staff)
+                    || !$staff->isAvailable()
+                    || in_array($staff->getEmail(), $sentlist)
+                ) {
+                    continue;
+                }
                 $alert = $this->replaceVars($msg, array('recipient' => $staff));
                 $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
                 $sentlist[] = $staff->getEmail();
@@ -1631,38 +2104,52 @@ class Ticket {
          return true;
     }
 
-    function claim() {
+    function claim(ClaimForm $form, &$errors) {
         global $thisstaff;
 
-        if (!$thisstaff || !$this->isOpen() || $this->isAssigned())
-            return false;
-
         $dept = $this->getDept();
-        if ($dept->assignMembersOnly() && !$dept->isMember($thisstaff))
-            return false;
+        $assignee = $form->getAssignee();
+        if (!($assignee instanceof Staff)
+                || !$thisstaff
+                || $thisstaff->getId() != $assignee->getId()) {
+            $errors['err'] = __('Unknown assignee');
+        } elseif (!$assignee->isAvailable()) {
+            $errors['err'] = __('Agent is unavailable for assignment');
+        } elseif ($dept->assignMembersOnly() && !$dept->isMember($assignee)) {
+            $errors['err'] = __('Permission denied');
+        }
 
-        $comments = sprintf(_S('Ticket claimed by %s'), $thisstaff->getName());
+        if ($errors)
+            return false;
 
-        return $this->assignToStaff($thisstaff->getId(), $comments, false);
+        return $this->assignToStaff($assignee, $form->getComments(), false);
     }
 
     function assignToStaff($staff, $note, $alert=true) {
 
-        if(!is_object($staff) && !($staff=Staff::lookup($staff)))
+        if(!is_object($staff) && !($staff = Staff::lookup($staff)))
             return false;
 
         if (!$staff->isAvailable() || !$this->setStaffId($staff->getId()))
             return false;
 
         $this->onAssign($staff, $note, $alert);
-        $this->logEvent('assigned');
+
+        global $thisstaff;
+        $data = array();
+        if ($thisstaff && $staff->getId() == $thisstaff->getId())
+            $data['claim'] = true;
+        else
+            $data['staff'] = $staff->getId();
+
+        $this->logEvent('assigned', $data);
 
         return true;
     }
 
     function assignToTeam($team, $note, $alert=true) {
 
-        if(!is_object($team) && !($team=Team::lookup($team)))
+        if(!is_object($team) && !($team = Team::lookup($team)))
             return false;
 
         if (!$team->isActive() || !$this->setTeamId($team->getId()))
@@ -1670,52 +2157,82 @@ class Ticket {
 
         //Clear - staff if it's a closed ticket
         //  staff_id is overloaded -> assigned to & closed by.
-        if($this->isClosed())
+        if ($this->isClosed())
             $this->setStaffId(0);
 
         $this->onAssign($team, $note, $alert);
-        $this->logEvent('assigned');
+        $this->logEvent('assigned', array('team' => $team->getId()));
 
         return true;
     }
 
-    //Assign ticket to staff or team - overloaded ID.
-    function assign($assignId, $note, $alert=true) {
+    function assign(AssignmentForm $form, &$errors, $alert=true) {
         global $thisstaff;
 
-        $rv=0;
-        $id=preg_replace("/[^0-9]/", "", $assignId);
-        if($assignId[0]=='t') {
-            $rv=$this->assignToTeam($id, $note, $alert);
-        } elseif($assignId[0]=='s' || is_numeric($assignId)) {
-            $alert=($alert && $thisstaff && $thisstaff->getId()==$id)?false:$alert; //No alerts on self assigned tickets!!!
-            //We don't care if a team is already assigned to the ticket - staff assignment takes precedence
-            $rv=$this->assignToStaff($id, $note, $alert);
+        $evd = array();
+        $assignee = $form->getAssignee();
+        if ($assignee instanceof Staff) {
+            $dept = $this->getDept();
+            if ($this->getStaffId() == $assignee->getId()) {
+                $errors['assignee'] = sprintf(__('%s already assigned to %s'),
+                        __('Ticket'),
+                        __('the agent')
+                        );
+            } elseif(!$assignee->isAvailable()) {
+                $errors['assignee'] = __('Agent is unavailable for assignment');
+            } elseif ($dept->assignMembersOnly() && !$dept->isMember($assignee)) {
+                $errors['err'] = __('Permission denied');
+            } else {
+                $this->staff_id = $assignee->getId();
+                if ($thisstaff && $thisstaff->getId() == $assignee->getId()) {
+                    $alert = false;
+                    $evd['claim'] = true;
+                } else {
+                    $evd['staff'] = array($assignee->getId(), (string) $assignee->getName()->getOriginal());
+                }
+            }
+        } elseif ($assignee instanceof Team) {
+            if ($this->getTeamId() == $assignee->getId()) {
+                $errors['assignee'] = sprintf(__('%s already assigned to %s'),
+                        __('Ticket'),
+                        __('the team')
+                        );
+            } else {
+                $this->team_id = $assignee->getId();
+                $evd = array('team' => $assignee->getId());
+            }
+        } else {
+            $errors['assignee'] = __('Unknown assignee');
         }
 
-        return $rv;
+        if ($errors || !$this->save(true))
+            return false;
+
+        $this->logEvent('assigned', $evd);
+
+        $this->onAssign($assignee, $form->getComments(), $alert);
+
+        return true;
     }
 
-    //unassign primary assignee
+    // Unassign primary assignee
     function unassign() {
-
-        if(!$this->isAssigned()) //We can't release what is not assigned buddy!
+        // We can't release what is not assigned buddy!
+        if (!$this->isAssigned())
             return true;
 
-        //We can only unassigned OPEN tickets.
-        if($this->isClosed())
+        // We can only unassigned OPEN tickets.
+        if ($this->isClosed())
             return false;
 
-        //Unassign staff (if any)
-        if($this->getStaffId() && !$this->setStaffId(0))
+        // Unassign staff (if any)
+        if ($this->getStaffId() && !$this->setStaffId(0))
             return false;
 
-        //unassign team (if any)
-        if($this->getTeamId() && !$this->setTeamId(0))
+        // Unassign team (if any)
+        if ($this->getTeamId() && !$this->setTeamId(0))
             return false;
 
-        $this->reload();
-
         return true;
     }
 
@@ -1728,62 +2245,61 @@ class Ticket {
         global $thisstaff;
 
         if (!$user
-                || ($user->getId() == $this->getOwnerId())
-                || !$thisstaff->canEditTickets())
+            || ($user->getId() == $this->getOwnerId())
+            || !($this->checkStaffPerm($thisstaff,
+                TicketModel::PERM_EDIT))
+        ) {
             return false;
+        }
 
-        $sql ='UPDATE '.TICKET_TABLE.' SET updated = NOW() '
-            .', user_id = '.db_input($user->getId())
-            .' WHERE ticket_id = '.db_input($this->getId());
-
-        if (!db_query($sql) || !db_affected_rows())
+        $this->user_id = $user->getId();
+        if (!$this->save())
             return false;
 
-        $this->ht['user_id'] = $user->getId();
-        $this->user = null;
+        unset($this->user);
         $this->collaborators = null;
         $this->recipients = null;
 
-        //Log an internal note
-        $note = sprintf(_S('%s changed ticket ownership to %s'),
-                $thisstaff->getName(), $user->getName());
-
-        //Remove the new owner from list of collaborators
-        $c = Collaborator::lookup(array('userId' => $user->getId(),
-                    'ticketId' => $this->getId()));
-        if ($c && $c->remove())
-            $note.= ' '._S('(removed as collaborator)');
+        // Remove the new owner from list of collaborators
+        $c = Collaborator::lookup(array(
+            'user_id' => $user->getId(),
+            'thread_id' => $this->getThreadId()
+        ));
+        if ($c)
+            $c->delete();
 
-        $this->logNote('Ticket ownership changed', $note);
+        $this->logEvent('edited', array('owner' => $user->getId()));
 
         return true;
     }
 
-    //Insert message from client
+    // Insert message from client
     function postMessage($vars, $origin='', $alerts=true) {
         global $cfg;
 
-        $vars['origin'] = $origin;
-        if(isset($vars['ip']))
+        if ($origin)
+            $vars['origin'] = $origin;
+        if (isset($vars['ip']))
             $vars['ip_address'] = $vars['ip'];
-        elseif(!$vars['ip_address'] && $_SERVER['REMOTE_ADDR'])
+        elseif (!$vars['ip_address'] && $_SERVER['REMOTE_ADDR'])
             $vars['ip_address'] = $_SERVER['REMOTE_ADDR'];
 
         $errors = array();
-        if(!($message = $this->getThread()->addMessage($vars, $errors)))
+        if (!($message = $this->getThread()->addMessage($vars, $errors)))
             return null;
 
         $this->setLastMessage($message);
 
-        //Add email recipients as collaborators...
+        // Add email recipients as collaborators...
         if ($vars['recipients']
-                && (strtolower($origin) != 'email' || ($cfg && $cfg->addCollabsViaEmail()))
-                //Only add if we have a matched local address
-                && $vars['to-email-id']) {
+            && (strtolower($origin) != 'email' || ($cfg && $cfg->addCollabsViaEmail()))
+            //Only add if we have a matched local address
+            && $vars['to-email-id']
+        ) {
             //New collaborators added by other collaborators are disable --
             // requires staff approval.
             $info = array(
-                    'isactive' => ($message->getUserId() == $this->getUserId())? 1: 0);
+                'isactive' => ($message->getUserId() == $this->getUserId())? 1: 0);
             $collabs = array();
             foreach ($vars['recipients'] as $recipient) {
                 // Skip virtual delivered-to addresses
@@ -1791,62 +2307,54 @@ class Ticket {
                     continue;
 
                 if (($user=User::fromVars($recipient)))
-                    if ($c=$this->addCollaborator($user, $info, $errors))
-                        $collabs[] = sprintf('%s%s',
-                            (string) $c,
-                            $recipient['source']
-                                ? " ".sprintf(_S('via %s'), $recipient['source'])
-                                : ''
-                            );
+                    if ($c=$this->addCollaborator($user, $info, $errors, false))
+                        // FIXME: This feels very unwise — should be a
+                        // string indexed array for future
+                        $collabs[$c->user_id] = array(
+                            'name' => $c->getName()->getOriginal(),
+                            'src' => $recipient['source'],
+                        );
             }
-            //TODO: Can collaborators add others?
+            // TODO: Can collaborators add others?
             if ($collabs) {
-                //TODO: Change EndUser to name of  user.
-                $this->logNote(_S('Collaborators added by end user'),
-                        implode("<br>", $collabs), _S('End User'), false);
+                $this->logEvent('collab', array('add' => $collabs), $message->user);
             }
         }
 
         // Do not auto-respond to bounces and other auto-replies
-        if ($alerts)
-            $alerts = isset($vars['flags'])
-                ? !$vars['flags']['bounce'] && !$vars['flags']['auto-reply']
+        $autorespond = isset($vars['mailflags'])
+                ? !$vars['mailflags']['bounce'] && !$vars['mailflags']['auto-reply']
                 : true;
-        if ($alerts && $message->isBounceOrAutoReply())
-            $alerts = false;
+        if ($autorespond && $message->isBounceOrAutoReply())
+            $autorespond = false;
 
-        $this->onMessage($message, $alerts); //must be called b4 sending alerts to staff.
+        $this->onMessage($message, ($autorespond && $alerts)); //must be called b4 sending alerts to staff.
 
-        if ($alerts && $cfg && $cfg->notifyCollabsONNewMessage())
+        if ($autorespond && $alerts && $cfg && $cfg->notifyCollabsONNewMessage())
             $this->notifyCollaborators($message, array('signature' => ''));
 
-        if (!$alerts)
+        if (!($alerts && $autorespond))
             return $message; //Our work is done...
 
         $dept = $this->getDept();
-
-
         $variables = array(
-                'message' => $message,
-                'poster' => ($vars['poster'] ? $vars['poster'] : $this->getName())
-                );
-        $options = array(
-                'inreplyto' => $message->getEmailMessageId(),
-                'references' => $message->getEmailReferences(),
-                'thread'=>$message);
-        //If enabled...send alert to staff (New Message Alert)
-        if($cfg->alertONNewMessage()
-                && ($email = $dept->getAlertEmail())
-                && ($tpl = $dept->getTemplate())
-                && ($msg = $tpl->getNewMessageAlertMsgTemplate())) {
+            'message' => $message,
+            'poster' => ($vars['poster'] ? $vars['poster'] : $this->getName())
+        );
 
+        $options = array('thread'=>$message);
+        // If enabled...send alert to staff (New Message Alert)
+        if ($cfg->alertONNewMessage()
+            && ($email = $dept->getAlertEmail())
+            && ($tpl = $dept->getTemplate())
+            && ($msg = $tpl->getNewMessageAlertMsgTemplate())
+        ) {
             $msg = $this->replaceVars($msg->asArray(), $variables);
-
-            //Build list of recipients and fire the alerts.
-            $recipients=array();
+            // Build list of recipients and fire the alerts.
+            $recipients = array();
             //Last respondent.
-            if($cfg->alertLastRespondentONNewMessage() || $cfg->alertAssignedONNewMessage())
-                $recipients[]=$this->getLastRespondent();
+            if ($cfg->alertLastRespondentONNewMessage() && ($lr = $this->getLastRespondent()))
+                $recipients[] = $lr;
 
             //Assigned staff if any...could be the last respondent
             if ($cfg->alertAssignedONNewMessage() && $this->isAssigned()) {
@@ -1856,9 +2364,13 @@ class Ticket {
                     $recipients = array_merge($recipients, $team->getMembers());
             }
 
-            //Dept manager
-            if($cfg->alertDeptManagerONNewMessage() && $dept && ($manager=$dept->getManager()))
+            // Dept manager
+            if ($cfg->alertDeptManagerONNewMessage()
+                && $dept
+                && ($manager = $dept->getManager())
+            ) {
                 $recipients[]=$manager;
+            }
 
             // Account manager
             if ($cfg->alertAcctManagerONNewMessage()
@@ -1870,75 +2382,91 @@ class Ticket {
                     $recipients[] = $acct_manager;
             }
 
-            $sentlist=array(); //I know it sucks...but..it works.
-            foreach( $recipients as $k=>$staff) {
-                if(!$staff || !$staff->getEmail() || !$staff->isAvailable() || in_array($staff->getEmail(), $sentlist)) continue;
+            $sentlist = array(); //I know it sucks...but..it works.
+            foreach ($recipients as $k=>$staff) {
+                if (!$staff || !$staff->getEmail()
+                    || !$staff->isAvailable()
+                    || in_array($staff->getEmail(), $sentlist)
+                ) {
+                    continue;
+                }
                 $alert = $this->replaceVars($msg, array('recipient' => $staff));
                 $email->sendAlert($staff, $alert['subj'], $alert['body'], null, $options);
                 $sentlist[] = $staff->getEmail();
             }
         }
-
         return $message;
     }
 
-    function postCannedReply($canned, $msgId, $alert=true) {
+    function postCannedReply($canned, $message, $alert=true) {
         global $ost, $cfg;
 
-        if((!is_object($canned) && !($canned=Canned::lookup($canned))) || !$canned->isEnabled())
+        if ((!is_object($canned) && !($canned=Canned::lookup($canned)))
+            || !$canned->isEnabled()
+        ) {
             return false;
-
+        }
         $files = array();
-        foreach ($canned->attachments->getAll() as $file)
-            $files[] = $file['id'];
+        foreach ($canned->attachments->getAll() as $file) {
+            $files[] = $file->file_id;
+            $_SESSION[':cannedFiles'][$file->file_id] = 1;
+        }
 
-        if ($cfg->isHtmlThreadEnabled())
-            $response = new HtmlThreadBody(
-                    $this->replaceVars($canned->getHtml()));
+        if ($cfg->isRichTextEnabled())
+            $response = new HtmlThreadEntryBody(
+                $this->replaceVars($canned->getHtml()));
         else
-            $response = new TextThreadBody(
-                    $this->replaceVars($canned->getPlainText()));
+            $response = new TextThreadEntryBody(
+                $this->replaceVars($canned->getPlainText()));
 
-        $info = array('msgId' => $msgId,
+        $info = array('msgId' => $message instanceof ThreadEntry ? $message->getId() : 0,
                       'poster' => __('SYSTEM (Canned Reply)'),
                       'response' => $response,
-                      'cannedattachments' => $files);
-
+                      'cannedattachments' => $files
+        );
         $errors = array();
         if (!($response=$this->postReply($info, $errors, false, false)))
             return null;
 
         $this->markUnAnswered();
 
-        if(!$alert) return $response;
+        if (!$alert)
+            return $response;
 
         $dept = $this->getDept();
 
-        if(($email=$dept->getEmail())
-                && ($tpl = $dept->getTemplate())
-                && ($msg=$tpl->getAutoReplyMsgTemplate())) {
-
-            if($dept && $dept->isPublic())
+        if (($email=$dept->getEmail())
+            && ($tpl = $dept->getTemplate())
+            && ($msg=$tpl->getAutoReplyMsgTemplate())
+        ) {
+            if ($dept && $dept->isPublic())
                 $signature=$dept->getSignature();
             else
                 $signature='';
 
             $msg = $this->replaceVars($msg->asArray(),
-                    array(
-                        'response' => $response,
-                        'signature' => $signature,
-                        'recipient' => $this->getOwner(),
-                        ));
+                array(
+                    'response' => $response,
+                    'signature' => $signature,
+                    'recipient' => $this->getOwner(),
+                )
+            );
+            $attachments = ($cfg->emailAttachments() && $files)
+                ? $response->getAttachments() : array();
+
+            $options = array('thread' => $response);
+            if (($message instanceof ThreadEntry)
+                    && $message->getUserId() == $this->getUserId()
+                    && ($mid=$message->getEmailMessageId())) {
+                $options += array(
+                        'inreplyto' => $mid,
+                        'references' => $message->getEmailReferences()
+                        );
+            }
 
-            $attachments =($cfg->emailAttachments() && $files)?$response->getAttachments():array();
-            $options = array(
-                'inreplyto'=>$response->getEmailMessageId(),
-                'references'=>$response->getEmailReferences(),
-                'thread'=>$response);
-            $email->sendAutoReply($this, $msg['subj'], $msg['body'], $attachments,
+            $email->sendAutoReply($this->getOwner(), $msg['subj'], $msg['body'], $attachments,
                 $options);
         }
-
         return $response;
     }
 
@@ -1949,68 +2477,96 @@ class Ticket {
         if (!$vars['poster'] && $thisstaff)
             $vars['poster'] = $thisstaff;
 
-        if(!$vars['staffId'] && $thisstaff)
+        if (!$vars['staffId'] && $thisstaff)
             $vars['staffId'] = $thisstaff->getId();
 
         if (!$vars['ip_address'] && $_SERVER['REMOTE_ADDR'])
             $vars['ip_address'] = $_SERVER['REMOTE_ADDR'];
 
-        if(!($response = $this->getThread()->addResponse($vars, $errors)))
+        if (!($response = $this->getThread()->addResponse($vars, $errors)))
             return null;
 
+        $dept = $this->getDept();
         $assignee = $this->getStaff();
         // Set status - if checked.
         if ($vars['reply_status_id']
-                && $vars['reply_status_id'] != $this->getStatusId())
+            && $vars['reply_status_id'] != $this->getStatusId()
+        ) {
             $this->setStatus($vars['reply_status_id']);
+        }
+
 
         // Claim on response bypasses the department assignment restrictions
-        if ($claim && $thisstaff && $this->isOpen() && !$this->getStaffId()
-            && $cfg->autoClaimTickets()
-        ) {
+        $claim = ($claim
+                && $cfg->autoClaimTickets()
+                && !$dept->disableAutoClaim());
+        if ($claim && $thisstaff && $this->isOpen() && !$this->getStaffId()) {
             $this->setStaffId($thisstaff->getId()); //direct assignment;
         }
 
+        $this->lastrespondent = $response->staff;
+
         $this->onResponse($response, array('assignee' => $assignee)); //do house cleaning..
 
         /* email the user??  - if disabled - then bail out */
-        if (!$alert) return $response;
-
-        $dept = $this->getDept();
+        if (!$alert)
+            return $response;
 
-        if($thisstaff && $vars['signature']=='mine')
+        $email = $dept->getEmail();
+        $options = array('thread'=>$response);
+        $signature = $from_name = '';
+        if ($thisstaff && $vars['signature']=='mine')
             $signature=$thisstaff->getSignature();
-        elseif($vars['signature']=='dept' && $dept && $dept->isPublic())
+        elseif ($vars['signature']=='dept' && $dept->isPublic())
             $signature=$dept->getSignature();
-        else
-            $signature='';
+
+        if ($thisstaff && ($type=$thisstaff->getReplyFromNameType())) {
+            switch ($type) {
+                case 'mine':
+                    if (!$cfg->hideStaffName())
+                        $from_name = (string) $thisstaff->getName();
+                    break;
+                case 'dept':
+                    if ($dept->isPublic())
+                        $from_name = $dept->getName();
+                    break;
+                case 'email':
+                default:
+                    $from_name =  $email->getName();
+            }
+
+            if ($from_name)
+                $options += array('from_name' => $from_name);
+
+        }
 
         $variables = array(
-                'response' => $response,
-                'signature' => $signature,
-                'staff' => $thisstaff,
-                'poster' => $thisstaff);
-        $options = array(
-                'inreplyto' => $response->getEmailMessageId(),
-                'references' => $response->getEmailReferences(),
-                'thread'=>$response);
-
-        if(($email=$dept->getEmail())
-                && ($tpl = $dept->getTemplate())
-                && ($msg=$tpl->getReplyMsgTemplate())) {
+            'response' => $response,
+            'signature' => $signature,
+            'staff' => $thisstaff,
+            'poster' => $thisstaff
+        );
 
+        $user = $this->getOwner();
+        if (($email=$dept->getEmail())
+            && ($tpl = $dept->getTemplate())
+            && ($msg=$tpl->getReplyMsgTemplate())
+        ) {
             $msg = $this->replaceVars($msg->asArray(),
-                    $variables + array('recipient' => $this->getOwner()));
-
+                $variables + array('recipient' => $user)
+            );
             $attachments = $cfg->emailAttachments()?$response->getAttachments():array();
-            $email->send($this->getOwner(), $msg['subj'], $msg['body'], $attachments,
+            $email->send($user, $msg['subj'], $msg['body'], $attachments,
                 $options);
         }
 
-        if($vars['emailcollab'])
+        if ($vars['emailcollab']) {
             $this->notifyCollaborators($response,
-                    array('signature' => $signature));
-
+                array(
+                    'signature' => $signature,
+                    'from_name' => $from_name)
+            );
+        }
         return $response;
     }
 
@@ -2020,74 +2576,56 @@ class Ticket {
     }
 
     // History log -- used for statistics generation (pretty reports)
-    function logEvent($state, $annul=null, $staff=null) {
-        global $thisstaff;
-
-        if ($staff === null) {
-            if ($thisstaff) $staff=$thisstaff->getUserName();
-            else $staff='SYSTEM';               # XXX: Security Violation ?
-        }
-        # Annul previous entries if requested (for instance, reopening a
-        # ticket will annul an 'closed' entry). This will be useful to
-        # easily prevent repeated statistics.
-        if ($annul) {
-            db_query('UPDATE '.TICKET_EVENT_TABLE.' SET annulled=1'
-                .' WHERE ticket_id='.db_input($this->getId())
-                  .' AND state='.db_input($annul));
-        }
-
-        return db_query('INSERT INTO '.TICKET_EVENT_TABLE
-            .' SET ticket_id='.db_input($this->getId())
-            .', staff_id='.db_input($this->getStaffId())
-            .', team_id='.db_input($this->getTeamId())
-            .', dept_id='.db_input($this->getDeptId())
-            .', topic_id='.db_input($this->getTopicId())
-            .', timestamp=NOW(), state='.db_input($state)
-            .', staff='.db_input($staff))
-            && db_affected_rows() == 1;
+    function logEvent($state, $data=null, $user=null, $annul=null) {
+        $this->getThread()->getEvents()->log($this, $state, $data, $user, $annul);
     }
 
     //Insert Internal Notes
     function logNote($title, $note, $poster='SYSTEM', $alert=true) {
-
-        $errors = array();
-        //Unless specified otherwise, assume HTML
+        // Unless specified otherwise, assume HTML
         if ($note && is_string($note))
-            $note = new HtmlThreadBody($note);
+            $note = new HtmlThreadEntryBody($note);
 
+        $errors = array();
         return $this->postNote(
-                array(
-                    'title' => $title,
-                    'note' => $note,
-                ),
-                $errors,
-                $poster,
-                $alert
+            array(
+                'title' => $title,
+                'note' => $note,
+            ),
+            $errors,
+            $poster,
+            $alert
         );
     }
 
-    function postNote($vars, &$errors, $poster, $alert=true) {
+    function postNote($vars, &$errors, $poster=false, $alert=true) {
         global $cfg, $thisstaff;
 
         //Who is posting the note - staff or system?
-        $vars['staffId'] = 0;
-        $vars['poster'] = 'SYSTEM';
-        if($poster && is_object($poster)) {
+        if ($vars['staffId'] && !$poster)
+            $poster = Staff::lookup($vars['staffId']);
+
+        $vars['staffId'] = $vars['staffId'] ?: 0;
+        if ($poster && is_object($poster)) {
             $vars['staffId'] = $poster->getId();
             $vars['poster'] = $poster->getName();
-        }elseif($poster) { //string
+        }
+        elseif ($poster) { //string
             $vars['poster'] = $poster;
         }
+        elseif (!isset($vars['poster'])) {
+            $vars['poster'] = 'SYSTEM';
+        }
         if (!$vars['ip_address'] && $_SERVER['REMOTE_ADDR'])
             $vars['ip_address'] = $_SERVER['REMOTE_ADDR'];
 
-        if(!($note=$this->getThread()->addNote($vars, $errors)))
+        if (!($note=$this->getThread()->addNote($vars, $errors)))
             return null;
 
         $alert = $alert && (
-            isset($vars['flags'])
+            isset($vars['mailflags'])
             // No alerts for bounce and auto-reply emails
-            ? !$vars['flags']['bounce'] && !$vars['flags']['auto-reply']
+            ? !$vars['mailflags']['bounce'] && !$vars['mailflags']['auto-reply']
             : true
         );
 
@@ -2095,9 +2633,9 @@ class Ticket {
         $assignee = $this->getStaff();
 
         if ($vars['note_status_id']
-                && ($status=TicketStatus::lookup($vars['note_status_id']))) {
-            if ($this->setStatus($status))
-                $this->reload();
+            && ($status=TicketStatus::lookup($vars['note_status_id']))
+        ) {
+            $this->setStatus($status);
         }
 
         $activity = $vars['activity'] ?: _S('New Internal Note');
@@ -2110,7 +2648,20 @@ class Ticket {
         return $note;
     }
 
-    //Print ticket... export the ticket thread as PDF.
+    // Threadable interface
+    function postThreadEntry($type, $vars, $options=array()) {
+        $errors = array();
+        switch ($type) {
+        case 'M':
+            return $this->postMessage($vars, $vars['origin']);
+        case 'N':
+            return $this->postNote($vars, $errors);
+        case 'R':
+            return $this->postReply($vars, $errors);
+        }
+    }
+
+    // Print ticket... export the ticket thread as PDF.
     function pdfExport($psize='Letter', $notes=false) {
         global $thisstaff;
 
@@ -2123,8 +2674,8 @@ class Ticket {
         }
 
         $pdf = new Ticket2PDF($this, $psize, $notes);
-        $name='Ticket-'.$this->getNumber().'.pdf';
-        $pdf->Output($name, 'I');
+        $name = 'Ticket-'.$this->getNumber().'.pdf';
+        Http::download($name, 'application/pdf', $pdf->Output($name, 'S'));
         //Remember what the user selected - for autoselect on the next print.
         $_SESSION['PAPER_SIZE'] = $psize;
         exit;
@@ -2137,8 +2688,7 @@ class Ticket {
         // Fetch thread prior to removing ticket entry
         $t = $this->getThread();
 
-        $sql = 'DELETE FROM '.TICKET_TABLE.' WHERE ticket_id='.$this->getId().' LIMIT 1';
-        if(!db_query($sql) || !db_affected_rows())
+        if (!parent::delete())
             return false;
 
         $t->delete();
@@ -2148,23 +2698,21 @@ class Ticket {
 
         $this->deleteDrafts();
 
-        $sql = 'DELETE FROM '.TICKET_TABLE.'__cdata WHERE `ticket_id`='
-            .db_input($this->getId());
-        // If the CDATA table doesn't exist, that's not an error
-        db_query($sql, false);
+        if ($this->cdata)
+            $this->cdata->delete();
 
         // Log delete
         $log = sprintf(__('Ticket #%1$s deleted by %2$s'),
-                $this->getNumber(),
-                $thisstaff ? $thisstaff->getName() : __('SYSTEM'));
-
+            $this->getNumber(),
+            $thisstaff ? $thisstaff->getName() : __('SYSTEM')
+        );
         if ($comments)
             $log .= sprintf('<hr>%s', $comments);
 
         $ost->logDebug(
-                sprintf( __('Ticket #%s deleted'), $this->getNumber()),
-                $log);
-
+            sprintf( __('Ticket #%s deleted'), $this->getNumber()),
+            $log
+        );
         return true;
     }
 
@@ -2172,77 +2720,107 @@ class Ticket {
         Draft::deleteForNamespace('ticket.%.' . $this->getId());
     }
 
-    function update($vars, &$errors) {
+    function save($refetch=false) {
+        if ($this->dirty) {
+            $this->updated = SqlFunction::NOW();
+        }
+        return parent::save($this->dirty || $refetch);
+    }
 
+    function update($vars, &$errors) {
         global $cfg, $thisstaff;
 
-        if(!$cfg || !$thisstaff || !$thisstaff->canEditTickets())
+        if (!$cfg
+            || !($this->checkStaffPerm($thisstaff,
+                TicketModel::PERM_EDIT))
+        ) {
             return false;
+        }
 
-        $vars['note'] = ThreadBody::clean($vars['note']);
-
-        $fields=array();
+        $fields = array();
         $fields['topicId']  = array('type'=>'int',      'required'=>1, 'error'=>__('Help topic selection is required'));
         $fields['slaId']    = array('type'=>'int',      'required'=>0, 'error'=>__('Select a valid SLA'));
         $fields['duedate']  = array('type'=>'date',     'required'=>0, 'error'=>__('Invalid date format - must be MM/DD/YY'));
 
-        $fields['note']     = array('type'=>'text',     'required'=>1, 'error'=>__('A reason for the update is required'));
         $fields['user_id']  = array('type'=>'int',      'required'=>0, 'error'=>__('Invalid user-id'));
 
-        if(!Validator::process($fields, $vars, $errors) && !$errors['err'])
+        if (!Validator::process($fields, $vars, $errors) && !$errors['err'])
             $errors['err'] = __('Missing or invalid data - check the errors and try again');
 
-        if($vars['duedate']) {
-            if($this->isClosed())
+        $vars['note'] = ThreadEntryBody::clean($vars['note']);
+
+        if ($vars['duedate']) {
+            if ($this->isClosed())
                 $errors['duedate']=__('Due date can NOT be set on a closed ticket');
-            elseif(!$vars['time'] || strpos($vars['time'],':')===false)
+            elseif (!$vars['time'] || strpos($vars['time'],':') === false)
                 $errors['time']=__('Select a time from the list');
-            elseif(strtotime($vars['duedate'].' '.$vars['time'])===false)
+            elseif (strtotime($vars['duedate'].' '.$vars['time']) === false)
                 $errors['duedate']=__('Invalid due date');
-            elseif(strtotime($vars['duedate'].' '.$vars['time'])<=time())
+            elseif (Misc::user2gmtime($vars['duedate'].' '.$vars['time']) <= Misc::user2gmtime())
                 $errors['duedate']=__('Due date must be in the future');
         }
 
+        if (isset($vars['source']) // Check ticket source if provided
+                && !array_key_exists($vars['source'], Ticket::getSources()))
+            $errors['source'] = sprintf( __('Invalid source given - %s'),
+                    Format::htmlchars($vars['source']));
+
         // Validate dynamic meta-data
         $forms = DynamicFormEntry::forTicket($this->getId());
         foreach ($forms as $form) {
             // Don't validate deleted forms
             if (!in_array($form->getId(), $vars['forms']))
                 continue;
+            $form->filterFields(function($f) { return !$f->isStorable(); });
             $form->setSource($_POST);
-            if (!$form->isValid())
+            if (!$form->isValid(function($f) {
+                return $f->isVisibleToStaff() && $f->isEditableToStaff();
+            })) {
                 $errors = array_merge($errors, $form->errors());
+            }
         }
 
-        if($errors) return false;
+        if ($errors)
+            return false;
 
-        $sql='UPDATE '.TICKET_TABLE.' SET updated=NOW() '
-            .' ,topic_id='.db_input($vars['topicId'])
-            .' ,sla_id='.db_input($vars['slaId'])
-            .' ,source='.db_input($vars['source'])
-            .' ,duedate='.($vars['duedate']?db_input(date('Y-m-d G:i',Misc::dbtime($vars['duedate'].' '.$vars['time']))):'NULL');
+        // Decide if we need to keep the just selected SLA
+        $keepSLA = ($this->getSLAId() != $vars['slaId']);
 
-        if($vars['user_id'])
-            $sql.=', user_id='.db_input($vars['user_id']);
-        if($vars['duedate']) { //We are setting new duedate...
-            $sql.=' ,isoverdue=0';
+        $this->topic_id = $vars['topicId'];
+        $this->sla_id = $vars['slaId'];
+        $this->source = $vars['source'];
+        $this->duedate = $vars['duedate']
+            ? date('Y-m-d G:i',Misc::dbtime($vars['duedate'].' '.$vars['time']))
+            : null;
+
+        if ($vars['user_id'])
+            $this->user_id = $vars['user_id'];
+        if ($vars['duedate'])
+            // We are setting new duedate...
+            $this->isoverdue = 0;
+
+        $changes = array();
+        foreach ($this->dirty as $F=>$old) {
+            switch ($F) {
+            case 'topic_id':
+            case 'user_id':
+            case 'source':
+            case 'duedate':
+            case 'sla_id':
+                $changes[$F] = array($old, $this->{$F});
+            }
         }
 
-        $sql.=' WHERE ticket_id='.db_input($this->getId());
-
-        if(!db_query($sql) || !db_affected_rows())
+        if (!$this->save())
             return false;
 
-        if(!$vars['note'])
-            $vars['note']=sprintf(_S('Ticket details updated by %s'), $thisstaff->getName());
-
-        $this->logNote(_S('Ticket Updated'), $vars['note'], $thisstaff);
-
-        // Decide if we need to keep the just selected SLA
-        $keepSLA = ($this->getSLAId() != $vars['slaId']);
+        if ($vars['note'])
+            $this->logNote(_S('Ticket Updated'), $vars['note'], $thisstaff);
 
         // Update dynamic meta-data
         foreach ($forms as $f) {
+            if ($C = $f->getChanges())
+                $changes['fields'] = ($changes['fields'] ?: array()) + $C;
             // Drop deleted forms
             $idx = array_search($f->getId(), $vars['forms']);
             if ($idx === false) {
@@ -2254,80 +2832,63 @@ class Ticket {
             }
         }
 
-        // Reload the ticket so we can do further checking
-        $this->reload();
+        if ($changes)
+            $this->logEvent('edited', $changes);
 
         // Reselect SLA if transient
         if (!$keepSLA
-                && (!$this->getSLA() || $this->getSLA()->isTransient()))
+            && (!$this->getSLA() || $this->getSLA()->isTransient())
+        ) {
             $this->selectSLAId();
+        }
+
+        // Update estimated due date in database
+        $estimatedDueDate = $this->getEstDueDate();
+        $this->updateEstDueDate();
 
         // Clear overdue flag if duedate or SLA changes and the ticket is no longer overdue.
         if($this->isOverdue()
-                && (!$this->getEstDueDate() //Duedate + SLA cleared
-                    || Misc::db2gmtime($this->getEstDueDate()) > Misc::gmtime() //New due date in the future.
-                    )) {
+            && (!$estimatedDueDate //Duedate + SLA cleared
+                || Misc::db2gmtime($estimatedDueDate) > Misc::gmtime() //New due date in the future.
+        )) {
             $this->clearOverdue();
         }
 
         Signal::send('model.updated', $this);
-        return true;
+        return $this->save();
     }
 
-
    /*============== Static functions. Use Ticket::function(params); =============nolint*/
-    function getIdByNumber($number, $email=null) {
+    static function getIdByNumber($number, $email=null, $ticket=false) {
 
-        if(!$number)
+        if (!$number)
             return 0;
 
-        $sql ='SELECT ticket.ticket_id FROM '.TICKET_TABLE.' ticket '
-             .' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id'
-             .' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id'
-             .' WHERE ticket.`number`='.db_input($number);
-
-        if($email)
-            $sql .= ' AND email.address = '.db_input($email);
-
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id)=db_fetch_row($res);
-
-        return $id;
-    }
+        $query = static::objects()
+            ->filter(array('number' => $number));
 
+        if ($email)
+            $query->filter(array('user__emails__address' => $email));
 
 
-    function lookup($id) { //Assuming local ID is the only lookup used!
-        return ($id
-                && is_numeric($id)
-                && ($ticket= new Ticket($id))
-                && $ticket->getId()==$id)
-            ?$ticket:null;
+        if (!$ticket) {
+            $query = $query->values_flat('ticket_id');
+            if ($row = $query->first())
+                return $row[0];
+        }
+        else {
+            return $query->first();
+        }
     }
 
-    function lookupByNumber($number, $email=null) {
-        return self::lookup(self::getIdByNumber($number, $email));
+    static function lookupByNumber($number, $email=null) {
+        return static::getIdByNumber($number, $email, true);
     }
 
     static function isTicketNumberUnique($number) {
-        return 0 == db_num_rows(db_query(
-            'SELECT ticket_id FROM '.TICKET_TABLE.' WHERE `number`='.db_input($number)));
-    }
-
-    function getIdByMessageId($mid, $email) {
-
-        if(!$mid || !$email)
-            return 0;
-
-        $sql='SELECT ticket.ticket_id FROM '.TICKET_TABLE. ' ticket '.
-             ' LEFT JOIN '.TICKET_THREAD_TABLE.' msg USING(ticket_id) '.
-             ' INNER JOIN '.TICKET_EMAIL_INFO_TABLE.' emsg ON (msg.id = emsg.message_id) '.
-             ' WHERE email_mid='.db_input($mid).' AND email='.db_input($email);
-        $id=0;
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id)=db_fetch_row($res);
-
-        return $id;
+        return 0 === static::objects()
+            ->filter(array('number' => $number))
+            ->count();
     }
 
     /* Quick staff's tickets stats */
@@ -2338,66 +2899,48 @@ class Ticket {
         if(!$staff || (!is_object($staff) && !($staff=Staff::lookup($staff))) || !$staff->isStaff())
             return null;
 
-        $where = array('(ticket.staff_id='.db_input($staff->getId()) .' AND
-                    status.state="open")');
-        $where2 = '';
-
-        if(($teams=$staff->getTeams()))
-            $where[] = ' ( ticket.team_id IN('.implode(',', db_input(array_filter($teams)))
-                        .') AND status.state="open")';
-
-        if(!$staff->showAssignedOnly() && ($depts=$staff->getDepts())) //Staff with limited access just see Assigned tickets.
-            $where[] = 'ticket.dept_id IN('.implode(',', db_input($depts)).') ';
-
-        if(!$cfg || !($cfg->showAssignedTickets() || $staff->showAssignedTickets()))
-            $where2 =' AND ticket.staff_id=0 ';
-        $where = implode(' OR ', $where);
-        if ($where) $where = 'AND ( '.$where.' ) ';
-
-        $sql =  'SELECT \'open\', count( ticket.ticket_id ) AS tickets '
-                .'FROM ' . TICKET_TABLE . ' ticket '
-                .'INNER JOIN '.TICKET_STATUS_TABLE. ' status
-                    ON (ticket.status_id=status.id
-                            AND status.state=\'open\') '
-                .'WHERE ticket.isanswered = 0 '
-                . $where . $where2
-
-                .'UNION SELECT \'answered\', count( ticket.ticket_id ) AS tickets '
-                .'FROM ' . TICKET_TABLE . ' ticket '
-                .'INNER JOIN '.TICKET_STATUS_TABLE. ' status
-                    ON (ticket.status_id=status.id
-                            AND status.state=\'open\') '
-                .'WHERE ticket.isanswered = 1 '
-                . $where
-
-                .'UNION SELECT \'overdue\', count( ticket.ticket_id ) AS tickets '
-                .'FROM ' . TICKET_TABLE . ' ticket '
-                .'INNER JOIN '.TICKET_STATUS_TABLE. ' status
-                    ON (ticket.status_id=status.id
-                            AND status.state=\'open\') '
-                .'WHERE ticket.isoverdue =1 '
-                . $where
-
-                .'UNION SELECT \'assigned\', count( ticket.ticket_id ) AS tickets '
-                .'FROM ' . TICKET_TABLE . ' ticket '
-                .'INNER JOIN '.TICKET_STATUS_TABLE. ' status
-                    ON (ticket.status_id=status.id
-                            AND status.state=\'open\') '
-                .'WHERE ticket.staff_id = ' . db_input($staff->getId()) . ' '
-                . $where
-
-                .'UNION SELECT \'closed\', count( ticket.ticket_id ) AS tickets '
-                .'FROM ' . TICKET_TABLE . ' ticket '
-                .'INNER JOIN '.TICKET_STATUS_TABLE. ' status
-                    ON (ticket.status_id=status.id
-                            AND status.state=\'closed\' ) '
-                .'WHERE 1 '
-                . $where;
-
-        $res = db_query($sql);
+        // -- Open and assigned to me
+        $assigned = Q::any(array(
+            'staff_id' => $staff->getId(),
+        ));
+        // -- Open and assigned to a team of mine
+        if ($teams = array_filter($staff->getTeams()))
+            $assigned->add(array('team_id__in' => $teams));
+
+        $visibility = Q::any(new Q(array('status__state'=>'open', $assigned)));
+
+        // -- Routed to a department of mine
+        if (!$staff->showAssignedOnly() && ($depts = $staff->getDepts()))
+            $visibility->add(array('dept_id__in' => $depts));
+
+        $blocks = Ticket::objects()
+            ->filter(Q::any($visibility))
+            ->filter(array('status__state' => 'open'))
+            ->aggregate(array('count' => SqlAggregate::COUNT('ticket_id')))
+            ->values('status__state', 'isanswered', 'isoverdue','staff_id', 'team_id');
+
         $stats = array();
-        while($row = db_fetch_row($res)) {
-            $stats[$row[0]] = $row[1];
+        $hideassigned = ($cfg && !$cfg->showAssignedTickets()) && !$staff->showAssignedTickets();
+        $showanswered = $cfg->showAnsweredTickets();
+        $id = $staff->getId();
+        foreach ($blocks as $S) {
+            if ($showanswered || !$S['isanswered']) {
+                if (!($hideassigned && ($S['staff_id'] || $S['team_id'])))
+                    $stats['open'] += $S['count'];
+            }
+            else {
+                $stats['answered'] += $S['count'];
+            }
+            if ($S['isoverdue'])
+                $stats['overdue'] += $S['count'];
+            if ($S['staff_id'] == $id)
+                $stats['assigned'] += $S['count'];
+            elseif ($S['team_id']
+                    && $S['staff_id'] == 0
+                    && $teams
+                    && in_array($S['team_id'], $teams))
+                // Assigned to my team but uassigned to an agent
+                $stats['assigned'] += $S['count'];
         }
         return $stats;
     }
@@ -2472,8 +3015,8 @@ class Ticket {
 
         try {
             // Make sure the email address is not banned
-            if (TicketFilter::isBanned($vars['email'])) {
-                throw new RejectedException(Banlist::getFilter(), $vars);
+            if (($filter=Banlist::isBanned($vars['email']))) {
+                throw new RejectedException($filter, $vars);
             }
 
             // Init ticket filters...
@@ -2494,7 +3037,7 @@ class Ticket {
      */
     static function create($vars, &$errors, $origin, $autorespond=true,
             $alertstaff=true) {
-        global $ost, $cfg, $thisclient, $_FILES;
+        global $ost, $cfg, $thisclient, $thisstaff;
 
         // Don't enforce form validation for email
         $field_filter = function($type) use ($origin) {
@@ -2508,10 +3051,9 @@ class Ticket {
                 case 'staff':
                     // Required 'Contact Information' fields aren't required
                     // when staff open tickets
-                    return $type != 'user'
-                        || in_array($f->get('name'), array('name','email'));
+                    return $f->isVisibleToStaff();
                 case 'web':
-                    return !$f->get('private');
+                    return $f->isVisibleToUsers();
                 default:
                     return true;
                 }
@@ -2542,15 +3084,11 @@ class Ticket {
             }
         }
 
-        if (!$form->isValid($field_filter('ticket')))
-            $errors += $form->errors();
-
         if ($vars['uid'])
             $user = User::lookup($vars['uid']);
 
         $id=0;
         $fields=array();
-        $fields['message']  = array('type'=>'*',     'required'=>1, 'error'=>__('Message content is required'));
         switch (strtolower($origin)) {
             case 'web':
                 $fields['topicId']  = array('type'=>'int',  'required'=>1, 'error'=>__('Select a help topic'));
@@ -2573,32 +3111,55 @@ class Ticket {
         if(!Validator::process($fields, $vars, $errors) && !$errors['err'])
             $errors['err'] =__('Missing or invalid data - check the errors and try again');
 
-        //Make sure the due date is valid
-        if($vars['duedate']) {
-            if(!$vars['time'] || strpos($vars['time'],':')===false)
+        // Make sure the due date is valid
+        if ($vars['duedate']) {
+            if (!$vars['time'] || strpos($vars['time'],':') === false)
                 $errors['time']=__('Select a time from the list');
-            elseif(strtotime($vars['duedate'].' '.$vars['time'])===false)
+            elseif (strtotime($vars['duedate'].' '.$vars['time']) === false)
                 $errors['duedate']=__('Invalid due date');
-            elseif(strtotime($vars['duedate'].' '.$vars['time'])<=time())
+            elseif (Misc::user2gmtime($vars['duedate'].' '.$vars['time']) <= Misc::user2gmtime())
                 $errors['duedate']=__('Due date must be in the future');
         }
 
+        $topic_forms = array();
         if (!$errors) {
 
-            # Perform ticket filter actions on the new ticket arguments
-            $__form = null;
+            // Handle the forms associate with the help topics. Instanciate the
+            // entries, disable and track the requested disabled fields.
             if ($vars['topicId']) {
-                if (($__topic=Topic::lookup($vars['topicId']))
-                    && ($__form = $__topic->getForm())
-                ) {
-                    $__form = $__form->instanciate();
-                    $__form->setSource($vars);
+                if ($__topic=Topic::lookup($vars['topicId'])) {
+                    foreach ($__topic->getForms() as $idx=>$__F) {
+                        $disabled = array();
+                        foreach ($__F->getFields() as $field) {
+                            if (!$field->isEnabled() && $field->hasFlag(DynamicFormField::FLAG_ENABLED))
+                                $disabled[] = $field->get('id');
+                        }
+                        // Special handling for the ticket form — disable fields
+                        // requested to be disabled as per the help topic.
+                        if ($__F->get('type') == 'T') {
+                            foreach ($form->getFields() as $field) {
+                                if (false !== array_search($field->get('id'), $disabled))
+                                    $field->disable();
+                            }
+                            $form->sort = $idx;
+                            $__F = $form;
+                        }
+                        else {
+                            $__F = $__F->instanciate($idx);
+                            $__F->setSource($vars);
+                            $topic_forms[] = $__F;
+                        }
+                        // Track fields currently disabled
+                        $__F->extra = JsonDataEncoder::encode(array(
+                            'disable' => $disabled
+                        ));
+                    }
                 }
             }
 
             try {
                 $vars = self::filterTicketData($origin, $vars,
-                    array($form, $__form), $user);
+                    array_merge(array($form), $topic_forms), $user);
             }
             catch (RejectedException $ex) {
                 return $reject_ticket(
@@ -2644,18 +3205,24 @@ class Ticket {
                 }
 
                 $user_form = UserForm::getUserForm()->getForm($vars);
+                $can_create = !$thisstaff || $thisstaff->hasPerm(User::PERM_CREATE);
                 if (!$user_form->isValid($field_filter('user'))
-                        || !($user=User::fromVars($user_form->getClean())))
-                    $errors['user'] = __('Incomplete client information');
+                    || !($user=User::fromVars($user_form->getClean(), $can_create))
+                ) {
+                    $errors['user'] = $can_create
+                        ? __('Incomplete client information')
+                        : __('You do not have permission to create users.');
+                }
             }
         }
 
+        if (!$form->isValid($field_filter('ticket')))
+            $errors += $form->errors();
+
         if ($vars['topicId']) {
             if ($topic=Topic::lookup($vars['topicId'])) {
-                if ($topic_form = $topic->getForm()) {
+                foreach ($topic_forms as $topic_form) {
                     $TF = $topic_form->getForm($vars);
-                    $topic_form = $topic_form->instanciate();
-                    $topic_form->setSource($vars);
                     if (!$TF->isValid($field_filter('topic')))
                         $errors = array_merge($errors, $TF->errors());
                 }
@@ -2761,42 +3328,57 @@ class Ticket {
 
         //We are ready son...hold on to the rails.
         $number = $topic ? $topic->getNewTicketNumber() : $cfg->getNewTicketNumber();
-        $sql='INSERT INTO '.TICKET_TABLE.' SET created=NOW() '
-            .' ,lastmessage= NOW()'
-            .' ,user_id='.db_input($user->getId())
-            .' ,`number`='.db_input($number)
-            .' ,dept_id='.db_input($deptId)
-            .' ,topic_id='.db_input($topicId)
-            .' ,ip_address='.db_input($ipaddress)
-            .' ,source='.db_input($source);
+        $ticket = new static(array(
+            'created' => SqlFunction::NOW(),
+            'lastupdate' => SqlFunction::NOW(),
+            'number' => $number,
+            'user' => $user,
+            'dept_id' => $deptId,
+            'topic_id' => $topicId,
+            'ip_address' => $ipaddress,
+            'source' => $source,
+        ));
 
         if (isset($vars['emailId']) && $vars['emailId'])
-            $sql.=', email_id='.db_input($vars['emailId']);
+            $ticket->email_id = $vars['emailId'];
 
         //Make sure the origin is staff - avoid firebug hack!
-        if($vars['duedate'] && !strcasecmp($origin,'staff'))
-             $sql.=' ,duedate='.db_input(date('Y-m-d G:i',Misc::dbtime($vars['duedate'].' '.$vars['time'])));
+        if ($vars['duedate'] && !strcasecmp($origin,'staff'))
+            $ticket->duedate = date('Y-m-d G:i',
+                Misc::dbtime($vars['duedate'].' '.$vars['time']));
 
 
-        if(!db_query($sql) || !($id=db_insert_id()) || !($ticket =Ticket::lookup($id)))
+        if (!$ticket->save())
+            return null;
+        if (!($thread = TicketThread::create($ticket->getId())))
             return null;
 
         /* -------------------- POST CREATE ------------------------ */
 
         // Save the (common) dynamic form
-        $form->setTicketId($id);
+        // Ensure we have a subject
+        $subject = $form->getAnswer('subject');
+        if ($subject && !$subject->getValue()) {
+            if ($topic) {
+                $form->setAnswer('subject', $topic->getFullName());
+            }
+        }
+        $form->setTicketId($ticket->getId());
         $form->save();
 
         // Save the form data from the help-topic form, if any
-        if ($topic_form) {
-            $topic_form->setTicketId($id);
+        foreach ($topic_forms as $topic_form) {
+            $topic_form->setTicketId($ticket->getId());
             $topic_form->save();
         }
 
-        $ticket->loadDynamicData();
+        $ticket->loadDynamicData(true);
 
         $dept = $ticket->getDept();
 
+        // Start tracking ticket lifecycle events (created should come first!)
+        $ticket->logEvent('created', null, $thisstaff ?: $user);
+
         // Add organizational collaborators
         if ($org && $org->autoAddCollabs()) {
             $pris = $org->autoAddPrimaryContactsAsCollabs();
@@ -2812,10 +3394,7 @@ class Ticket {
             }
             //TODO: Can collaborators add others?
             if ($collabs) {
-                //TODO: Change EndUser to name of  user.
-                $ticket->logNote(sprintf(_S('Collaborators for %s organization added'),
-                        $org->getName()),
-                    implode("<br>", $collabs), $org->getName(), false);
+                $ticket->logEvent('collab', array('org' => $org->getId()));
             }
         }
 
@@ -2824,29 +3403,48 @@ class Ticket {
         $vars['userId'] = $ticket->getUserId();
         $message = $ticket->postMessage($vars , $origin, false);
 
+        // If a message was posted, flag it as the orignal message. This
+        // needs to be done on new ticket, so as to otherwise separate the
+        // concept from the first message entry in a thread.
+        if ($message instanceof ThreadEntry) {
+            $message->setFlag(ThreadEntry::FLAG_ORIGINAL_MESSAGE);
+            $message->save();
+        }
+
         // Configure service-level-agreement for this ticket
         $ticket->selectSLAId($vars['slaId']);
 
         // Assign ticket to staff or team (new ticket by staff)
-        if($vars['assignId']) {
-            $ticket->assign($vars['assignId'], $vars['note']);
+        if ($vars['assignId']) {
+            $asnform = $ticket->getAssignmentForm(array(
+                        'assignee' => $vars['assignId'],
+                        'comments' => $vars['note'])
+                    );
+            $e = array();
+            $ticket->assign($asnform, $e);
         }
         else {
             // Auto assign staff or team - auto assignment based on filter
             // rules. Both team and staff can be assigned
             if ($vars['staffId'])
-                 $ticket->assignToStaff($vars['staffId'], _S('Auto Assignment'));
+                 $ticket->assignToStaff($vars['staffId'], false);
             if ($vars['teamId'])
                 // No team alert if also assigned to an individual agent
-                $ticket->assignToTeam($vars['teamId'], _S('Auto Assignment'),
-                    !$vars['staffId']);
+                $ticket->assignToTeam($vars['teamId'], false, !$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, false);
+        if ($statusId) {
+            if (!$ticket->setStatus($statusId, false, $errors, false)) {
+                // Tickets _must_ have a status. Forceably set one here
+                $ticket->setStatusId($cfg->getDefaultTicketStatusId());
+            }
+        }
 
         /**********   double check auto-response  ************/
         //Override auto responder if the FROM email is one of the internal emails...loop control.
@@ -2855,43 +3453,41 @@ class Ticket {
 
         # Messages that are clearly auto-responses from email systems should
         # not have a return 'ping' message
-        if (isset($vars['flags']) && $vars['flags']['bounce'])
+        if (isset($vars['mailflags']) && $vars['mailflags']['bounce'])
             $autorespond = false;
-        if ($autorespond && $message->isAutoReply())
+        if ($autorespond && $message instanceof ThreadEntry && $message->isAutoReply())
             $autorespond = false;
 
-        //post canned auto-response IF any (disables new ticket auto-response).
+        // Post canned auto-response IF any (disables new ticket auto-response).
         if ($vars['cannedResponseId']
-            && $ticket->postCannedReply($vars['cannedResponseId'], $message->getId(), $autorespond)) {
+            && $ticket->postCannedReply($vars['cannedResponseId'], $message, $autorespond)) {
                 $ticket->markUnAnswered(); //Leave the ticket as unanswred.
                 $autorespond = false;
         }
 
-        //Check department's auto response settings
+        // Check department's auto response settings
         // XXX: Dept. setting doesn't affect canned responses.
-        if($autorespond && $dept && !$dept->autoRespONNewTicket())
+        if ($autorespond && $dept && !$dept->autoRespONNewTicket())
             $autorespond=false;
 
-        //Don't send alerts to staff when the message is a bounce
-        //  this is necessary to avoid possible loop (especially on new ticket)
-        if ($alertstaff && $message->isBounce())
+        // Don't send alerts to staff when the message is a bounce
+        // this is necessary to avoid possible loop (especially on new ticket)
+        if ($alertstaff && $message instanceof ThreadEntry && $message->isBounce())
             $alertstaff = false;
 
         /***** See if we need to send some alerts ****/
         $ticket->onNewTicket($message, $autorespond, $alertstaff);
 
         /************ check if the user JUST reached the max. open tickets limit **********/
-        if($cfg->getMaxOpenTickets()>0
-                    && ($user=$ticket->getOwner())
-                    && ($user->getNumOpenTickets()==$cfg->getMaxOpenTickets())) {
-            $ticket->onOpenLimit(($autorespond && strcasecmp($origin, 'staff')));
+        if ($cfg->getMaxOpenTickets()>0
+            && ($user=$ticket->getOwner())
+            && ($user->getNumOpenTickets()==$cfg->getMaxOpenTickets())
+        ) {
+            $ticket->onOpenLimit($autorespond && strcasecmp($origin, 'staff'));
         }
 
-        /* Start tracking ticket lifecycle events */
-        $ticket->logEvent('created');
-
         // Fire post-create signal (for extra email sending, searching)
-        Signal::send('model.created', $ticket);
+        Signal::send('ticket.created', $ticket);
 
         /* Phew! ... time for tea (KETEPA) */
 
@@ -2902,13 +3498,25 @@ class Ticket {
     static function open($vars, &$errors) {
         global $thisstaff, $cfg;
 
-        if(!$thisstaff || !$thisstaff->canCreateTickets()) return false;
+        if (!$thisstaff)
+            return false;
+
+        if ($vars['deptId']
+            && ($role = $thisstaff->getRole($vars['deptId']))
+            && !$role->hasPerm(TicketModel::PERM_CREATE)
+        ) {
+            $errors['err'] = __('You do not have permission to create a ticket in this department');
+            return false;
+        }
+
+        if (isset($vars['source']) // Check ticket source if provided
+                && !array_key_exists($vars['source'], Ticket::getSources()))
+            $errors['source'] = sprintf( __('Invalid source given - %s'),
+                    Format::htmlchars($vars['source']));
 
-        if($vars['source'] && !in_array(strtolower($vars['source']),array('email','phone','other')))
-            $errors['source']=sprintf(__('Invalid source given - %s'),Format::htmlchars($vars['source']));
 
         if (!$vars['uid']) {
-            //Special validation required here
+            // Special validation required here
             if (!$vars['email'] || !Validator::is_email($vars['email']))
                 $errors['email'] = __('Valid email address is required');
 
@@ -2916,25 +3524,35 @@ class Ticket {
                 $errors['name'] = __('Name is required');
         }
 
-        if (!$thisstaff->canAssignTickets())
-            unset($vars['assignId']);
+        // Ensure agent has rights to make assignment in the cited
+        // department
+        if ($vars['assignId'] && !(
+            $role
+            ? $role->hasPerm(TicketModel::PERM_ASSIGN)
+            : $thisstaff->hasPerm(TicketModel::PERM_ASSIGN, false)
+        )) {
+            $errors['assignId'] = __('Action Denied. You are not allowed to assign/reassign tickets.');
+        }
 
-        $vars['response'] = ThreadBody::clean($vars['response']);
-        $vars['note'] = ThreadBody::clean($vars['note']);
+        // TODO: Deny action based on selected department.
+        $vars['response'] = ThreadEntryBody::clean($vars['response']);
+        $vars['note'] = ThreadEntryBody::clean($vars['note']);
         $create_vars = $vars;
         $tform = TicketForm::objects()->one()->getForm($create_vars);
         $create_vars['cannedattachments']
             = $tform->getField('message')->getWidget()->getAttachments()->getClean();
 
-        if(!($ticket=Ticket::create($create_vars, $errors, 'staff', false)))
+        if (!($ticket=self::create($create_vars, $errors, 'staff', false)))
             return false;
 
         $vars['msgId']=$ticket->getLastMsgId();
 
+        // Effective role for the department
+        $role = $thisstaff->getRole($ticket->getDeptId());
+
         // post response - if any
         $response = null;
-        if($vars['response'] && $thisstaff->canPostReply()) {
-
+        if($vars['response'] && $role->hasPerm(TicketModel::PERM_REPLY)) {
             $vars['response'] = $ticket->replaceVars($vars['response']);
             // $vars['cannedatachments'] contains the attachments placed on
             // the response form.
@@ -2943,75 +3561,75 @@ class Ticket {
 
         // Not assigned...save optional note if any
         if (!$vars['assignId'] && $vars['note']) {
-            if (!$cfg->isHtmlThreadEnabled()) {
-                $vars['note'] = new TextThreadBody($vars['note']);
+            if (!$cfg->isRichTextEnabled()) {
+                $vars['note'] = new TextThreadEntryBody($vars['note']);
             }
             $ticket->logNote(_S('New Ticket'), $vars['note'], $thisstaff, false);
         }
-        else {
-            // Not assignment and no internal note - log activity
-            $ticket->logActivity(_S('New Ticket by Agent'),
-                sprintf(_S('Ticket created by agent - %s'), $thisstaff->getName()));
-        }
 
-        $ticket->reload();
-
-        if(!$cfg->notifyONNewStaffTicket()
-                || !isset($vars['alertuser'])
-                || !($dept=$ticket->getDept()))
+        if (!$cfg->notifyONNewStaffTicket()
+            || !isset($vars['alertuser'])
+            || !($dept=$ticket->getDept())
+        ) {
             return $ticket; //No alerts.
-
-        //Send Notice to user --- if requested AND enabled!!
-        if(($tpl=$dept->getTemplate())
-                && ($msg=$tpl->getNewTicketNoticeMsgTemplate())
-                && ($email=$dept->getEmail())) {
-
+        }
+        // Send Notice to user --- if requested AND enabled!!
+        if (($tpl=$dept->getTemplate())
+            && ($msg=$tpl->getNewTicketNoticeMsgTemplate())
+            && ($email=$dept->getEmail())
+        ) {
             $message = (string) $ticket->getLastMessage();
-            if($response) {
-                $message .= ($cfg->isHtmlThreadEnabled()) ? "<br><br>" : "\n\n";
+            if ($response) {
+                $message .= ($cfg->isRichTextEnabled()) ? "<br><br>" : "\n\n";
                 $message .= $response->getBody();
             }
 
-            if($vars['signature']=='mine')
+            if ($vars['signature']=='mine')
                 $signature=$thisstaff->getSignature();
-            elseif($vars['signature']=='dept' && $dept && $dept->isPublic())
+            elseif ($vars['signature']=='dept' && $dept && $dept->isPublic())
                 $signature=$dept->getSignature();
             else
                 $signature='';
 
-            $attachments =($cfg->emailAttachments() && $response)?$response->getAttachments():array();
+            $attachments = ($cfg->emailAttachments() && $response)
+                ? $response->getAttachments() : array();
 
             $msg = $ticket->replaceVars($msg->asArray(),
-                    array(
-                        'message'   => $message,
-                        'signature' => $signature,
-                        'response'  => ($response) ? $response->getBody() : '',
-                        'recipient' => $ticket->getOwner(), //End user
-                        'staff'     => $thisstaff,
-                        )
-                    );
-
-            $references = $ticket->getLastMessage()->getEmailMessageId();
-            if (isset($response))
-                $references = array($response->getEmailMessageId(), $references);
+                array(
+                    'message'   => $message,
+                    'signature' => $signature,
+                    'response'  => ($response) ? $response->getBody() : '',
+                    'recipient' => $ticket->getOwner(), //End user
+                    'staff'     => $thisstaff,
+                )
+            );
+            $message = $ticket->getLastMessage();
             $options = array(
-                'references' => $references,
-                'thread' => $ticket->getLastMessage()
+                'thread' => $message ?: $ticket->getThread(),
             );
             $email->send($ticket->getOwner(), $msg['subj'], $msg['body'], $attachments,
                 $options);
         }
-
         return $ticket;
-
     }
 
-    function checkOverdue() {
+    static function checkOverdue() {
+        /*
+        $overdue = static::objects()
+            ->filter(array(
+                'isoverdue' => 0,
+                Q::any(array(
+                    Q::all(array(
+                        'reopened__isnull' => true,
+                        'duedate__isnull' => true,
+
+         Punt for now
+         */
 
         $sql='SELECT ticket_id FROM '.TICKET_TABLE.' T1 '
             .' INNER JOIN '.TICKET_STATUS_TABLE.' status
                 ON (status.id=T1.status_id AND status.state="open") '
-            .' LEFT JOIN '.SLA_TABLE.' T2 ON (T1.sla_id=T2.id AND T2.isactive=1) '
+            .' LEFT JOIN '.SLA_TABLE.' T2 ON (T1.sla_id=T2.id AND T2.flags & 1 = 1) '
             .' WHERE isoverdue=0 '
             .' AND ((reopened is NULL AND duedate is NULL AND TIME_TO_SEC(TIMEDIFF(NOW(),T1.created))>=T2.grace_period*3600) '
             .' OR (reopened is NOT NULL AND duedate is NULL AND TIME_TO_SEC(TIMEDIFF(NOW(),reopened))>=T2.grace_period*3600) '
@@ -3020,9 +3638,8 @@ class Ticket {
 
         if(($res=db_query($sql)) && db_num_rows($res)) {
             while(list($id)=db_fetch_row($res)) {
-                if(($ticket=Ticket::lookup($id)) && $ticket->markOverdue())
-                    $ticket->logActivity(_S('Ticket Marked Overdue'),
-                        _S('Ticket flagged as overdue by the system.'));
+                if ($ticket=Ticket::lookup($id))
+                    $ticket->markOverdue();
             }
         } else {
             //TODO: Trigger escalation on already overdue tickets - make sure last overdue event > grace_period.
@@ -3030,5 +3647,11 @@ class Ticket {
         }
    }
 
+    static function agentActions($agent, $options=array()) {
+        if (!$agent)
+            return;
+
+        require STAFFINC_DIR.'templates/tickets-actions.tmpl.php';
+    }
 }
 ?>
diff --git a/include/class.timezone.php b/include/class.timezone.php
index bb9f2944fc797acd205ad8739005c54b97dcd248..272444b458130a7f6eff208708ab1e0b48c15743 100644
--- a/include/class.timezone.php
+++ b/include/class.timezone.php
@@ -2,7 +2,7 @@
 /*********************************************************************
     class.timezone.php
 
-    Time zone get utils.
+    Database time zone get utils.
 
     Peter Rotich <peter@osticket.com>
     Copyright (c)  2006-2013 osTicket
@@ -14,58 +14,383 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 
-class Timezone {
+// This class adopted from jstimezone
 
-    var $id;
-    var $ht;
+class DbTimezone {
+    const HEMISPHERE_SOUTH = 's';
+    const DAY = 86400;
+    const HOUR = 3600;
+    const MINUTE = 60;
+    const SECOND = 1;
+    const BASELINE_YEAR = 2014;
+    const MAX_SCORE = 864000; // 10 days
 
-    function Timezone($id){
-        $this->id=0;
-        return $this->load($id);
-    }
+    static $AMBIGUITIES = array(
+        'America/Denver' =>       array('America/Mazatlan'),
+        'America/Chicago' =>      array('America/Mexico_City'),
+        'America/Santiago' =>     array('America/Asuncion', 'America/Campo_Grande'),
+        'America/Montevideo' =>   array('America/Sao_Paulo'),
+        // Europe/Minsk should not be in this list... but Windows.
+        'Asia/Beirut' =>          array('Asia/Amman', 'Asia/Jerusalem', 'Europe/Helsinki', 'Asia/Damascus', 'Africa/Cairo', 'Asia/Gaza', 'Europe/Minsk'),
+        'Pacific/Auckland' =>     array('Pacific/Fiji'),
+        'America/Los_Angeles' =>  array('America/Santa_Isabel'),
+        'America/New_York' =>     array('America/Havana'),
+        'America/Halifax' =>      array('America/Goose_Bay'),
+        'America/Godthab' =>      array('America/Miquelon'),
+        'Asia/Dubai' =>           array('Asia/Yerevan'),
+        'Asia/Jakarta' =>         array('Asia/Krasnoyarsk'),
+        'Asia/Shanghai' =>        array('Asia/Irkutsk', 'Australia/Perth'),
+        'Australia/Sydney' =>     array('Australia/Lord_Howe'),
+        'Asia/Tokyo' =>           array('Asia/Yakutsk'),
+        'Asia/Dhaka' =>           array('Asia/Omsk'),
+        // In the real world Yerevan is not ambigous for Baku... but Windows.
+        'Asia/Baku' =>            array('Asia/Yerevan'),
+        'Australia/Brisbane' =>   array('Asia/Vladivostok'),
+        'Pacific/Noumea' =>       array('Asia/Vladivostok'),
+        'Pacific/Majuro' =>       array('Asia/Kamchatka', 'Pacific/Fiji'),
+        'Pacific/Tongatapu' =>    array('Pacific/Apia'),
+        'Asia/Baghdad' =>         array('Europe/Minsk', 'Europe/Moscow'),
+        'Asia/Karachi' =>         array('Asia/Yekaterinburg'),
+        'Africa/Johannesburg' =>  array('Asia/Gaza', 'Africa/Cairo')
+    );
 
-    function load($id=0) {
+    static $olsonTimezones = array(
+        '-720,0' => 'Etc/GMT+12',
+        '-660,0' => 'Pacific/Pago_Pago',
+        '-660,1,s' => 'Pacific/Apia', // Why? Because windows... cry!
+        '-600,1' => 'America/Adak',
+        '-600,0' => 'Pacific/Honolulu',
+        '-570,0' => 'Pacific/Marquesas',
+        '-540,0' => 'Pacific/Gambier',
+        '-540,1' => 'America/Anchorage',
+        '-480,1' => 'America/Los_Angeles',
+        '-480,0' => 'Pacific/Pitcairn',
+        '-420,0' => 'America/Phoenix',
+        '-420,1' => 'America/Denver',
+        '-360,0' => 'America/Guatemala',
+        '-360,1' => 'America/Chicago',
+        '-360,1,s' => 'Pacific/Easter',
+        '-300,0' => 'America/Bogota',
+        '-300,1' => 'America/New_York',
+        '-270,0' => 'America/Caracas',
+        '-240,1' => 'America/Halifax',
+        '-240,0' => 'America/Santo_Domingo',
+        '-240,1,s' => 'America/Santiago',
+        '-210,1' => 'America/St_Johns',
+        '-180,1' => 'America/Godthab',
+        '-180,0' => 'America/Argentina/Buenos_Aires',
+        '-180,1,s' => 'America/Montevideo',
+        '-120,0' => 'America/Noronha',
+        '-120,1' => 'America/Noronha',
+        '-60,1' => 'Atlantic/Azores',
+        '-60,0' => 'Atlantic/Cape_Verde',
+        '0,0' => 'UTC',
+        '0,1' => 'Europe/London',
+        '60,1' => 'Europe/Berlin',
+        '60,0' => 'Africa/Lagos',
+        '60,1,s' => 'Africa/Windhoek',
+        '120,1' => 'Asia/Beirut',
+        '120,0' => 'Africa/Johannesburg',
+        '180,0' => 'Asia/Baghdad',
+        '180,1' => 'Europe/Moscow',
+        '210,1' => 'Asia/Tehran',
+        '240,0' => 'Asia/Dubai',
+        '240,1' => 'Asia/Baku',
+        '270,0' => 'Asia/Kabul',
+        '300,1' => 'Asia/Yekaterinburg',
+        '300,0' => 'Asia/Karachi',
+        '330,0' => 'Asia/Kolkata',
+        '345,0' => 'Asia/Kathmandu',
+        '360,0' => 'Asia/Dhaka',
+        '360,1' => 'Asia/Omsk',
+        '390,0' => 'Asia/Rangoon',
+        '420,1' => 'Asia/Krasnoyarsk',
+        '420,0' => 'Asia/Jakarta',
+        '480,0' => 'Asia/Shanghai',
+        '480,1' => 'Asia/Irkutsk',
+        '525,0' => 'Australia/Eucla',
+        '525,1,s' => 'Australia/Eucla',
+        '540,1' => 'Asia/Yakutsk',
+        '540,0' => 'Asia/Tokyo',
+        '570,0' => 'Australia/Darwin',
+        '570,1,s' => 'Australia/Adelaide',
+        '600,0' => 'Australia/Brisbane',
+        '600,1' => 'Asia/Vladivostok',
+        '600,1,s' => 'Australia/Sydney',
+        '630,1,s' => 'Australia/Lord_Howe',
+        '660,1' => 'Asia/Kamchatka',
+        '660,0' => 'Pacific/Noumea',
+        '690,0' => 'Pacific/Norfolk',
+        '720,1,s' => 'Pacific/Auckland',
+        '720,0' => 'Pacific/Majuro',
+        '765,1,s' => 'Pacific/Chatham',
+        '780,0' => 'Pacific/Tongatapu',
+        '780,1,s' => 'Pacific/Apia',
+        '840,0' => 'Pacific/Kiritimati'
+    );
 
-        if(!$id && !($id=$this->getId()))
-            return false;
 
-        $sql='SELECT * FROM '.TIMEZONE_TABLE.' WHERE id='.db_input($id);
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
+    function get_date_offset($checks) {
+        static $fragment =
+            "time_to_sec(timediff('%s', convert_tz('%s', @@session.time_zone, '+00:00'))) DIV 60";
 
-        $this->ht=db_fetch_array($res);
-        $this->id=$this->ht['id'];
-        
-        return $this->id;
+        if (!is_array($checks))
+            $checks = func_get_args();
+        $dates = array();
+        foreach ($checks as $time) {
+            $date = date('Y-m-d h:i:s', $time);
+            $dates[] = sprintf($fragment, $date, $date);
+        }
+
+        $sql = 'SELECT '.implode(',', $dates);
+        return db_fetch_row(db_query($sql));
     }
 
-    function reload() {
-        return $this->load();
+    function lookup_key() {
+        list($january_offset, $june_offset) =
+            $this->get_date_offset(
+                mktime(0, 0, 0, 1, 2, self::BASELINE_YEAR),
+                mktime(0, 0, 0, 6, 2, self::BASELINE_YEAR));
+        $diff = $january_offset - $june_offset;
+
+        if ($diff < 0) {
+            return $january_offset . ",1";
+        } else if ($diff > 0) {
+            return $june_offset . ",1," . self::HEMISPHERE_SOUTH;
+        }
+
+        return $january_offset . ",0";
     }
 
-    function getId() { 
-        return $this->id;
+    function get_from_database() {
+        // Attempt to fetch timezone direct from the database
+        $TZ = db_timezone();
+
+        // Forbid timezone abbreviations like 'CDT'
+        if (!in_array($TZ, array('UTC', 'GMT')) && strpos($TZ, '/') === false)
+            // Attempt to lookup based on the abbreviation
+            if (!($TZ = timezone_name_from_abbr($TZ)))
+                // Abbreviation doesn't point to anything valid
+                return false;
+
+        // SYSTEM does not describe a time zone, ensure we have a valid zone
+        // by attempting to create an instance of DateTimeZone()
+        try {
+            new DateTimeZone($TZ);
+            return $TZ;
+        }
+        catch (Exception $ex) {
+            return false;
+        }
     }
-        
-    function getOffset() {
-        return $this->ht['offset'];    
+
+    function dst_dates($year) {
+        $yearstart = mktime(0, 0, 1, 1, 1, $year);
+        $yearend = mktime(23, 59, 59, 12, 31, $year);
+        $current = $yearstart;
+        list($date_offset) = $this->get_date_offset($current);
+        $dst_start = null;
+        $dst_end = null;
+
+        $checks = array();
+        while ($current < $yearend - 86400) {
+            $checks[] = $current;
+            $current += 86400;
+        }
+
+        foreach ($this->get_date_offset($checks) as $i=>$offset) {
+            if ($offset !== $date_offset) {
+                if ($offset < $date_offset) {
+                    $dst_start = $checks[$i];
+                }
+                if ($offset > $date_offset) {
+                    $dst_end = $checks[$i];
+                }
+            }
+        }
+        // $offset will remain the last item in ::get_date_offset($checks)
+
+        if ($dst_start && $dst_end) {
+            return array(
+                's' => $this->find_dst_fold($dst_start),
+                'e' => $this->find_dst_fold($dst_end),
+            );
+        }
+
+        return false;
     }
 
-    function getName() {
-        return $this->info['timezone'];
+    function find_dst_fold($a_date, $padding=self::DAY, $iterator=self::HOUR) {
+        $date_start = $a_date - $padding;
+        $date_end = $a_date + $padding;
+        list($date_offset) = $this->get_date_offset($date_start);
+
+        $current = $date_start;
+
+        $dst_change = null;
+        while ($current < $date_end - $iterator) {
+            $checks = array();
+            for ($i=0; $i<12; $i++) {
+                $checks[] = $current;
+                $current += $iterator;
+            }
+
+            foreach ($this->get_date_offset($checks) as $i=>$offset) {
+                if ($offset !== $date_offset) {
+                    $dst_change = $checks[$i];
+                    break;
+                }
+            }
+            if ($dst_change)
+                break;
+        }
+
+        if ($padding === self::DAY) {
+            return $this->find_dst_fold($dst_change, self::HOUR, self::MINUTE);
+        }
+
+        if ($padding === self::HOUR) {
+            return $this->find_dst_fold($dst_change, self::MINUTE, self::SECOND);
+        }
+
+        return $dst_change;
     }
 
-    function getDesc() {
-        return $this->getName();
+    function windows7_adaptations($rule_list, $preliminary_timezone, $score, $sample) {
+        if ($score !== 'N/A') {
+            return $score;
+        }
+        if ($preliminary_timezone === 'Asia/Beirut') {
+            if ($sample['name'] === 'Africa/Cairo') {
+                if ($rule_list[6]['s'] === 1398376800 && $rule_list[6]['e'] === 1411678800) {
+                    return 0;
+                }
+            }
+            if ($sample['name'] === 'Asia/Jerusalem') {
+                if ($rule_list[6]['s'] === 1395964800 && $rule_list[6]['e'] === 1411858800) {
+                    return 0;
+                }
+            }
+        } else if ($preliminary_timezone === 'America/Santiago') {
+            if ($sample['name'] === 'America/Asuncion') {
+                if ($rule_list[6]['s'] === 1412481600 && $rule_list[6]['e'] === 1397358000) {
+                    return 0;
+                }
+            }
+            if ($sample['name'] === 'America/Campo_Grande') {
+                if ($rule_list[6]['s'] === 1413691200 && $rule_list[6]['e'] === 1392519600) {
+                    return 0;
+                }
+            }
+        } else if ($preliminary_timezone === 'America/Montevideo') {
+            if ($sample['name'] === 'America/Sao_Paulo') {
+                if ($rule_list[6]['s'] === 1413687600 && $rule_list[6]['e'] === 1392516000) {
+                    return 0;
+                }
+            }
+        } else if ($preliminary_timezone === 'Pacific/Auckland') {
+            if ($sample['name'] === 'Pacific/Fiji') {
+                if ($rule_list[6]['s'] === 1414245600 && $rule_list[6]['e'] === 1396101600) {
+                    return 0;
+                }
+            }
+        }
+
+        return $score;
     }
 
-    /* static functions */
-    function lookup($id) {
-        return ($id && is_numeric($id) && ($tz= new Timezone($id)) && $tz->getId()==$id)?$tz:null;
+    function best_dst_match($rule_list, $preliminary_timezone) {
+        $self = $this;
+        $score_sample = function ($sample) use ($rule_list, $self, $preliminary_timezone) {
+            $score = 0;
+
+            for ($j = 0; $j < count($rule_list); $j++) {
+
+                // Both sample and current time zone report DST during the year.
+                if (!!$sample['rules'][$j] && !!$rule_list[$j]) {
+
+                    // The current time zone's DST rules are inside the sample's. Include.
+                    if ($rule_list[$j]['s'] >= $sample['rules'][$j]['s'] && $rule_list[$j]['e'] <= $sample['rules'][$j]['e']) {
+                        $score = 0;
+                        $score += abs($rule_list[$j]['s'] - $sample['rules'][$j]['s']);
+                        $score += abs($sample['rules'][$j]['e'] - $rule_list[$j]['e']);
+
+                    // The current time zone's DST rules are outside the sample's. Discard.
+                    } else {
+                        $score = 'N/A';
+                        break;
+                    }
+
+                    // The max score has been reached. Discard.
+                    if ($score > self::MAX_SCORE) {
+                        $score = 'N/A';
+                        break;
+                    }
+                }
+            }
+
+            $score = $self->windows7_adaptations($rule_list, $preliminary_timezone, $score, $sample);
+
+            return $score;
+        };
+        $scoreboard = array();
+        $dst_zones = self::$dst_rules['zones'];
+        $ambiguities = @self::$AMBIGUITIES[$preliminary_timezone];
+
+        foreach ($dst_zones as $sample) {
+            $score = $score_sample($sample);
+
+            if ($score !== 'N/A') {
+                $scoreboard[$sample['name']] = $score;
+            }
+        }
+
+        foreach ($scoreboard as $tz) {
+            if (in_array($tz, $ambiguities)) {
+                return $tz;
+            }
+        }
+
+        return $preliminary_timezone;
+    }
+
+    function get_by_dst($preliminary_timezone) {
+        $rules = array();
+        foreach (self::$dst_rules['years'] as $Y) {
+            $rules[] = $this->dst_dates($Y);
+        }
+        $has_dst = false;
+        foreach ($rules as $R) {
+            if ($R !== false) {
+                $has_dst = true; break;
+            }
+        }
+
+        if ($has_dst) {
+            return $this->best_dst_match($rules, $preliminary_timezone);
+        }
+
+        return $preliminary_timezone;
     }
 
-    function getOffsetById($id) {
-        return ($tz=Timezone::lookup($id))?$tz->getOffset():0;
+    static function determine() {
+        $self = new static();
+        $preliminary_tz = $self->get_from_database();
+
+        if (!$preliminary_tz) {
+            $preliminary_tz = self::$olsonTimezones[$self->lookup_key()];
+
+            if (isset(self::$AMBIGUITIES[$preliminary_tz])) {
+                $preliminary_tz = $self->get_by_dst($preliminary_tz);
+            }
+        }
+
+        return $preliminary_tz;
     }
+
+    // Rules compiled from jstz rules.js file by
+    // str_replace('000,', ',', var_export(json_decode('...', true)));
+    static $dst_rules = array('years'=>array(0=>2008,1=>2009,2=>2010,3=>2011,4=>2012,5=>2013,6=>2014,),'zones'=>array(0=>array('name'=>'Africa/Cairo','rules'=>array(0=>array('e'=>1219957200,'s'=>1209074400,),1=>array('e'=>1250802000,'s'=>1240524000,),2=>array('e'=>1285880400,'s'=>1284069600,),3=>false,4=>false,5=>false,6=>array('e'=>1411678800,'s'=>1406844000,),),),1=>array('name'=>'America/Asuncion','rules'=>array(0=>array('e'=>1205031600,'s'=>1224388800,),1=>array('e'=>1236481200,'s'=>1255838400,),2=>array('e'=>1270954800,'s'=>1286078400,),3=>array('e'=>1302404400,'s'=>1317528000,),4=>array('e'=>1333854000,'s'=>1349582400,),5=>array('e'=>1364094000,'s'=>1381032000,),6=>array('e'=>1395543600,'s'=>1412481600,),),),2=>array('name'=>'America/Campo_Grande','rules'=>array(0=>array('e'=>1203217200,'s'=>1224388800,),1=>array('e'=>1234666800,'s'=>1255838400,),2=>array('e'=>1266721200,'s'=>1287288000,),3=>array('e'=>1298170800,'s'=>1318737600,),4=>array('e'=>1330225200,'s'=>1350792000,),5=>array('e'=>1361070000,'s'=>1382241600,),6=>array('e'=>1392519600,'s'=>1413691200,),),),3=>array('name'=>'America/Goose_Bay','rules'=>array(0=>array('e'=>1225594860,'s'=>1205035260,),1=>array('e'=>1257044460,'s'=>1236484860,),2=>array('e'=>1289098860,'s'=>1268539260,),3=>array('e'=>1320555600,'s'=>1299988860,),4=>array('e'=>1352005200,'s'=>1331445600,),5=>array('e'=>1383454800,'s'=>1362895200,),6=>array('e'=>1414904400,'s'=>1394344800,),),),4=>array('name'=>'America/Havana','rules'=>array(0=>array('e'=>1224997200,'s'=>1205643600,),1=>array('e'=>1256446800,'s'=>1236488400,),2=>array('e'=>1288501200,'s'=>1268542800,),3=>array('e'=>1321160400,'s'=>1300597200,),4=>array('e'=>1352005200,'s'=>1333256400,),5=>array('e'=>1383454800,'s'=>1362891600,),6=>array('e'=>1414904400,'s'=>1394341200,),),),5=>array('name'=>'America/Mazatlan','rules'=>array(0=>array('e'=>1225008000,'s'=>1207472400,),1=>array('e'=>1256457600,'s'=>1238922000,),2=>array('e'=>1288512000,'s'=>1270371600,),3=>array('e'=>1319961600,'s'=>1301821200,),4=>array('e'=>1351411200,'s'=>1333270800,),5=>array('e'=>1382860800,'s'=>1365325200,),6=>array('e'=>1414310400,'s'=>1396774800,),),),6=>array('name'=>'America/Mexico_City','rules'=>array(0=>array('e'=>1225004400,'s'=>1207468800,),1=>array('e'=>1256454000,'s'=>1238918400,),2=>array('e'=>1288508400,'s'=>1270368000,),3=>array('e'=>1319958000,'s'=>1301817600,),4=>array('e'=>1351407600,'s'=>1333267200,),5=>array('e'=>1382857200,'s'=>1365321600,),6=>array('e'=>1414306800,'s'=>1396771200,),),),7=>array('name'=>'America/Miquelon','rules'=>array(0=>array('e'=>1225598400,'s'=>1205038800,),1=>array('e'=>1257048000,'s'=>1236488400,),2=>array('e'=>1289102400,'s'=>1268542800,),3=>array('e'=>1320552000,'s'=>1299992400,),4=>array('e'=>1352001600,'s'=>1331442000,),5=>array('e'=>1383451200,'s'=>1362891600,),6=>array('e'=>1414900800,'s'=>1394341200,),),),8=>array('name'=>'America/Santa_Isabel','rules'=>array(0=>array('e'=>1225011600,'s'=>1207476000,),1=>array('e'=>1256461200,'s'=>1238925600,),2=>array('e'=>1288515600,'s'=>1270375200,),3=>array('e'=>1319965200,'s'=>1301824800,),4=>array('e'=>1351414800,'s'=>1333274400,),5=>array('e'=>1382864400,'s'=>1365328800,),6=>array('e'=>1414314000,'s'=>1396778400,),),),9=>array('name'=>'America/Sao_Paulo','rules'=>array(0=>array('e'=>1203213600,'s'=>1224385200,),1=>array('e'=>1234663200,'s'=>1255834800,),2=>array('e'=>1266717600,'s'=>1287284400,),3=>array('e'=>1298167200,'s'=>1318734000,),4=>array('e'=>1330221600,'s'=>1350788400,),5=>array('e'=>1361066400,'s'=>1382238000,),6=>array('e'=>1392516000,'s'=>1413687600,),),),10=>array('name'=>'Asia/Amman','rules'=>array(0=>array('e'=>1225404000,'s'=>1206655200,),1=>array('e'=>1256853600,'s'=>1238104800,),2=>array('e'=>1288303200,'s'=>1269554400,),3=>array('e'=>1319752800,'s'=>1301608800,),4=>false,5=>false,6=>array('e'=>1414706400,'s'=>1395957600,),),),11=>array('name'=>'Asia/Damascus','rules'=>array(0=>array('e'=>1225486800,'s'=>1207260000,),1=>array('e'=>1256850000,'s'=>1238104800,),2=>array('e'=>1288299600,'s'=>1270159200,),3=>array('e'=>1319749200,'s'=>1301608800,),4=>array('e'=>1351198800,'s'=>1333058400,),5=>array('e'=>1382648400,'s'=>1364508000,),6=>array('e'=>1414702800,'s'=>1395957600,),),),12=>array('name'=>'Asia/Dubai','rules'=>array(0=>false,1=>false,2=>false,3=>false,4=>false,5=>false,6=>false,),),13=>array('name'=>'Asia/Gaza','rules'=>array(0=>array('e'=>1219957200,'s'=>1206655200,),1=>array('e'=>1252015200,'s'=>1238104800,),2=>array('e'=>1281474000,'s'=>1269640860,),3=>array('e'=>1312146000,'s'=>1301608860,),4=>array('e'=>1348178400,'s'=>1333058400,),5=>array('e'=>1380229200,'s'=>1364508000,),6=>array('e'=>1411678800,'s'=>1395957600,),),),14=>array('name'=>'Asia/Irkutsk','rules'=>array(0=>array('e'=>1224957600,'s'=>1206813600,),1=>array('e'=>1256407200,'s'=>1238263200,),2=>array('e'=>1288461600,'s'=>1269712800,),3=>false,4=>false,5=>false,6=>false,),),15=>array('name'=>'Asia/Jerusalem','rules'=>array(0=>array('e'=>1223161200,'s'=>1206662400,),1=>array('e'=>1254006000,'s'=>1238112000,),2=>array('e'=>1284246000,'s'=>1269561600,),3=>array('e'=>1317510000,'s'=>1301616000,),4=>array('e'=>1348354800,'s'=>1333065600,),5=>array('e'=>1382828400,'s'=>1364515200,),6=>array('e'=>1414278000,'s'=>1395964800,),),),16=>array('name'=>'Asia/Kamchatka','rules'=>array(0=>array('e'=>1224943200,'s'=>1206799200,),1=>array('e'=>1256392800,'s'=>1238248800,),2=>array('e'=>1288450800,'s'=>1269698400,),3=>false,4=>false,5=>false,6=>false,),),17=>array('name'=>'Asia/Krasnoyarsk','rules'=>array(0=>array('e'=>1224961200,'s'=>1206817200,),1=>array('e'=>1256410800,'s'=>1238266800,),2=>array('e'=>1288465200,'s'=>1269716400,),3=>false,4=>false,5=>false,6=>false,),),18=>array('name'=>'Asia/Omsk','rules'=>array(0=>array('e'=>1224964800,'s'=>1206820800,),1=>array('e'=>1256414400,'s'=>1238270400,),2=>array('e'=>1288468800,'s'=>1269720000,),3=>false,4=>false,5=>false,6=>false,),),19=>array('name'=>'Asia/Vladivostok','rules'=>array(0=>array('e'=>1224950400,'s'=>1206806400,),1=>array('e'=>1256400000,'s'=>1238256000,),2=>array('e'=>1288454400,'s'=>1269705600,),3=>false,4=>false,5=>false,6=>false,),),20=>array('name'=>'Asia/Yakutsk','rules'=>array(0=>array('e'=>1224954000,'s'=>1206810000,),1=>array('e'=>1256403600,'s'=>1238259600,),2=>array('e'=>1288458000,'s'=>1269709200,),3=>false,4=>false,5=>false,6=>false,),),21=>array('name'=>'Asia/Yekaterinburg','rules'=>array(0=>array('e'=>1224968400,'s'=>1206824400,),1=>array('e'=>1256418000,'s'=>1238274000,),2=>array('e'=>1288472400,'s'=>1269723600,),3=>false,4=>false,5=>false,6=>false,),),22=>array('name'=>'Asia/Yerevan','rules'=>array(0=>array('e'=>1224972000,'s'=>1206828000,),1=>array('e'=>1256421600,'s'=>1238277600,),2=>array('e'=>1288476000,'s'=>1269727200,),3=>array('e'=>1319925600,'s'=>1301176800,),4=>false,5=>false,6=>false,),),23=>array('name'=>'Australia/Lord_Howe','rules'=>array(0=>array('e'=>1207407600,'s'=>1223134200,),1=>array('e'=>1238857200,'s'=>1254583800,),2=>array('e'=>1270306800,'s'=>1286033400,),3=>array('e'=>1301756400,'s'=>1317483000,),4=>array('e'=>1333206000,'s'=>1349537400,),5=>array('e'=>1365260400,'s'=>1380987000,),6=>array('e'=>1396710000,'s'=>1412436600,),),),24=>array('name'=>'Australia/Perth','rules'=>array(0=>array('e'=>1206813600,'s'=>1224957600,),1=>false,2=>false,3=>false,4=>false,5=>false,6=>false,),),25=>array('name'=>'Europe/Helsinki','rules'=>array(0=>array('e'=>1224982800,'s'=>1206838800,),1=>array('e'=>1256432400,'s'=>1238288400,),2=>array('e'=>1288486800,'s'=>1269738000,),3=>array('e'=>1319936400,'s'=>1301187600,),4=>array('e'=>1351386000,'s'=>1332637200,),5=>array('e'=>1382835600,'s'=>1364691600,),6=>array('e'=>1414285200,'s'=>1396141200,),),),26=>array('name'=>'Europe/Minsk','rules'=>array(0=>array('e'=>1224979200,'s'=>1206835200,),1=>array('e'=>1256428800,'s'=>1238284800,),2=>array('e'=>1288483200,'s'=>1269734400,),3=>false,4=>false,5=>false,6=>false,),),27=>array('name'=>'Europe/Moscow','rules'=>array(0=>array('e'=>1224975600,'s'=>1206831600,),1=>array('e'=>1256425200,'s'=>1238281200,),2=>array('e'=>1288479600,'s'=>1269730800,),3=>false,4=>false,5=>false,6=>false,),),28=>array('name'=>'Pacific/Apia','rules'=>array(0=>false,1=>false,2=>false,3=>array('e'=>1301752800,'s'=>1316872800,),4=>array('e'=>1333202400,'s'=>1348927200,),5=>array('e'=>1365256800,'s'=>1380376800,),6=>array('e'=>1396706400,'s'=>1411826400,),),),29=>array('name'=>'Pacific/Fiji','rules'=>array(0=>false,1=>false,2=>array('e'=>1269698400,'s'=>1287842400,),3=>array('e'=>1327154400,'s'=>1319292000,),4=>array('e'=>1358604000,'s'=>1350741600,),5=>array('e'=>1390050000,'s'=>1382796000,),6=>array('e'=>1421503200,'s'=>1414850400,),),),),);
 }
+
 ?>
diff --git a/include/class.topic.php b/include/class.topic.php
index 6dc368f4a2dc6a030172dec6d4a0d68ee6f28660..c25ab788a4288beaae3bd1163ecb72c6b2ea183f 100644
--- a/include/class.topic.php
+++ b/include/class.topic.php
@@ -15,15 +15,52 @@
 **********************************************************************/
 
 require_once INCLUDE_DIR . 'class.sequence.php';
-
-class Topic {
-    var $id;
-
-    var $ht;
-
-    var $parent;
-    var $page;
-    var $form;
+require_once INCLUDE_DIR . 'class.filter.php';
+
+class Topic extends VerySimpleModel
+implements TemplateVariable {
+
+    static $meta = array(
+        'table' => TOPIC_TABLE,
+        'pk' => array('topic_id'),
+        'ordering' => array('topic'),
+        'joins' => array(
+            'parent' => array(
+                'list' => false,
+                'constraint' => array(
+                    'topic_pid' => 'Topic.topic_id',
+                ),
+            ),
+            'faqs' => array(
+                'list' => true,
+                'reverse' => 'FaqTopic.topic'
+            ),
+            'page' => array(
+                'null' => true,
+                'constraint' => array(
+                    'page_id' => 'Page.id',
+                ),
+            ),
+            'dept' => array(
+                'null' => true,
+                'constraint' => array(
+                    'dept_id' => 'Dept.id',
+                ),
+            ),
+            'priority' => array(
+                'null' => true,
+                'constraint' => array(
+                    'priority_id' => 'Priority.priority_id',
+                ),
+            ),
+            'forms' => array(
+                'reverse' => 'TopicFormModel.topic',
+                'null' => true,
+            ),
+        ),
+    );
+
+    var $_forms;
 
     const DISPLAY_DISABLED = 2;
 
@@ -31,66 +68,51 @@ class Topic {
 
     const FLAG_CUSTOM_NUMBERS = 0x0001;
 
-    function Topic($id) {
-        $this->id=0;
-        $this->load($id);
-    }
-
-    function load($id=0) {
-        global $cfg;
-
-        if(!$id && !($id=$this->getId()))
-            return false;
-
-        $sql='SELECT ht.* '
-            .' FROM '.TOPIC_TABLE.' ht '
-            .' WHERE ht.topic_id='.db_input($id);
-
-        if(!($res=db_query($sql)) || !db_num_rows($res))
-            return false;
-
-        $this->ht = db_fetch_array($res);
-        $this->id = $this->ht['topic_id'];
-
-        $this->page = $this->form = null;
-
-        // Handle upgrade case where sort has not yet been defined
-        if (!$this->ht['sort'] && $cfg->getTopicSortMode() == 'a') {
-            static::updateSortOrder();
-        }
-
-        return true;
-    }
-
-    function reload() {
-        return $this->load();
-    }
+    const SORT_ALPHA = 'a';
+    const SORT_MANUAL = 'm';
 
     function asVar() {
         return $this->getName();
     }
 
+    static function getVarScope() {
+        return array(
+            'dept' => array(
+                'class' => 'Dept', 'desc' => __('Department'),
+            ),
+            'fullname' => __('Help topic full path'),
+            'name' => __('Help topic'),
+            'parent' => array(
+                'class' => 'Topic', 'desc' => __('Parent'),
+            ),
+            'sla' => array(
+                'class' => 'SLA', 'desc' => __('Service Level Agreement'),
+            ),
+        );
+    }
+
     function getId() {
-        return $this->id;
+        return $this->topic_id;
     }
 
     function getPid() {
-        return $this->ht['topic_pid'];
+        return $this->topic_pid;
     }
 
     function getParent() {
-        if(!$this->parent && $this->getPid())
-            $this->parent = self::lookup($this->getPid());
-
         return $this->parent;
     }
 
     function getName() {
-        return $this->ht['topic'];
+        return $this->topic;
+    }
+
+    function getLocalName() {
+        return $this->getLocal('name');
     }
 
     function getFullName() {
-        return self::getTopicName($this->getId());
+        return self::getTopicName($this->getId()) ?: $this->topic;
     }
 
     static function getTopicName($id) {
@@ -99,57 +121,51 @@ class Topic {
     }
 
     function getDeptId() {
-        return $this->ht['dept_id'];
+        return $this->dept_id;
     }
 
     function getSLAId() {
-        return $this->ht['sla_id'];
+        return $this->sla_id;
     }
 
     function getPriorityId() {
-        return $this->ht['priority_id'];
+        return $this->priority_id;
     }
 
     function getStatusId() {
-        return $this->ht['status_id'];
+        return $this->status_id;
     }
 
     function getStaffId() {
-        return $this->ht['staff_id'];
+        return $this->staff_id;
     }
 
     function getTeamId() {
-        return $this->ht['team_id'];
+        return $this->team_id;
     }
 
     function getPageId() {
-        return $this->ht['page_id'];
+        return $this->page_id;
     }
 
     function getPage() {
-        if(!$this->page && $this->getPageId())
-            $this->page = Page::lookup($this->getPageId());
-
         return $this->page;
     }
 
-    function getFormId() {
-        return $this->ht['form_id'];
-    }
-
-    function getForm() {
-        $id = $this->getFormId();
-
-        if ($id == self::FORM_USE_PARENT && ($p = $this->getParent()))
-            $this->form = $p->getForm();
-        elseif ($id && !$this->form)
-            $this->form = DynamicForm::lookup($id);
-
-        return $this->form;
+    function getForms() {
+        if (!isset($this->_forms)) {
+            $this->_forms = array();
+            foreach ($this->forms->select_related('form') as $F) {
+                $extra = JsonDataParser::decode($F->extra) ?: array();
+                $F->form->disableFields($extra['disable'] ?: array());
+                $this->_forms[] = $F->form;
+            }
+        }
+        return $this->_forms;
     }
 
     function autoRespond() {
-        return (!$this->ht['noautoresp']);
+        return !$this->noautoresp;
     }
 
     function isEnabled() {
@@ -170,7 +186,7 @@ class Topic {
      *      there is a loop in the ancestry
      */
     function isActive(array $chain=array()) {
-        if (!$this->ht['isactive'])
+        if (!$this->isactive)
             return false;
 
         if (!isset($chain[$this->getId()]) && ($p = $this->getParent())) {
@@ -178,12 +194,12 @@ class Topic {
             return $p->isActive($chain);
         }
         else {
-            return $this->ht['isactive'];
+            return $this->isactive;
         }
     }
 
     function isPublic() {
-        return ($this->ht['ispublic']);
+        return ($this->ispublic);
     }
 
     function getHashtable() {
@@ -197,7 +213,7 @@ class Topic {
     }
 
     function hasFlag($flag) {
-        return $this->ht['flags'] & $flag != 0;
+        return $this->flags & $flag != 0;
     }
 
     function getNewTicketNumber() {
@@ -206,76 +222,111 @@ class Topic {
         if (!$this->hasFlag(self::FLAG_CUSTOM_NUMBERS))
             return $cfg->getNewTicketNumber();
 
-        if ($this->ht['sequence_id'])
-            $sequence = Sequence::lookup($this->ht['sequence_id']);
+        if ($this->sequence_id)
+            $sequence = Sequence::lookup($this->sequence_id);
         if (!$sequence)
             $sequence = new RandomSequence();
 
-        return $sequence->next($this->ht['number_format'] ?: '######',
+        return $sequence->next($this->number_format ?: '######',
             array('Ticket', 'isTicketNumberUnique'));
     }
 
+    function getTranslateTag($subtag) {
+        return _H(sprintf('topic.%s.%s', $subtag, $this->getId()));
+    }
+    function getLocal($subtag) {
+        $tag = $this->getTranslateTag($subtag);
+        $T = CustomDataTranslation::translate($tag);
+        return $T != $tag ? $T : $this->ht[$subtag];
+    }
+
     function setSortOrder($i) {
-        if ($i != $this->ht['sort']) {
-            $sql = 'UPDATE '.TOPIC_TABLE.' SET `sort`='.db_input($i)
-                .' WHERE `topic_id`='.db_input($this->getId());
-            return (db_query($sql) && db_affected_rows() == 1);
+        if ($i != $this->sort) {
+            $this->sort = $i;
+            return $this->save();
         }
         // Noop
         return true;
     }
 
-    function update($vars, &$errors) {
-
-        if(!$this->save($this->getId(), $vars, $errors))
-            return false;
-
-        $this->reload();
-        return true;
-    }
-
     function delete() {
         global $cfg;
 
         if ($this->getId() == $cfg->getDefaultTopicId())
             return false;
 
-        $sql='DELETE FROM '.TOPIC_TABLE.' WHERE topic_id='.db_input($this->getId()).' LIMIT 1';
-        if(db_query($sql) && ($num=db_affected_rows())) {
-            db_query('UPDATE '.TOPIC_TABLE.' SET topic_pid=0 WHERE topic_pid='.db_input($this->getId()));
+        if (parent::delete()) {
+            self::objects()->filter(array(
+                'topic_pid' => $this->getId()
+            ))->update(array(
+                'topic_pid' => 0
+            ));
+            FaqTopic::objects()->filter(array(
+                'topic_id' => $this->getId()
+            ))->delete();
             db_query('UPDATE '.TICKET_TABLE.' SET topic_id=0 WHERE topic_id='.db_input($this->getId()));
-            db_query('DELETE FROM '.FAQ_TOPIC_TABLE.' WHERE topic_id='.db_input($this->getId()));
         }
 
-        return $num;
+        return true;
     }
+
+    function __toString() {
+        return $this->getFullName();
+    }
+
     /*** Static functions ***/
-    function create($vars, &$errors) {
-        return self::save(0, $vars, $errors);
+
+    static function create($vars=array()) {
+        $topic = new static($vars);
+        $topic->created = SqlFunction::NOW();
+        return $topic;
+    }
+
+    static function __create($vars, &$errors) {
+        $topic = self::create($vars);
+        if (!isset($vars['dept_id']))
+            $vars['dept_id'] = 0;
+        $vars['id'] = $vars['topic_id'];
+        $topic->update($vars, $errors);
+        return $topic;
     }
 
-    static function getHelpTopics($publicOnly=false, $disabled=false) {
+    static function getHelpTopics($publicOnly=false, $disabled=false, $localize=true) {
         global $cfg;
         static $topics, $names = array();
 
-        if (!$names) {
-            $sql = 'SELECT topic_id, topic_pid, ispublic, isactive, topic FROM '.TOPIC_TABLE
-                . ' ORDER BY `sort`';
-            $res = db_query($sql);
+        // If localization is specifically requested, then rebuild the list.
+        if (!$names || $localize) {
+            $objects = self::objects()->values_flat(
+                'topic_id', 'topic_pid', 'ispublic', 'isactive', 'topic'
+            )
+            ->order_by('sort');
 
             // Fetch information for all topics, in declared sort order
             $topics = array();
-            while (list($id, $pid, $pub, $act, $topic) = db_fetch_row($res))
+            foreach ($objects as $T) {
+                list($id, $pid, $pub, $act, $topic) = $T;
                 $topics[$id] = array('pid'=>$pid, 'public'=>$pub,
                     'disabled'=>!$act, 'topic'=>$topic);
+            }
+
+            $localize_this = function($id, $default) use ($localize) {
+                if (!$localize)
+                    return $default;
+
+                $tag = _H("topic.name.{$id}");
+                $T = CustomDataTranslation::translate($tag);
+                return $T != $tag ? $T : $default;
+            };
 
             // Resolve parent names
             foreach ($topics as $id=>$info) {
-                $name = $info['topic'];
+                $name = $localize_this($id, $info['topic']);
                 $loop = array($id=>true);
                 $parent = false;
-                while ($info['pid'] && ($info = $topics[$info['pid']])) {
-                    $name = sprintf('%s / %s', $info['topic'], $name);
+                while (($pid = $info['pid']) && ($info = $topics[$info['pid']])) {
+                    $name = sprintf('%s / %s', $localize_this($pid, $info['topic']),
+                        $name);
                     if ($parent && $parent['disabled'])
                         // Cascade disabled flag
                         $topics[$id]['disabled'] = true;
@@ -301,45 +352,53 @@ class Topic {
             $requested_names[$id] = $n;
         }
 
+        // If localization requested and the current locale is not the
+        // primary, the list may need to be sorted. Caching is ok here,
+        // because the locale is not going to be changed within a single
+        // request.
+        if ($localize && $cfg->getTopicSortMode() == self::SORT_ALPHA)
+            return Internationalization::sortKeyedList($requested_names);
+
         return $requested_names;
     }
 
-    function getPublicHelpTopics() {
+    static function getPublicHelpTopics() {
         return self::getHelpTopics(true);
     }
 
-    function getAllHelpTopics() {
-        return self::getHelpTopics(false, true);
+    static function getAllHelpTopics($localize=false) {
+        return self::getHelpTopics(false, true, $localize);
     }
 
-    function getIdByName($name, $pid=0) {
-
-        $sql='SELECT topic_id FROM '.TOPIC_TABLE
-            .' WHERE topic='.db_input($name)
-            .' AND topic_pid='.db_input($pid);
-        if(($res=db_query($sql)) && db_num_rows($res))
-            list($id) = db_fetch_row($res);
-
-        return $id;
+    static function getLocalNameById($id) {
+        $topics = static::getHelpTopics(false, true);
+        return $topics[$id];
     }
 
-    static function lookup($id) {
-        return ($id && is_numeric($id) && ($t= new Topic($id)) && $t->getId()==$id)?$t:null;
+    static function getIdByName($name, $pid=0) {
+        $list = self::objects()->filter(array(
+            'topic'=>$name,
+            'topic_pid'=>$pid,
+        ))->values_flat('topic_id')->first();
+
+        if ($list)
+            return $list[0];
     }
 
-    function save($id, $vars, &$errors) {
+    function update($vars, &$errors) {
         global $cfg;
 
-        $vars['topic']=Format::striptags(trim($vars['topic']));
+        $vars['topic'] = Format::striptags(trim($vars['topic']));
 
-        if($id && $id!=$vars['id'])
+        if (isset($this->topic_id) && $this->getId() != $vars['id'])
             $errors['err']=__('Internal error occurred');
 
-        if(!$vars['topic'])
+        if (!$vars['topic'])
             $errors['topic']=__('Help topic name is required');
-        elseif(strlen($vars['topic'])<5)
+        elseif (strlen($vars['topic'])<5)
             $errors['topic']=__('Topic is too short. Five characters minimum');
-        elseif(($tid=self::getIdByName($vars['topic'], $vars['topic_pid'])) && $tid!=$id)
+        elseif (($tid=self::getIdByName($vars['topic'], $vars['topic_pid']))
+                && (!isset($this->topic_id) || $tid!=$this->getId()))
             $errors['topic']=__('Topic already exists');
 
         if (!is_numeric($vars['dept_id']))
@@ -349,73 +408,131 @@ class Topic {
             $errors['number_format'] =
                 'Ticket number format requires at least one hash character (#)';
 
-        if($errors) return false;
-
-        foreach (array('sla_id','form_id','page_id','topic_pid') as $f)
-            if (!isset($vars[$f]))
-                $vars[$f] = 0;
-
-        $sql=' updated=NOW() '
-            .',topic='.db_input($vars['topic'])
-            .',topic_pid='.db_input($vars['topic_pid'])
-            .',dept_id='.db_input($vars['dept_id'])
-            .',priority_id='.db_input($vars['priority_id'])
-            .',status_id='.db_input($vars['status_id'])
-            .',sla_id='.db_input($vars['sla_id'])
-            .',form_id='.db_input($vars['form_id'])
-            .',page_id='.db_input($vars['page_id'])
-            .',isactive='.db_input($vars['isactive'])
-            .',ispublic='.db_input($vars['ispublic'])
-            .',sequence_id='.db_input($vars['custom-numbers'] ? $vars['sequence_id'] : 0)
-            .',number_format='.db_input($vars['custom-numbers'] ? $vars['number_format'] : '')
-            .',flags='.db_input($vars['custom-numbers'] ? self::FLAG_CUSTOM_NUMBERS : 0)
-            .',noautoresp='.db_input(isset($vars['noautoresp']) && $vars['noautoresp']?1:0)
-            .',notes='.db_input(Format::sanitize($vars['notes']));
+        if ($errors)
+            return false;
+
+        $this->topic = $vars['topic'];
+        $this->topic_pid = $vars['topic_pid'] ?: 0;
+        $this->dept_id = $vars['dept_id'];
+        $this->priority_id = $vars['priority_id'] ?: 0;
+        $this->status_id = $vars['status_id'] ?: 0;
+        $this->sla_id = $vars['sla_id'] ?: 0;
+        $this->page_id = $vars['page_id'] ?: 0;
+        $this->isactive = !!$vars['isactive'];
+        $this->ispublic = !!$vars['ispublic'];
+        $this->sequence_id = $vars['custom-numbers'] ? $vars['sequence_id'] : 0;
+        $this->number_format = $vars['custom-numbers'] ? $vars['number_format'] : '';
+        $this->flags = $vars['custom-numbers'] ? self::FLAG_CUSTOM_NUMBERS : 0;
+        $this->noautoresp = !!$vars['noautoresp'];
+        $this->notes = Format::sanitize($vars['notes']);
 
         //Auto assign ID is overloaded...
-        if($vars['assign'] && $vars['assign'][0]=='s')
-             $sql.=',team_id=0, staff_id='.db_input(preg_replace("/[^0-9]/", "", $vars['assign']));
-        elseif($vars['assign'] && $vars['assign'][0]=='t')
-            $sql.=',staff_id=0, team_id='.db_input(preg_replace("/[^0-9]/", "", $vars['assign']));
-        else
-            $sql.=',staff_id=0, team_id=0 '; //no auto-assignment!
+        if ($vars['assign'] && $vars['assign'][0] == 's') {
+            $this->team_id = 0;
+            $this->staff_id = preg_replace("/[^0-9]/", "", $vars['assign']);
+        }
+        elseif ($vars['assign'] && $vars['assign'][0] == 't') {
+            $this->staff_id = 0;
+            $this->team_id = preg_replace("/[^0-9]/", "", $vars['assign']);
+        }
+        else {
+            $this->staff_id = 0;
+            $this->team_id = 0;
+        }
 
         $rv = false;
-        if ($id) {
-            $sql='UPDATE '.TOPIC_TABLE.' SET '.$sql.' WHERE topic_id='.db_input($id);
-            if (!($rv = db_query($sql)))
-                $errors['err']=sprintf(__('Unable to update %s.'), __('this help topic'))
-                .' '.__('Internal error occurred');
-        } else {
-            if (isset($vars['topic_id']))
-                $sql .= ', topic_id='.db_input($vars['topic_id']);
-            // If in manual sort mode, place the new item directly below the
-            // parent item
-            if ($vars['topic_pid'] && $cfg && $cfg->getTopicSortMode() != 'a') {
-                $sql .= ', `sort`='.db_input(
-                    db_result(db_query('SELECT COALESCE(`sort`,0)+1 FROM '.TOPIC_TABLE
-                        .' WHERE `topic_id`='.db_input($vars['topic_pid']))));
+        if ($this->__new__) {
+            if (isset($this->topic_pid)
+                    && ($parent = Topic::lookup($this->topic_pid))) {
+                $this->sort = ($parent->sort ?: 0) + 1;
             }
-
-            $sql='INSERT INTO '.TOPIC_TABLE.' SET '.$sql.',created=NOW()';
-            if (db_query($sql) && ($id = db_insert_id()))
-                $rv = $id;
-            else
+            if (!($rv = $this->save())) {
                 $errors['err']=sprintf(__('Unable to create %s.'), __('this help topic'))
                .' '.__('Internal error occurred');
+            }
+        }
+        elseif (!($rv = $this->save())) {
+            $errors['err']=sprintf(__('Unable to update %s.'), __('this help topic'))
+            .' '.__('Internal error occurred');
         }
-        if (!$cfg || $cfg->getTopicSortMode() == 'a') {
-            static::updateSortOrder();
+        if ($rv) {
+            if (!$cfg || $cfg->getTopicSortMode() == 'a') {
+                static::updateSortOrder();
+            }
+            $this->updateForms($vars, $errors);
         }
         return $rv;
     }
 
+    function updateForms($vars, &$errors) {
+        $find_disabled = function($form) use ($vars) {
+            $fields = $vars['fields'];
+            $disabled = array();
+            foreach ($form->fields->values_flat('id') as $row) {
+                list($id) = $row;
+                if (false === ($idx = array_search($id, $fields))) {
+                    $disabled[] = $id;
+                }
+            }
+            return $disabled;
+        };
+
+        // Consider all the forms in the request
+        $current = array();
+        if (is_array($form_ids = $vars['forms'])) {
+            $forms = TopicFormModel::objects()
+                ->select_related('form')
+                ->filter(array('topic_id' => $this->getId()));
+            foreach ($forms as $F) {
+                if (false !== ($idx = array_search($F->form_id, $form_ids))) {
+                    $current[] = $F->form_id;
+                    $F->sort = $idx + 1;
+                    $F->extra = JsonDataEncoder::encode(
+                        array('disable' => $find_disabled($F->form))
+                    );
+                    $F->save();
+                    unset($form_ids[$idx]);
+                }
+                elseif ($F->form->get('type') != 'T') {
+                    $F->delete();
+                }
+            }
+            foreach ($form_ids as $sort=>$id) {
+                if (!($form = DynamicForm::lookup($id))) {
+                    continue;
+                }
+                elseif (in_array($id, $current)) {
+                    // Don't add a form more than once
+                    continue;
+                }
+                $tf = new TopicFormModel(array(
+                    'topic_id' => $this->getId(),
+                    'form_id' => $id,
+                    'sort' => $sort + 1,
+                    'extra' => JsonDataEncoder::encode(
+                        array('disable' => $find_disabled($form))
+                    )
+                ));
+                $tf->save();
+            }
+        }
+        return true;
+    }
+
+    function save($refetch=false) {
+        if ($this->dirty)
+            $this->updated = SqlFunction::NOW();
+        return parent::save($refetch || $this->dirty);
+    }
+
     static function updateSortOrder() {
+        global $cfg;
+
         // Fetch (un)sorted names
-        if (!($names = static::getHelpTopics(false, true)))
+        if (!($names = static::getHelpTopics(false, true, false)))
             return;
 
-        uasort($names, function($a, $b) { return strcasecmp($a, $b); });
+        $names = Internationalization::sortKeyedList($names);
 
         $update = array_keys($names);
         foreach ($update as $idx=>&$id) {
@@ -434,3 +551,19 @@ class Topic {
 
 // Add fields from the standard ticket form to the ticket filterable fields
 Filter::addSupportedMatches(/* @trans */ 'Help Topic', array('topicId' => 'Topic ID'), 100);
+
+class TopicFormModel extends VerySimpleModel {
+    static $meta = array(
+        'table' => TOPIC_FORM_TABLE,
+        'pk' => array('id'),
+        'ordering' => array('sort'),
+        'joins' => array(
+            'topic' => array(
+                'constraint' => array('topic_id' => 'Topic.topic_id'),
+            ),
+            'form' => array(
+                'constraint' => array('form_id' => 'DynamicForm.id'),
+            ),
+        ),
+    );
+}
diff --git a/include/class.translation.php b/include/class.translation.php
index 0f0924ed299a2adabae8e56156085d6b0e7781a1..2e864b39383fd0dc26b4244eb4f68e304eb532d8 100644
--- a/include/class.translation.php
+++ b/include/class.translation.php
@@ -119,7 +119,7 @@ class gettext_reader {
    * @param object Reader the StreamReader object
    * @param boolean enable_cache Enable or disable caching of strings (default on)
    */
-  function gettext_reader($Reader, $enable_cache = true) {
+  function __construct($Reader, $enable_cache = true) {
     // If there isn't a StreamReader, turn on short circuit mode.
     if (! $Reader || isset($Reader->error) ) {
       $this->short_circuit = true;
@@ -462,7 +462,7 @@ class FileReader {
   var $_fd;
   var $_length;
 
-  function FileReader($filename) {
+  function __construct($filename) {
     if (is_resource($filename)) {
         $this->_length = strlen(stream_get_contents($filename));
         rewind($filename);
@@ -651,16 +651,16 @@ class Translation extends gettext_reader implements Serializable {
     }
 
     static function resurrect($key) {
-        if (!function_exists('apc_fetch'))
+        if (!function_exists('apcu_fetch'))
             return false;
 
         $success = true;
-        if (($translation = apc_fetch($key, $success)) && $success)
+        if (($translation = apcu_fetch($key, $success)) && $success)
             return $translation;
     }
     function cache($key) {
-        if (function_exists('apc_add'))
-            apc_add($key, $this);
+        if (function_exists('apcu_add'))
+            apcu_add($key, $this);
     }
 
 
@@ -759,8 +759,7 @@ class TextDomain {
 
     static function configureForUser($user=false) {
         $lang = Internationalization::getCurrentLanguage($user);
-
-        $info = Internationalization::getLanguageInfo(strtolower($lang));
+        $info = Internationalization::getLanguageInfo($lang);
         if (!$info)
             // Not a supported language
             return;
@@ -855,6 +854,198 @@ class TextDomain {
     }
 }
 
+require_once INCLUDE_DIR . 'class.orm.php';
+class CustomDataTranslation extends VerySimpleModel {
+
+    static $meta = array(
+        'table' => TRANSLATION_TABLE,
+        'pk' => array('id')
+    );
+
+    const FLAG_FUZZY        = 0x01;     // Source string has been changed
+    const FLAG_UNAPPROVED   = 0x02;     // String has been reviewed by an authority
+    const FLAG_CURRENT      = 0x04;     // If more than one version exist, this is current
+    const FLAG_COMPLEX      = 0x08;     // Multiple strings in one translation. For instance article title and body
+
+    var $_complex;
+
+    static function lookup($msgid, $flags=0) {
+        if (!is_string($msgid))
+            return parent::lookup($msgid);
+
+        // Hash is 16 char of md5
+        $hash = substr(md5($msgid), -16);
+
+        $criteria = array('object_hash'=>$hash);
+
+        if ($flags)
+            $criteria += array('flags__hasbit'=>$flags);
+
+        return parent::lookup($criteria);
+    }
+
+    static function getTranslation($locale, $cache=true) {
+        static $_cache = array();
+
+        if ($cache && isset($_cache[$locale]))
+            return $_cache[$locale];
+
+        $criteria = array(
+            'lang' => $locale,
+            'type' => 'phrase',
+        );
+
+        $mo = array();
+        foreach (static::objects()->filter($criteria) as $t) {
+            $mo[$t->object_hash] = $t;
+        }
+
+        return $_cache[$locale] = $mo;
+    }
+
+    static function translate($msgid, $locale=false, $cache=true, $type='phrase') {
+        global $thisstaff, $thisclient;
+
+        // Support sending a User as the locale
+        if (is_object($locale) && method_exists($locale, 'getLanguage'))
+            $locale = $locale->getLanguage();
+        elseif (!$locale)
+            $locale = Internationalization::getCurrentLanguage();
+
+        // Perhaps a slight optimization would be to check if the selected
+        // locale is also the system primary. If so, short-circuit
+
+        if ($locale) {
+            if ($cache) {
+                $mo = static::getTranslation($locale);
+                if (isset($mo[$msgid]))
+                    $msgid = $mo[$msgid]->text;
+            }
+            elseif ($p = static::lookup(array(
+                    'type' => $type,
+                    'lang' => $locale,
+                    'object_hash' => $msgid
+            ))) {
+                $msgid = $p->text;
+            }
+        }
+        return $msgid;
+    }
+
+    /**
+     * Decode complex translation message. Format is given in the $text
+     * parameter description. Complex data should be stored with the
+     * FLAG_COMPLEX flag set, and allows for complex key:value paired data
+     * to be translated. This is useful for strings which are translated
+     * together, such as the title and the body of an article. Storing the
+     * data in a single, complex record allows for a single database query
+     * to fetch or update all data for a particular object, such as a
+     * knowledgebase article. It also simplifies search indexing as only one
+     * translation record could be added for all the translatable elements
+     * for a single translatable object.
+     *
+     * Caveats:
+     * ::$text will return the stored, complex text. Use ::getComplex() to
+     * decode the complex storage format and retrieve the array.
+     *
+     * Parameters:
+     * $text - (string) - encoded text with the following format
+     *      version \x03 key \x03 item1 \x03 key \x03 item2 ...
+     *
+     * Returns:
+     * (array) key:value pairs of translated content
+     */
+    function decodeComplex($text) {
+        $blocks = explode("\x03", $text);
+        $version = array_shift($blocks);
+
+        $data = array();
+        switch ($version) {
+        case 'A':
+            while (count($blocks) > 1) {
+                $key = array_shift($blocks);
+                $data[$key] = array_shift($blocks);
+            }
+            break;
+        default:
+            throw new Exception($version . ': Unknown complex format');
+        }
+
+        return $data;
+    }
+
+    /**
+     * Encode complex content using the format outlined in ::decodeComplex.
+     *
+     * Caveats:
+     * This method does not set the FLAG_COMPLEX flag for this record, which
+     * should be set when storing complex data.
+     */
+    static function encodeComplex(array $data) {
+        $encoded = 'A';
+        foreach ($data as $key=>$text) {
+            $encoded .= "\x03{$key}\x03{$text}";
+        }
+        return $encoded;
+    }
+
+    function getComplex() {
+        if (!$this->flags && self::FLAG_COMPLEX)
+            throw new Exception('Data consistency error. Translation is not complex');
+        if (!isset($this->_complex))
+            $this->_complex = $this->decodeComplex($this->text);
+        return $this->_complex;
+    }
+
+    static function translateArticle($msgid, $locale=false) {
+        return static::translate($msgid, $locale, false, 'article');
+    }
+
+    function save($refetch=false) {
+        if (isset($this->text) && is_array($this->text)) {
+            $this->text = static::encodeComplex($this->text);
+            $this->flags |= self::FLAG_COMPLEX;
+        }
+        return parent::save($refetch);
+    }
+
+    static function create($ht=false) {
+        if (!is_array($ht))
+            return null;
+
+        if (is_array($ht['text'])) {
+            // The parent constructor does not honor arrays
+            $ht['text'] = static::encodeComplex($ht['text']);
+            $ht['flags'] = ($ht['flags'] ?: 0) | self::FLAG_COMPLEX;
+        }
+        return new static($ht);
+    }
+
+    static function allTranslations($msgid, $type='phrase', $lang=false) {
+        $criteria = array('type' => $type);
+
+        if (is_array($msgid))
+            $criteria['object_hash__in'] = $msgid;
+        else
+            $criteria['object_hash'] = $msgid;
+
+        if ($lang)
+            $criteria['lang'] = $lang;
+
+        try {
+            return static::objects()->filter($criteria)->all();
+        }
+        catch (OrmException $e) {
+            // Translation table might not exist yet — happens on the upgrader
+            return array();
+        }
+    }
+}
+
+class CustomTextDomain {
+
+}
+
 // Functions for gettext library. Since the gettext extension for PHP is not
 // used as a fallback, there is no detection and compat funciton
 // installation for the gettext library function calls.
@@ -910,6 +1101,15 @@ function _dcnpgettext($domain, $context, $singular, $plural, $category, $n) {
         ->npgettext($context, $singular, $plural, $n);
 }
 
+// Custom data translations
+function _H($tag) {
+    return substr(md5($tag), -16);
+}
+
+interface Translatable {
+    function getTranslationTag();
+    function getLocalName($user=false);
+}
 
 do {
   if (PHP_SAPI != 'cli') break;
diff --git a/include/class.upgrader.php b/include/class.upgrader.php
index 6246e542d0b524b3dec26364297a506d9aab18ce..87fb63c89be95b0cab01f8beb838a111e1b06841 100644
--- a/include/class.upgrader.php
+++ b/include/class.upgrader.php
@@ -18,7 +18,7 @@ require_once INCLUDE_DIR.'class.setup.php';
 require_once INCLUDE_DIR.'class.migrater.php';
 
 class Upgrader {
-    function Upgrader($prefix, $basedir) {
+    function __construct($prefix, $basedir) {
         global $ost;
 
         $this->streams = array();
@@ -72,8 +72,10 @@ class Upgrader {
 
     function setState($state) {
         $this->state = $state;
-        if ($state == 'done')
+        if ($state == 'done') {
+            ModelMeta::flushModelCache();
             $this->createUpgradedTicket();
+        }
     }
 
     function createUpgradedTicket() {
@@ -112,44 +114,6 @@ class Upgrader {
             return call_user_func_array($callable, $args);
         }
     }
-
-    function getTask() {
-        if($this->getCurrentStream())
-            return $this->getCurrentStream()->getTask();
-    }
-
-    function doTask() {
-        return $this->getCurrentStream()->doTask();
-    }
-
-    function getErrors() {
-        if ($this->getCurrentStream())
-            return $this->getCurrentStream()->getErrors();
-    }
-
-    function getUpgradeSummary() {
-        if ($this->getCurrentStream())
-            return $this->getCurrentStream()->getUpgradeSummary();
-    }
-
-    function getNextAction() {
-        if ($this->getCurrentStream())
-            return $this->getCurrentStream()->getNextAction();
-    }
-
-    function getNextVersion() {
-        return $this->getCurrentStream()->getNextVersion();
-    }
-
-    function getSchemaSignature() {
-        if ($this->getCurrentStream())
-            return $this->getCurrentStream()->getSchemaSignature();
-    }
-
-    function getSHash() {
-        if ($this->getCurrentStream())
-            return $this->getCurrentStream()->getSHash();
-    }
 }
 
 /**
@@ -183,7 +147,7 @@ class StreamUpgrader extends SetupWizard {
      * sqldir - (string<path>) Path of sql patches
      * upgrader - (Upgrader) Parent coordinator of parallel stream updates
      */
-    function StreamUpgrader($schema_signature, $target, $stream, $prefix, $sqldir, $upgrader) {
+    function __construct($schema_signature, $target, $stream, $prefix, $sqldir, $upgrader) {
 
         $this->signature = $schema_signature;
         $this->target = $target;
@@ -348,7 +312,8 @@ class StreamUpgrader extends SetupWizard {
         if (!isset($this->task)) {
             $class = (include $task_file);
             if (!is_string($class) || !class_exists($class))
-                return $ost->logError("Bogus migration task", "{$this->phash}:{$class}") ;
+                return $ost->logError("Bogus migration task",
+                        "{$this->phash}:{$class}"); //FIXME: This can cause crash
             $this->task = new $class();
             if (isset($_SESSION['ost_upgrader']['task'][$this->phash]))
                 $this->task->wakeup($_SESSION['ost_upgrader']['task'][$this->phash]);
@@ -369,6 +334,10 @@ class StreamUpgrader extends SetupWizard {
         if(!($max_time = ini_get('max_execution_time')))
             $max_time = 30; //Default to 30 sec batches.
 
+        // Drop any model meta cache to ensure model changes do not cause
+        // crashes
+        ModelMeta::flushModelCache();
+
         $task->run($max_time);
         if (!$task->isFinished()) {
             $_SESSION['ost_upgrader']['task'][$this->phash] = $task->sleep();
diff --git a/include/class.user.php b/include/class.user.php
index c563f24b11899758abdd6f256ffc9fa3ed0cb865..82e1f9cfeea202b79ea971050a1076084bbc70ff 100644
--- a/include/class.user.php
+++ b/include/class.user.php
@@ -16,6 +16,8 @@
 **********************************************************************/
 require_once INCLUDE_DIR . 'class.orm.php';
 require_once INCLUDE_DIR . 'class.util.php';
+require_once INCLUDE_DIR . 'class.organization.php';
+require_once INCLUDE_DIR . 'class.variable.php';
 
 class UserEmailModel extends VerySimpleModel {
     static $meta = array(
@@ -29,34 +31,7 @@ class UserEmailModel extends VerySimpleModel {
     );
 
     function __toString() {
-        return $this->address;
-    }
-}
-
-class TicketModel extends VerySimpleModel {
-    static $meta = array(
-        'table' => TICKET_TABLE,
-        'pk' => array('ticket_id'),
-        'joins' => array(
-            'user' => array(
-                'constraint' => array('user_id' => 'UserModel.id')
-            ),
-            'status' => array(
-                'constraint' => array('status_id' => 'TicketStatus.id')
-            )
-        )
-    );
-
-    function getId() {
-        return $this->ticket_id;
-    }
-
-    function delete() {
-
-        if (($ticket=Ticket::lookup($this->getId())) && @$ticket->delete())
-            return true;
-
-        return false;
+        return (string) $this->address;
     }
 }
 
@@ -64,41 +39,77 @@ class UserModel extends VerySimpleModel {
     static $meta = array(
         'table' => USER_TABLE,
         'pk' => array('id'),
+        'select_related' => array('default_email', 'org', 'account'),
         'joins' => array(
             'emails' => array(
                 'reverse' => 'UserEmailModel.user',
             ),
             'tickets' => array(
+                'null' => true,
                 'reverse' => 'TicketModel.user',
             ),
             'account' => array(
                 'list' => false,
-                'reverse' => 'UserAccount.user',
+                'null' => true,
+                'reverse' => 'ClientAccount.user',
             ),
             'org' => array(
+                'null' => true,
                 'constraint' => array('org_id' => 'Organization.id')
             ),
             'default_email' => array(
                 'null' => true,
                 'constraint' => array('default_email_id' => 'UserEmailModel.id')
             ),
+            'cdata' => array(
+                'constraint' => array('id' => 'UserCdata.user_id'),
+                'null' => true,
+            ),
             'cdata_entry' => array(
                 'constraint' => array(
                     'id' => 'DynamicFormEntry.object_id',
                     "'U'" => 'DynamicFormEntry.object_type',
                 ),
-                null => true,
+                'null' => true,
             ),
         )
     );
 
     const PRIMARY_ORG_CONTACT   = 0x0001;
 
-    static function objects() {
-        $qs = parent::objects();
-        #$qs->select_related('default_email');
-        return $qs;
-    }
+    const PERM_CREATE =     'user.create';
+    const PERM_EDIT =       'user.edit';
+    const PERM_DELETE =     'user.delete';
+    const PERM_MANAGE =     'user.manage';
+    const PERM_DIRECTORY =  'user.dir';
+
+    static protected $perms = array(
+        self::PERM_CREATE => array(
+            'title' => /* @trans */ 'Create',
+            'desc' => /* @trans */ 'Ability to add new users',
+            'primary' => true,
+        ),
+        self::PERM_EDIT => array(
+            'title' => /* @trans */ 'Edit',
+            'desc' => /* @trans */ 'Ability to manage user information',
+            'primary' => true,
+        ),
+        self::PERM_DELETE => array(
+            'title' => /* @trans */ 'Delete',
+            'desc' => /* @trans */ 'Ability to delete users',
+            'primary' => true,
+        ),
+        self::PERM_MANAGE => array(
+            'title' => /* @trans */ 'Manage Account',
+            'desc' => /* @trans */ 'Ability to manage active user accounts',
+            'primary' => true,
+        ),
+        self::PERM_DIRECTORY => array(
+            'title' => /* @trans */ 'User Directory',
+            'desc' => /* @trans */ 'Ability to access the user directory',
+            'primary' => true,
+        ),
+    );
 
     function getId() {
         return $this->id;
@@ -112,6 +123,9 @@ class UserModel extends VerySimpleModel {
         return $this->default_email;
     }
 
+    function hasAccount() {
+        return !is_null($this->account);
+    }
     function getAccount() {
         return $this->account;
     }
@@ -156,22 +170,43 @@ class UserModel extends VerySimpleModel {
         else
             $this->clearStatus(User::PRIMARY_ORG_CONTACT);
     }
+
+    static function getPermissions() {
+        return self::$perms;
+    }
 }
+include_once INCLUDE_DIR.'class.role.php';
+RolePermission::register(/* @trans */ 'Users', UserModel::getPermissions());
 
-class User extends UserModel {
+class UserCdata extends VerySimpleModel {
+    static $meta = array(
+        'table' => USER_CDATA_TABLE,
+        'pk' => array('user_id'),
+        'joins' => array(
+            'user' => array(
+                'constraint' => array('user_id' => 'UserModel.id'),
+            ),
+        ),
+    );
+}
+
+class User extends UserModel
+implements TemplateVariable {
 
     var $_entries;
     var $_forms;
 
-    static function fromVars($vars, $update=false) {
+    static function fromVars($vars, $create=true, $update=false) {
         // Try and lookup by email address
         $user = static::lookupByEmail($vars['email']);
-        if (!$user) {
+        if (!$user && $create) {
             $name = $vars['name'];
-            if (!$name)
+            if (is_array($name))
+                $name = implode(', ', $name);
+            elseif (!$name)
                 list($name) = explode('@', $vars['email'], 2);
 
-            $user = User::create(array(
+            $user = new User(array(
                 'name' => Format::htmldecode(Format::sanitize($name, false)),
                 'created' => new SqlFunction('NOW'),
                 'updated' => new SqlFunction('NOW'),
@@ -195,6 +230,7 @@ class User extends UserModel {
             catch (OrmException $e) {
                 return null;
             }
+            Signal::send('user.created', $user);
         }
         elseif ($update) {
             $errors = array();
@@ -204,7 +240,7 @@ class User extends UserModel {
         return $user;
     }
 
-    static function fromForm($form) {
+    static function fromForm($form, $create=true) {
         global $thisstaff;
 
         if(!$form) return null;
@@ -225,11 +261,17 @@ class User extends UserModel {
             $valid = false;
         }
 
-        return $valid ? self::fromVars($form->getClean()) : null;
+        return $valid ? self::fromVars($form->getClean(), $create) : null;
     }
 
     function getEmail() {
-        return $this->default_email->address;
+        return new EmailAddress($this->default_email->address);
+    }
+
+    function getAvatar() {
+        global $cfg;
+        $source = $cfg->getClientAvatarSource();
+        return $source->getAvatar($this);
     }
 
     function getFullName() {
@@ -247,7 +289,7 @@ class User extends UserModel {
             list($name) = explode('@', $this->getDefaultEmailAddress(), 2);
         else
             $name = $this->name;
-        return new PersonsName($name);
+        return new UsersName($name);
     }
 
     function getUpdateDate() {
@@ -258,6 +300,15 @@ class User extends UserModel {
         return $this->created;
     }
 
+    function getTimezone() {
+        global $cfg;
+
+        if (($acct = $this->getAccount()) && ($tz = $acct->getTimezone())) {
+            return $tz;
+        }
+        return $cfg->getDefaultTimezone();
+    }
+
     function addForm($form, $sort=1, $data=null) {
         $entry = $form->instanciate($sort, $data);
         $entry->set('object_type', 'U');
@@ -266,6 +317,11 @@ class User extends UserModel {
         return $entry;
     }
 
+    function getLanguage($flags=false) {
+        if ($acct = $this->getAccount())
+            return $acct->getLanguage($flags);
+    }
+
     function to_json() {
 
         $info = array(
@@ -286,26 +342,33 @@ class User extends UserModel {
     }
 
     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;
     }
 
+    static function getVarScope() {
+        $base = array(
+            'email' => array(
+                'class' => 'EmailAddress', 'desc' => __('Default email address')
+            ),
+            'name' => array(
+                'class' => 'PersonsName', 'desc' => 'User name, default format'
+            ),
+            'organization' => array('class' => 'Organization', 'desc' => __('Organization')),
+        );
+        $extra = VariableReplacer::compileFormScope(UserForm::getInstance());
+        return $base + $extra;
+    }
+
     function addDynamicData($data) {
-        $entry = $this->addForm(UserForm::objects()->one(), 1, $data);
-        // FIXME: For some reason, the second save here is required or the
-        //        custom data is not properly saved
-        $entry->save();
-        return $entry;
+        return $this->addForm(UserForm::objects()->one(), 1, $data);
     }
 
     function getDynamicData($create=true) {
         if (!isset($this->_entries)) {
-            $this->_entries = DynamicFormEntry::forClient($this->id)->all();
+            $this->_entries = DynamicFormEntry::forObject($this->id, 'U')->all();
             if (!$this->_entries && $create) {
                 $g = UserForm::getNewInstance();
                 $g->setClientId($this->id);
@@ -320,12 +383,12 @@ class User extends UserModel {
     function getFilterData() {
         $vars = array();
         foreach ($this->getDynamicData() as $entry) {
-            if ($entry->getForm()->get('type') != 'U')
+            if ($entry->getDynamicForm()->get('type') != 'U')
                 continue;
             $vars += $entry->getFilterData();
             // Add in special `name` and `email` fields
             foreach (array('name', 'email') as $name) {
-                if ($f = $entry->getForm()->getField($name))
+                if ($f = $entry->getField($name))
                     $vars['field.'.$f->get('id')] =
                         $name == 'name' ? $this->getName() : $this->getEmail();
             }
@@ -337,12 +400,12 @@ class User extends UserModel {
 
         if (!isset($this->_forms)) {
             $this->_forms = array();
-            foreach ($this->getDynamicData() as $cd) {
-                $cd->addMissingFields();
+            foreach ($this->getDynamicData() as $entry) {
+                $entry->addMissingFields();
                 if(!$data
-                        && ($form = $cd->getForm())
+                        && ($form = $entry->getDynamicForm())
                         && $form->get('type') == 'U' ) {
-                    foreach ($cd->getFields() as $f) {
+                    foreach ($entry->getFields() as $f) {
                         if ($f->get('name') == 'name')
                             $f->value = $this->getFullName();
                         elseif ($f->get('name') == 'email')
@@ -350,7 +413,7 @@ class User extends UserModel {
                     }
                 }
 
-                $this->_forms[] = $cd;
+                $this->_forms[] = $entry;
             }
         }
 
@@ -365,6 +428,12 @@ class User extends UserModel {
         return (string) $account->getStatus();
     }
 
+    function canSeeOrgTickets() {
+        return $this->org && (
+                $this->org->shareWithEverybody()
+            || ($this->isPrimaryContact() && $this->org->shareWithPrimaryContacts()));
+    }
+
     function register($vars, &$errors) {
 
         // user already registered?
@@ -375,130 +444,31 @@ class User extends UserModel {
     }
 
     static function importCsv($stream, $defaults=array()) {
-        //Read the header (if any)
-        $headers = array('name' => __('Full Name'), 'email' => __('Email Address'));
-        $uform = UserForm::getUserForm();
-        $all_fields = $uform->getFields();
-        $named_fields = array();
-        $has_header = true;
-        foreach ($all_fields as $f)
-            if ($f->get('name'))
-                $named_fields[] = $f;
-
-        if (!($data = fgetcsv($stream, 1000, ",")))
-            return __('Whoops. Perhaps you meant to send some CSV records');
-
-        if (Validator::is_email($data[1])) {
-            $has_header = false; // We don't have an header!
-        }
-        else {
-            $headers = array();
-            foreach ($data as $h) {
-                $found = false;
-                foreach ($all_fields as $f) {
-                    if (in_array(mb_strtolower($h), array(
-                            mb_strtolower($f->get('name')), mb_strtolower($f->get('label'))))) {
-                        $found = true;
-                        if (!$f->get('name'))
-                            return sprintf(__(
-                                '%s: Field must have `variable` set to be imported'), $h);
-                        $headers[$f->get('name')] = $f->get('label');
-                        break;
-                    }
-                }
-                if (!$found) {
-                    $has_header = false;
-                    if (count($data) == count($named_fields)) {
-                        // Number of fields in the user form matches the number
-                        // of fields in the data. Assume things line up
-                        $headers = array();
-                        foreach ($named_fields as $f)
-                            $headers[$f->get('name')] = $f->get('label');
-                        break;
-                    }
-                    else {
-                        return sprintf(__('%s: Unable to map header to a user field'), $h);
-                    }
-                }
-            }
-        }
-
-        // 'name' and 'email' MUST be in the headers
-        if (!isset($headers['name']) || !isset($headers['email']))
-            return __('CSV file must include `name` and `email` columns');
-
-        if (!$has_header)
-            fseek($stream, 0);
-
-        $users = $fields = $keys = array();
-        foreach ($headers as $h => $label) {
-            if (!($f = $uform->getField($h)))
-                continue;
-
-            $name = $keys[] = $f->get('name');
-            $fields[$name] = $f->getImpl();
-        }
-
-        // Add default fields (org_id, etc).
-        foreach ($defaults as $key => $val) {
-            // Don't apply defaults which are also being imported
-            if (isset($header[$key]))
-                unset($defaults[$key]);
-            $keys[] = $key;
-        }
-
-        while (($data = fgetcsv($stream, 1000, ",")) !== false) {
-            if (count($data) == 1 && $data[0] == null)
-                // Skip empty rows
-                continue;
-            elseif (count($data) != count($headers))
-                return sprintf(__('Bad data. Expected: %s'), implode(', ', $headers));
-            // Validate according to field configuration
-            $i = 0;
-            foreach ($headers as $h => $label) {
-                $f = $fields[$h];
-                $T = $f->parse($data[$i]);
-                if ($f->validateEntry($T) && $f->errors())
-                    return sprintf(__(
-                        /* 1 will be a field label, and 2 will be error messages */
-                        '%1$s: Invalid data: %2$s'),
-                        $label, implode(', ', $f->errors()));
-                // Convert to database format
-                $data[$i] = $f->to_database($T);
-                $i++;
+        require_once INCLUDE_DIR . 'class.import.php';
+
+        $importer = new CsvImporter($stream);
+        $imported = 0;
+        try {
+            db_autocommit(false);
+            $records = $importer->importCsv(UserForm::getUserForm()->getFields(), $defaults);
+            foreach ($records as $data) {
+                if (!isset($data['email']) || !isset($data['name']))
+                    throw new ImportError('Both `name` and `email` fields are required');
+                if (!($user = static::fromVars($data, true, true)))
+                    throw new ImportError(sprintf(__('Unable to import user: %s'),
+                        print_r($data, true)));
+                $imported++;
             }
-            // Add default fields
-            foreach ($defaults as $key => $val)
-                $data[] = $val;
-
-            $users[] = $data;
+            db_autocommit(true);
         }
-
-        foreach ($users as $u) {
-            $vars = array_combine($keys, $u);
-            if (!static::fromVars($vars, true))
-                return sprintf(__('Unable to import user: %s'),
-                    print_r($vars, true));
+        catch (Exception $ex) {
+            db_rollback();
+            return $ex->getMessage();
         }
-
-        return count($users);
+        return $imported;
     }
 
-    function importFromPost($stuff, $extra=array()) {
-        if (is_array($stuff) && !$stuff['error']) {
-            // Properly detect Macintosh style line endings
-            ini_set('auto_detect_line_endings', true);
-            $stream = fopen($stuff['tmp_name'], 'r');
-        }
-        elseif ($stuff) {
-            $stream = fopen('php://temp', 'w+');
-            fwrite($stream, $stuff);
-            rewind($stream);
-        }
-        else {
-            return __('Unable to parse submitted users');
-        }
-
+    function importFromPost($stream, $extra=array()) {
         return User::importCsv($stream, $extra);
     }
 
@@ -506,45 +476,57 @@ class User extends UserModel {
 
         $valid = true;
         $forms = $this->getForms($vars);
-        foreach ($forms as $cd) {
-            $cd->setSource($vars);
-            if ($staff && !$cd->isValidForStaff())
+        foreach ($forms as $entry) {
+            $entry->setSource($vars);
+            if ($staff && !$entry->isValidForStaff())
                 $valid = false;
-            elseif (!$staff && !$cd->isValidForClient())
+            elseif (!$staff && !$entry->isValidForClient())
                 $valid = false;
-            elseif (($form= $cd->getForm())
-                        && $form->get('type') == 'U'
-                        && ($f=$form->getField('email'))
-                        && $f->getClean()
-                        && ($u=User::lookup(array('emails__address'=>$f->getClean())))
-                        && $u->id != $this->getId()) {
+            elseif ($entry->getDynamicForm()->get('type') == 'U'
+                    && ($f=$entry->getField('email'))
+                    &&  $f->getClean()
+                    && ($u=User::lookup(array('emails__address'=>$f->getClean())))
+                    && $u->id != $this->getId()) {
                 $valid = false;
                 $f->addError(__('Email is assigned to another user'));
             }
+
+            if (!$valid)
+                $errors = array_merge($errors, $entry->errors());
         }
 
+
         if (!$valid)
             return false;
 
-        foreach ($forms as $cd) {
-            if (($f=$cd->getForm()) && $f->get('type') == 'U') {
-                if (($name = $f->getField('name'))) {
-                    $this->name = $name->getClean();
+        // Save the entries
+        foreach ($forms as $entry) {
+            if ($entry->getDynamicForm()->get('type') == 'U') {
+                //  Name field
+                if (($name = $entry->getField('name'))) {
+                    $name = $name->getClean();
+                    if (is_array($name))
+                        $name = implode(', ', $name);
+                    $this->name = $name;
                 }
 
-                if (($email = $f->getField('email'))) {
+                // Email address field
+                if (($email = $entry->getField('email'))) {
                     $this->default_email->address = $email->getClean();
                     $this->default_email->save();
                 }
             }
+
             // DynamicFormEntry::save returns the number of answers updated
-            if ($cd->save()) {
+            if ($entry->save()) {
                 $this->updated = SqlFunction::NOW();
             }
         }
+
         return $this->save();
     }
 
+
     function save($refetch=false) {
         // Drop commas and reorganize the name without them
         $parts = array_map('trim', explode(',', $this->name));
@@ -589,20 +571,79 @@ class User extends UserModel {
         $this->emails->expunge();
 
         // Drop dynamic data
-        foreach ($this->getDynamicData() as $cd) {
-            $cd->delete();
+        foreach ($this->getDynamicData() as $entry) {
+            $entry->delete();
         }
 
         // Delete user
         return parent::delete();
     }
 
+    function deleteAllTickets() {
+        $deleted = TicketStatus::lookup(array('state' => 'deleted'));
+        foreach($this->tickets as $ticket) {
+            if (!$T = Ticket::lookup($ticket->getId()))
+                continue;
+            if (!$T->setStatus($deleted))
+                return false;
+        }
+        $this->tickets->reset();
+        return true;
+    }
+
     static function lookupByEmail($email) {
-        return self::lookup(array('emails__address'=>$email));
+        return static::lookup(array('emails__address'=>$email));
+    }
+
+    static function getNameById($id) {
+        if ($user = static::lookup($id))
+            return $user->getName();
     }
 }
 
-class PersonsName {
+class EmailAddress
+implements TemplateVariable {
+    var $address;
+
+    function __construct($address) {
+        $this->address = $address;
+    }
+
+    function __toString() {
+        return (string) $this->address;
+    }
+
+    function getVar($what) {
+        require_once PEAR_DIR . 'Mail/RFC822.php';
+        require_once PEAR_DIR . 'PEAR.php';
+        if (!($mails = Mail_RFC822::parseAddressList($this->address)) || PEAR::isError($mails))
+            return '';
+
+        if (count($mails) > 1)
+            return '';
+
+        $info = $mails[0];
+        switch ($what) {
+        case 'domain':
+            return $info->host;
+        case 'personal':
+            return trim($info->personal, '"');
+        case 'mailbox':
+            return $info->mailbox;
+        }
+    }
+
+    static function getVarScope() {
+        return array(
+            'domain' => __('Domain'),
+            'mailbox' => __('Mailbox'),
+            'personal' => __('Personal name'),
+        );
+    }
+}
+
+class PersonsName
+implements TemplateVariable {
     var $format;
     var $parts;
     var $name;
@@ -623,10 +664,10 @@ class PersonsName {
     function __construct($name, $format=null) {
         global $cfg;
 
-        if ($format && !isset(static::$formats[$format]))
+        if ($format && isset(static::$formats[$format]))
             $this->format = $format;
-        elseif($cfg)
-            $this->format = $cfg->getDefaultNameFormat();
+        else
+            $this->format = 'original';
 
         if (!is_array($name)) {
             $this->parts = static::splitName($name);
@@ -721,6 +762,16 @@ class PersonsName {
         return $this->__toString();
     }
 
+    static function getVarScope() {
+        $formats = array();
+        foreach (static::$formats as $name=>$info) {
+            if (in_array($name, array('original', 'complete')))
+                continue;
+            $formats[$name] = $info[0];
+        }
+        return $formats;
+    }
+
     function __toString() {
 
         @list(, $func) = static::$formats[$this->format];
@@ -794,11 +845,33 @@ class PersonsName {
 
 }
 
+class AgentsName extends PersonsName {
+    function __construct($name, $format=null) {
+        global $cfg;
+
+        if (!$format && $cfg)
+            $format = $cfg->getAgentNameFormat();
+
+        parent::__construct($name, $format);
+    }
+}
+
+class UsersName extends PersonsName {
+    function __construct($name, $format=null) {
+        global $cfg;
+        if (!$format && $cfg)
+            $format = $cfg->getClientNameFormat();
+
+        parent::__construct($name, $format);
+    }
+}
+
+
 class UserEmail extends UserEmailModel {
     static function ensure($address) {
         $email = static::lookup(array('address'=>$address));
         if (!$email) {
-            $email = static::create(array('address'=>$address));
+            $email = new static(array('address'=>$address));
             $email->save();
         }
         return $email;
@@ -806,7 +879,7 @@ class UserEmail extends UserEmailModel {
 }
 
 
-class UserAccountModel extends VerySimpleModel {
+class UserAccount extends VerySimpleModel {
     static $meta = array(
         'table' => USER_ACCOUNT_TABLE,
         'pk' => array('id'),
@@ -818,15 +891,19 @@ class UserAccountModel extends VerySimpleModel {
         ),
     );
 
+    const LANG_MAILOUTS = 1;            // Language preference for mailouts
+
     var $_status;
+    var $_extra;
 
-    function __construct() {
-        call_user_func_array(array('parent', '__construct'), func_get_args());
-        $this->_status = new UserAccountStatus($this->get('status'));
+    function getStatus() {
+        if (!isset($this->_status))
+            $this->_status = new UserAccountStatus($this->get('status'));
+        return $this->_status;
     }
 
     protected function hasStatus($flag) {
-        return $this->_status->check($flag);
+        return $this->getStatus()->check($flag);
     }
 
     protected function clearStatus($flag) {
@@ -843,7 +920,7 @@ class UserAccountModel extends VerySimpleModel {
     }
 
     function isConfirmed() {
-        return $this->_status->isConfirmed();
+        return $this->getStatus()->isConfirmed();
     }
 
     function lock() {
@@ -857,7 +934,7 @@ class UserAccountModel extends VerySimpleModel {
     }
 
     function isLocked() {
-        return $this->_status->isLocked();
+        return $this->getStatus()->isLocked();
     }
 
     function forcePasswdReset() {
@@ -873,10 +950,6 @@ class UserAccountModel extends VerySimpleModel {
         return !$this->hasStatus(UserAccountStatus::FORBID_PASSWD_RESET);
     }
 
-    function getStatus() {
-        return $this->_status;
-    }
-
     function getInfo() {
         return $this->ht;
     }
@@ -890,16 +963,68 @@ class UserAccountModel extends VerySimpleModel {
     }
 
     function getUser() {
-        $this->user->set('account', $this);
+        // FIXME: The ORM will expect a ClientAccount instance as the
+        // User.account relationship is defined thusly; however, $this is an
+        // instance of UserAccount. Therefore we will (cast) to a
+        // ClientAccount instance first. This could be better rectified by
+        // collapsing UserAccount into ClientAccount.
+        $acct = new ClientAccount($this->ht);
+        $this->user->set('account', $acct);
         return $this->user;
     }
 
-    function getLanguage() {
-        return $this->get('lang');
+    function getExtraAttr($attr=false, $default=null) {
+        if (!isset($this->_extra))
+            $this->_extra = JsonDataParser::decode($this->get('extra', ''));
+
+        return $attr ? (@$this->_extra[$attr] ?: $default) : $this->_extra;
+    }
+
+    function setExtraAttr($attr, $value) {
+        $this->getExtraAttr();
+        $this->_extra[$attr] = $value;
+    }
+
+    /**
+     * Function: getLanguage
+     *
+     * Returns the language preference for the user or false if no
+     * preference is defined. False indicates the browser indicated
+     * preference should be used. For requests apart from browser requests,
+     * the last language preference of the browser is set in the
+     * 'browser_lang' extra attribute upon logins. Send the LANG_MAILOUTS
+     * flag to also consider this saved value. Such is useful when sending
+     * the user a message (such as an email), and the user's browser
+     * preference is not available in the HTTP request.
+     *
+     * Parameters:
+     * $flags - (int) Send UserAccount::LANG_MAILOUTS if the user's
+     *      last-known browser preference should be considered. Normally
+     *      only the user's saved language preference is considered.
+     *
+     * Returns:
+     * Current or last-known language preference or false if no language
+     * preference is currently set or known.
+     */
+    function getLanguage($flags=false) {
+        $lang = $this->get('lang', false);
+        if (!$lang && ($flags & UserAccount::LANG_MAILOUTS))
+            $lang = $this->getExtraAttr('browser_lang', false);
+
+        return $lang;
     }
-}
 
-class UserAccount extends UserAccountModel {
+    function getTimezone() {
+        return $this->timezone;
+    }
+
+    function save($refetch=false) {
+        // Serialize the extra column on demand
+        if (isset($this->_extra)) {
+            $this->extra = JsonDataEncoder::encode($this->_extra);
+        }
+        return parent::save($refetch);
+    }
 
     function hasPassword() {
         return (bool) $this->get('passwd');
@@ -913,16 +1038,20 @@ class UserAccount extends UserAccountModel {
         return static::sendUnlockEmail('registration-client') === true;
     }
 
+    function setPassword($new) {
+        $this->set('passwd', Passwd::hash($new));
+    }
+
     protected function sendUnlockEmail($template) {
         global $ost, $cfg;
 
         $token = Misc::randCode(48); // 290-bits
 
         $email = $cfg->getDefaultEmail();
-        $content = Page::lookup(Page::getIdByType($template));
+        $content = Page::lookupByType($template);
 
         if (!$email ||  !$content)
-            return new Error(sprintf(_S('%s: Unable to retrieve template'),
+            return new BaseError(sprintf(_S('%s: Unable to retrieve template'),
                 $template));
 
         $vars = array(
@@ -940,13 +1069,14 @@ class UserAccount extends UserAccountModel {
         $info = array('email' => $email, 'vars' => &$vars, 'log'=>true);
         Signal::send('auth.pwreset.email', $this->getUser(), $info);
 
+        $lang = $this->getLanguage(UserAccount::LANG_MAILOUTS);
         $msg = $ost->replaceTemplateVariables(array(
-            'subj' => $content->getName(),
-            'body' => $content->getBody(),
+            'subj' => $content->getLocalName($lang),
+            'body' => $content->getLocalBody($lang),
         ), $vars);
 
         $_config = new Config('pwreset');
-        $_config->set($vars['token'], $this->getUser()->getId());
+        $_config->set($vars['token'], 'c'.$this->getUser()->getId());
 
         $email->send($this->getUser()->getEmail(),
             Format::striptags($msg['subj']), $msg['body']);
@@ -972,8 +1102,8 @@ class UserAccount extends UserAccountModel {
 
         // TODO: Make sure the username is unique
 
-        if (!$vars['timezone_id'])
-            $errors['timezone_id'] = __('Time zone selection is required');
+        // Timezone selection is not required. System default is a valid
+        // fallback
 
         // Changing password?
         if ($vars['passwd1'] || $vars['passwd2']) {
@@ -992,12 +1122,11 @@ class UserAccount extends UserAccountModel {
 
         if ($errors) return false;
 
-        $this->set('timezone_id', $vars['timezone_id']);
-        $this->set('dst', isset($vars['dst']) ? 1 : 0);
+        $this->set('timezone', $vars['timezone']);
         $this->set('username', $vars['username']);
 
         if ($vars['passwd1']) {
-            $this->set('passwd', Passwd::hash($vars['passwd1']));
+            $this->setPassword($vars['passwd1']);
             $this->setStatus(UserAccountStatus::CONFIRMED);
         }
 
@@ -1017,7 +1146,7 @@ class UserAccount extends UserAccountModel {
     }
 
     static function createForUser($user, $defaults=false) {
-        $acct = static::create(array('user_id'=>$user->getId()));
+        $acct = new static(array('user_id'=>$user->getId()));
         if ($defaults && is_array($defaults)) {
             foreach ($defaults as $k => $v)
                 $acct->set($k, $v);
@@ -1052,13 +1181,11 @@ class UserAccount extends UserAccountModel {
 
         if ($errors) return false;
 
-        $account = UserAccount::create(array('user_id' => $user->getId()));
-        if (!$account)
-            return false;
-
-        $account->set('dst', isset($vars['dst'])?1:0);
-        $account->set('timezone_id', $vars['timezone_id']);
-        $account->set('backend', $vars['backend']);
+        $account = new UserAccount(array(
+            'user_id' => $user->getId(),
+            'timezone' => $vars['timezone'],
+            'backend' => $vars['backend'],
+        ));
 
         if ($vars['username'] && strcasecmp($vars['username'], $user->getEmail()))
             $account->set('username', $vars['username']);
@@ -1129,21 +1256,47 @@ class UserAccountStatus {
 /*
  *  Generic user list.
  */
-class UserList extends ListObject {
+class UserList extends ListObject
+implements TemplateVariable {
 
     function __toString() {
+        return $this->getNames();
+    }
 
+    function getNames() {
         $list = array();
         foreach($this->storage as $user) {
             if (is_object($user))
                 $list [] = $user->getName();
         }
+        return $list ? implode(', ', $list) : '';
+    }
+
+    function getFull() {
+        $list = array();
+        foreach($this->storage as $user) {
+            if (is_object($user))
+                $list[] = sprintf("%s <%s>", $user->getName(), $user->getEmail());
+        }
 
         return $list ? implode(', ', $list) : '';
     }
-}
 
-require_once(INCLUDE_DIR . 'class.organization.php');
-User::_inspect();
-UserAccount::_inspect();
+    function getEmails() {
+        $list = array();
+        foreach($this->storage as $user) {
+            if (is_object($user))
+                $list[] = $user->getEmail();
+        }
+        return $list ? implode(', ', $list) : '';
+    }
+
+    static function getVarScope() {
+        return array(
+            'names' => __('List of names'),
+            'emails' => __('List of email addresses'),
+            'full' => __('List of names and email addresses'),
+        );
+    }
+}
 ?>
diff --git a/include/class.usersession.php b/include/class.usersession.php
index 9447c109f784af9273642943158019f24e2d07b4..bb113f5d3bf2f82fcff08c11c4fb933dcf48b011 100644
--- a/include/class.usersession.php
+++ b/include/class.usersession.php
@@ -25,7 +25,7 @@ class UserSession {
    var $ip = '';
    var $validated=FALSE;
 
-   function UserSession($userid){
+   function __construct($userid){
 
       $this->browser=(!empty($_SERVER['HTTP_USER_AGENT'])) ? $_SERVER['HTTP_USER_AGENT'] : $_ENV['HTTP_USER_AGENT'];
       $this->ip=(!empty($_SERVER['REMOTE_ADDR'])) ? $_SERVER['REMOTE_ADDR'] : getenv('REMOTE_ADDR');
@@ -165,10 +165,12 @@ class StaffSession extends Staff {
     var $session;
     var $token;
 
-    function __construct($var) {
-        parent::__construct($var);
-        $this->token = &$_SESSION[':token']['staff'];
-        $this->session= new UserSession($this->getId());
+    static function lookup($var) {
+        if ($staff = parent::lookup($var)) {
+            $staff->token = &$_SESSION[':token']['staff'];
+            $staff->session= new UserSession($staff->getId());
+        }
+        return $staff;
     }
 
     function isValid(){
diff --git a/include/class.util.php b/include/class.util.php
index 8fb9b3967a4ec5cb4e6d2117c58ece03baa1b0fd..b4adf4985ae2b1321d9247ab0ea6a5ae4359ae2a 100644
--- a/include/class.util.php
+++ b/include/class.util.php
@@ -15,7 +15,9 @@ class ListObject implements IteratorAggregate, ArrayAccess, Serializable, Counta
 
     protected $storage = array();
 
-    function __construct(array $array=array()) {
+    function __construct($array=array()) {
+        if (!is_array($array) && !$array instanceof Traversable)
+            throw new InvalidArgumentException('Traversable object or array expected');
         foreach ($array as $v)
             $this->storage[] = $v;
     }
@@ -37,6 +39,8 @@ class ListObject implements IteratorAggregate, ArrayAccess, Serializable, Counta
     }
 
     function insert($i, $value) {
+        if ($i < 0)
+            $i += count($this->storage) + 1;
         array_splice($this->storage, $i, 0, array($value));
     }
 
diff --git a/include/class.validator.php b/include/class.validator.php
index 88075ab45b41f52c553caa2b6dd18a4b68add2fe..2cce38f21432dc457efbae59bd4277bec9d213d3 100644
--- a/include/class.validator.php
+++ b/include/class.validator.php
@@ -20,7 +20,7 @@ class Validator {
     var $fields=array();
     var $errors=array();
 
-    function Validator($fields=null) {
+    function __construct($fields=null) {
         $this->setFields($fields);
     }
     function setFields(&$fields){
@@ -99,15 +99,15 @@ class Validator {
                 break;
             case 'phone':
             case 'fax':
-                if(!$this->is_phone($this->input[$k]))
+                if(!self::is_phone($this->input[$k]))
                     $this->errors[$k]=$field['error'];
                 break;
             case 'email':
-                if(!$this->is_email($this->input[$k]))
+                if(!self::is_email($this->input[$k]))
                     $this->errors[$k]=$field['error'];
                 break;
             case 'url':
-                if(!$this->is_url($this->input[$k]))
+                if(!self::is_url($this->input[$k]))
                     $this->errors[$k]=$field['error'];
                 break;
             case 'password':
@@ -116,7 +116,7 @@ class Validator {
                 break;
             case 'username':
                 $error = '';
-                if (!$this->is_username($this->input[$k], $error))
+                if (!self::is_username($this->input[$k], $error))
                     $this->errors[$k]=$field['error'].": $error";
                 break;
             case 'zipcode':
@@ -140,10 +140,11 @@ class Validator {
 
     /*** Functions below can be called directly without class instance.
          Validator::func(var..);  (nolint) ***/
-    function is_email($email, $list=false, $verify=false) {
+    static function is_email($email, $list=false, $verify=false) {
         require_once PEAR_DIR . 'Mail/RFC822.php';
         require_once PEAR_DIR . 'PEAR.php';
-        if (!($mails = Mail_RFC822::parseAddressList($email)) || PEAR::isError($mails))
+        $rfc822 = new Mail_RFC822();
+        if (!($mails = $rfc822->parseAddressList($email)) || PEAR::isError($mails))
             return false;
 
         if (!$list && count($mails) > 1)
@@ -166,24 +167,24 @@ class Validator {
         return true;
     }
 
-    function is_valid_email($email) {
+    static function is_valid_email($email) {
         global $cfg;
         // Default to FALSE for installation
         return self::is_email($email, false, $cfg && $cfg->verifyEmailAddrs());
     }
 
-    function is_phone($phone) {
+    static function is_phone($phone) {
         /* We're not really validating the phone number but just making sure it doesn't contain illegal chars and of acceptable len */
         $stripped=preg_replace("(\(|\)|\-|\.|\+|[  ]+)","",$phone);
         return (!is_numeric($stripped) || ((strlen($stripped)<7) || (strlen($stripped)>16)))?false:true;
     }
 
-    function is_url($url) {
+    static function is_url($url) {
         //XXX: parse_url is not ideal for validating urls but it's ideal for basic checks.
         return ($url && ($info=parse_url($url)) && $info['host']);
     }
 
-    function is_ip($ip) {
+    static function is_ip($ip) {
 
         if(!$ip or empty($ip))
             return false;
@@ -203,7 +204,7 @@ class Validator {
         return false;
     }
 
-    function is_username($username, &$error='') {
+    static function is_username($username, &$error='') {
         if (strlen($username)<2)
             $error = __('Username must have at least two (2) characters');
         elseif (!preg_match('/^[\p{L}\d._-]+$/u', $username))
diff --git a/include/class.variable.php b/include/class.variable.php
index 6b0c6b5b8f8368b22984c2beb07b679572a61584..fb968336a697c505654d485829ab8e79620ab87f 100644
--- a/include/class.variable.php
+++ b/include/class.variable.php
@@ -21,18 +21,16 @@ class VariableReplacer {
     var $start_delim;
     var $end_delim;
 
-    var $objects;
-    var $variables;
+    var $objects = array();
+    var $variables = array();
+    var $extras = array();
 
     var $errors;
 
-    function VariableReplacer($start_delim='(?:%{|%%7B)', $end_delim='(?:}|%7D)') {
+    function __construct($start_delim='(?:%{|%%7B)', $end_delim='(?:}|%7D)') {
 
         $this->start_delim = $start_delim;
         $this->end_delim = $end_delim;
-
-        $this->objects = array();
-        $this->variables = array();
     }
 
     function setError($error) {
@@ -61,46 +59,67 @@ class VariableReplacer {
 
     function getVar($obj, $var) {
 
-        if(!$obj) return "";
+        if (!$obj)
+            return "";
 
-        if (!$var) {
-            if (method_exists($obj, 'asVar'))
-                return call_user_func(array($obj, 'asVar'));
-            elseif (method_exists($obj, '__toString'))
-                return (string) $obj;
+        // Order or resolving %{... .tag.remainder}
+        // 1. $obj[$tag]
+        // 2. $obj->tag
+        // 3. $obj->getVar(tag)
+        // 4. $obj->getTag()
+        @list($tag, $remainder) = explode('.', $var ?: '', 2);
+        $tag = mb_strtolower($tag);
+        $rv = null;
+
+        if (!is_object($obj)) {
+            if ($tag && is_array($obj) && array_key_exists($tag, $obj))
+                $rv = $obj[$tag];
+            else
+                // Not able to continue the lookup
+                return '';
         }
-
-        list($v, $part) = explode('.', $var, 2);
-        if ($v && is_callable(array($obj, 'get'.ucfirst($v)))) {
-            $rv = call_user_func(array($obj, 'get'.ucfirst($v)));
-            if(!$rv || !is_object($rv))
-                return $rv;
-
-            return $this->getVar($rv, $part);
+        else {
+            if (!$var) {
+                if (method_exists($obj, 'asVar'))
+                    return call_user_func(array($obj, 'asVar'), $this);
+                elseif (method_exists($obj, '__toString'))
+                    return (string) $obj;
+            }
+            if (method_exists($obj, 'getVar')) {
+                $rv = $obj->getVar($tag, $this);
+            }
+            if (!isset($rv) && property_exists($obj, $tag)) {
+                $rv = $obj->{$tag};
+            }
+            if (!isset($rv) && is_callable(array($obj, 'get'.ucfirst($tag)))) {
+                $rv = call_user_func(array($obj, 'get'.ucfirst($tag)));
+            }
         }
 
-        if (!$var || !method_exists($obj, 'getVar'))
-            return "";
-
-        list($tag, $remainder) = explode('.', $var, 2);
-        if(($rv = call_user_func(array($obj, 'getVar'), $tag))===false)
-            return "";
-
-        if(!is_object($rv))
-            return $rv;
+        // Recurse with $rv
+        if (is_object($rv) || $remainder)
+            return $this->getVar($rv, $remainder);
 
-        return $this->getVar($rv, $remainder);
+        return $rv;
     }
 
     function replaceVars($input) {
 
+        // Preserve existing extras
+        if ($input instanceof TextWithExtras)
+            $this->extras = $input->extras;
+
         if($input && is_array($input))
             return array_map(array($this, 'replaceVars'), $input);
 
         if(!($vars=$this->_parse($input)))
             return $input;
 
-        return str_replace(array_keys($vars), array_values($vars), $input);
+        $text = str_replace(array_keys($vars), array_values($vars), $input);
+        if ($this->extras) {
+            return new TextWithExtras($text, $this->extras);
+        }
+        return $text;
     }
 
     function _resolveVar($var) {
@@ -110,10 +129,24 @@ class VariableReplacer {
             return $this->variables[$var];
 
         $parts = explode('.', $var, 2);
-        if($parts && ($obj=$this->getObj($parts[0])))
-            return $this->getVar($obj, $parts[1]);
-        elseif($parts[0] && @isset($this->variables[$parts[0]])) //root override
+        try {
+            if ($parts && ($obj=$this->getObj($parts[0])))
+                return $this->getVar($obj, $parts[1]);
+        }
+        catch (OOBContent $content) {
+            $type = $content->getType();
+            $existing = @$this->extras[$type] ?: array();
+            $this->extras[$type] = array_merge($existing, $content->getContent());
+            return $content->asVar();
+        }
+
+        if ($parts[0] && @isset($this->variables[$parts[0]])) { //root override
+            if (is_array($this->variables[$parts[0]])
+                    && isset($this->variables[$parts[0]][$parts[1]]))
+                return $this->variables[$parts[0]][$parts[1]];
+
             return $this->variables[$parts[0]];
+        }
 
         //Unknown object or variable - leavig it alone.
         $this->setError(sprintf(__('Unknown object for "%s" tag'), $var));
@@ -139,5 +172,225 @@ class VariableReplacer {
 
         return $vars;
     }
+
+    static function compileScope($scope, $recurse=5, $exclude=false) {
+        $items = array();
+        foreach ($scope as $name => $info) {
+            if ($exclude === $name)
+                continue;
+            if ($recurse && is_array($info) && isset($info['class'])) {
+                $items[$name] = $info['desc'];
+                foreach (static::compileScope($info['class']::getVarScope(), $recurse-1,
+                    @$info['exclude'] ?: $name)
+                as $name2=>$desc) {
+                    $items["{$name}.{$name2}"] = $desc;
+                }
+            }
+            if (!is_array($info)) {
+                $items[$name] = $info;
+            }
+        }
+        return $items;
+    }
+
+    static function compileFormScope($form) {
+        $items = array();
+        foreach ($form->getFields() as $f) {
+            if (!($name = $f->get('name')))
+                continue;
+            if (!$f->isStorable() || !$f->hasData())
+                continue;
+
+            $desc = $f->getLocal('label');
+            if (($class = $f->asVarType()) && class_exists($class)) {
+                $desc = array('desc' => $desc, 'class' => $class);
+            }
+            $items[$name] = $desc;
+            foreach (VariableReplacer::compileFieldScope($f) as $name2=>$desc) {
+                $items["$name.$name2"] = $desc;
+            }
+        }
+        return $items;
+    }
+
+    static function compileFieldScope($field, $recurse=2, $exclude=false) {
+        $items = array();
+        if (!$field->hasSubFields())
+            return $items;
+
+        foreach ($field->getSubFields() as $f) {
+            if (!($name = $f->get('name')))
+                continue;
+            if ($exclude === $name)
+                continue;
+            $items[$name] = $f->getLabel();
+            if ($recurse) {
+                foreach (static::compileFieldScope($f, $recurse-1, $name)
+                as $name2=>$desc) {
+                    if (($class = $f->asVarType()) && class_exists($class)) {
+                        $desc = array('desc' => $desc, 'class' => $class);
+                    }
+                    $items["$name.$name2"] = $desc;
+                }
+            }
+        }
+        return $items;
+    }
+
+    static function getContextForRoot($root) {
+        switch ($root) {
+        case 'cannedresponse':
+            $roots = array('ticket');
+            break;
+
+        case 'fa:send_email':
+            // FIXME: Make this pluggable
+            require_once INCLUDE_DIR . 'class.filter_action.php';
+            return FA_SendEmail::getVarScope();
+
+        default:
+            if ($info = Page::getContext($root)) {
+                $roots = $info;
+                break;
+            }
+
+            // Get the context for an email template
+            if ($tpl_info = EmailTemplateGroup::getTemplateDescription($root))
+                $roots = $tpl_info['context'];
+        }
+
+        if (!$roots)
+            return false;
+
+        $contextTypes = array(
+            'activity' => array('class' => 'ThreadActivity', 'desc' => __('Type of recent activity')),
+            'assignee' => array('class' => 'Staff', 'desc' => __('Assigned agent/team')),
+            'assigner' => array('class' => 'Staff', 'desc' => __('Agent performing the assignment')),
+            'comments' => __('Assign/transfer comments'),
+            'link' => __('Access link'),
+            'message' => array('class' => 'MessageThreadEntry', 'desc' => 'Message from the EndUser'),
+            'note' => array('class' => 'NoteThreadEntry', 'desc' => __('Internal note')),
+            'poster' => array('class' => 'User', 'desc' => 'EndUser or Agent originating the message'),
+            // XXX: This could be EndUser -or- Staff object
+            'recipient' => array('class' => 'TicketUser', 'desc' => 'Message recipient'),
+            'response' => array('class' => 'ResponseThreadEntry', 'desc' => __('Outgoing response')),
+            'signature' => 'Selected staff or department signature',
+            'staff' => array('class' => 'Staff', 'desc' => 'Agent originating the activity'),
+            'ticket' => array('class' => 'Ticket', 'desc' => 'The ticket'),
+            'task' => array('class' => 'Task', 'desc' => 'The task'),
+            'user' => array('class' => 'User', 'desc' => __('Message recipient')),
+        );
+        $context = array();
+        foreach ($roots as $C=>$desc) {
+            // $desc may be either the root or the description array
+            if (is_array($desc))
+                $context[$C] = $desc;
+            else
+                $context[$desc] = $contextTypes[$desc];
+        }
+        $global = osTicket::getVarScope();
+        return self::compileScope($context + $global);
+    }
+}
+
+class PlaceholderList
+/* implements TemplateVariable */ {
+    var $items;
+
+    function __construct($items) {
+        $this->items = $items;
+    }
+
+    function asVar() {
+        $items = array();
+        foreach ($this->items as $I) {
+            if (method_exists($I, 'asVar')) {
+                $items[] = $I->asVar();
+            }
+            else {
+                $items[] = (string) $I;
+            }
+        }
+        return implode(',', $items);
+    }
+
+    function getVar($tag) {
+        $items = array();
+        foreach ($this->items as $I) {
+            if (is_object($I) && method_exists($I, 'get'.ucfirst($tag))) {
+                $items[] = call_user_func(array($I, 'get'.ucfirst($tag)));
+            }
+            elseif (method_exists($I, 'getVar')) {
+                $items[] = $I->getVar($tag);
+            }
+        }
+        if (count($items) == 1) {
+            return $items[0];
+        }
+        return new static(array_filter($items));
+    }
+
+    function __toString() {
+        return $this->asVar();
+    }
+}
+
+/**
+ * Exception used in the variable replacement process to indicate non text
+ * content (such as attachments)
+ */
+class OOBContent extends Exception {
+    var $type;
+    var $content;
+    var $text;
+
+    const FILES = 'files';
+
+    function __construct($type, $content, $asVar='') {
+        $this->type = $type;
+        $this->content = $content;
+        $this->text = $asVar;
+    }
+
+    function getType() { return $this->type; }
+    function getContent() { return $this->content; }
+    function asVar() { return $this->text; }
+}
+
+/**
+ * Simple wrapper to represent a rendered or partially rendered template
+ * with extra content such as attachments
+ */
+class TextWithExtras {
+    var $text = '';
+    var $extras;
+
+    function __construct($text, array $extras) {
+        $this->setText($text);
+        $this->extras = $extras;
+    }
+
+    function setText($text) {
+        try {
+            $this->text = (string) $text;
+        }
+        catch (Exception $e) {
+            throw new InvalidArgumentException('String type is required', 0, $e);
+        }
+    }
+
+    function __toString() {
+        return $this->text;
+    }
+
+    function getFiles() {
+        return $this->extras[OOBContent::FILES];
+    }
+}
+
+interface TemplateVariable {
+    // function asVar(); — not absolutely required
+    // function getVar($name, $parser); — not absolutely required
+    static function getVarScope();
 }
 ?>
diff --git a/include/class.xml.php b/include/class.xml.php
index 129e05877609686f298154bd554cad38d6c55b5a..c73a7cf7379a4461d3aebed1f615f9759b50e1b8 100644
--- a/include/class.xml.php
+++ b/include/class.xml.php
@@ -18,7 +18,7 @@
 
 class XmlDataParser {
 
-    function XmlDataParser() {
+    function __construct() {
         $this->parser = xml_parser_create('utf-8');
         xml_set_object($this->parser, $this);
         xml_set_element_handler($this->parser, "startElement", "endElement");
diff --git a/include/class.yaml.php b/include/class.yaml.php
index 63ceb37543d3fe4044172e1a7775c84e6734feb8..9fffc9a3f08ecad2515fda5b8681d2de020546b7 100644
--- a/include/class.yaml.php
+++ b/include/class.yaml.php
@@ -36,7 +36,7 @@ class YamlDataParser {
     }
 }
 
-class YamlParserError extends Error {
+class YamlParserError extends BaseError {
     static $title = 'Error parsing YAML document';
 }
 ?>
diff --git a/setup/cli/cli.inc.php b/include/cli/cli.inc.php
similarity index 84%
rename from setup/cli/cli.inc.php
rename to include/cli/cli.inc.php
index 31bdbfe8993cdadadbc4896c1e9257b3909b8cc0..fcecaf4a1aaa71bbb1da16e270329c483e265230 100644
--- a/setup/cli/cli.inc.php
+++ b/include/cli/cli.inc.php
@@ -19,13 +19,9 @@
 if(!strcasecmp(basename($_SERVER['SCRIPT_NAME']),basename(__FILE__))) die('kwaheri rafiki!');
 
 define('ROOT_PATH', '/');
+define('DISABLE_SESSION', true);
 define('INC_DIR',dirname(__file__).'/../inc/'); //local include dir!
 
-require_once(dirname(__file__).'/../../bootstrap.php');
+require_once INCLUDE_DIR . "class.cli.php";
 
-Bootstrap::loadConfig();
-Bootstrap::defineTables(TABLE_PREFIX);
-Bootstrap::loadCode();
 Bootstrap::i18n_prep();
-
-?>
diff --git a/include/cli/modules/agent.php b/include/cli/modules/agent.php
new file mode 100644
index 0000000000000000000000000000000000000000..7a55a351d6677a747e57e6f1b05d8f9e4d88978f
--- /dev/null
+++ b/include/cli/modules/agent.php
@@ -0,0 +1,185 @@
+<?php
+
+class AgentManager extends Module {
+    var $prologue = 'CLI agent manager';
+
+    var $arguments = array(
+        'action' => array(
+            'help' => 'Action to be performed',
+            'options' => array(
+                'import' => 'Import agents from CSV file',
+                'export' => 'Export agents from the system to CSV',
+                'list' => 'List agents based on search criteria',
+                'login' => 'Attempt login as an agent',
+                'backends' => 'List agent authentication backends',
+            ),
+        ),
+    );
+
+    var $options = array(
+        'file' => array('-f', '--file', 'metavar'=>'path',
+            'help' => 'File or stream to process'),
+        'verbose' => array('-v', '--verbose', 'default'=>false,
+            'action'=>'store_true', 'help' => 'Be more verbose'),
+
+        'welcome' => array('-w', '--welcome', 'default'=>false,
+            'action'=>'store_true', 'help'=>'Send a welcome email on import'),
+
+        'backend' => array('', '--backend',
+            'help'=>'Specify the authentication backend (used with `login` and `import`)'),
+
+        // -- Search criteria
+        'username' => array('-U', '--username',
+            'help' => 'Search by username'),
+        'email' => array('-E', '--email',
+            'help' => 'Search by email address'),
+        'id' => array('-U', '--id',
+            'help' => 'Search by user id'),
+        'dept' => array('-D', '--dept', 'help' => 'Search by access to department name or id'),
+        'team' => array('-T', '--team', 'help' => 'Search by membership in team name or id'),
+    );
+
+    var $stream;
+
+    function run($args, $options) {
+        global $ost, $cfg;
+
+        Bootstrap::connect();
+
+        if (!($ost=osTicket::start()) || !($cfg = $ost->getConfig()))
+            $this->fail('Unable to load config info!');
+
+        switch ($args['action']) {
+        case 'import':
+            // Properly detect Macintosh style line endings
+            ini_set('auto_detect_line_endings', true);
+
+            if (!$options['file'] || $options['file'] == '-')
+                $options['file'] = 'php://stdin';
+            if (!($this->stream = fopen($options['file'], 'rb')))
+                $this->fail("Unable to open input file [{$options['file']}]");
+
+            // Defaults
+            $extras = array(
+                'isadmin' => 0,
+                'isactive' => 1,
+                'isvisible' => 1,
+                'dept_id' => $cfg->getDefaultDeptId(),
+                'timezone' => $cfg->getDefaultTimezone(),
+                'welcome_email' => $options['welcome'],
+            );
+
+            if ($options['backend'])
+                $extras['backend'] = $options['backend'];
+
+            $stderr = $this->stderr;
+            $status = Staff::importCsv($this->stream, $extras,
+                function ($agent, $data) use ($stderr, $options) {
+                    if (!$options['verbose'])
+                        return;
+                    $stderr->write(
+                        sprintf("\n%s - %s  --- imported!",
+                        $agent->getName(),
+                        $agent->getUsername()));
+                }
+            );
+            if (is_numeric($status))
+                $this->stderr->write("Successfully processed $status agents\n");
+            else
+                $this->fail($status);
+            break;
+
+        case 'export':
+            $stream = $options['file'] ?: 'php://stdout';
+            if (!($this->stream = fopen($stream, 'c')))
+                $this->fail("Unable to open output file [{$options['file']}]");
+
+            fputcsv($this->stream, array('First Name', 'Last Name', 'Email', 'UserName'));
+            foreach ($this->getAgents($options) as $agent)
+                fputcsv($this->stream, array(
+                    $agent->getFirstName(),
+                    $agent->getLastName(),
+                    $agent->getEmail(),
+                    $agent->getUserName(),
+                ));
+            break;
+
+        case 'list':
+            $agents = $this->getAgents($options);
+            foreach ($agents as $A) {
+                $this->stdout->write(sprintf(
+                    "%d \t - %s\t<%s>\n",
+                    $A->staff_id, $A->getName(), $A->getEmail()));
+            }
+            break;
+
+        case 'login':
+            $this->stderr->write('Username: ');
+            $username = trim(fgets(STDIN));
+            $this->stderr->write('Password: ');
+            $password = trim(fgets(STDIN));
+
+            $agent = null;
+            foreach (StaffAuthenticationBackend::allRegistered() as $id=>$bk) {
+                if ((!$options['backend'] || $options['backend'] == $id)
+                    && $bk->supportsInteractiveAuthentication()
+                    && ($agent = $bk->authenticate($username, $password))
+                    && $agent instanceof AuthenticatedUser
+                ) {
+                    break;
+                }
+            }
+
+            if ($agent instanceof Staff) {
+                $this->stdout->write(sprintf("Successfully authenticated as '%s', using '%s'\n",
+                    (string) $agent->getName(),
+                    $bk->getName()
+                ));
+            }
+            else {
+                $this->stdout->write('Authentication failed');
+            }
+            break;
+
+        case 'backends':
+            foreach (StaffAuthenticationBackend::allRegistered() as $name=>$bk) {
+                if (!$bk->supportsInteractiveAuthentication())
+                    continue;
+                $this->stdout->write(sprintf("%s\t%s\n",
+                    $name, $bk->getName()
+                ));
+            }
+            break;
+
+        default:
+            $this->fail($args['action'].': Unknown action!');
+        }
+        @fclose($this->stream);
+    }
+
+    function getAgents($options, $requireOne=false) {
+        $agents = Staff::objects();
+
+        if ($options['email'])
+            $agents->filter(array('email__contains' => $options['email']));
+        if ($options['username'])
+            $agents->filter(array('username__contains' => $options['username']));
+        if ($options['id'])
+            $agents->filter(array('staff_id' => $options['id']));
+        if ($options['dept'])
+            $agents->filter(Q::any(array(
+                'dept_id' => $options['dept'],
+                'dept__name__contains' => $options['dept'],
+                'dept_access__dept_id' => $options['dept'],
+                'dept_access__dept__name__contains' => $options['dept'],
+            )));
+        if ($options['team'])
+            $agents->filter(Q::any(array(
+                'teams__team_id' => $options['team'],
+                'teams__team__name__contains' => $options['team'],
+            )));
+
+        return $agents->distinct('staff_id');
+    }
+}
+Module::register('agent', 'AgentManager');
diff --git a/include/cli/modules/cron.php b/include/cli/modules/cron.php
new file mode 100644
index 0000000000000000000000000000000000000000..c2df9458be0e35bfcfb6be5edcf2445019784f15
--- /dev/null
+++ b/include/cli/modules/cron.php
@@ -0,0 +1,32 @@
+<?php
+
+class CronManager extends Module {
+    var $prologue = 'CLI cron manager for osTicket';
+
+    var $arguments = array(
+        'action' => array(
+            'help' => 'Action to be performed',
+            'options' => array(
+                'fetch' => 'Fetch email',
+                'search' => 'Build search index'
+            ),
+        ),
+    );
+
+    function run($args, $options) {
+        Bootstrap::connect();
+        $ost = osTicket::start();
+
+        switch (strtolower($args[0])) {
+        case 'fetch':
+            Cron::MailFetcher();
+            break;
+        case 'search':
+            $ost->searcher->backend->IndexOldStuff();
+            break;
+        }
+    }
+}
+
+Module::register('cron', 'CronManager');
+?>
diff --git a/setup/cli/modules/deploy.php b/include/cli/modules/deploy.php
similarity index 99%
rename from setup/cli/modules/deploy.php
rename to include/cli/modules/deploy.php
index 096f4a125cc8703b72b76b030b067fa52dc8d060..74a11f02c3b103a106ac495eb225f86f7acad818 100644
--- a/setup/cli/modules/deploy.php
+++ b/include/cli/modules/deploy.php
@@ -1,5 +1,4 @@
 <?php
-require_once dirname(__file__) . "/class.module.php";
 require_once dirname(__file__) . "/unpack.php";
 
 class Deployment extends Unpacker {
diff --git a/setup/cli/modules/export.php b/include/cli/modules/export.php
similarity index 96%
rename from setup/cli/modules/export.php
rename to include/cli/modules/export.php
index a6c45fcee9f0f43de791ea17e2745bef70953dc3..dc7d7ae3a82c806c284587c74e4a1388ddb6f3cc 100644
--- a/setup/cli/modules/export.php
+++ b/include/cli/modules/export.php
@@ -13,8 +13,6 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
-require_once dirname(__file__) . "/class.module.php";
-require_once dirname(__file__) . "../../cli.inc.php";
 
 class Exporter extends Module {
     var $prologue =
diff --git a/setup/cli/modules/file.php b/include/cli/modules/file.php
similarity index 84%
rename from setup/cli/modules/file.php
rename to include/cli/modules/file.php
index b1a2f9be697e36f6c837b4584d6a6f5c7eed6735..ed7916ad8c7c930ece31fd6b9deb88e7901ad909 100644
--- a/setup/cli/modules/file.php
+++ b/include/cli/modules/file.php
@@ -1,6 +1,4 @@
 <?php
-require_once dirname(__file__) . "/class.module.php";
-require_once dirname(__file__) . "/../cli.inc.php";
 
 class FileManager extends Module {
     var $prologue = 'CLI file manager for osTicket';
@@ -61,7 +59,7 @@ class FileManager extends Module {
         switch ($args['action']) {
         case 'backends':
             // List configured backends
-            foreach (FileStorageBackend::allRegistered() as $char=>$bk) {
+            foreach (FileStorageBackend::allRegistered(true) as $char=>$bk) {
                 print "$char -- {$bk::$desc} ($bk)\n";
             }
             break;
@@ -69,7 +67,7 @@ class FileManager extends Module {
         case 'list':
             // List files matching criteria
             // ORM would be nice!
-            $files = FileModel::objects();
+            $files = AttachmentFile::objects();
             $this->_applyCriteria($options, $files);
             foreach ($files as $f) {
                 printf("% 5d %s % 8d %s % 16s %s\n", $f->id, $f->bk,
@@ -81,24 +79,36 @@ class FileManager extends Module {
             break;
 
         case 'dump':
-            $files = FileModel::objects();
+            $files = AttachmentFile::objects();
             $this->_applyCriteria($options, $files);
-            if ($files->count() != 1)
+            try {
+                $f = $files->one();
+            }
+            catch (DoesNotExist $e) {
+                $this->fail('No file matches the given criteria');
+            }
+            catch (ObjectNotUnique $e) {
                 $this->fail('Criteria must select exactly 1 file');
+            }
 
-            if (($f = AttachmentFile::lookup($files[0]->id))
-                    && ($bk = $f->open()))
+            if ($bk = $f->open())
                 $bk->passthru();
             break;
 
         case 'load':
             // Load file content from STDIN
-            $files = FileModel::objects();
+            $files = AttachmentFile::objects();
             $this->_applyCriteria($options, $files);
-            if ($files->count() != 1)
+            try {
+                $f = $files->one();
+            }
+            catch (DoesNotExist $e) {
+                $this->fail('No file matches the given criteria');
+            }
+            catch (ObjectNotUnique $e) {
                 $this->fail('Criteria must select exactly 1 file');
+            }
 
-            $f = AttachmentFile::lookup($files[0]->id);
             try {
                 if ($bk = $f->open())
                     $bk->unlink();
@@ -124,24 +134,34 @@ class FileManager extends Module {
             }
             else {
                 $stream = fopen('php://stdin', 'rb');
-                while ($block = fread($stream, $bk->getBlockSize())) {
-                    if (!$bk->write($block))
+                // reading from the stream will likely return an amount of
+                // data different from the backend requested block size. Loop
+                // until $read_size bytes are recieved.
+                while (true) {
+                    $contents = '';
+                    $read_size = $bk->getBlockSize();
+                    while ($read_size > 0 && ($block = fread($stream, $read_size))) {
+                        $contents .= $block;
+                        $read_size -= strlen($block);
+                    }
+                    if (!$contents)
+                        break;
+                    if (!$bk->write($contents))
                         $this->fail('Unable to send file contents to backend');
                     if (!$type)
-                        $type = $finfo->buffer($block);
+                        $type = $finfo->buffer($contents);
                 }
                 if (!$bk->flush())
                     $this->fail('Unable to commit file contents to backend');
             }
 
             // TODO: Update file metadata
-            $sql = 'UPDATE '.FILE_TABLE.' SET bk='.db_input($bk->getBkChar())
-                .', created=CURRENT_TIMESTAMP'
-                .', type='.db_input($type)
-                .', signature='.db_input($signature)
-                .' WHERE id='.db_input($f->getId());
+            $f->bk = $bk->getBkChar();
+            $f->created = SqlFunction::NOW();
+            $f->type = $type;
+            $f->signature = $signature;
 
-            if (!db_query($sql) || db_affected_rows()!=1)
+            if (!$f->save())
                 $this->fail('Unable to update file metadata');
 
             $this->stdout->write("Successfully saved contents\n");
@@ -154,19 +174,18 @@ class FileManager extends Module {
             if (!FileStorageBackend::isRegistered($options['to']))
                 $this->fail('Target backend is not installed. See `backends` action');
 
-            $files = FileModel::objects();
+            $files = AttachmentFile::objects();
             $this->_applyCriteria($options, $files);
 
             $count = 0;
-            foreach ($files as $m) {
-                $f = AttachmentFile::lookup($m->id);
+            foreach ($files as $f) {
                 if ($f->getBackend() == $options['to'])
                     continue;
                 if ($options['verbose'])
-                    $this->stdout->write('Migrating '.$m->name."\n");
+                    $this->stdout->write('Migrating '.$f->name."\n");
                 try {
                     if (!$f->migrate($options['to']))
-                        $this->stderr->write('Unable to migrate '.$m->name."\n");
+                        $this->stderr->write('Unable to migrate '.$f->name."\n");
                     else
                         $count++;
                 }
@@ -201,7 +220,7 @@ class FileManager extends Module {
          *              is stdout
          */
         case 'export':
-            $files = FileModel::objects();
+            $files = AttachmentFile::objects();
             $this->_applyCriteria($options, $files);
 
             if (!$options['file'] || $options['file'] == '-')
@@ -210,10 +229,9 @@ class FileManager extends Module {
             if (!($stream = fopen($options['file'], 'wb')))
                 $this->fail($options['file'].': Unable to open file for export stream');
 
-            foreach ($files as $m) {
-                $f = AttachmentFile::lookup($m->id);
+            foreach ($files as $f) {
                 if ($options['verbose'])
-                    $this->stderr->write($m->name."\n");
+                    $this->stderr->write($f->name."\n");
 
                 // TODO: Log %attachment and %ticket_attachment entries
                 $info = array('file' => $f->getInfo());
@@ -298,8 +316,11 @@ class FileManager extends Module {
                 }
                 // Create a new file
                 else {
-                    $fm = FileModel::create($finfo);
-                    if (!$fm->save() || !($f = AttachmentFile::lookup($fm->id))) {
+                    // Bypass the AttachmentFile::create() because we do not
+                    // have the data to send yet.
+                    $f = new AttachmentFile($finfo);
+                    $f->__new__ = true;
+                    if (!$f->save(true)) {
                         $this->fail(sprintf(
                             '%s: Unable to create new file record',
                             $finfo['name']));
@@ -376,10 +397,8 @@ class FileManager extends Module {
                     }
 
                     // Update file to record current backend
-                    $sql = 'UPDATE '.FILE_TABLE.' SET bk='
-                        .db_input($bk->getBkChar())
-                        .' WHERE id='.db_input($f->getId());
-                    if (!db_query($sql) || db_affected_rows()!=1)
+                    $f->bk = $bk->getBkChar();
+                    if (!$f->save())
                         return false;
 
                 } // end try
@@ -401,7 +420,7 @@ class FileManager extends Module {
 
         case 'zip':
             // Create a temporary ZIP file
-            $files = FileModel::objects();
+            $files = AttachmentFile::objects();
             $this->_applyCriteria($options, $files);
             if (!$options['file'])
                 $this->fail('Please specify zip file with `-f`');
@@ -411,20 +430,21 @@ class FileManager extends Module {
                     ZipArchive::CREATE)))
                 $this->fail($reason.': Unable to create zip file');
 
-            foreach ($files as $m) {
-                $f = AttachmentFile::lookup($m->id);
+            foreach ($files as $f) {
                 if ($options['verbose'])
-                    $this->stderr->write($m->name."\n");
-                $name = Format::encode(sprintf(
-                    '%d-%s', $f->getId(), $f->getName()
-                    ), 'utf-8', 'cp437');
+                    $this->stderr->write($f->name."\n");
+                $info = pathinfo($f->getName());
+                $name = Charset::transcode(
+                    sprintf('%s-%d.%s',
+                        $info['filename'], $f->getId(), $info['extension']),
+                    'utf-8', 'cp437');
                 $zip->addFromString($name, $f->getData());
             }
             $zip->close();
             break;
 
         case 'expunge':
-            $files = FileModel::objects();
+            $files = AttachmentFile::objects();
             $this->_applyCriteria($options, $files);
 
             foreach ($files as $m) {
@@ -447,7 +467,8 @@ class FileManager extends Module {
             if (!$val) continue;
             switch ($name) {
             case 'ticket':
-                $qs->filter(array('tickets__ticket_id'=>$val));
+                $qs->filter(array('attachments__thread_entry__thread__ticket__ticket_id'=>$val));
+                $qs->distinct('id');
                 break;
             case 'file-id':
                 $qs->filter(array('id'=>$val));
@@ -459,10 +480,11 @@ class FileManager extends Module {
                 $qs->filter(array('bk'=>$val));
                 break;
             case 'status':
-                if (!in_array($val, array('open','closed')))
+                if (!in_array($val, array('open','closed','archived','deleted')))
                     $this->fail($val.': Unknown ticket status');
 
-                $qs->filter(array('tickets__ticket__status'=>$val));
+                $qs->filter(array('attachments__thread_entry__thread__ticket__status__state'=>$val));
+                $qs->distinct('id');
                 break;
 
             case 'min-size':
@@ -490,40 +512,4 @@ class FileManager extends Module {
         }
     }
 }
-
-require_once INCLUDE_DIR . 'class.orm.php';
-
-class FileModel extends VerySimpleModel {
-    static $meta = array(
-        'table' => FILE_TABLE,
-        'pk' => 'id',
-        'joins' => array(
-            'tickets' => array(
-                'null' => true,
-                'constraint' => array('id' => 'TicketAttachmentModel.file_id')
-            ),
-        ),
-    );
-}
-class TicketAttachmentModel extends VerySimpleModel {
-    static $meta = array(
-        'table' => TICKET_ATTACHMENT_TABLE,
-        'pk' => 'attach_id',
-        'joins' => array(
-            'ticket' => array(
-                'null' => false,
-                'constraint' => array('ticket_id' => 'TicketModel.ticket_id'),
-            ),
-        ),
-    );
-}
-
-class AttachmentModel extends VerySimpleModel {
-    static $meta = array(
-        'table' => ATTACHMENT_TABLE,
-        'pk' => array('object_id', 'type', 'file_id'),
-    );
-}
-
 Module::register('file', 'FileManager');
-?>
diff --git a/setup/cli/modules/i18n.php b/include/cli/modules/i18n.php
similarity index 95%
rename from setup/cli/modules/i18n.php
rename to include/cli/modules/i18n.php
index 9d5e6ca4c1fe6af88c246ff7798f3418853e195d..60f935abf7594cc7241019faead868fc734017c3 100644
--- a/setup/cli/modules/i18n.php
+++ b/include/cli/modules/i18n.php
@@ -1,7 +1,5 @@
 <?php
 
-require_once dirname(__file__) . "/class.module.php";
-require_once dirname(__file__) . "/../cli.inc.php";
 require_once INCLUDE_DIR . 'class.format.php';
 
 class i18n_Compiler extends Module {
@@ -14,6 +12,7 @@ class i18n_Compiler extends Module {
             "options" => array(
                 'list' =>       'Show list of available translations',
                 'build' =>      'Compile a language pack',
+                'similar' =>    'Find very similar strings',
                 'make-pot' =>   'Build the PO file for gettext translations',
                 'sign' =>       'Sign a language pack',
             ),
@@ -103,6 +102,9 @@ class i18n_Compiler extends Module {
                 $this->fail('Language code is required. See `list`');
             $this->_build($options['lang']);
             break;
+        case 'similar':
+            $this->find_similar($options);
+            break;
         case 'make-pot':
             $this->_make_pot($options);
             break;
@@ -593,7 +595,7 @@ class i18n_Compiler extends Module {
         }
     }
 
-    function _make_pot($options) {
+    function find_strings($options) {
         error_reporting(E_ALL);
         $funcs = array(
             '__'    => array('forms'=>1),
@@ -619,10 +621,38 @@ class i18n_Compiler extends Module {
                 self::__addString($strings, $call, $F);
             }
         }
-        $strings = array_merge($strings, $this->__getAllJsPhrases($root));
+        return array_merge($strings, $this->__getAllJsPhrases($root));
+    }
+
+    function _make_pot($options) {
+        $strings = $this->find_strings($options);
         $this->__write_pot($strings);
     }
 
+    function find_similar($options) {
+        $strings = $this->find_strings($options);
+        $roots = array();
+        foreach ($strings as $root=>$S) {
+            $roots[] = array(substr(mb_strtoupper($root), 0, 255), $S['usage'][0], $root);
+        }
+        sort($roots);
+        $length = count($roots);
+        foreach ($roots as $idx=>$root) {
+            list($phrase, $usage, $orig) = $root;
+            $i = $idx;
+            $similar = ((int) strlen($phrase) * 0.1) ?: 1;
+            while (++$i < $length) {
+                list($other, $other_usage, $other_orig) = $roots[$i];
+                if (levenshtein($phrase, $other) < $similar) {
+                    $this->stdout->write(sprintf(
+                        "'%s' (%s) and '%s' (%s)\n",
+                       $orig, $usage, $other_orig, $other_usage
+                    )); 
+                }
+            }
+        }
+    }
+
     static function __addString(&$strings, $call, $file=false) {
         if (!($forms = @$call['forms']))
             // Transation of non-constant
diff --git a/setup/cli/modules/import.php b/include/cli/modules/import.php
similarity index 98%
rename from setup/cli/modules/import.php
rename to include/cli/modules/import.php
index 27bf8962ef8d73880547ad3347c533c1c5ac3cbf..78946919c4f554bc2dd52ff9c62700f2bf02c1c4 100644
--- a/setup/cli/modules/import.php
+++ b/include/cli/modules/import.php
@@ -13,8 +13,6 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
-require_once dirname(__file__) . "/class.module.php";
-require_once dirname(__file__) . "../../cli.inc.php";
 require_once INCLUDE_DIR . "class.export.php";
 require_once INCLUDE_DIR . 'class.json.php';
 
diff --git a/include/cli/modules/list.php b/include/cli/modules/list.php
new file mode 100644
index 0000000000000000000000000000000000000000..c1c5512d2d659730d3c2296f7e4a12225232cb35
--- /dev/null
+++ b/include/cli/modules/list.php
@@ -0,0 +1,87 @@
+<?php
+include_once INCLUDE_DIR .'class.translation.php';
+
+
+class ListManager extends Module {
+    var $prologue = 'CLI list manager';
+    var $arguments = array(
+        'action' => array(
+            'help' => 'Action to be performed',
+            'options' => array(
+                'import' => 'Import list items to the system',
+                'export' => 'Export list items from the system',
+                'show' => 'Show the lists',
+            ),
+        ),
+    );
+
+
+    var $options = array(
+        'file' => array('-f', '--file', 'metavar'=>'path',
+            'help' => 'File or stream to process'),
+        'id' => array('-ID', '--id', 'metavar'=>'id',
+            'help' => 'List ID'),
+        );
+
+    var $stream;
+
+    function run($args, $options) {
+
+        Bootstrap::connect();
+
+        $list = null;
+        if ($options['id'])
+            $list = DynamicList::lookup($options['id']);
+
+        switch ($args['action']) {
+            case 'import':
+                if (!$list)
+                    $this->fail("List ID required for items import");
+
+                // Properly detect Macintosh style line endings
+                ini_set('auto_detect_line_endings', true);
+                if (!$options['file'])
+                    $this->fail('CSV file to import list items from is required!');
+                elseif (!($this->stream = fopen($options['file'], 'rb')))
+                    $this->fail("Unable to open input file [{$options['file']}]");
+
+                $extras = array();
+                $status = $list->importCsv($this->stream, $extras);
+                if (is_numeric($status))
+                    $this->stderr->write("Successfully imported $status list items\n");
+                else
+                    $this->fail($status);
+                break;
+            case 'export':
+
+                if (!$list)
+                    $this->fail("List ID required for export");
+
+                $stream = $options['file'] ?: 'php://stdout';
+                if (!($this->stream = fopen($stream, 'c')))
+                    $this->fail("Unable to open output file [{$options['file']}]");
+
+                fputcsv($this->stream, array('Value', 'Abbrev'));
+                foreach ($list->getItems() as $item)
+                    fputcsv($this->stream, array(
+                                (string) $item->getValue(),
+                                $item->getAbbrev()));
+                break;
+            case 'show':
+                $lists = DynamicList::objects()->order_by('-type', 'name');
+                foreach ($lists as $list) {
+                    $this->stdout->write(sprintf("%d %s \n",
+                                $list->getId(),
+                                $list->getName(),
+                                $list->getPluralName() ?: $list->getName()
+                                ));
+                }
+                break;
+            default:
+                $this->stderr->write('Unknown action!');
+        }
+        @fclose($this->stream);
+    }
+}
+Module::register('list', 'ListManager');
+?>
diff --git a/setup/cli/modules/org.php b/include/cli/modules/org.php
similarity index 95%
rename from setup/cli/modules/org.php
rename to include/cli/modules/org.php
index c47e5cd7d628d1638d922e1f28a09ac9cc7599a9..ae2fd79320324fdc1f7ecb6cfeffb7eb9203255f 100644
--- a/setup/cli/modules/org.php
+++ b/include/cli/modules/org.php
@@ -1,6 +1,4 @@
 <?php
-require_once dirname(__file__) . "/class.module.php";
-require_once dirname(__file__) . "/../cli.inc.php";
 
 class OrganizationManager extends Module {
     var $prologue = 'CLI organization manager';
diff --git a/setup/cli/modules/package.php b/include/cli/modules/package.php
similarity index 98%
rename from setup/cli/modules/package.php
rename to include/cli/modules/package.php
index ae7f6791e2f8c8fa30e0fbf594f520e3f0fbe48f..0374f27d6f6831bc868fc9f0aa2e743b2260412a 100644
--- a/setup/cli/modules/package.php
+++ b/include/cli/modules/package.php
@@ -95,8 +95,6 @@ class Packager extends Deployment {
     }
 
     function print_dns() {
-        include dirname(__file__).'/../cli.inc.php';
-
         $streams = DatabaseMigrater::getUpgradeStreams(INCLUDE_DIR.'upgrader/streams/');
         $this->stdout->write(sprintf(
             '"v=1; m=%s; V=%s; c=%s; s=%s"',
diff --git a/include/cli/modules/serve.php b/include/cli/modules/serve.php
new file mode 100644
index 0000000000000000000000000000000000000000..26075c6bd14e9b76b210665a105bd6c198a5998f
--- /dev/null
+++ b/include/cli/modules/serve.php
@@ -0,0 +1,95 @@
+<?php
+
+class CliServerModule extends Module {
+    var $prologue = "Run a CLI server for osTicket";
+
+    var $options = array(
+        'port' => array('-p','--port',
+            'default'=>'8000',
+            'help'=>'Specify the listening port number. Default is 8000',
+        ),
+        'host' => array('-h','--host',
+            'default'=>'localhost',
+            'help'=>'Specify the bind address. Default is "localhost"',
+        ),
+    );
+
+    function make_router() {
+        $temp = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
+        $router_path = $temp
+            . substr(md5('osticket-router'.getcwd()), -12)
+            . '.php';
+
+        // Ensure that the router file is cleaned up on exit
+        $cleanup = function() use ($router_path) {
+            @unlink($router_path);
+        };
+        if (function_exists('pcntl_signal'))
+            pcntl_signal(SIGINT, $cleanup);
+
+        // This will very likely not fire
+        register_shutdown_function($cleanup);
+
+        $fp = fopen($router_path, 'wt');
+        fwrite($fp, <<<EOF
+<?php
+\$full_path = \$_SERVER["DOCUMENT_ROOT"] . \$_SERVER["REQUEST_URI"];
+# Ensure trailing slash on folders
+if (is_dir(\$full_path)
+    && rtrim(\$full_path, '/') == \$full_path
+) {
+    header("Location: " . \$_SERVER["REQUEST_URI"] . '/');
+}
+elseif (file_exists(\$_SERVER['SCRIPT_FILENAME'])) {
+    return false;
+}
+// Support various dispatchers
+elseif (\$offs = stripos(\$_SERVER["REQUEST_URI"], 'scp/apps/')) {
+    \$_SERVER["PATH_INFO"] = substr(\$_SERVER["REQUEST_URI"], \$offs + 8);
+    chdir('scp/');
+    require "apps/dispatcher.php";
+}
+elseif (\$offs = stripos(\$_SERVER["REQUEST_URI"], 'pages/')) {
+    \$_SERVER["PATH_INFO"] = substr(\$_SERVER["REQUEST_URI"], \$offs + 5);
+    require "pages/index.php";
+}
+elseif (\$offs = stripos(\$_SERVER["REQUEST_URI"], 'api/')) {
+    \$_SERVER["PATH_INFO"] = substr(\$_SERVER["REQUEST_URI"], \$offs + 3);
+    require "api/http.php";
+}
+EOF
+        );
+        fclose($fp);
+
+        return $router_path;
+    }
+
+    function run($args, $options) {
+        $router = $this->make_router();
+        $pipes = array();
+        $php = proc_open(
+            sprintf("php -S %s:%s -t %s %s", $options['host'], $options['port'],
+                ROOT_DIR, $router),
+            array(
+                1 => array('pipe', 'w'),
+                2 => array('pipe', 'w'),
+            ), $pipes);
+
+        stream_set_blocking($pipes[1], 0);
+        stream_set_blocking($pipes[2], 0);
+
+        while (true) {
+            if (feof($pipes[1]) || feof($pipes[2])) {
+                fclose($pipes[1]);
+                fclose($pipes[2]);
+                break;
+            }
+            if ($block = fgets($pipes[1], 1024))
+                fwrite(STDOUT, $block);
+            if ($block = fgets($pipes[2], 1024))
+                fwrite(STDERR, $block);
+            usleep(100);
+        }
+    }
+}
+Module::register('serve', 'CliServerModule');
diff --git a/setup/cli/modules/unpack.php b/include/cli/modules/unpack.php
similarity index 92%
rename from setup/cli/modules/unpack.php
rename to include/cli/modules/unpack.php
index b064a435be9bbb2d4d990677bb7949467707570c..75b73353deb0f212bed2a65cb420707c5b3eaedd 100644
--- a/setup/cli/modules/unpack.php
+++ b/include/cli/modules/unpack.php
@@ -1,7 +1,5 @@
 <?php
 
-require_once dirname(__file__) . "/class.module.php";
-
 class Unpacker extends Module {
 
     var $prologue = "Unpacks osTicket into target install path";
@@ -199,21 +197,27 @@ class Unpacker extends Module {
         if (isset($location))
             return $location;
 
-        $bootstrap_php = $this->destination . '/bootstrap.php';
-        if (!is_file($bootstrap_php))
-            return @$this->include_path ?: '';
+        $pipes = array();
+        $php = proc_open('php', array(
+            0 => array('pipe', 'r'),
+            1 => array('pipe', 'w'),
+        ), $pipes);
 
-        $lines = preg_grep("/define\s*\(\s*'INCLUDE_DIR'/",
-            explode("\n", file_get_contents($bootstrap_php)));
+        fwrite($pipes[0], "<?php
+        include '{$this->destination}/bootstrap.php';
+        print INCLUDE_DIR;
+        ");
+        fclose($pipes[0]);
 
-        // NOTE: that this won't work for crafty folks who have a define or some
-        //       variable in the value of their include path
-        if (!defined('ROOT_DIR'))
-            define('ROOT_DIR', rtrim($this->destination, '/').'/');
-        foreach ($lines as $line)
-            @eval($line);
+        $INCLUDE_DIR = fread($pipes[1], 8192);
+        proc_close($php);
+
+        return $location = rtrim($INCLUDE_DIR, '/').'/';
+    }
 
-        return $location = rtrim(INCLUDE_DIR, '/').'/';
+    function bootstrap() {
+        // Don't load config and frieds as that will likely crash if not yet
+        // installed
     }
 
     function run($args, $options) {
diff --git a/include/cli/modules/upgrade.php b/include/cli/modules/upgrade.php
new file mode 100644
index 0000000000000000000000000000000000000000..297d6d56f84d36dcdc1ad23c42410a09d483b165
--- /dev/null
+++ b/include/cli/modules/upgrade.php
@@ -0,0 +1,96 @@
+<?php
+require_once INCLUDE_DIR.'class.upgrader.php';
+
+class CliUpgrader extends Module {
+    var $prologue = "Upgrade an osTicket help desk";
+
+    var $options = array(
+        'summary' => array('-s', '--summary',
+            'action' => 'store_true',
+            'help' => 'Print an upgrade summary and exit'),
+    );
+
+    function run($args, $options) {
+        Bootstrap::connect();
+
+        // Pre-checks
+        global $ost;
+        $ost = osTicket::start();
+        $upgrader = $this->getUpgrader();
+
+        if ($options['summary']) {
+            $this->doSummary($upgrader);
+        }
+        else {
+            $this->upgrade($upgrader);
+        }
+    }
+
+    function getUpgrader() {
+        global $ost;
+
+        if (!$ost->isUpgradePending()) {
+            $this->fail('No upgrade is pending for this account');
+        }
+
+        $upgrader = new Upgrader(TABLE_PREFIX, UPGRADE_DIR.'streams/');
+        if (!$upgrader->isUpgradable()) {
+            $this->fail(__('The upgrader does NOT support upgrading from the current vesion!'));
+        }
+        elseif (!$upgrader->check_prereq()) {
+            $this->fail(__('Minimum requirements not met! Refer to Release Notes for more information'));
+        }
+        elseif (!strcasecmp(basename(CONFIG_FILE), 'settings.php')) {
+            $this->fail(__('Config file rename required to continue!'));
+        }
+        return $upgrader;
+    }
+
+    function upgrade($upgrader) {
+        global $ost, $cfg;
+        $cfg = $ost->getConfig();
+
+        while (true) {
+            if ($upgrader->getTask()) {
+                // If there's anythin in the model cache (like a Staff
+                // object or something), ensure that changes to the database
+                // model won't cause crashes
+                ModelInstanceManager::flushCache();
+
+                // More pending tasks - doTasks returns the number of pending tasks
+                $this->stdout->write("... {$upgrader->getNextAction()}\n");
+                $upgrader->doTask();
+            }
+            elseif ($ost->isUpgradePending()) {
+                if ($upgrader->isUpgradable()) {
+                    $this->stdout->write("... {$upgrader->getNextVersion()}\n");
+                    $upgrader->upgrade();
+                    // Reload config to pull schema_signature changes
+                    $cfg->load();
+                }
+                else {
+                    $this->fail(sprintf(
+                        __('Upgrade Failed: Invalid or wrong hash [%s]'),
+                        $ost->getDBSignature()
+                    ));
+                }
+            }
+            elseif (!$ost->isUpgradePending()) {
+                $this->stdout->write("Yay! All finished!\n");
+                break;
+            }
+        }
+    }
+
+    function doSummary($upgrader) {
+        foreach ($upgrader->getPatches() as $p) {
+            $info = $upgrader->readPatchInfo($p);
+            $this->stdout->write(sprintf(
+                "%s :: %s (%s)\n", $info['version'], $info['title'],
+                substr($info['signature'], 0, 8)
+            ));
+        }
+    }
+}
+
+Module::register('upgrade', 'CliUpgrader');
diff --git a/include/cli/modules/user.php b/include/cli/modules/user.php
new file mode 100644
index 0000000000000000000000000000000000000000..00dd6af894463a53fe61e5567c5d84ae3f7c118d
--- /dev/null
+++ b/include/cli/modules/user.php
@@ -0,0 +1,216 @@
+<?php
+
+class UserManager extends Module {
+    var $prologue = 'CLI user manager';
+
+    var $arguments = array(
+        'action' => array(
+            'help' => 'Action to be performed',
+            'options' => array(
+                'import' => 'Import users from CSV file',
+                'export' => 'Export users from the system to CSV',
+                'activate' => 'Create or activate an account',
+                'lock' => "Lock a user's account",
+                'set-password' => "Set a user's account password",
+                'list' => 'List users based on search criteria',
+            ),
+        ),
+    );
+
+
+    var $options = array(
+        'file' => array('-f', '--file', 'metavar'=>'path',
+            'help' => 'File or stream to process'),
+        'org' => array('-O', '--org', 'metavar'=>'ORGID',
+            'help' => 'Set the organization ID on import'),
+
+        'send-mail' => array('-m', '--send-mail',
+            'help' => 'Send the user an email. Used with `activate` and `set-password`',
+            'default' => false, 'action' => 'store_true'),
+
+        'verbose' => array('-v', '--verbose', 'default'=>false,
+            'action'=>'store_true', 'help' => 'Be more verbose'
+        ),
+
+        // -- Search criteria
+        'account' => array('-A', '--account', 'type'=>'bool', 'metavar'=>'bool',
+            'help' => 'Search for users based on activation status'),
+        'isconfirmed' => array('-C', '--isconfirmed', 'type'=>'bool', 'metavar'=>'bool',
+            'help' => 'Search for users based on confirmation status'),
+        'islocked' => array('-L', '--islocked', 'type'=>'bool', 'metavar'=>'bool',
+            'help' => 'Search for users based on locked status'),
+        'email' => array('-E', '--email',
+            'help' => 'Search by email address'),
+        'id' => array('-U', '--id',
+            'help' => 'Search by user id'),
+        );
+
+    var $stream;
+
+    function run($args, $options) {
+
+        Bootstrap::connect();
+
+        switch ($args['action']) {
+        case 'import':
+            // Properly detect Macintosh style line endings
+            ini_set('auto_detect_line_endings', true);
+
+            if (!$options['file'] || $options['file'] == '-')
+                $options['file'] = 'php://stdin';
+            if (!($this->stream = fopen($options['file'], 'rb')))
+                $this->fail("Unable to open input file [{$options['file']}]");
+
+            $extras = array();
+            if ($options['org']) {
+                if (!($org = Organization::lookup($options['org'])))
+                    $this->fail($options['org'].': Unknown organization ID');
+                $extras['org_id'] = $options['org'];
+            }
+            $status = User::importCsv($this->stream, $extras);
+            if (is_numeric($status))
+                $this->stderr->write("Successfully imported $status clients\n");
+            else
+                $this->fail($status);
+            break;
+
+        case 'export':
+            $stream = $options['file'] ?: 'php://stdout';
+            if (!($this->stream = fopen($stream, 'c')))
+                $this->fail("Unable to open output file [{$options['file']}]");
+
+            fputcsv($this->stream, array('Name', 'Email'));
+            foreach (User::objects() as $user)
+                fputcsv($this->stream,
+                        array((string) $user->getName(), $user->getEmail()));
+            break;
+
+        case 'activate':
+            $users = $this->getQuerySet($options);
+            foreach ($users as $U) {
+                if ($options['verbose']) {
+                    $this->stderr->write(sprintf(
+                        "Activating %s <%s>\n",
+                        $U->getName(), $U->getDefaultEmail()
+                    ));
+                }
+                if (!($account = $U->getAccount())) {
+                    $account = UserAccount::create(array('user' => $U));
+                    $U->account = $account;
+                    $U->save();
+                }
+
+                if ($options['send-mail']) {
+                    global $ost, $cfg;
+                    $ost = osTicket::start();
+                    $cfg = $ost->getConfig();
+
+                    if (($error = $account->sendConfirmEmail()) && $error !== true) {
+                        $this->warn(sprintf('%s: Unable to send email: %s',
+                            $U->getDefaultEmail(), $error->getMessage()
+                        ));
+                    }
+                }
+            }
+
+            break;
+
+        case 'lock':
+            $users = $this->getQuerySet($options);
+            $users->select_related('account');
+            foreach ($users as $U) {
+                if (!($account = $U->getAccount())) {
+                    $this->warn(sprintf(
+                        '%s: User does not have a client account',
+                        $U->getName()
+                    ));
+                }
+                $account->setFlag(UserAccountStatus::LOCKED);
+                $account->save();
+            }
+
+            break;
+
+        case 'list':
+            $users = $this->getQuerySet($options);
+
+            foreach ($users as $U) {
+                $this->stdout->write(sprintf(
+                    "%d %s <%s>%s\n",
+                    $U->id, $U->getName(), $U->getDefaultEmail(),
+                    ($O = $U->getOrganization()) ? " ({$O->getName()})" : ''
+                ));
+            }
+
+            break;
+
+        case 'set-password':
+            $this->stderr->write('Enter new password: ');
+            $ps1 = fgets(STDIN);
+            if (!function_exists('posix_isatty') || !posix_isatty(STDIN)) {
+                $this->stderr->write('Re-enter new password: ');
+                $ps2 = fgets(STDIN);
+
+                if ($ps1 != $ps2)
+                    $this->fail('Passwords do not match');
+            }
+
+            // Account is required
+            $options['account'] = true;
+            $users = $this->getQuerySet($options);
+
+            $updated  = 0;
+            foreach ($users as $U) {
+                $U->account->setPassword($ps1);
+                if ($U->account->save())
+                    $updated++;
+            }
+            $this->stdout->write(sprintf('Updated %d users', $updated));
+            break;
+
+        default:
+            $this->stderr->write('Unknown action!');
+        }
+        @fclose($this->stream);
+    }
+
+    function getQuerySet($options, $requireOne=false) {
+        $users = User::objects();
+        foreach ($options as $O=>$V) {
+            if (!isset($V))
+                continue;
+            switch ($O) {
+            case 'account':
+                $users->filter(array('account__isnull' => !$V));
+                break;
+
+            case 'isconfirmed':
+            case 'islocked':
+                $flags = array(
+                    'isconfirmed' => UserAccountStatus::CONFIRMED,
+                    'islocked' => UserAccountStatus::LOCKED,
+                );
+                $Q = new Q(array('account__status__hasbit'=>$flags[$O]));
+                if (!$V)
+                    $Q->negate();
+                $users->filter($Q);
+                break;
+
+            case 'org':
+                if (is_numeric($V))
+                    $users->filter(array('org__id'=>$V));
+                else
+                    $users->filter(array('org__name__contains'=>$V));
+                break;
+
+            case 'id':
+                $users->filter(array('id'=>$V));
+                break;
+            }
+
+        }
+        return $users;
+    }
+}
+Module::register('user', 'UserManager');
+?>
diff --git a/include/client/accesslink.inc.php b/include/client/accesslink.inc.php
index 9143186b8ed6ea92ce42790ea84ac437a0ebcbb8..b73dae29f1cf287842317b7117983ef88429a711 100644
--- a/include/client/accesslink.inc.php
+++ b/include/client/accesslink.inc.php
@@ -25,12 +25,12 @@ else
     <div>
         <label for="email"><?php echo __('E-Mail Address'); ?>:
         <input id="email" placeholder="<?php echo __('e.g. john.doe@osticket.com'); ?>" type="text"
-            name="lemail" size="30" value="<?php echo $email; ?>"></label>
+            name="lemail" size="30" value="<?php echo $email; ?>" class="nowarn"></label>
     </div>
     <div>
         <label for="ticketno"><?php echo __('Ticket Number'); ?>:
         <input id="ticketno" type="text" name="lticket" placeholder="<?php echo __('e.g. 051243'); ?>"
-            size="30" value="<?php echo $ticketid; ?>"></label>
+            size="30" value="<?php echo $ticketid; ?>" class="nowarn"></label>
     </div>
     <p>
         <input class="btn" type="submit" value="<?php echo $button; ?>">
@@ -50,7 +50,11 @@ else
 </form>
 <br>
 <p>
-<?php echo sprintf(
-__("If this is your first time contacting us or you've lost the ticket number, please %s open a new ticket %s"),
-    '<a href="open.php">','</a>'); ?>
+<?php
+if ($cfg->getClientRegistrationMode() != 'disabled'
+    || !$cfg->isClientLoginRequired()) {
+    echo sprintf(
+    __("If this is your first time contacting us or you've lost the ticket number, please %s open a new ticket %s"),
+        '<a href="open.php">','</a>');
+} ?>
 </p>
diff --git a/include/client/faq-category.inc.php b/include/client/faq-category.inc.php
index 5a17e2176b75349d231bf22a00f50c02f39d88c3..3ce0b7230dcdff9e3ded235c902b8db55b2f9315 100644
--- a/include/client/faq-category.inc.php
+++ b/include/client/faq-category.inc.php
@@ -1,34 +1,66 @@
 <?php
 if(!defined('OSTCLIENTINC') || !$category || !$category->isPublic()) die('Access Denied');
 ?>
-<h1><strong><?php echo $category->getName() ?></strong></h1>
+
+<div class="row">
+<div class="span8">
+    <h1><?php echo __('Frequently Asked Questions');?></h1>
+    <h2><strong><?php echo $category->getLocalName() ?></strong></h2>
 <p>
-<?php echo Format::safe_html($category->getDescription()); ?>
+<?php echo Format::safe_html($category->getLocalDescriptionWithImages()); ?>
 </p>
 <hr>
 <?php
-$sql='SELECT faq.faq_id, question, count(attach.file_id) as attachments '
-    .' FROM '.FAQ_TABLE.' faq '
-    .' LEFT JOIN '.ATTACHMENT_TABLE.' attach
-         ON(attach.object_id=faq.faq_id AND attach.type=\'F\' AND attach.inline = 0) '
-    .' WHERE faq.ispublished=1 AND faq.category_id='.db_input($category->getId())
-    .' GROUP BY faq.faq_id '
-    .' ORDER BY question';
-if(($res=db_query($sql)) && db_num_rows($res)) {
+$faqs = FAQ::objects()
+    ->filter(array('category'=>$category))
+    ->exclude(array('ispublished'=>FAQ::VISIBILITY_PRIVATE))
+    ->annotate(array('has_attachments' => SqlAggregate::COUNT(SqlCase::N()
+        ->when(array('attachments__inline'=>0), 1)
+        ->otherwise(null)
+    )))
+    ->order_by('-ispublished', 'question');
+
+if ($faqs->exists(true)) {
     echo '
-         <h2>'.__('Frequently Asked Questions').'</h2>
+         <h2>'.__('Further Articles').'</h2>
          <div id="faq">
             <ol>';
-    while($row=db_fetch_array($res)) {
-        $attachments=$row['attachments']?'<span class="Icon file"></span>':'';
+foreach ($faqs as $F) {
+        $attachments=$F->has_attachments?'<span class="Icon file"></span>':'';
         echo sprintf('
             <li><a href="faq.php?id=%d" >%s &nbsp;%s</a></li>',
-            $row['faq_id'],Format::htmlchars($row['question']), $attachments);
+            $F->getId(),Format::htmlchars($F->question), $attachments);
     }
     echo '  </ol>
-         </div>
-         <p><a class="back" href="index.php">&laquo; '.__('Go Back').'</a></p>';
+         </div>';
 }else {
     echo '<strong>'.__('This category does not have any FAQs.').' <a href="index.php">'.__('Back To Index').'</a></strong>';
 }
 ?>
+</div>
+
+<div class="span4">
+    <div class="sidebar">
+    <div class="searchbar">
+        <form method="get" action="faq.php">
+        <input type="hidden" name="a" value="search"/>
+        <input type="text" name="q" class="search" placeholder="<?php
+            echo __('Search our knowledge base'); ?>"/>
+        <input type="submit" style="display:none" value="search"/>
+        </form>
+    </div>
+    <div class="content">
+        <section>
+            <div class="header"><?php echo __('Help Topics'); ?></div>
+<?php
+foreach (Topic::objects()
+    ->filter(array('faqs__faq__category__category_id'=>$category->getId()))
+    as $t) { ?>
+        <a href="?topicId=<?php echo urlencode($t->getId()); ?>"
+            ><?php echo $t->getFullName(); ?></a>
+<?php } ?>
+        </section>
+    </div>
+    </div>
+</div>
+</div>
diff --git a/include/client/faq.inc.php b/include/client/faq.inc.php
index 72a395b966aaec5962742b13721a874b554f3cb9..c684fda771b03aec476768075297efe4537a843a 100644
--- a/include/client/faq.inc.php
+++ b/include/client/faq.inc.php
@@ -4,29 +4,62 @@ if(!defined('OSTCLIENTINC') || !$faq  || !$faq->isPublished()) die('Access Denie
 $category=$faq->getCategory();
 
 ?>
+<div class="row">
+<div class="span8">
+
 <h1><?php echo __('Frequently Asked Questions');?></h1>
 <div id="breadcrumbs">
     <a href="index.php"><?php echo __('All Categories');?></a>
     &raquo; <a href="faq.php?cid=<?php echo $category->getId(); ?>"><?php echo $category->getName(); ?></a>
 </div>
-<div style="width:700px;padding-top:2px;" class="pull-left">
-<strong style="font-size:16px;"><?php echo $faq->getQuestion() ?></strong>
+
+<div class="faq-content">
+<div class="article-title flush-left">
+<?php echo $faq->getLocalQuestion() ?>
+</div>
+<div class="faded"><?php echo __('Last updated').' '
+    . Format::relativeTime(Misc::db2gmtime($category->getUpdateDate())); ?></div>
+<br/>
+<div class="thread-body bleed">
+<?php echo $faq->getLocalAnswerWithImages(); ?>
+</div>
+</div>
+</div>
+
+<div class="span4 pull-right">
+<div class="sidebar">
+<div class="searchbar">
+    <form method="get" action="faq.php">
+    <input type="hidden" name="a" value="search"/>
+    <input type="text" name="q" class="search" placeholder="<?php
+        echo __('Search our knowledge base'); ?>"/>
+    <input type="submit" style="display:none" value="search"/>
+    </form>
+</div>
+<div class="content"><?php
+    if ($attachments = $faq->getLocalAttachments()->all()) { ?>
+<section>
+    <strong><?php echo __('Attachments');?>:</strong>
+<?php foreach ($attachments as $att) { ?>
+    <div>
+    <a href="<?php echo $att->file->getDownloadUrl(); ?>" class="no-pjax">
+        <i class="icon-file"></i>
+        <?php echo Format::htmlchars($att->getFilename()); ?>
+    </a>
+    </div>
+<?php } ?>
+</section>
+<?php }
+if ($faq->getHelpTopics()->count()) { ?>
+<section>
+    <strong><?php echo __('Help Topics'); ?></strong>
+<?php foreach ($faq->getHelpTopics() as $T) { ?>
+    <div><?php echo $T->topic->getFullName(); ?></div>
+<?php } ?>
+</section>
+<?php }
+?></div>
 </div>
-<div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;"></div>
-<div class="clear"></div>
-<div class="thread-body">
-<?php echo Format::safe_html($faq->getAnswerWithImages()); ?>
 </div>
-<p>
-<?php
-if($faq->getNumAttachments()) { ?>
- <div><span class="faded"><b><?php echo __('Attachments');?>:</b></span>  <?php echo $faq->getAttachmentsLinks(); ?></div>
-<?php
-} ?>
 
-<div class="article-meta"><span class="faded"><b><?php echo __('Help Topics');?>:</b></span>
-    <?php echo ($topics=$faq->getHelpTopics())?implode(', ',$topics):' '; ?>
 </div>
-</p>
-<hr>
-<div class="faded">&nbsp;<?php echo __('Last updated').' '.Format::db_daydatetime($category->getUpdateDate()); ?></div>
diff --git a/include/client/footer.inc.php b/include/client/footer.inc.php
index 9ff4ad15265c68a7a7938535786cae3ea9f910db..9521fbfc73a61d03735241bf4a6890b98c13b4a0 100644
--- a/include/client/footer.inc.php
+++ b/include/client/footer.inc.php
@@ -14,5 +14,12 @@ if (($lang = Internationalization::getCurrentLanguage()) && $lang != 'en_US') {
     <script type="text/javascript" src="ajax.php/i18n/<?php
         echo $lang; ?>/js"></script>
 <?php } ?>
+<script type="text/javascript">
+    getConfig().resolve(<?php
+        include INCLUDE_DIR . 'ajax.config.php';
+        $api = new ConfigAjaxAPI();
+        print $api->client(false);
+    ?>);
+</script>
 </body>
 </html>
diff --git a/include/client/header.inc.php b/include/client/header.inc.php
index 14dc65411cd81ae7926ac07c7fc9593f42f654c6..680dc71aedc7e8006a6f9be60958c5c9c9fb66eb 100644
--- a/include/client/header.inc.php
+++ b/include/client/header.inc.php
@@ -6,13 +6,21 @@ $signin_url = ROOT_PATH . "login.php"
 $signout_url = ROOT_PATH . "logout.php?auth=".$ost->getLinkToken();
 
 header("Content-Type: text/html; charset=UTF-8");
+if (($lang = Internationalization::getCurrentLanguage())) {
+    $langs = array_unique(array($lang, $cfg->getPrimaryLanguage()));
+    $langs = Internationalization::rfc1766($langs);
+    header("Content-Language: ".implode(', ', $langs));
+}
 ?>
 <!DOCTYPE html>
-<html <?php
-if (($lang = Internationalization::getCurrentLanguage())
+<html<?php
+if ($lang
         && ($info = Internationalization::getLanguageInfo($lang))
         && (@$info['direction'] == 'rtl'))
-    echo 'dir="rtl" class="rtl"';
+    echo ' dir="rtl" class="rtl"';
+if ($lang) {
+    echo ' lang="' . $lang . '"';
+}
 ?>>
 <head>
     <meta charset="utf-8">
@@ -33,19 +41,40 @@ if (($lang = Internationalization::getCurrentLanguage())
     <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/font-awesome.min.css">
     <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/flags.css">
     <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/rtl.css"/>
-    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-1.8.3.min.js"></script>
+    <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/select2.min.css">
+    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-1.11.2.min.js"></script>
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-1.10.3.custom.min.js"></script>
     <script src="<?php echo ROOT_PATH; ?>js/osticket.js"></script>
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/filedrop.field.js"></script>
-    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery.multiselect.min.js"></script>
     <script src="<?php echo ROOT_PATH; ?>scp/js/bootstrap-typeahead.js"></script>
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor.min.js"></script>
+    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor-plugins.js"></script>
     <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor-osticket.js"></script>
-    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor-fonts.js"></script>
+    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/select2.min.js"></script>
+    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/fabric.min.js"></script>
     <?php
     if($ost && ($headers=$ost->getExtraHeaders())) {
         echo "\n\t".implode("\n\t", $headers)."\n";
     }
+
+    // Offer alternate links for search engines
+    // @see https://support.google.com/webmasters/answer/189077?hl=en
+    if (($all_langs = Internationalization::getConfiguredSystemLanguages())
+        && (count($all_langs) > 1)
+    ) {
+        $langs = Internationalization::rfc1766(array_keys($all_langs));
+        $qs = array();
+        parse_str($_SERVER['QUERY_STRING'], $qs);
+        foreach ($langs as $L) {
+            $qs['lang'] = $L; ?>
+        <link rel="alternate" href="//<?php echo $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; ?>?<?php
+            echo http_build_query($qs); ?>" hreflang="<?php echo $L; ?>" />
+<?php
+        } ?>
+        <link rel="alternate" href="//<?php echo $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; ?>"
+            hreflang="x-default";
+<?php
+    }
     ?>
 </head>
 <body>
@@ -77,14 +106,17 @@ if (($lang = Internationalization::getCurrentLanguage())
             </p>
             <p>
 <?php
-if (($all_langs = Internationalization::availableLanguages())
+if (($all_langs = Internationalization::getConfiguredSystemLanguages())
     && (count($all_langs) > 1)
 ) {
+    $qs = array();
+    parse_str($_SERVER['QUERY_STRING'], $qs);
     foreach ($all_langs as $code=>$info) {
         list($lang, $locale) = explode('_', $code);
+        $qs['lang'] = $code;
 ?>
         <a class="flag flag-<?php echo strtolower($locale ?: $info['flag'] ?: $lang); ?>"
-            href="?<?php echo urlencode($_GET['QUERY_STRING']); ?>&amp;lang=<?php echo $code;
+            href="?<?php echo http_build_query($qs);
             ?>" title="<?php echo Internationalization::getLanguageDescription($code); ?>">&nbsp;</a>
 <?php }
 } ?>
diff --git a/include/client/kb-categories.inc.php b/include/client/kb-categories.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..236847fb6dbf75439b83d8e08932df4d767e8997
--- /dev/null
+++ b/include/client/kb-categories.inc.php
@@ -0,0 +1,68 @@
+<div class="row">
+<div class="span8">
+<?php
+    $categories = Category::objects()
+        ->exclude(Q::any(array(
+            'ispublic'=>Category::VISIBILITY_PRIVATE,
+            'faqs__ispublished'=>FAQ::VISIBILITY_PRIVATE,
+        )))
+        ->annotate(array('faq_count'=>SqlAggregate::COUNT('faqs')))
+        ->filter(array('faq_count__gt'=>0));
+    if ($categories->exists(true)) { ?>
+        <div><?php echo __('Click on the category to browse FAQs.'); ?></div>
+        <ul id="kb">
+<?php
+        foreach ($categories as $C) { ?>
+            <li><i></i>
+            <div style="margin-left:45px">
+            <h4><?php echo sprintf('<a href="faq.php?cid=%d">%s (%d)</a>',
+                $C->getId(), Format::htmlchars($C->getLocalName()), $C->faq_count); ?></h4>
+            <div class="faded" style="margin:10px 0">
+                <?php echo Format::safe_html($C->getLocalDescriptionWithImages()); ?>
+            </div>
+<?php       foreach ($C->faqs
+                    ->exclude(array('ispublished'=>FAQ::VISIBILITY_PRIVATE))
+                    ->limit(5) as $F) { ?>
+                <div class="popular-faq"><i class="icon-file-alt"></i>
+                <a href="faq.php?id=<?php echo $F->getId(); ?>">
+                <?php echo $F->getLocalQuestion() ?: $F->getQuestion(); ?>
+                </a></div>
+<?php       } ?>
+            </div>
+            </li>
+<?php   } ?>
+       </ul>
+<?php
+    } else {
+        echo __('NO FAQs found');
+    }
+?>
+</div>
+<div class="span4">
+    <div class="sidebar">
+    <div class="searchbar">
+        <form method="get" action="faq.php">
+        <input type="hidden" name="a" value="search"/>
+        <select name="topicId"  style="width:100%;max-width:100%"
+            onchange="javascript:this.form.submit();">
+            <option value="">— Browse by Topic —</option>
+<?php
+$topics = Topic::objects()
+    ->annotate(array('has_faqs'=>SqlAggregate::COUNT('faqs')))
+    ->filter(array('has_faqs__gt'=>0));
+foreach ($topics as $T) { ?>
+        <option value="<?php echo $T->getId(); ?>"><?php echo $T->getFullName();
+            ?></option>
+<?php } ?>
+        </select>
+        </form>
+    </div>
+    <br/>
+    <div class="content">
+        <section>
+            <div class="header"><?php echo __('Other Resources'); ?></div>
+        </section>
+    </div>
+    </div>
+</div>
+</div>
diff --git a/include/client/kb-search.inc.php b/include/client/kb-search.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..a81f5e4fdea5f7064c1f5dacbeb88fab99fb296c
--- /dev/null
+++ b/include/client/kb-search.inc.php
@@ -0,0 +1,58 @@
+<div class="row">
+<div class="span8">
+    <h1><?php echo __('Frequently Asked Questions');?></h1>
+    <div><strong><?php echo __('Search Results'); ?></strong></div>
+<?php
+    if ($faqs->exists(true)) {
+        echo '<div id="faq">'.sprintf(__('%d FAQs matched your search criteria.'),
+            $faqs->count())
+            .'<ol>';
+        foreach ($faqs as $F) {
+            echo sprintf(
+                '<li><a href="faq.php?id=%d" class="previewfaq">%s</a></li>',
+                $F->getId(), $F->getLocalQuestion(), $F->getVisibilityDescription());
+        }
+        echo '</ol></div>';
+    } else {
+        echo '<strong class="faded">'.__('The search did not match any FAQs.').'</strong>';
+    }
+?>
+</div>
+
+<div class="span4">
+    <div class="sidebar">
+    <div class="searchbar">
+        <form method="get" action="faq.php">
+        <input type="hidden" name="a" value="search"/>
+        <input type="text" name="q" class="search" placeholder="<?php
+            echo __('Search our knowledge base'); ?>"/>
+        <input type="submit" style="display:none" value="search"/>
+        </form>
+    </div>
+    <div class="content">
+        <section>
+            <div class="header"><?php echo __('Help Topics'); ?></div>
+<?php
+foreach (Topic::objects()
+    ->annotate(array('faqs_count'=>SqlAggregate::count('faqs')))
+    ->filter(array('faqs_count__gt'=>0))
+    as $t) { ?>
+        <div><a href="?topicId=<?php echo urlencode($t->getId()); ?>"
+            ><?php echo $t->getFullName(); ?></a></div>
+<?php } ?>
+        </section>
+        <section>
+            <div class="header"><?php echo __('Categories'); ?></div>
+<?php
+foreach (Category::objects()
+    ->annotate(array('faqs_count'=>SqlAggregate::count('faqs')))
+    ->filter(array('faqs_count__gt'=>0))
+    as $C) { ?>
+        <div><a href="?cid=<?php echo urlencode($C->getId()); ?>"
+            ><?php echo $C->getLocalName(); ?></a></div>
+<?php } ?>
+        </section>
+    </div>
+    </div>
+</div>
+</div>
diff --git a/include/client/knowledgebase.inc.php b/include/client/knowledgebase.inc.php
index c3ab59ef5098f48963ae85212d14ee1ad9b5a60a..ac4a82f6941f7263b97bef48974865457fbf9fe1 100644
--- a/include/client/knowledgebase.inc.php
+++ b/include/client/knowledgebase.inc.php
@@ -2,123 +2,33 @@
 if(!defined('OSTCLIENTINC')) die('Access Denied');
 
 ?>
-<h1><?php echo __('Frequently Asked Questions');?></h1>
-<form action="index.php" method="get" id="kb-search">
-    <input type="hidden" name="a" value="search">
-    <div>
-        <input id="query" type="text" size="20" name="q" value="<?php echo Format::htmlchars($_REQUEST['q']); ?>">
-        <select name="cid" id="cid">
-            <option value="">&mdash; <?php echo __('All Categories');?> &mdash;</option>
-            <?php
-            $sql='SELECT category_id, name, count(faq.category_id) as faqs '
-                .' FROM '.FAQ_CATEGORY_TABLE.' cat '
-                .' LEFT JOIN '.FAQ_TABLE.' faq USING(category_id) '
-                .' WHERE cat.ispublic=1 AND faq.ispublished=1 '
-                .' GROUP BY cat.category_id '
-                .' HAVING faqs>0 '
-                .' ORDER BY cat.name DESC ';
-            if(($res=db_query($sql)) && db_num_rows($res)) {
-                while($row=db_fetch_array($res))
-                    echo sprintf('<option value="%d" %s>%s (%d)</option>',
-                            $row['category_id'],
-                            ($_REQUEST['cid'] && $row['category_id']==$_REQUEST['cid']?'selected="selected"':''),
-                            $row['name'],
-                            $row['faqs']);
-            }
-            ?>
-        </select>
-        <input id="searchSubmit" type="submit" value="<?php echo __('Search');?>">
-    </div>
-    <div>
-        <select name="topicId" id="topic-id">
-            <option value="">&mdash; <?php echo __('All Help Topics');?> &mdash;</option>
-            <?php
-            $sql='SELECT ht.topic_id, CONCAT_WS(" / ", pht.topic, ht.topic) as helptopic, count(faq.topic_id) as faqs '
-                .' FROM '.TOPIC_TABLE.' ht '
-                .' LEFT JOIN '.TOPIC_TABLE.' pht ON (pht.topic_id=ht.topic_pid) '
-                .' LEFT JOIN '.FAQ_TOPIC_TABLE.' faq ON(faq.topic_id=ht.topic_id) '
-                .' WHERE ht.ispublic=1 '
-                .' GROUP BY ht.topic_id '
-                .' HAVING faqs>0 '
-                .' ORDER BY helptopic ';
-            if(($res=db_query($sql)) && db_num_rows($res)) {
-                while($row=db_fetch_array($res))
-                    echo sprintf('<option value="%d" %s>%s (%d)</option>',
-                            $row['topic_id'],
-                            ($_REQUEST['topicId'] && $row['topic_id']==$_REQUEST['topicId']?'selected="selected"':''),
-                            $row['helptopic'], $row['faqs']);
-            }
-            ?>
-        </select>
-    </div>
-</form>
-<hr>
-<div>
 <?php
-if($_REQUEST['q'] || $_REQUEST['cid'] || $_REQUEST['topicId']) { //Search.
-    $sql='SELECT faq.faq_id, question '
-        .' FROM '.FAQ_TABLE.' faq '
-        .' LEFT JOIN '.FAQ_CATEGORY_TABLE.' cat ON(cat.category_id=faq.category_id) '
-        .' LEFT JOIN '.FAQ_TOPIC_TABLE.' ft ON(ft.faq_id=faq.faq_id) '
-        .' WHERE faq.ispublished=1 AND cat.ispublic=1';
+if($_REQUEST['q'] || $_REQUEST['cid'] || $_REQUEST['topicId']) { //Search
+    $faqs = FAQ::allPublic()
+        ->annotate(array(
+            'attachment_count'=>SqlAggregate::COUNT('attachments'),
+            'topic_count'=>SqlAggregate::COUNT('topics')
+        ))
+        ->order_by('question');
 
-    if($_REQUEST['cid'])
-        $sql.=' AND faq.category_id='.db_input($_REQUEST['cid']);
+    if ($_REQUEST['cid'])
+        $faqs->filter(array('category_id'=>$_REQUEST['cid']));
 
-    if($_REQUEST['topicId'])
-        $sql.=' AND ft.topic_id='.db_input($_REQUEST['topicId']);
+    if ($_REQUEST['topicId'])
+        $faqs->filter(array('topics__topic_id'=>$_REQUEST['topicId']));
 
+    if ($_REQUEST['q'])
+        $faqs->filter(Q::ANY(array(
+            'question__contains'=>$_REQUEST['q'],
+            'answer__contains'=>$_REQUEST['q'],
+            'keywords__contains'=>$_REQUEST['q'],
+            'category__name__contains'=>$_REQUEST['q'],
+            'category__description__contains'=>$_REQUEST['q'],
+        )));
 
-    if($_REQUEST['q']) {
-        $sql.=" AND (question LIKE ('%".db_input($_REQUEST['q'],false)."%')
-                 OR answer LIKE ('%".db_input($_REQUEST['q'],false)."%')
-                 OR keywords LIKE ('%".db_input($_REQUEST['q'],false)."%')
-                 OR cat.name LIKE ('%".db_input($_REQUEST['q'],false)."%')
-                 OR cat.description LIKE ('%".db_input($_REQUEST['q'],false)."%')
-                 )";
-    }
+    include CLIENTINC_DIR . 'kb-search.inc.php';
 
-    $sql.=' GROUP BY faq.faq_id ORDER BY question';
-    echo "<div><strong>".__('Search Results').'</strong></div><div class="clear"></div>';
-    if(($res=db_query($sql)) && ($num=db_num_rows($res))) {
-        echo '<div id="faq">'.sprintf(__('%d FAQs matched your search criteria.'),$num).'
-                <ol>';
-        while($row=db_fetch_array($res)) {
-            echo sprintf('
-                <li><a href="faq.php?id=%d" class="previewfaq">%s</a></li>',
-                $row['faq_id'],$row['question'],$row['ispublished']?__('Published'):__('Internal'));
-        }
-        echo '  </ol>
-             </div>';
-    } else {
-        echo '<strong class="faded">'.__('The search did not match any FAQs.').'</strong>';
-    }
 } else { //Category Listing.
-    $sql='SELECT cat.category_id, cat.name, cat.description, cat.ispublic, count(faq.faq_id) as faqs '
-        .' FROM '.FAQ_CATEGORY_TABLE.' cat '
-        .' LEFT JOIN '.FAQ_TABLE.' faq ON(faq.category_id=cat.category_id AND faq.ispublished=1) '
-        .' WHERE cat.ispublic=1 '
-        .' GROUP BY cat.category_id '
-        .' HAVING faqs>0 '
-        .' ORDER BY cat.name';
-    if(($res=db_query($sql)) && db_num_rows($res)) {
-        echo '<div>'.__('Click on the category to browse FAQs.').'</div>
-                <ul id="kb">';
-        while($row=db_fetch_array($res)) {
-
-            echo sprintf('
-                <li>
-                    <i></i>
-                    <h4><a href="faq.php?cid=%d">%s (%d)</a></h4>
-                    %s
-                </li>',$row['category_id'],
-                Format::htmlchars($row['name']),$row['faqs'],
-                Format::safe_html($row['description']));
-        }
-        echo '</ul>';
-    } else {
-        echo __('NO FAQs found');
-    }
+    include CLIENTINC_DIR . 'kb-categories.inc.php';
 }
 ?>
-</div>
diff --git a/include/client/login.inc.php b/include/client/login.inc.php
index b024a39e41c2e7b81db651f210a373ba640204b1..d368f5d237d7931d54bdf77f2a93c2697b78854c 100644
--- a/include/client/login.inc.php
+++ b/include/client/login.inc.php
@@ -4,7 +4,7 @@ if(!defined('OSTCLIENTINC')) die('Access Denied');
 $email=Format::input($_POST['luser']?:$_GET['e']);
 $passwd=Format::input($_POST['lpasswd']?:$_GET['t']);
 
-$content = Page::lookup(Page::getIdByType('banner-client'));
+$content = Page::lookupByType('banner-client');
 
 if ($content) {
     list($title, $body) = $ost->replaceTemplateVariables(
@@ -23,10 +23,10 @@ if ($content) {
     <div class="login-box">
     <strong><?php echo Format::htmlchars($errors['login']); ?></strong>
     <div>
-        <input id="username" placeholder="<?php echo __('Email or Username'); ?>" type="text" name="luser" size="30" value="<?php echo $email; ?>">
+        <input id="username" placeholder="<?php echo __('Email or Username'); ?>" type="text" name="luser" size="30" value="<?php echo $email; ?>" class="nowarn">
     </div>
     <div>
-        <input id="passwd" placeholder="<?php echo __('Password'); ?>" type="password" name="lpasswd" size="30" value="<?php echo $passwd; ?>"></td>
+        <input id="passwd" placeholder="<?php echo __('Password'); ?>" type="password" name="lpasswd" size="30" value="<?php echo $passwd; ?>" class="nowarn"></td>
     </div>
     <p>
         <input class="btn" type="submit" value="<?php echo __('Sign In'); ?>">
@@ -63,7 +63,9 @@ if ($cfg && $cfg->isClientRegistrationEnabled()) {
 </form>
 <br>
 <p>
-<?php if ($cfg && !$cfg->isClientLoginRequired()) {
+<?php
+if ($cfg->getClientRegistrationMode() != 'disabled'
+    || !$cfg->isClientLoginRequired()) {
     echo sprintf(__('If this is your first time contacting us or you\'ve lost the ticket number, please %s open a new ticket %s'),
         '<a href="open.php">', '</a>');
 } ?>
diff --git a/include/client/open.inc.php b/include/client/open.inc.php
index 5bd45eed8ca30fdc5a76f0ce411ce2780bd12f6d..d0f4c24d55bb14a8535221d6ac7fae75ccf79a85 100644
--- a/include/client/open.inc.php
+++ b/include/client/open.inc.php
@@ -17,11 +17,16 @@ if (!$info['topicId']) {
         $info['topicId'] = $cfg->getDefaultTopicId();
 }
 
+$forms = array();
 if ($info['topicId'] && ($topic=Topic::lookup($info['topicId']))) {
-    $form = $topic->getForm();
-    if ($_POST && $form) {
-        $form = $form->instanciate();
-        $form->isValidForClient();
+    foreach ($topic->getForms() as $F) {
+        if (!$F->hasAnyVisibleFields())
+            continue;
+        if ($_POST) {
+            $F = $F->instanciate();
+            $F->isValidForClient();
+        }
+        $forms[] = $F;
     }
 }
 
@@ -33,9 +38,28 @@ if ($info['topicId'] && ($topic=Topic::lookup($info['topicId']))) {
   <input type="hidden" name="a" value="open">
   <table width="800" cellpadding="1" cellspacing="0" border="0">
     <tbody>
+<?php
+        if (!$thisclient) {
+            $uform = UserForm::getUserForm()->getForm($_POST);
+            if ($_POST) $uform->isValid();
+            $uform->render(false);
+        }
+        else { ?>
+            <tr><td colspan="2"><hr /></td></tr>
+        <tr><td><?php echo __('Email'); ?>:</td><td><?php
+            echo $thisclient->getEmail(); ?></td></tr>
+        <tr><td><?php echo __('Client'); ?>:</td><td><?php
+            echo Format::htmlchars($thisclient->getName()); ?></td></tr>
+        <?php } ?>
+    </tbody>
+    <tbody>
+    <tr><td colspan="2"><hr />
+        <div class="form-header" style="margin-bottom:0.5em">
+        <b><?php echo __('Help Topic'); ?></b>
+        </div>
+    </td></tr>
     <tr>
-        <td class="required"><?php echo __('Help Topic');?>:</td>
-        <td>
+        <td colspan="2">
             <select id="topicId" name="topicId" onchange="javascript:
                     var data = $(':input[name]', '#dynamic-form').serialize();
                     $.ajax(
@@ -63,31 +87,12 @@ if ($info['topicId'] && ($topic=Topic::lookup($info['topicId']))) {
             <font class="error">*&nbsp;<?php echo $errors['topicId']; ?></font>
         </td>
     </tr>
-<?php
-        if (!$thisclient) {
-            $uform = UserForm::getUserForm()->getForm($_POST);
-            if ($_POST) $uform->isValid();
-            $uform->render(false);
-        }
-        else { ?>
-            <tr><td colspan="2"><hr /></td></tr>
-        <tr><td><?php echo __('Email'); ?>:</td><td><?php echo $thisclient->getEmail(); ?></td></tr>
-        <tr><td><?php echo __('Client'); ?>:</td><td><?php echo
-        Format::htmlchars($thisclient->getName()); ?></td></tr>
-        <?php } ?>
     </tbody>
     <tbody id="dynamic-form">
-        <?php if ($form) {
+        <?php foreach ($forms as $form) {
             include(CLIENTINC_DIR . 'templates/dynamic-form.tmpl.php');
         } ?>
     </tbody>
-    <tbody><?php
-        $tform = TicketForm::getInstance();
-        if ($_POST) {
-            $tform->isValidForClient();
-        }
-        $tform->render(false); ?>
-    </tbody>
     <tbody>
     <?php
     if($cfg && $cfg->isCaptchaEnabled() && (!$thisclient || !$thisclient->isValid())) {
@@ -110,7 +115,7 @@ if ($info['topicId'] && ($topic=Topic::lookup($info['topicId']))) {
     </tbody>
   </table>
 <hr/>
-  <p style="text-align:center;">
+  <p class="buttons" style="text-align:center;">
         <input type="submit" value="<?php echo __('Create Ticket');?>">
         <input type="reset" name="reset" value="<?php echo __('Reset');?>">
         <input type="button" name="cancel" value="<?php echo __('Cancel'); ?>" onclick="javascript:
diff --git a/include/client/profile.inc.php b/include/client/profile.inc.php
index 054267ac4b4e66445a15f4e9d348193afcc97117..5a72f571033c02035b16c1505a4ca1f4d90dd749 100644
--- a/include/client/profile.inc.php
+++ b/include/client/profile.inc.php
@@ -20,41 +20,26 @@ if ($acct = $thisclient->getAccount()) {
         </div>
     </td>
 </tr>
-    <td><?php echo __('Time Zone'); ?>:</td>
-    <td>
-        <select name="timezone_id" id="timezone_id">
-            <option value="0">&mdash; <?php echo __('Select Time Zone'); ?> &mdash;</option>
+    <tr>
+        <td width="180">
+            <?php echo __('Time Zone');?>:
+        </td>
+        <td>
             <?php
-            $sql='SELECT id, offset,timezone FROM '.TIMEZONE_TABLE.' ORDER BY id';
-            if(($res=db_query($sql)) && db_num_rows($res)){
-                while(list($id,$offset, $tz)=db_fetch_row($res)){
-                    $sel=($info['timezone_id']==$id)?'selected="selected"':'';
-                    echo sprintf('<option value="%d" %s>GMT %s - %s</option>',$id,$sel,$offset,$tz);
-                }
-            }
-            ?>
-        </select>
-        &nbsp;<span class="error"><?php echo $errors['timezone_id']; ?></span>
-    </td>
-</tr>
-<tr>
-    <td width="180">
-        <?php echo __('Daylight Saving') ?>:
-    </td>
-    <td>
-        <input type="checkbox" name="dst" value="1" <?php echo $info['dst']?'checked="checked"':''; ?>>
-        <?php echo __('Observe daylight saving'); ?>
-        <em>(<?php echo __('Current Time'); ?>:
-            <strong><?php echo Format::date($cfg->getDateTimeFormat(),Misc::gmtime(),$info['tz_offset'],$info['dst']); ?></strong>)</em>
-    </td>
-</tr>
+            $TZ_NAME = 'timezone';
+            $TZ_TIMEZONE = $info['timezone'];
+            include INCLUDE_DIR.'staff/templates/timezone.tmpl.php'; ?>
+            <div class="error"><?php echo $errors['timezone']; ?></div>
+        </td>
+    </tr>
+<?php if ($cfg->getSecondaryLanguages()) { ?>
     <tr>
         <td width="180">
             <?php echo __('Preferred Language'); ?>:
         </td>
         <td>
     <?php
-    $langs = Internationalization::availableLanguages(); ?>
+    $langs = Internationalization::getConfiguredSystemLanguages(); ?>
             <select name="lang">
                 <option value="">&mdash; <?php echo __('Use Browser Preference'); ?> &mdash;</option>
 <?php foreach($langs as $l) {
@@ -66,9 +51,10 @@ $selected = ($info['lang'] == $l['code']) ? 'selected="selected"' : ''; ?>
             <span class="error">&nbsp;<?php echo $errors['lang']; ?></span>
         </td>
     </tr>
-<?php if ($acct->isPasswdResetEnabled()) { ?>
+<?php }
+      if ($acct->isPasswdResetEnabled()) { ?>
 <tr>
-    <td colspan=2">
+    <td colspan="2">
         <div><hr><h3><?php echo __('Access Credentials'); ?></h3></div>
     </td>
 </tr>
diff --git a/include/client/register.inc.php b/include/client/register.inc.php
index 78b42ac9474fc251c3e937f5989677cbc2a70a3e..4f683c718d8fc27cb7404ae21f722a289f51967a 100644
--- a/include/client/register.inc.php
+++ b/include/client/register.inc.php
@@ -1,9 +1,7 @@
 <?php
 $info = $_POST;
-if (!isset($info['timezone_id']))
+if (!isset($info['timezone']))
     $info += array(
-        'timezone_id' => $cfg->getDefaultTimezoneId(),
-        'dst' => $cfg->observeDaylightSaving(),
         'backend' => null,
     );
 if (isset($user) && $user instanceof ClientCreateRequest) {
@@ -29,7 +27,7 @@ $info = Format::htmlchars(($errors && $_POST)?$_POST:$info);
 <tbody>
 <?php
     $cf = $user_form ?: UserForm::getInstance();
-    $cf->render(false);
+    $cf->render(false, false, array('mode' => 'create'));
 ?>
 <tr>
     <td colspan="2">
@@ -37,33 +35,18 @@ $info = Format::htmlchars(($errors && $_POST)?$_POST:$info);
         </div>
     </td>
 </tr>
-    <td><?php echo __('Time Zone'); ?>:</td>
-    <td>
-        <select name="timezone_id" id="timezone_id">
+    <tr>
+        <td width="180">
+            <?php echo __('Time Zone');?>:
+        </td>
+        <td>
             <?php
-            $sql='SELECT id, offset,timezone FROM '.TIMEZONE_TABLE.' ORDER BY id';
-            if(($res=db_query($sql)) && db_num_rows($res)){
-                while(list($id,$offset, $tz)=db_fetch_row($res)){
-                    $sel=($info['timezone_id']==$id)?'selected="selected"':'';
-                    echo sprintf('<option value="%d" %s>GMT %s - %s</option>',$id,$sel,$offset,$tz);
-                }
-            }
-            ?>
-        </select>
-        &nbsp;<span class="error"><?php echo $errors['timezone_id']; ?></span>
-    </td>
-</tr>
-<tr>
-    <td width="180">
-        <?php echo __('Daylight Saving'); ?>:
-    </td>
-    <td>
-        <input type="checkbox" name="dst" value="1" <?php echo $info['dst']?'checked="checked"':''; ?>>
-        <?php echo __('Observe daylight saving'); ?>
-        <em>(<?php echo __('Current Time'); ?>:
-            <strong><?php echo Format::date($cfg->getDateTimeFormat(),Misc::gmtime(),$info['tz_offset'],$info['dst']); ?></strong>)</em>
-    </td>
-</tr>
+            $TZ_NAME = 'timezone';
+            $TZ_TIMEZONE = $info['timezone'];
+            include INCLUDE_DIR.'staff/templates/timezone.tmpl.php'; ?>
+            <div class="error"><?php echo $errors['timezone']; ?></div>
+        </td>
+    </tr>
 <tr>
     <td colspan=2">
         <div><hr><h3><?php echo __('Access Credentials'); ?></h3></div>
@@ -114,4 +97,13 @@ $info = Format::htmlchars(($errors && $_POST)?$_POST:$info);
         window.location.href='index.php';"/>
 </p>
 </form>
-
+<?php if (!isset($info['timezone'])) { ?>
+<!-- Auto detect client's timezone where possible -->
+<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jstz.min.js"></script>
+<script type="text/javascript">
+$(function() {
+    var zone = jstz.determine();
+    $('#timezone-dropdown').val(zone.name()).trigger('change');
+});
+</script>
+<?php }
diff --git a/include/client/templates/dynamic-form.tmpl.php b/include/client/templates/dynamic-form.tmpl.php
index 9c36caaed5787557b8f2ee6a4bc52320126d416b..cfde56a64e9d00c27adc7ed06290c51cb0b69346 100644
--- a/include/client/templates/dynamic-form.tmpl.php
+++ b/include/client/templates/dynamic-form.tmpl.php
@@ -6,7 +6,7 @@
     <tr><td colspan="2"><hr />
     <div class="form-header" style="margin-bottom:0.5em">
     <h3><?php echo Format::htmlchars($form->getTitle()); ?></h3>
-    <em><?php echo Format::htmlchars($form->getInstructions()); ?></em>
+    <div><?php echo Format::display($form->getInstructions()); ?></div>
     </div>
     </td></tr>
     <?php
@@ -14,35 +14,38 @@
     // 'private' are not included in the output for clients
     global $thisclient;
     foreach ($form->getFields() as $field) {
-        if (!$field->isVisibleToUsers())
+        if (isset($options['mode']) && $options['mode'] == 'create') {
+            if (!$field->isVisibleToUsers() && !$field->isRequiredForUsers())
+                continue;
+        }
+        elseif (!$field->isVisibleToUsers() && !$field->isEditableToUsers()) {
             continue;
+        }
         ?>
         <tr>
-            <?php if ($field->isBlockLevel()) { ?>
-                <td colspan="2">
-            <?php
-            }
-            else { ?>
-                <td><label for="<?php echo $field->getFormName(); ?>" class="<?php
-                    if ($field->get('required')) echo 'required'; ?>">
-                <?php echo Format::htmlchars($field->get('label')); ?>:</label></td><td>
-            <?php
-            }
-            $field->render('client'); ?>
-            <?php if ($field->get('required')) { ?>
-                <font class="error">*</font>
-            <?php
-            }
-            if ($field->get('hint') && !$field->isBlockLevel()) { ?>
-                <br /><em style="color:gray;display:inline-block"><?php
-                    echo Format::htmlchars($field->get('hint')); ?></em>
+            <td colspan="2" style="padding-top:10px;">
+            <?php if (!$field->isBlockLevel()) { ?>
+                <label for="<?php echo $field->getFormName(); ?>"><span class="<?php
+                    if ($field->isRequiredForUsers()) echo 'required'; ?>">
+                <?php echo Format::htmlchars($field->getLocal('label')); ?>
+            <?php if ($field->isRequiredForUsers()) { ?>
+                <span class="error">*</span>
+            <?php }
+            ?></span><?php
+                if ($field->get('hint')) { ?>
+                    <br /><em style="color:gray;display:inline-block"><?php
+                        echo Format::viewableImages($field->getLocal('hint')); ?></em>
+                <?php
+                } ?>
+            <br/>
             <?php
             }
+            $field->render(array('client'=>true));
+            ?></label><?php
             foreach ($field->errors() as $e) { ?>
-                <br />
-                <font class="error"><?php echo $e; ?></font>
+                <div class="error"><?php echo $e; ?></div>
             <?php }
-            $field->renderExtras('client');
+            $field->renderExtras(array('client'=>true));
             ?>
             </td>
         </tr>
diff --git a/include/client/templates/inline-form.tmpl.php b/include/client/templates/inline-form.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..696e1d7cf76c0ab2c1cb824aa25cd6118764a29d
--- /dev/null
+++ b/include/client/templates/inline-form.tmpl.php
@@ -0,0 +1,24 @@
+<div><?php
+foreach ($form->getFields() as $field) { ?>
+    <span style="display:inline-block;padding-right:5px;vertical-align:top">
+        <span class="<?php if ($field->get('required')) echo 'required'; ?>">
+            <?php echo Format::htmlchars($field->get('label')); ?></span>
+        <div><?php
+        $field->render(); ?>
+        <?php if ($field->get('required')) { ?>
+            <span class="error">*</span>
+        <?php
+        }
+        if ($field->get('hint') && !$field->isBlockLevel()) { ?>
+            <br/><em style="color:gray;display:inline-block"><?php
+                echo Format::htmlchars($field->get('hint')); ?></em>
+        <?php
+        }
+        foreach ($field->errors() as $e) { ?>
+            <br />
+            <span class="error"><?php echo Format::htmlchars($e); ?></span>
+        <?php } ?>
+        </div>
+    </span><?php
+} ?>
+</div>
diff --git a/include/client/templates/sidebar.tmpl.php b/include/client/templates/sidebar.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..09f9bd689c80c7be183b95bde2de14084c3ce031
--- /dev/null
+++ b/include/client/templates/sidebar.tmpl.php
@@ -0,0 +1,45 @@
+<?php
+$BUTTONS = isset($BUTTONS) ? $BUTTONS : true;
+?>
+    <div class="sidebar pull-right">
+<?php if ($BUTTONS) { ?>
+        <div class="front-page-button flush-right">
+<p>
+<?php
+    if ($cfg->getClientRegistrationMode() != 'disabled'
+        || !$cfg->isClientLoginRequired()) { ?>
+            <a href="open.php" style="display:block" class="blue button"><?php
+                echo __('Open a New Ticket');?></a>
+</p>
+<?php } ?>
+<p>
+            <a href="view.php" style="display:block" class="green button"><?php
+                echo __('Check Ticket Status');?></a>
+</p>
+        </div>
+<?php } ?>
+        <div class="content"><?php
+    $faqs = FAQ::getFeatured()->select_related('category')->limit(5);
+    if ($faqs->all()) { ?>
+            <section><div class="header"><?php echo __('Featured Questions'); ?></div>
+<?php   foreach ($faqs as $F) { ?>
+            <div><a href="<?php echo ROOT_PATH; ?>kb/faq.php?id=<?php
+                echo urlencode($F->getId());
+                ?>"><?php echo $F->getLocalQuestion(); ?></a></div>
+<?php   } ?>
+            </section>
+<?php
+    }
+    $resources = Page::getActivePages()->filter(array('type'=>'other'));
+    if ($resources->all()) { ?>
+            <section><div class="header"><?php echo __('Other Resources'); ?></div>
+<?php   foreach ($resources as $page) { ?>
+            <div><a href="<?php echo ROOT_PATH; ?>pages/<?php echo $page->getNameAsSlug();
+            ?>"><?php echo $page->getLocalName(); ?></a></div>
+<?php   } ?>
+            </section>
+<?php
+    }
+        ?></div>
+    </div>
+
diff --git a/include/client/templates/thread-entries.tmpl.php b/include/client/templates/thread-entries.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..b91b91d74ccb7a32d7949e3ce64f3a1eb2ea7db1
--- /dev/null
+++ b/include/client/templates/thread-entries.tmpl.php
@@ -0,0 +1,55 @@
+<?php
+$events = $events
+    ->filter(array('state__in' => array('created', 'closed', 'reopened', 'edited', 'collab')))
+    ->order_by('id');
+$events = $events->getIterator();
+$events->rewind();
+$event = $events->current();
+
+$htmlId = $options['html-id'] ?: ('thread-'.$this->getId());
+?>
+<div id="<?php echo $htmlId; ?>" data-thread-id="<?php echo $this->getId(); ?>">
+<?php
+if (count($entries)) {
+    // Go through all the entries and bucket them by time frame
+    $buckets = array();
+    $rel = 0;
+    foreach ($entries as $i=>$E) {
+        // First item _always_ shows up
+        if ($i != 0)
+            // Set relative time resolution to 12 hours
+            $rel = Format::relativeTime(Misc::db2gmtime($E->created, false, 43200));
+        $buckets[$rel][] = $E;
+    }
+
+    // Go back through the entries and render them on the page
+    $i = 0;
+    foreach ($buckets as $rel=>$entries) {
+        // TODO: Consider adding a date boundary to indicate significant
+        //       changes in dates between thread items.
+        foreach ($entries as $entry) {
+            // Emit all events prior to this entry
+            while ($event && $event->timestamp < $entry->created) {
+                $event->render(ThreadEvent::MODE_CLIENT);
+                $events->next();
+                $event = $events->current();
+            }
+            include 'thread-entry.tmpl.php';
+        }
+        $i++;
+    }
+}
+
+// Emit all other events
+while ($event) {
+    $event->render(ThreadEvent::MODE_CLIENT);
+    $events->next();
+    $event = $events->current();
+}
+
+// This should never happen
+if (count($entries) + count($events) == 0) {
+    echo '<p><em>'.__('No entries have been posted to this thread.').'</em></p>';
+}
+?>
+</div>
diff --git a/include/client/templates/thread-entry.tmpl.php b/include/client/templates/thread-entry.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..c87edfa0bf87a8049e0df818e6f35cbd12a20ce8
--- /dev/null
+++ b/include/client/templates/thread-entry.tmpl.php
@@ -0,0 +1,73 @@
+<?php
+global $cfg;
+$entryTypes = array('M'=>'message', 'R'=>'response', 'N'=>'note');
+$user = $entry->getUser() ?: $entry->getStaff();
+$name = $user ? $user->getName() : $entry->poster;
+$avatar = '';
+if ($cfg->isAvatarsEnabled() && $user)
+    $avatar = $user->getAvatar();
+?>
+
+<div class="thread-entry <?php echo $entryTypes[$entry->type]; ?> <?php if ($avatar) echo 'avatar'; ?>">
+<?php if ($avatar) { ?>
+    <span class="<?php echo ($entry->type == 'M') ? 'pull-left' : 'pull-right'; ?> avatar">
+<?php echo $avatar; ?>
+    </span>
+<?php } ?>
+    <div class="header">
+        <div class="pull-right">
+            <span style="vertical-align:middle;" class="textra">
+        <?php if ($entry->flags & ThreadEntry::FLAG_EDITED) { ?>
+                <span class="label label-bare" title="<?php
+        echo sprintf(__('Edited on %s by %s'), Format::datetime($entry->updated), 'You');
+                ?>"><?php echo __('Edited'); ?></span>
+        <?php } ?>
+            </span>
+        </div>
+<?php
+            echo sprintf(__('<b>%s</b> posted %s'), $name,
+                sprintf('<time datetime="%s" title="%s">%s</time>',
+                    date(DateTime::W3C, Misc::db2gmtime($entry->created)),
+                    Format::daydatetime($entry->created),
+                    Format::datetime($entry->created)
+                )
+            ); ?>
+            <span style="max-width:500px" class="faded title truncate"><?php
+                echo $entry->title; ?></span>
+            </span>
+    </div>
+    <div class="thread-body" id="thread-id-<?php echo $entry->getId(); ?>">
+        <div><?php echo $entry->getBody()->toHtml(); ?></div>
+        <div class="clear"></div>
+<?php
+    if ($entry->has_attachments) { ?>
+    <div class="attachments"><?php
+        foreach ($entry->attachments as $A) {
+            if ($A->inline)
+                continue;
+            $size = '';
+            if ($A->file->size)
+                $size = sprintf('<small class="filesize faded">%s</small>', Format::file_size($A->file->size));
+?>
+        <span class="attachment-info">
+        <i class="icon-paperclip icon-flip-horizontal"></i>
+        <a class="no-pjax truncate filename" href="<?php echo $A->file->getDownloadUrl();
+            ?>" download="<?php echo Format::htmlchars($A->getFilename()); ?>"
+            target="_blank"><?php echo Format::htmlchars($A->getFilename());
+        ?></a><?php echo $size;?>
+        </span>
+<?php   }  ?>
+    </div>
+<?php } ?>
+    </div>
+<?php
+    if ($urls = $entry->getAttachmentUrls()) { ?>
+        <script type="text/javascript">
+            $('#thread-id-<?php echo $entry->getId(); ?>')
+                .data('urls', <?php
+                    echo JsonDataEncoder::encode($urls); ?>)
+                .data('id', <?php echo $entry->getId(); ?>);
+        </script>
+<?php
+    } ?>
+</div>
diff --git a/include/client/templates/thread-event.tmpl.php b/include/client/templates/thread-event.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..42fd8027e0024324e15407e0cd4add7c0b69dc83
--- /dev/null
+++ b/include/client/templates/thread-event.tmpl.php
@@ -0,0 +1,11 @@
+<?php
+$desc = $event->getDescription(ThreadEvent::MODE_CLIENT);
+if (!$desc)
+    return;
+?>
+<div class="thread-event <?php if ($event->uid) echo 'action'; ?>">
+        <span class="type-icon">
+          <i class="faded icon-<?php echo $event->getIcon(); ?>"></i>
+        </span>
+        <span class="faded description"><?php echo $desc; ?></span>
+</div>
diff --git a/include/client/templates/ticket-print.tmpl.php b/include/client/templates/ticket-print.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..a068470cd8fce5cd254cfc89fd6dcd7239ec4087
--- /dev/null
+++ b/include/client/templates/ticket-print.tmpl.php
@@ -0,0 +1,230 @@
+<html>
+
+<head>
+    <style type="text/css">
+@page {
+    header: html_def;
+    footer: html_def;
+    margin: 15mm;
+    margin-top: 30mm;
+    margin-bottom: 22mm;
+}
+.logo {
+  max-width: 220px;
+  max-height: 71px;
+  width: auto;
+  height: auto;
+  margin: 0;
+}
+#ticket_thread .message,
+#ticket_thread .response,
+#ticket_thread .note {
+    margin-top:10px;
+    border:1px solid #aaa;
+    border-bottom:2px solid #aaa;
+}
+#ticket_thread .header {
+    text-align:left;
+    border-bottom:1px solid #aaa;
+    padding:3px;
+    width: 100%;
+    table-layout: fixed;
+}
+#ticket_thread .message .header {
+    background:#C3D9FF;
+}
+#ticket_thread .response .header {
+    background:#DDD;
+}
+#ticket_thread .note .header {
+    background:#FFE;
+}
+#ticket_thread .info {
+    padding:5px;
+    background: snow;
+    border-top: 0.3mm solid #ccc;
+}
+
+table.meta-data {
+    width: 100%;
+}
+table.custom-data {
+    margin-top: 10px;
+}
+table.custom-data th {
+    width: 25%;
+}
+table.custom-data th,
+table.meta-data th {
+    text-align: right;
+    background-color: #ddd;
+    padding: 3px 8px;
+}
+table.meta-data td {
+    padding: 3px 8px;
+}
+.faded {
+    color:#666;
+}
+.pull-left {
+    float: left;
+}
+.pull-right {
+    float: right;
+}
+.flush-right {
+    text-align: right;
+}
+.flush-left {
+    text-align: left;
+}
+.ltr {
+    direction: ltr;
+    unicode-bidi: embed;
+}
+.headline {
+    border-bottom: 2px solid black;
+    font-weight: bold;
+}
+div.hr {
+    border-top: 0.2mm solid #bbb;
+    margin: 0.5mm 0;
+    font-size: 0.0001em;
+}
+.thread-entry, .thread-body {
+    page-break-inside: avoid;
+}
+<?php include ROOT_DIR . 'css/thread.css'; ?>
+    </style>
+</head>
+<body>
+
+<htmlpageheader name="def" style="display:none">
+<?php if ($logo = $cfg->getClientLogo()) { ?>
+    <img src="cid:<?php echo $logo->getKey(); ?>" class="logo"/>
+<?php } else { ?>
+    <img src="<?php echo INCLUDE_DIR . 'fpdf/print-logo.png'; ?>" class="logo"/>
+<?php } ?>
+    <div class="hr">&nbsp;</div>
+    <table><tr>
+        <td class="flush-left"><?php echo (string) $ost->company; ?></td>
+        <td class="flush-right"><?php echo Format::daydatetime(Misc::gmtime()); ?></td>
+    </tr></table>
+</htmlpageheader>
+
+<htmlpagefooter name="def" style="display:none">
+    <div class="hr">&nbsp;</div>
+    <table width="100%"><tr><td class="flush-left">
+        Ticket #<?php echo $ticket->getNumber(); ?> printed by
+        <?php echo $thisclient->getName()->getFirst(); ?> on
+        <?php echo Format::daydatetime(Misc::gmtime()); ?>
+    </td>
+    <td class="flush-right">
+        Page {PAGENO}
+    </td>
+    </tr></table>
+</htmlpagefooter>
+
+<!-- Ticket metadata -->
+<h1>Ticket #<?php echo $ticket->getNumber(); ?></h1>
+<table class="meta-data" cellpadding="0" cellspacing="0">
+<tbody>
+<tr>
+    <th><?php echo __('Status'); ?></th>
+    <td><?php echo $ticket->getStatus(); ?></td>
+    <th><?php echo __('Name'); ?></th>
+    <td><?php echo $ticket->getOwner()->getName(); ?></td>
+</tr>
+<tr>
+    <th><?php echo __('Priority'); ?></th>
+    <td><?php echo $ticket->getPriority(); ?></td>
+    <th><?php echo __('Email'); ?></th>
+    <td><?php echo $ticket->getEmail(); ?></td>
+</tr>
+<tr>
+    <th><?php echo __('Department'); ?></th>
+    <td><?php echo $ticket->getDept(); ?></td>
+    <th><?php echo __('Phone'); ?></th>
+    <td><?php echo $ticket->getPhoneNumber(); ?></td>
+</tr>
+<tr>
+    <th><?php echo __('Create Date'); ?></th>
+    <td><?php echo Format::datetime($ticket->getCreateDate()); ?></td>
+    <th><?php echo __('Source'); ?></th>
+    <td><?php echo $ticket->getSource(); ?></td>
+</tr>
+</tbody>
+</table>
+
+<!-- Custom Data -->
+<?php
+foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) {
+    // Skip core fields shown earlier in the ticket view
+    $answers = $form->getAnswers()->exclude(Q::any(array(
+        'field__flags__hasbit' => DynamicFormField::FLAG_EXT_STORED,
+        Q::not(array('field__flags__hasbit' => DynamicFormField::FLAG_CLIENT_VIEW)),
+        'field__name__in' => array('subject', 'priority'),
+    )));
+    if (count($answers) == 0)
+        continue;
+    ?>
+        <table class="custom-data" cellspacing="0" cellpadding="4" width="100%" border="0">
+        <tr><td colspan="2" class="headline flush-left"><?php echo $form->getTitle(); ?></th></tr>
+        <?php foreach($answers as $a) {
+            if (!($v = $a->display())) continue; ?>
+            <tr>
+                <th><?php
+    echo $a->getField()->get('label');
+                ?>:</th>
+                <td><?php
+    echo $v;
+                ?></td>
+            </tr>
+            <?php } ?>
+        </table>
+    <?php
+    $idx++;
+} ?>
+
+<!-- Ticket Thread -->
+<h2><?php echo $ticket->getSubject(); ?></h2>
+<div id="ticket_thread">
+<?php
+$types = array('M', 'R');
+
+if ($thread = $ticket->getThreadEntries($types)) {
+    $threadTypes=array('M'=>'message','R'=>'response', 'N'=>'note');
+    foreach ($thread as $entry) { ?>
+        <div class="thread-entry <?php echo $threadTypes[$entry->type]; ?>">
+            <table class="header"><tr><td>
+                    <span><?php
+                        echo Format::datetime($entry->created);?></span>
+                    <span style="padding:0 1em" class="faded title"><?php
+                        echo Format::truncate($entry->title, 100); ?></span>
+                </td>
+                <td class="flush-right faded title" style="white-space:no-wrap">
+                    <?php
+                        echo Format::htmlchars($entry->getName()); ?></span>
+                </td>
+            </tr></table>
+            <div class="thread-body">
+                <div><?php echo $entry->getBody()->display('pdf'); ?></div>
+            </div>
+            <?php
+            if ($entry->has_attachments
+                    && ($files = $entry->attachments)) { ?>
+                <div class="info">
+<?php           foreach ($files as $A) { ?>
+                    <div>
+                        <span><?php echo Format::htmlchars($A->file->name); ?></span>
+                        <span class="faded">(<?php echo Format::file_size($A->file->size); ?>)</span>
+                    </div>
+<?php           } ?>
+                </div>
+<?php       } ?>
+        </div>
+<?php }
+} ?>
+</div>
+</body>
+</html>
diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php
index 3665c8a3134da4786c2d352186384e8ad96587c5..f37874b55a8916081c924599130f64c863d36f37 100644
--- a/include/client/tickets.inc.php
+++ b/include/client/tickets.inc.php
@@ -1,141 +1,198 @@
 <?php
 if(!defined('OSTCLIENTINC') || !is_object($thisclient) || !$thisclient->isValid()) die('Access Denied');
 
+$settings = &$_SESSION['client:Q'];
+
+// Unpack search, filter, and sort requests
+if (isset($_REQUEST['clear']))
+    $settings = array();
+if (isset($_REQUEST['keywords'])) {
+    $settings['keywords'] = $_REQUEST['keywords'];
+}
+if (isset($_REQUEST['topic_id'])) {
+    $settings['topic_id'] = $_REQUEST['topic_id'];
+}
+if (isset($_REQUEST['status'])) {
+    $settings['status'] = $_REQUEST['status'];
+}
+
+$org_tickets = $thisclient->canSeeOrgTickets();
+if ($settings['keywords']) {
+    // Don't show stat counts for searches
+    $openTickets = $closedTickets = -1;
+}
+elseif ($settings['topic_id']) {
+    $openTickets = $thisclient->getNumTopicTicketsInState($settings['topic_id'],
+        'open', $org_tickets);
+    $closedTickets = $thisclient->getNumTopicTicketsInState($settings['topic_id'],
+        'closed', $org_tickets);
+}
+else {
+    $openTickets = $thisclient->getNumOpenTickets($org_tickets);
+    $closedTickets = $thisclient->getNumClosedTickets($org_tickets);
+}
+
+$tickets = Ticket::objects();
+
 $qs = array();
 $status=null;
-if(isset($_REQUEST['status'])) { //Query string status has nothing to do with the real status used below.
-    $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'])) {
-     case 'open':
-		$results_type=__('Open Tickets');
-     case 'closed':
-		$results_type=__('Closed Tickets');
-        break;
-     case 'resolved':
-        $results_type=__('Resolved Tickets');
-        break;
-     default:
-        $status=''; //ignore
-    }
-} elseif($thisclient->getNumOpenTickets()) {
-    $status='open'; //Defaulting to open
-	$results_type=__('Open Tickets');
-}
 
-$sortOptions=array('id'=>'`number`', 'subject'=>'cdata.subject',
-                    'status'=>'status.name', 'dept'=>'dept_name','date'=>'ticket.created');
-$orderWays=array('DESC'=>'DESC','ASC'=>'ASC');
+$sortOptions=array('id'=>'number', 'subject'=>'cdata__subject',
+                    'status'=>'status__name', 'dept'=>'dept__name','date'=>'created');
+$orderWays=array('DESC'=>'-','ASC'=>'');
 //Sorting options...
 $order_by=$order=null;
 $sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'date';
 if($sort && $sortOptions[$sort])
     $order_by =$sortOptions[$sort];
 
-$order_by=$order_by?$order_by:'ticket_created';
+$order_by=$order_by ?: $sortOptions['date'];
 if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])])
     $order=$orderWays[strtoupper($_REQUEST['order'])];
 
-$order=$order?$order:'ASC';
-if($order_by && strpos($order_by,','))
-    $order_by=str_replace(','," $order,",$order_by);
-
 $x=$sort.'_sort';
-$$x=' class="'.strtolower($order).'" ';
-
-$qselect='SELECT ticket.ticket_id,ticket.`number`,ticket.dept_id,isanswered, '
-    .'dept.ispublic, cdata.subject,'
-    .'dept_name, status.name as status, status.state, ticket.source, ticket.created ';
-
-$qfrom='FROM '.TICKET_TABLE.' ticket '
-      .' LEFT JOIN '.TICKET_STATUS_TABLE.' status
-            ON (status.id = ticket.status_id) '
-      .' LEFT JOIN '.TABLE_PREFIX.'ticket__cdata cdata ON (cdata.ticket_id = ticket.ticket_id)'
-      .' LEFT JOIN '.DEPT_TABLE.' dept ON (ticket.dept_id=dept.dept_id) '
-      .' LEFT JOIN '.TICKET_COLLABORATOR_TABLE.' collab
-        ON (collab.ticket_id = ticket.ticket_id
-                AND collab.user_id ='.$thisclient->getId().' )';
-
-$qwhere = sprintf(' WHERE ( ticket.user_id=%d OR collab.user_id=%d )',
-            $thisclient->getId(), $thisclient->getId());
-
-$states = array(
-        'open' => 'open',
-        'closed' => 'closed');
-if($status && isset($states[$status])){
-    $qwhere.=' AND status.state='.db_input($states[$status]);
+$$x=' class="'.strtolower($_REQUEST['order'] ?: 'desc').'" ';
+
+$basic_filter = Ticket::objects();
+if ($settings['topic_id']) {
+    $basic_filter = $basic_filter->filter(array('topic_id' => $settings['topic_id']));
+}
+
+if ($settings['status'])
+    $status = strtolower($settings['status']);
+    switch ($status) {
+    default:
+        $status = 'open';
+    case 'open':
+    case 'closed':
+		$results_type = ($status == 'closed') ? __('Closed Tickets') : __('Open Tickets');
+        $basic_filter->filter(array('status__state' => $status));
+        break;
 }
 
-$search=($_REQUEST['a']=='search' && $_REQUEST['q']);
-if($search) {
-    $qs += array('a' => $_REQUEST['a'], 'q' => $_REQUEST['q']);
-    $queryterm=db_real_escape($_REQUEST['q'],false); //escape the term ONLY...no quotes.
-    if(is_numeric($_REQUEST['q'])) {
-        $qwhere.=" AND ticket.`number` LIKE '$queryterm%'";
-    } else {//Deep search!
-        $qwhere.=' AND ( '
-                ." cdata.subject LIKE '%$queryterm%'"
-                ." OR thread.body LIKE '%$queryterm%'"
-                .' ) ';
-        $deep_search=true;
-        //Joins needed for search
-        $qfrom.=' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON ('
-               .'ticket.ticket_id=thread.ticket_id AND thread.thread_type IN ("M","R"))';
+// Add visibility constraints — use a union query to use multiple indexes,
+// use UNION without "ALL" (false as second parameter to union()) to imply
+// unique values
+$visibility = $basic_filter->copy()
+    ->values_flat('ticket_id')
+    ->filter(array('user_id' => $thisclient->getId()))
+    ->union($basic_filter->copy()
+        ->values_flat('ticket_id')
+        ->filter(array('thread__collaborators__user_id' => $thisclient->getId()))
+    , false);
+
+if ($thisclient->canSeeOrgTickets()) {
+    $visibility = $visibility->union(
+        $basic_filter->copy()->values_flat('ticket_id')
+            ->filter(array('user__org_id' => $thisclient->getOrgId()))
+    , false);
+}
+
+// Perform basic search
+if ($settings['keywords']) {
+    $q = trim($settings['keywords']);
+    if (is_numeric($q)) {
+        $tickets->filter(array('number__startswith'=>$q));
+    } elseif (strlen($q) > 2) { //Deep search!
+        // Use the search engine to perform the search
+        $tickets = $ost->searcher->find($q, $tickets);
     }
 }
 
+$tickets->distinct('ticket_id');
+
 TicketForm::ensureDynamicDataView();
 
-$total=db_count('SELECT count(DISTINCT ticket.ticket_id) '.$qfrom.' '.$qwhere);
+$total=$tickets->count();
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 $pageNav=new Pagenate($total, $page, PAGE_LIMIT);
 $qstr = '&amp;'. Http::build_query($qs);
 $qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
 $pageNav->setURL('tickets.php', $qs);
+$tickets->filter(array('ticket_id__in' => $visibility));
+$pageNav->paginate($tickets);
 
-//more stuff...
-$qselect.=' ,count(attach_id) as attachments ';
-$qfrom.=' LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON  ticket.ticket_id=attach.ticket_id ';
-$qgroup=' GROUP BY ticket.ticket_id';
-
-$query="$qselect $qfrom $qwhere $qgroup ORDER BY $order_by $order LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
-//echo $query;
-$res = db_query($query);
-$showing=($res && db_num_rows($res))?$pageNav->showing():"";
+$showing =$total ? $pageNav->showing() : "";
 if(!$results_type)
 {
-	$results_type=ucfirst($status).' Tickets';
+	$results_type=ucfirst($status).' '.__('Tickets');
 }
 $showing.=($status)?(' '.$results_type):' '.__('All Tickets');
 if($search)
     $showing=__('Search Results').": $showing";
 
-$negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting
+$negorder=$order=='-'?'ASC':'DESC'; //Negate the sorting
+
+$tickets->values(
+    'ticket_id', 'number', 'created', 'isanswered', 'source', 'status_id',
+    'status__state', 'status__name', 'cdata__subject', 'dept_id',
+    'dept__name', 'dept__ispublic', 'user__default_email__address'
+);
 
 ?>
-<h1><?php echo __('Tickets');?></h1>
-<br>
+<div class="search well">
+<div class="flush-left">
 <form action="tickets.php" method="get" id="ticketSearchForm">
     <input type="hidden" name="a"  value="search">
-    <input type="text" name="q" size="20" value="<?php echo Format::htmlchars($_REQUEST['q']); ?>">
-    <select name="status">
-        <option value="">&mdash; <?php echo __('Any Status');?> &mdash;</option>
-        <option value="open"
-            <?php echo ($status=='open') ? 'selected="selected"' : '';?>>
-            <?php echo _P('ticket-status', 'Open');?> (<?php echo $thisclient->getNumOpenTickets(); ?>)</option>
-        <?php
-        if($thisclient->getNumClosedTickets()) {
-            ?>
-        <option value="closed"
-            <?php echo ($status=='closed') ? 'selected="selected"' : '';?>>
-            <?php echo __('Closed');?> (<?php echo $thisclient->getNumClosedTickets(); ?>)</option>
-        <?php
-        } ?>
+    <input type="text" name="keywords" size="30" value="<?php echo Format::htmlchars($settings['keywords']); ?>">
+    <input type="submit" value="<?php echo __('Search');?>">
+<div class="pull-right">
+    <?php echo __('Help Topic'); ?>:
+    <select name="topic_id" class="nowarn" onchange="javascript: this.form.submit(); ">
+        <option value="">&mdash; <?php echo __('All Help Topics');?> &mdash;</option>
+<?php
+foreach (Topic::getHelpTopics(true) as $id=>$name) {
+        $count = $thisclient->getNumTopicTickets($id, $org_tickets);
+        if ($count == 0)
+            continue;
+?>
+        <option value="<?php echo $id; ?>"i
+            <?php if ($settings['topic_id'] == $id) echo 'selected="selected"'; ?>
+            ><?php echo sprintf('%s (%d)', Format::htmlchars($name),
+                $thisclient->getNumTopicTickets($id)); ?></option>
+<?php } ?>
     </select>
-    <input type="submit" value="<?php echo __('Go');?>">
+</div>
 </form>
-<a class="refresh" href="<?php echo Format::htmlchars($_SERVER['REQUEST_URI']); ?>"><?php echo __('Refresh'); ?></a>
+</div>
+
+<?php if ($settings['keywords'] || $settings['topic_id'] || $_REQUEST['sort']) { ?>
+<div style="margin-top:10px"><strong><a href="?clear" style="color:#777"><i class="icon-remove-circle"></i> <?php echo __('Clear all filters and sort'); ?></a></strong></div>
+<?php } ?>
+
+</div>
+
+
+<h1 style="margin:10px 0">
+    <a href="<?php echo Format::htmlchars($_SERVER['REQUEST_URI']); ?>"
+        ><i class="refresh icon-refresh"></i>
+    <?php echo __('Tickets'); ?>
+    </a>
+
+<div class="pull-right states">
+    <small>
+<?php if ($openTickets) { ?>
+    <i class="icon-file-alt"></i>
+    <a class="state <?php if ($status == 'open') echo 'active'; ?>"
+        href="?<?php echo Http::build_query(array('a' => 'search', 'status' => 'open')); ?>">
+    <?php echo _P('ticket-status', 'Open'); if ($openTickets > 0) echo sprintf(' (%d)', $openTickets); ?>
+    </a>
+    <?php if ($closedTickets) { ?>
+    &nbsp;
+    <span style="color:lightgray">|</span>
+    <?php }
+}
+if ($closedTickets) {?>
+    &nbsp;
+    <i class="icon-file-text"></i>
+    <a class="state <?php if ($status == 'closed') echo 'active'; ?>"
+        href="?<?php echo Http::build_query(array('a' => 'search', 'status' => 'closed')); ?>">
+    <?php echo __('Closed'); if ($closedTickets > 0) echo sprintf(' (%d)', $closedTickets); ?>
+    </a>
+<?php } ?>
+    </small>
+</div>
+</h1>
 <table id="ticketTable" width="800" border="0" cellspacing="0" cellpadding="0">
     <caption><?php echo $showing; ?></caption>
     <thead>
@@ -160,45 +217,48 @@ $negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting
     <tbody>
     <?php
      $subject_field = TicketForm::objects()->one()->getField('subject');
-     if($res && ($num=db_num_rows($res))) {
-        $defaultDept=Dept::getDefaultDeptName(); //Default public dept.
-        while ($row = db_fetch_array($res)) {
-            $dept= $row['ispublic']? $row['dept_name'] : $defaultDept;
-            $subject = Format::truncate($subject_field->display(
-                $subject_field->to_php($row['subject']) ?: $row['subject']
-            ), 40);
-            if($row['attachments'])
+     $defaultDept=Dept::getDefaultDeptName(); //Default public dept.
+     if ($tickets->exists(true)) {
+         foreach ($tickets as $T) {
+            $dept = $T['dept__ispublic']
+                ? Dept::getLocalById($T['dept_id'], 'name', $T['dept__name'])
+                : $defaultDept;
+            $subject = $subject_field->display(
+                $subject_field->to_php($T['cdata__subject']) ?: $T['cdata__subject']
+            );
+            $status = TicketStatus::getLocalById($T['status_id'], 'value', $T['status__name']);
+            if (false) // XXX: Reimplement attachment count support
                 $subject.='  &nbsp;&nbsp;<span class="Icon file"></span>';
 
-            $ticketNumber=$row['number'];
-            if($row['isanswered'] && !strcasecmp($row['state'], 'open')) {
+            $ticketNumber=$T['number'];
+            if($T['isanswered'] && !strcasecmp($T['status__state'], 'open')) {
                 $subject="<b>$subject</b>";
                 $ticketNumber="<b>$ticketNumber</b>";
             }
             ?>
-            <tr id="<?php echo $row['ticket_id']; ?>">
+            <tr id="<?php echo $T['ticket_id']; ?>">
                 <td>
-                <a class="Icon <?php echo strtolower($row['source']); ?>Ticket" title="<?php echo $row['email']; ?>"
-                    href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $ticketNumber; ?></a>
+                <a class="Icon <?php echo strtolower($T['source']); ?>Ticket" title="<?php echo $T['user__default_email__address']; ?>"
+                    href="tickets.php?id=<?php echo $T['ticket_id']; ?>"><?php echo $ticketNumber; ?></a>
                 </td>
-                <td>&nbsp;<?php echo Format::db_date($row['created']); ?></td>
-                <td>&nbsp;<?php echo $row['status']; ?></td>
+                <td>&nbsp;<?php echo Format::date($T['created']); ?></td>
+                <td>&nbsp;<?php echo $status; ?></td>
                 <td>
-                    <a href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $subject; ?></a>
+                    <div style="max-height: 1.2em; max-width: 320px;" class="link truncate" href="tickets.php?id=<?php echo $T['ticket_id']; ?>"><?php echo $subject; ?></div>
                 </td>
-                <td>&nbsp;<?php echo Format::truncate($dept,30); ?></td>
+                <td>&nbsp;<span class="truncate"><?php echo $dept; ?></span></td>
             </tr>
         <?php
         }
 
      } else {
-         echo '<tr><td colspan="6">'.__('Your query did not match any records').'</td></tr>';
+         echo '<tr><td colspan="5">'.__('Your query did not match any records').'</td></tr>';
      }
     ?>
     </tbody>
 </table>
 <?php
-if($res && $num>0) {
+if ($total) {
     echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
 }
 ?>
diff --git a/include/client/view.inc.php b/include/client/view.inc.php
index 1821aea04ef1e06ca51f36faad023eb56916e49e..05ea6f44057d9440e31c57092f61355bcdad5b80 100644
--- a/include/client/view.inc.php
+++ b/include/client/view.inc.php
@@ -17,7 +17,7 @@ if ($thisclient && $thisclient->isGuest()
 
 <div id="msg_info">
     <i class="icon-compass icon-2x pull-left"></i>
-    <strong><?php echo __('Looking for your other tickets?'); ?></strong></br>
+    <strong><?php echo __('Looking for your other tickets?'); ?></strong><br />
     <a href="<?php echo ROOT_PATH; ?>login.php?e=<?php
         echo urlencode($thisclient->getEmail());
     ?>" style="text-decoration:underline"><?php echo __('Sign In'); ?></a>
@@ -31,23 +31,36 @@ if ($thisclient && $thisclient->isGuest()
     <tr>
         <td colspan="2" width="100%">
             <h1>
-                <?php echo sprintf(__('Ticket #%s'), $ticket->getNumber()); ?> &nbsp;
-                <a href="tickets.php?id=<?php echo $ticket->getId(); ?>" title="Reload"><span class="Icon refresh">&nbsp;</span></a>
-<?php if ($cfg->allowClientUpdates()
+                <a href="tickets.php?id=<?php echo $ticket->getId(); ?>" title="<?php echo __('Reload'); ?>"><i class="refresh icon-refresh"></i></a>
+                <b>
+                <?php $subject_field = TicketForm::getInstance()->getField('subject');
+                    echo $subject_field->display($ticket->getSubject()); ?>
+                </b>
+                <small>#<?php echo $ticket->getNumber(); ?></small>
+<div class="pull-right">
+    <a class="action-button" href="tickets.php?a=print&id=<?php
+        echo $ticket->getId(); ?>"><i class="icon-print"></i> <?php echo __('Print'); ?></a>
+<?php if ($ticket->hasClientEditableFields()
         // Only ticket owners can edit the ticket details (and other forms)
         && $thisclient->getId() == $ticket->getUserId()) { ?>
-                <a class="action-button pull-right" href="tickets.php?a=edit&id=<?php
-                     echo $ticket->getId(); ?>"><i class="icon-edit"></i> Edit</a>
+                <a class="action-button" href="tickets.php?a=edit&id=<?php
+                     echo $ticket->getId(); ?>"><i class="icon-edit"></i> <?php echo __('Edit'); ?></a>
 <?php } ?>
+</div>
             </h1>
         </td>
     </tr>
     <tr>
         <td width="50%">
             <table class="infoTable" cellspacing="1" cellpadding="3" width="100%" border="0">
+                <thead>
+                    <tr><td class="headline" colspan="2">
+                        <?php echo __('Basic Ticket Information'); ?>
+                    </td></tr>
+                </thead>
                 <tr>
                     <th width="100"><?php echo __('Ticket Status');?>:</th>
-                    <td><?php echo $ticket->getStatus(); ?></td>
+                    <td><?php echo ($S = $ticket->getStatus()) ? $S->getLocalName() : ''; ?></td>
                 </tr>
                 <tr>
                     <th><?php echo __('Department');?>:</th>
@@ -55,12 +68,17 @@ if ($thisclient && $thisclient->isGuest()
                 </tr>
                 <tr>
                     <th><?php echo __('Create Date');?>:</th>
-                    <td><?php echo Format::db_datetime($ticket->getCreateDate()); ?></td>
+                    <td><?php echo Format::datetime($ticket->getCreateDate()); ?></td>
                 </tr>
            </table>
        </td>
        <td width="50%">
            <table class="infoTable" cellspacing="1" cellpadding="3" width="100%" border="0">
+                <thead>
+                    <tr><td class="headline" colspan="2">
+                        <?php echo __('User Information'); ?>
+                    </td></tr>
+                </thead>
                <tr>
                    <th width="100"><?php echo __('Name');?>:</th>
                    <td><?php echo mb_convert_case(Format::htmlchars($ticket->getName()), MB_CASE_TITLE); ?></td>
@@ -77,71 +95,53 @@ if ($thisclient && $thisclient->isGuest()
        </td>
     </tr>
     <tr>
+        <td colspan="2">
+<!-- Custom Data -->
 <?php
-foreach (DynamicFormEntry::forTicket($ticket->getId()) as $idx=>$form) {
-    $answers = $form->getAnswers();
-    if ($idx > 0 and $idx % 2 == 0) { ?>
-        </tr><tr>
-    <?php } ?>
-    <td width="50%">
-        <table class="infoTable" cellspacing="1" cellpadding="3" width="100%" border="0">
-    <?php foreach ($answers as $answer) {
-        if (in_array($answer->getField()->get('name'), array('name', 'email', 'subject')))
-            continue;
-        elseif ($answer->getField()->get('private'))
-            continue;
-        ?>
+$sections = array();
+foreach (DynamicFormEntry::forTicket($ticket->getId()) as $i=>$form) {
+    // Skip core fields shown earlier in the ticket view
+    $answers = $form->getAnswers()->exclude(Q::any(array(
+        'field__flags__hasbit' => DynamicFormField::FLAG_EXT_STORED,
+        'field__name__in' => array('subject', 'priority'),
+        Q::not(array('field__flags__hasbit' => DynamicFormField::FLAG_CLIENT_VIEW)),
+    )));
+    // Skip display of forms without any answers
+    foreach ($answers as $j=>$a) {
+        if ($v = $a->display())
+            $sections[$i][$j] = array($v, $a);
+    }
+}
+foreach ($sections as $i=>$answers) {
+    ?>
+        <table class="custom-data" cellspacing="0" cellpadding="4" width="100%" border="0">
+        <tr><td colspan="2" class="headline flush-left"><?php echo $form->getTitle(); ?></th></tr>
+<?php foreach ($answers as $A) {
+    list($v, $a) = $A; ?>
         <tr>
-        <th width="100"><?php echo $answer->getField()->get('label');
+            <th><?php
+echo $a->getField()->get('label');
             ?>:</th>
-        <td><?php echo $answer->display(); ?></td>
+            <td><?php
+echo $v;
+            ?></td>
         </tr>
-    <?php } ?>
-    </table></td>
 <?php } ?>
+        </table>
+    <?php
+} ?>
+    </td>
 </tr>
 </table>
 <br>
-<div class="subject"><?php echo __('Subject'); ?>: <strong><?php echo Format::htmlchars($ticket->getSubject()); ?></strong></div>
-<div id="ticketThread">
-<?php
-if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) {
-    $threadType=array('M' => 'message', 'R' => 'response');
-    foreach($thread as $entry) {
 
-        //Making sure internal notes are not displayed due to backend MISTAKES!
-        if(!$threadType[$entry['thread_type']]) continue;
-        $poster = $entry['poster'];
-        if($entry['thread_type']=='R' && ($cfg->hideStaffName() || !$entry['staff_id']))
-            $poster = ' ';
-        ?>
-        <table class="thread-entry <?php echo $threadType[$entry['thread_type']]; ?>" cellspacing="0" cellpadding="1" width="800" border="0">
-            <tr><th><div>
-<?php echo Format::db_datetime($entry['created']); ?>
-                &nbsp;&nbsp;<span class="textra"></span>
-                <span><?php echo $poster; ?></span>
-            </div>
-            </th></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']))
-                    && ($urls = $tentry->getAttachmentUrls())
-                    && ($links=$tentry->getAttachmentsLinks())) { ?>
-                <tr><td class="info"><?php echo $links; ?></td></tr>
-<?php       }
-            if ($urls) { ?>
-                <script type="text/javascript">
-                    $(function() { showImagesInline(<?php echo
-                        JsonDataEncoder::encode($urls); ?>); });
-                </script>
-<?php       } ?>
-        </table>
-    <?php
-    }
-}
+<?php
+    $ticket->getThread()->render(array('M', 'R'), array(
+                'mode' => Thread::MODE_CLIENT,
+                'html-id' => 'ticketThread')
+            );
 ?>
-</div>
+
 <div class="clear" style="padding-bottom:10px;"></div>
 <?php if($errors['err']) { ?>
     <div id="msg_error"><?php echo $errors['err']; ?></div>
@@ -149,44 +149,37 @@ if($ticket->getThreadCount() && ($thread=$ticket->getClientThread())) {
     <div id="msg_notice"><?php echo $msg; ?></div>
 <?php }elseif($warn) { ?>
     <div id="msg_warning"><?php echo $warn; ?></div>
-<?php } ?>
-
-<?php
+<?php }
 
 if (!$ticket->isClosed() || $ticket->isReopenable()) { ?>
-<form id="reply" action="tickets.php?id=<?php echo $ticket->getId(); ?>#reply" name="reply" method="post" enctype="multipart/form-data">
+<form id="reply" action="tickets.php?id=<?php echo $ticket->getId();
+?>#reply" name="reply" method="post" enctype="multipart/form-data">
     <?php csrf_token(); ?>
     <h2><?php echo __('Post a Reply');?></h2>
     <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>">
     <input type="hidden" name="a" value="reply">
-    <table border="0" cellspacing="0" cellpadding="3" style="width:100%">
-        <tr>
-            <td colspan="2">
-                <?php
-                if($ticket->isClosed()) {
-                    $msg='<b>'.__('Ticket will be reopened on message post').'</b>';
-                } else {
-                    $msg=__('To best assist you, we request that you be specific and detailed');
-                }
-                ?>
-                <span id="msg"><em><?php echo $msg; ?> </em></span><font class="error">*&nbsp;<?php echo $errors['message']; ?></font>
-                <br/>
-                <textarea name="message" id="message" cols="50" rows="9" wrap="soft"
-                    data-draft-namespace="ticket.client"
-                    data-draft-object-id="<?php echo $ticket->getId(); ?>"
-                    class="richtext ifhtml draft"><?php echo $info['message']; ?></textarea>
-        <?php
-        if ($messageField->isAttachmentsEnabled()) { ?>
-<?php
-            print $attachments->render(true);
-            print $attachments->getForm()->getMedia();
-?>
-        <?php
-        } ?>
-            </td>
-        </tr>
-    </table>
-    <p style="padding-left:165px;">
+    <div>
+        <p><em><?php
+         echo __('To best assist you, we request that you be specific and detailed'); ?></em>
+        <font class="error">*&nbsp;<?php echo $errors['message']; ?></font>
+        </p>
+        <textarea name="message" id="message" cols="50" rows="9" wrap="soft"
+            class="<?php if ($cfg->isRichTextEnabled()) echo 'richtext';
+                ?> draft" <?php
+list($draft, $attrs) = Draft::getDraftAndDataAttrs('ticket.client', $ticket->getId(), $info['message']);
+echo $attrs; ?>><?php echo $draft ?: $info['message'];
+            ?></textarea>
+    <?php
+    if ($messageField->isAttachmentsEnabled()) {
+        print $attachments->render(array('client'=>true));
+    } ?>
+    </div>
+<?php if ($ticket->isClosed()) { ?>
+    <div class="warning-banner">
+        <?php echo __('Ticket will be reopened on message post'); ?>
+    </div>
+<?php } ?>
+    <p style="text-align:center">
         <input type="submit" value="<?php echo __('Post Reply');?>">
         <input type="reset" value="<?php echo __('Reset');?>">
         <input type="button" value="<?php echo __('Cancel');?>" onClick="history.go(-1)">
@@ -194,3 +187,18 @@ if (!$ticket->isClosed() || $ticket->isReopenable()) { ?>
 </form>
 <?php
 } ?>
+<script type="text/javascript">
+<?php
+// Hover support for all inline images
+$urls = array();
+foreach (AttachmentFile::objects()->filter(array(
+    'attachments__thread_entry__thread__id' => $ticket->getThreadId(),
+    'attachments__inline' => true,
+)) as $file) {
+    $urls[strtolower($file->getKey())] = array(
+        'download_url' => $file->getDownloadUrl(),
+        'filename' => $file->name,
+    );
+} ?>
+showImagesInline(<?php echo JsonDataEncoder::encode($urls); ?>);
+</script>
diff --git a/include/htmLawed.php b/include/htmLawed.php
index 0a6e3e2fca09d02b2bfc4bf0437e92965ed410c2..334bad307cf5caca6f4acf48a662535e415c0870 100644
--- a/include/htmLawed.php
+++ b/include/htmLawed.php
@@ -379,7 +379,8 @@ return $r;
 function hl_spec($t){
 // final $spec
 $s = array();
-$t = str_replace(array("\t", "\r", "\n", ' '), '', preg_replace_callback('/"(?>(`.|[^"])*)"/sm', create_function('$m', 'return substr(str_replace(array(";", "|", "~", " ", ",", "/", "(", ")", \'`"\'), array("\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08", "\""), $m[0]), 1, -1);'), trim($t)));
+$t = str_replace(array("\t", "\r", "\n", ' '), '',
+preg_replace_callback('/"(?>(`.|[^"])*)"/sm', function($m) {return substr(str_replace(array(";", "|", "~", " ", ",", "/", "(", ")", '`"'), array("\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08", "\""), $m[0]), 1, -1);}, trim($t)));
 for($i = count(($t = explode(';', $t))); --$i>=0;){
  $w = $t[$i];
  if(empty($w) or ($e = strpos($w, '=')) === false or !strlen(($a =  substr($w, $e+1)))){continue;}
diff --git a/include/html2text.php b/include/html2text.php
index 1d12c733c3d3582bc3daede37cd9580b08a88314..0fcaa5cab999744bac97d90e0c8092b476bfbe4c 100644
--- a/include/html2text.php
+++ b/include/html2text.php
@@ -48,8 +48,18 @@ function convert_html_to_text($html, $width=74) {
     $elements->getRoot()->addStylesheet(
         HtmlStylesheet::fromArray(array(
             'html' => array('white-space' => 'pre'), # Don't wrap footnotes
+            'center' => array('text-align' => 'center'),
             'p' => array('margin-bottom' => '1em'),
             'pre' => array('white-space' => 'pre'),
+            'u' => array('text-decoration' => 'underline'),
+            'a' => array('text-decoration' => 'underline'),
+            'b' => array('text-transform' => 'uppercase'),
+            'strong' => array('text-transform' => 'uppercase'),
+            'h4' => array('text-transform' => 'uppercase'),
+
+            // Crazy M$ styles
+            '.MsoNormal' => array('margin' => 0, 'margin-bottom' => 0.0001),
+            '.MsoPlainText' => array('margin' => 0, 'margin-bottom' => 0.0001),
         ))
     );
     $options = array();
@@ -108,6 +118,7 @@ function identify_node($node, $parent=null) {
         case "head":
         case "html":
         case "body":
+        case "center":
         case "div":
         case "p":
         case "pre":
@@ -129,13 +140,6 @@ function identify_node($node, $parent=null) {
         case "a":
             return new HtmlAElement($node, $parent);
 
-        case "b":
-        case "strong":
-            return new HtmlBElement($node, $parent);
-
-        case "u":
-            return new HtmlUElement($node, $parent);
-
         case "ol":
             return new HtmlListElement($node, $parent);
         case "ul":
@@ -152,8 +156,8 @@ function identify_node($node, $parent=null) {
 
         default:
             // print out contents of unknown tags
-            if ($node->hasChildNodes() && $node->childNodes->length == 1)
-                return identify_node($node->childNodes->item(0), $parent);
+            //if ($node->hasChildNodes() && $node->childNodes->length == 1)
+            //    return identify_node($node->childNodes->item(0), $parent);
 
             return new HtmlInlineElement($node, $parent);
     }
@@ -170,13 +174,14 @@ class HtmlInlineElement {
         $this->parent = $parent;
         $this->node = $node;
         $this->traverse($node);
+        $this->style = new CssStyleRules();
         if ($node instanceof DomElement
                 && ($style = $this->node->getAttribute('style')))
-            $this->style = new CssStyleRules($style);
+            $this->style->add($style);
     }
 
     function traverse($node) {
-        if ($node->hasChildNodes()) {
+        if ($node && $node->hasChildNodes()) {
             for ($i = 0; $i < $node->childNodes->length; $i++) {
                 $n = $node->childNodes->item($i);
                 $this->children[] = identify_node($n, $this);
@@ -188,6 +193,21 @@ class HtmlInlineElement {
         $output = '';
         $after_block = false;
         $this->ws = $this->getStyle('white-space', 'normal');
+        // Direction
+        if ($this->node)
+            $dir = $this->node->getAttribute('dir');
+
+        // Ensure we have a value, but don't emit a control char unless
+        // direction is declared
+        $this->dir = $dir ?: 'ltr';
+        switch (strtolower($dir)) {
+        case 'ltr':
+            $output .= "\xE2\x80\x8E"; # LEFT-TO-RIGHT MARK
+            break;
+        case 'rtl':
+            $output .= "\xE2\x80\x8F"; # RIGHT-TO-LEFT MARK
+            break;
+        }
         foreach ($this->children as $c) {
             if ($c instanceof DOMText) {
                 // Collapse white-space
@@ -201,6 +221,10 @@ class HtmlInlineElement {
                 case 'normal':
                 default:
                     if ($after_block) $more = ltrim($more);
+                    if ($this instanceof HtmlBlockElement && trim($more) == '')
+                        // Ignore pure whitespace in-between elements inside
+                        // block elements
+                        $more = '';
                     $more = preg_replace('/[ \r\n\t\f]+/mu', ' ', $more);
                 }
             }
@@ -209,6 +233,10 @@ class HtmlInlineElement {
             }
             else {
                 $more = $c;
+                if (!$after_block)
+                    // Prepend a newline. Block elements should start to the
+                    // far left
+                    $output .= "\n";
             }
             $after_block = ($c instanceof HtmlBlockElement);
             if ($more instanceof PreFormattedText)
@@ -216,7 +244,22 @@ class HtmlInlineElement {
             elseif (is_string($more))
                 $output .= $more;
         }
+        switch ($this->getStyle('text-transform', 'none')) {
+        case 'uppercase':
+            $output = mb_strtoupper($output);
+            break;
+        }
+        switch ($this->getStyle('text-decoration', 'none')) {
+        case 'underline':
+            // Split diacritics and underline chars which do not go below
+            // the baseline
+            if (class_exists('Normalizer'))
+                $output = Normalizer::normalize($output, Normalizer::FORM_D);
+            $output = preg_replace("/[a-fhik-or-xzA-PR-Z0-9#]/u", "$0\xcc\xb2", $output);
+            break;
+        }
         if ($this->footnotes) {
+            $output = rtrim($output, "\n");
             $output .= "\n\n" . str_repeat('-', $width/2) . "\n";
             $id = 1;
             foreach ($this->footnotes as $name=>$content)
@@ -232,20 +275,25 @@ class HtmlInlineElement {
                 if ($c instanceof HtmlInlineElement)
                     $this->weight += $c->getWeight();
                 elseif ($c instanceof DomText)
-                    $this->weight += mb_strwidth($c->wholeText);
+                    $this->weight += mb_strwidth2($c->wholeText);
             }
         }
         return $this->weight;
     }
 
+    function setStyle($property, $value) {
+        $this->style->set($property, $value);
+    }
+
     function getStyle($property, $default=null, $tag=false, $classes=false) {
         if ($this->style && $this->style->has($property))
-            return $this->style->get($property);
+            return $this->style->get($property, $default);
 
-        if ($tag === false)
+        if ($this->node && $tag === false)
             $tag = $this->node->nodeName;
+
         if ($classes === false) {
-            if ($c = $this->node->getAttribute('class'))
+            if ($this->node && ($c = $this->node->getAttribute('class')))
                 $classes = explode(' ', $c);
             else
                 $classes = array();
@@ -284,6 +332,14 @@ class HtmlInlineElement {
 
 class HtmlBlockElement extends HtmlInlineElement {
     var $min_width = false;
+    var $pad_left;
+    var $pad_right;
+
+    function __construct($node, $parent) {
+        parent::__construct($node, $parent);
+        $this->pad_left = str_repeat(' ', $this->getStyle('padding-left', 0.0));
+        $this->pad_right = str_repeat(' ', $this->getStyle('padding-right', 0.0));
+    }
 
     function render($width, $options) {
         // Allow room for the border.
@@ -295,12 +351,16 @@ class HtmlBlockElement extends HtmlInlineElement {
         $output = parent::render($width, $options);
         if ($output instanceof PreFormattedText)
             // TODO: Consider CSS rules
-            return new PreFormattedText("\n" . $output);
+            return $output;
 
+        // Leading and trailing whitespace is ignored in block elements
         $output = trim($output);
         if (!strlen($output))
             return "";
 
+        // Padding
+        $width -= strlen($this->pad_left) + strlen($this->pad_right);
+
         // Wordwrap the content to the width
         switch ($this->ws) {
             case 'nowrap':
@@ -313,17 +373,41 @@ class HtmlBlockElement extends HtmlInlineElement {
                 $output = mb_wordwrap($output, $width, "\n", true);
         }
 
-        // Apply stylesheet styles
-        // TODO: Padding
-        // TODO: Justification
+        // Justification
+        static $aligns = array(
+            'left' => STR_PAD_RIGHT,
+            'right' => STR_PAD_LEFT,
+            'center' => STR_PAD_BOTH,
+        );
+        $talign = $this->getStyle('text-align', 'none');
+        $self = $this;
+        if (isset($aligns[$talign])) {
+            // Explode lines, justify, implode again
+            $output = array_map(function($l) use ($talign, $aligns, $width, $self) {
+                return $self->pad_left.mb_str_pad($l, $width, ' ', $aligns[$talign]).$self->pad_right;
+            }, explode("\n", $output)
+            );
+            $output = implode("\n", $output);
+        }
+        // Apply left and right padding, if specified
+        elseif ($this->pad_left || $this->pad_right) {
+            $output = array_map(function($l) use ($self) {
+                return $self->pad_left.$l.$self->pad_right;
+            }, explode("\n", $output)
+            );
+            $output = implode("\n", $output);
+        }
+
         // Border
         if ($bw)
             $output = self::borderize($output, $width);
+
         // Margin
-        $mb = $this->getStyle('margin-bottom', 0);
+        $mb = $this->getStyle('margin-bottom', 0.0)
+            + $this->getStyle('padding-bottom', 0.0);
         $output .= str_repeat("\n", (int)$mb);
 
-        return "\n" . $output;
+        return $output."\n";
     }
 
     function borderize($what, $width) {
@@ -340,11 +424,11 @@ class HtmlBlockElement extends HtmlInlineElement {
                 if ($c instanceof HtmlBlockElement)
                     $this->min_width = max($c->getMinWidth(), $this->min_width);
                 elseif ($c instanceof DomText)
-                    $this->min_width = max(max(array_map('mb_strwidth',
+                    $this->min_width = max(max(array_map('mb_strwidth2',
                         explode(' ', $c->wholeText))), $this->min_width);
             }
         }
-        return $this->min_width;
+        return $this->min_width + strlen($this->pad_left) + strlen($this->pad_right);
     }
 }
 
@@ -353,25 +437,10 @@ class HtmlBrElement extends HtmlBlockElement {
         return "\n";
     }
 }
-class HtmlUElement extends HtmlInlineElement {
-    function render($width, $options) {
-        $output = parent::render($width, $options);
-        return "_".str_replace(" ", "_", $output)."_";
-    }
-    function getWeight() { return parent::getWeight() + 2; }
-}
-
-class HtmlBElement extends HtmlInlineElement {
-    function render($width, $options) {
-        $output = parent::render($width, $options);
-        return "*".$output."*";
-    }
-    function getWeight() { return parent::getWeight() + 2; }
-}
 
 class HtmlHrElement extends HtmlBlockElement {
     function render($width, $options) {
-        return "\n".str_repeat('-', $width)."\n";
+        return str_repeat("\xE2\x94\x80", $width)."\n";
     }
     function getWeight() { return 1; }
     function getMinWidth() { return 0; }
@@ -384,18 +453,19 @@ class HtmlHeadlineElement extends HtmlBlockElement {
             return "";
         switch ($this->node->nodeName) {
             case 'h1':
+                $line = "\xE2\x95\x90"; # U+2505
+                break;
             case 'h2':
-                $line = '=';
+                $line = "\xE2\x94\x81"; # U+2501
                 break;
             case 'h3':
-            case 'h4':
-                $line = '-';
+                $line = "\xE2\x94\x80"; # U+2500
                 break;
             default:
                 return $headline;
         }
-        $length = max(array_map('mb_strwidth', explode("\n", $headline)));
-        $headline .= "\n" . str_repeat($line, $length) . "\n";
+        $length = max(array_map('mb_strwidth2', explode("\n", $headline)));
+        $headline .= str_repeat($line, $length) . "\n";
         return $headline;
     }
 }
@@ -430,7 +500,7 @@ class HtmlImgElement extends HtmlInlineElement {
         return "[image:$alt$title] ";
     }
     function getWeight() {
-        return mb_strwidth($this->node->getAttribute("alt")) + 8;
+        return mb_strwidth2($this->node->getAttribute("alt")) + 8;
     }
 }
 
@@ -447,8 +517,8 @@ class HtmlAElement extends HtmlInlineElement {
         } elseif (strpos($href, 'mailto:') === 0) {
             $href = substr($href, 7);
             $output = (($href != $output) ? "$href " : '') . "<$output>";
-        } elseif (mb_strwidth($href) > $width / 2) {
-            if (mb_strwidth($output) > $width / 2) {
+        } elseif (mb_strwidth2($href) > $width / 2) {
+            if (mb_strwidth2($output) > $width / 2) {
                 // Parse URL and use relative path part
                 if ($PU = parse_url($output))
                     $output = $PU['host'] . $PU['path'];
@@ -491,17 +561,17 @@ class HtmlUnorderedListElement extends HtmlListElement {
 }
 
 class HtmlListItem extends HtmlBlockElement {
-    function HtmlListItem($node, $parent, $number) {
+    function __construct($node, $parent, $number) {
         parent::__construct($node, $parent);
         $this->number = $number;
     }
 
     function render($width, $options) {
         $prefix = sprintf($options['marker'], $this->number);
-        $lines = explode("\n", trim(parent::render($width-mb_strwidth($prefix), $options)));
+        $lines = explode("\n", trim(parent::render($width-mb_strwidth2($prefix), $options)));
         $lines[0] = $prefix . $lines[0];
         return new PreFormattedText(
-            implode("\n".str_repeat(" ", mb_strwidth($prefix)), $lines)."\n");
+            implode("\n".str_repeat(" ", mb_strwidth2($prefix)), $lines)."\n");
     }
 }
 
@@ -516,11 +586,23 @@ class HtmlCodeElement extends HtmlInlineElement {
 }
 
 class HtmlTable extends HtmlBlockElement {
+    var $body;
+    var $foot;
+    var $rows;
+    var $border = true;
+    var $padding = true;
+
     function __construct($node, $parent) {
         $this->body = array();
         $this->foot = array();
         $this->rows = &$this->body;
         parent::__construct($node, $parent);
+        $A = $this->node->getAttribute('border');
+        if (isset($A))
+            $this->border = (bool) $A;
+        $A = $this->node->getAttribute('cellpadding');
+        if (isset($A))
+            $this->padding = (bool) $A;
     }
 
     function getMinWidth() {
@@ -529,7 +611,7 @@ class HtmlTable extends HtmlBlockElement {
                 foreach ($r as $cell)
                     $this->min_width = max($this->min_width, $cell->getMinWidth());
         }
-        return $this->min_width + 4;
+        return $this->min_width + ($this->border ? 2 : 0) + ($this->padding ? 2 : 0);
     }
 
     function getWeight() {
@@ -628,6 +710,7 @@ class HtmlTable extends HtmlBlockElement {
             $i = 0;
             foreach ($r as $cell) {
                 for ($j=0; $j<$cell->cols; $j++) {
+                    // TODO: Use cell-specified width
                     $weights[$i] = max($weights[$i], $cell->getWeight());
                     $mins[$i] = max($mins[$i], $cell->getMinWidth());
                 }
@@ -636,7 +719,8 @@ class HtmlTable extends HtmlBlockElement {
         }
 
         # Subtract internal padding and borders from the available width
-        $inner_width = $width - $cols*3 - 1;
+        $inner_width = $width - ($this->border ? $cols + 1 : 0)
+            - ($this->padding ? $cols*2 : 0);
 
         # Optimal case, where the preferred width of all the columns is
         # doable
@@ -655,7 +739,9 @@ class HtmlTable extends HtmlBlockElement {
                 $widths[] = (int)($inner_width * $c / $total);
             $this->_fixupWidths($widths, $mins);
         }
-        $outer_width = array_sum($widths) + $cols*3 + 1;
+        $outer_width = array_sum($widths)
+            + ($this->border ? $cols + 1 : 0)
+            + ($this->padding ? $cols * 2 : 0);
 
         $contents = array();
         $heights = array();
@@ -671,7 +757,8 @@ class HtmlTable extends HtmlBlockElement {
                 # Compute the effective cell width for spanned columns
                 # Add extra space for the unneeded border padding for
                 # spanned columns
-                $cwidth = ($cell->cols - 1) * 3;
+                $cwidth = ($this->border ? ($cell->cols - 1) : 0)
+                    + ($this->padding ? ($cell->cols - 1) * 2 : 0);
                 for ($j = 0; $j < $cell->cols; $j++)
                     $cwidth += $widths[$x+$j];
                 # Stash the computed width so it doesn't need to be
@@ -679,7 +766,8 @@ class HtmlTable extends HtmlBlockElement {
                 $cell->width = $cwidth;
                 unset($data);
                 $data = explode("\n", $cell->render($cwidth, $options));
-                $heights[$y] = max(count($data), $heights[$y]);
+                // NOTE: block elements have trailing newline
+                $heights[$y] = max(count($data)-1, $heights[$y]);
                 $contents[$y][$i] = &$data;
                 $x += $cell->cols;
             }
@@ -687,29 +775,34 @@ class HtmlTable extends HtmlBlockElement {
 
         # Build the header
         $header = "";
-        for ($i = 0; $i < $cols; $i++)
-            $header .= "+-" . str_repeat("-", $widths[$i]) . "-";
-        $header .= "+";
+        if ($this->border) {
+            $padding = $this->padding ? '-' : '';
+            for ($i = 0; $i < $cols; $i++) {
+                $header .= '+'.$padding.str_repeat("-", $widths[$i]).$padding;
+            }
+            $header .= "+\n";
+        }
 
         # Emit the rows
-        $output = "\n";
         if (isset($this->caption)) {
             $this->caption = $this->caption->render($outer_width, $options);
         }
+        $border = $this->border ? '|' : '';
+        $padding = $this->padding ? ' ' : '';
         foreach ($rows as $y=>$r) {
-            $output .= $header . "\n";
+            $output .= $header;
             for ($x = 0, $k = 0; $k < $heights[$y]; $k++) {
-                $output .= "|";
+                $output .= $border;
                 foreach ($r as $x=>$cell) {
                     $content = (isset($contents[$y][$x][$k]))
                         ? $contents[$y][$x][$k] : "";
-                    $output .= " ".mb_str_pad($content, $cell->width)." |";
+                    $output .= $padding.mb_str_pad($content, $cell->width).$padding.$border;
                     $x += $cell->cols;
                 }
                 $output .= "\n";
             }
         }
-        $output .= $header . "\n";
+        $output .= $header;
         return new PreFormattedText($output);
     }
 }
@@ -722,10 +815,14 @@ class HtmlTableCell extends HtmlBlockElement {
 
         if (!$this->cols) $this->cols = 1;
         if (!$this->rows) $this->rows = 1;
+
+        // Upgrade old attributes
+        if ($A = $this->node->getAttribute('align'))
+            $this->setStyle('text-align', $A);
     }
 
     function render($width, $options) {
-        return ltrim(parent::render($width, $options));
+        return parent::render($width, $options);
     }
 
     function getWeight() {
@@ -784,13 +881,48 @@ class HtmlStylesheet {
 class CssStyleRules {
     var $rules = array();
 
-    function __construct($rules) {
+    static $compact_rules = array(
+        'padding' => 1,
+    );
+
+    function __construct($rules='') {
+        if ($rules)
+            $this->add($rules);
+    }
+
+    function add($rules) {
         foreach (explode(';', $rules) as $r) {
             if (strpos($r, ':') === false)
                 continue;
             list($prop, $val) = explode(':', $r);
-            $this->rules[trim($prop)] = trim($val);
+            $prop = trim($prop);
             // TODO: Explode compact rules, like 'border', 'margin', etc.
+            if (isset(self::$compact_rules[$prop]))
+                $this->expand($prop, trim($val));
+            else
+                $this->rules[$prop] = trim($val);
+        }
+    }
+
+    function expand($prop, $val) {
+        switch (strtolower($prop)) {
+        case 'padding':
+            @list($a, $b, $c, $d) = preg_split('/\s+/', $val);
+            if (!isset($b)) {
+                $d = $c = $b = $a;
+            }
+            elseif (!isset($c)) {
+                $d = $b;
+                $c = $a;
+            }
+            elseif (!isset($d)) {
+                $d = $b;
+            }
+            $this->rules['padding-top'] = $a;
+            $this->styles['padding-right'] = $b;
+            $this->rules['padding-bottom'] = $c;
+            $this->rules['padding-left'] = $d;
+
         }
     }
 
@@ -816,18 +948,26 @@ class CssStyleRules {
         return $val;
     }
 
-    static function convert($value, $units) {
+    function set($prop, $value) {
+        $this->rules[$prop] = $value;
+    }
+
+    static function convert($value, $units, $max=0) {
         if ($value === null)
             return $value;
 
         // Converts common CSS units to units of characters
         switch ($units) {
+            default:
+                if (substr($units, -1) == '%') {
+                    return ((float) $value) * 0.01 * $max;
+                }
             case 'px':
-                return $value / 20.0;
+                // 600px =~ 60chars
+                return (int) ($value / 10.0);
             case 'pt':
                 return $value / 12.0;
             case 'em':
-            default:
                 return $value;
         }
     }
@@ -853,6 +993,10 @@ if (!function_exists('mb_strwidth')) {
         return mb_strlen($string);
     }
 }
+function mb_strwidth2($string) {
+    $junk = array();
+    return mb_strwidth($string) - preg_match_all("/\p{M}/u", $string, $junk);
+}
 
 // Thanks http://www.php.net/manual/en/function.wordwrap.php#107570
 // @see http://www.tads.org/t3doc/doc/htmltads/linebrk.htm
@@ -864,7 +1008,8 @@ function mb_wordwrap($string, $width=75, $break="\n", $cut=false) {
   if ($cut) {
     // Match anything 1 to $width chars long followed by whitespace or EOS,
     // otherwise match anything $width chars long
-    $search = '/(.{1,'.$width.'})(?:\s|$|(\p{Ps}))|(.{'.$width.'})/uS';
+    $search = '/((?>[^\n\p{M}]\p{M}*){1,'.$width.'})(?:[ \n]|$|(\p{Ps}))|((?>[^\n\p{M}]\p{M}*){'
+          .$width.'})/uS'; # <?php
     $replace = '$1$3'.$break.'$2';
   } else {
     // Anchor the beginning of the pattern with a lookahead
@@ -878,8 +1023,10 @@ function mb_wordwrap($string, $width=75, $break="\n", $cut=false) {
 // Thanks http://www.php.net/manual/en/ref.mbstring.php#90611
 function mb_str_pad($input, $pad_length, $pad_string=" ",
         $pad_style=STR_PAD_RIGHT) {
+    $match = array();
+    $marks = preg_match_all('/\p{M}/u', $input, $match);
     return str_pad($input,
-        strlen($input)-mb_strwidth($input)+$pad_length, $pad_string,
+        strlen($input)-mb_strwidth($input)+$marks+$pad_length, $pad_string,
         $pad_style);
 }
 
diff --git a/include/i18n/en_US/config.yaml b/include/i18n/en_US/config.yaml
index b29df10ace86324407ba25ab5605ed7d569cdf04..c849302f40086e18fa46a4bf47143faedf075622 100644
--- a/include/i18n/en_US/config.yaml
+++ b/include/i18n/en_US/config.yaml
@@ -4,11 +4,10 @@
 #
 ---
 core:
-    time_format: 'h:i A'
-    date_format: 'm/d/Y'
-    datetime_format: 'm/d/Y g:i a'
-    daydatetime_format: 'D, M j Y g:ia'
-    default_timezone_id: 8
+    time_format: 'hh:mm a'
+    date_format: 'MM/dd/y'
+    datetime_format: 'MM/dd/y h:mm a'
+    daydatetime_format: 'EEE, MMM d y h:mm a'
     default_priority_id: 2
     enable_daylight_saving: 0
 
@@ -74,8 +73,10 @@ core:
     hide_staff_name: 0
     overlimit_notice_active: 0
     email_attachments: 1
-    number_format: '######'
-    sequence_id: 0
+    ticket_number_format: '######'
+    ticket_sequence_id: 0
+    task_number_format: '#'
+    task_sequence_id: 2
     log_level: 2
     log_graceperiod: 12
     client_registration: 'public'
diff --git a/include/i18n/en_US/file.yaml b/include/i18n/en_US/file.yaml
index 0464b6a23d6f2e600b2242880089133d65fdc214..d77bcd3e9da33e29462550b07c4c2efabca5f4c4 100644
--- a/include/i18n/en_US/file.yaml
+++ b/include/i18n/en_US/file.yaml
@@ -3,7 +3,7 @@
 #
 # Files initially inserted into the system. Canned responses have their own
 # method for attachments; however, this file will make it easier to add
-# thinkgs like inline images.
+# things like inline images.
 #
 # NOTE: If the files aren't attached to something by the installer, they
 # bill be cleaned up shortly after installation (by the autocron).
diff --git a/include/i18n/en_US/filter.yaml b/include/i18n/en_US/filter.yaml
index da95a9869e9037cdd5eb2c46307de32706128821..95da7ae6bbe1d53f92b537e03e7c5a3a91f6d44e 100644
--- a/include/i18n/en_US/filter.yaml
+++ b/include/i18n/en_US/filter.yaml
@@ -8,8 +8,6 @@
 # Fields:
 # isactive - (bool:0|1) true or false if the filter is initially enabled
 # execorder - (int) order the filters should be executed in (lowest first)
-# reject_ticket - (bool:0|1) if a ticket matches the filter it should be
-#       rejected (ie. not be created).
 # name - (string) Descriptive name for the filter
 # notes - (string) Administrative notes (viewable internally only)
 # rules - (list<FilterRule>) List of rules for the filter
@@ -19,19 +17,25 @@
 # what - (enum<email|>) field to check
 # how - (enum<equals|contains|dncontain>) how to check for <val>
 # val - (string) search value
+#
+# Fields for FilterAction
+# type - type of filter action
+#
 ---
 - isactive: 1
   execorder: 99
-  reject_ticket: 1
-  # NOTE: Don't translate 'Email'
-  target: Email
-  # NOTE: Don't translate 'SYSTEM BAN LIST'
-  name: SYSTEM BAN LIST
+  match_all_rules: 0
+  email_id: 0
+  target: Email #notrans
+  name: SYSTEM BAN LIST #notrans
   notes: |
     Internal list for email banning. Do not remove
 
   rules:
     - isactive: 1
-      what: email
-      how: equal
-      val: test@example.com
+      what: email #notrans
+      how: equal #notrans
+      val: test@example.com #notrans
+  actions:
+    - sort: 1
+      type: reject #notrans
diff --git a/include/i18n/en_US/form.yaml b/include/i18n/en_US/form.yaml
index b5bde9db9af53ae3189a77080db5085e649184cf..9a807a05e86b4d87461bed3a6947bc8d7dfecef2 100644
--- a/include/i18n/en_US/form.yaml
+++ b/include/i18n/en_US/form.yaml
@@ -7,7 +7,8 @@
 # title:    Bold section title of the form
 # instructions: Title deck, detailed instructions on entering form data
 # notes:    Notes for the form, shown under the fields
-# deletable: True if the form can be removed from the system
+# flags:
+#   0x0001  If the form can be removed from the system
 # fields:   List of fields for the form
 #   type:       Field type (short name) (eg. 'text', 'memo', 'phone', ...)
 #   label:      Field label shown to the user
@@ -15,10 +16,16 @@
 #               useful for page and email templates, where %{ ticket.<name> }
 #               will be used to retrieve the data from the field.
 #   hint:       Help text shown with the field
-#   edit_mask:  Mask out edits to the field (1=>delete, 2=>change name,
-#                   4=>privacy setting, 8=>requirement setting)
-#   private:    True if the field should be hidden from the client
-#   required:   True if entry for the field is required
+#   flags:      Bit mask for settings & options
+#     # From class DynamicFormField
+#     const FLAG_MASK_CHANGE      = 0x0010;     # Type cannot change
+#     const FLAG_MASK_DELETE      = 0x0020;     # Cannot be deleted
+#     const FLAG_MASK_EDIT        = 0x0040;     # Data cannot be edited
+#     const FLAG_MASK_DISABLE     = 0x0080;     # Field cannot be disabled
+#     const FLAG_MASK_REQUIRE     = 0x10000;    # Requirement cannot be changed
+#     const FLAG_MASK_VIEW        = 0x20000;    # View settings cannot be changed
+#     const FLAG_MASK_NAME        = 0x40000;    # Name cannot be changed
+#
 #   configuration: Field-specific configuration
 #     size:     (text) width of the field
 #     length:   (text) maximum size of the data in the field
@@ -29,14 +36,13 @@
 - id: 1
   type: U # notrans
   title: Contact Information
-  deletable: false
+  flags: 0
   fields:
     - type: text # notrans
       name: email # notrans
       label: Email Address
-      required: true
       sort: 1
-      edit_mask: 15
+      flags: 0x777A3
       configuration:
         size: 40
         length: 64
@@ -44,23 +50,21 @@
     - type: text # notrans
       name: name # notrans
       label: Full Name
-      required: true
       sort: 2
-      edit_mask: 15
+      flags: 0x777A3
       configuration:
         size: 40
         length: 64
     - type: phone # notrans
       name: phone # notrans
       label: Phone Number
-      required: false
       sort: 3
+      flags: 0x3301
     - type: memo # notrans
       name: notes
       label: Internal Notes
-      required: false
-      private: true
       sort: 4
+      flags: 0x3001
       configuration:
         rows: 4
         cols: 40
@@ -72,15 +76,14 @@
       This form will be attached to every ticket, regardless of its source.
       You can add any fields to this form and they will be available to all
       tickets, and will be searchable with advanced search and filterable.
-  deletable: false
+  flags: 0
   fields:
     - id: 20
       type: text # notrans
       name: subject # notrans
       label: Issue Summary
-      required: true
-      edit_mask: 15
       sort: 1
+      flags: 0x77721
       configuration:
         size: 40
         length: 50
@@ -89,28 +92,24 @@
       name: message # notrans
       label: Issue Details
       hint: Details on the reason(s) for opening the ticket.
-      required: true
-      edit_mask: 15
       sort: 2
+      flags: 0x75523
     - id: 22
       type: priority # notrans
       name: priority # notrans
       label: Priority Level
-      required: false
-      private: true
-      edit_mask: 3
+      flags: 0x430B1
       sort: 3
 - type: C # notrans
   title: Company Information
   instructions: Details available in email templates
-  deletable: false
+  flags: 0
   fields:
     - type: text # notrans
       name: name # notrans
       label: Company Name
-      required: true
       sort: 1
-      edit_mask: 3
+      flags: 0x471A1
       configuration:
         size: 40
         length: 64
@@ -118,21 +117,22 @@
       name: website # notrans
       label: Website
       sort: 2
+      flags: 0x3101
       configuration:
         size: 40
         length: 64
     - type: phone # notrans
       name: phone # notrans
       label: Phone Number
-      required: false
       sort: 3
+      flags: 0x3101
       configuration:
         ext: false
     - type: memo # notrans
       name: address
       label: Address
-      required: false
       sort: 4
+      flags: 0x3101
       configuration:
         rows: 2
         cols: 40
@@ -141,22 +141,21 @@
 - type: O # notrans
   title: Organization Information
   instructions: Details on user organization
-  deletable: false
+  flags: 0
   fields:
     - type: text # notrans
       name: name # notrans
       label: Name
-      required: true
       sort: 1
-      edit_mask: 15
+      flags: 0x777A3
       configuration:
         size: 40
         length: 64
     - type: memo
       name: address
       label: Address
-      required: false
       sort: 2
+      flags: 0x3301
       configuration:
         rows: 2
         cols: 40
@@ -165,21 +164,42 @@
     - type: phone
       name: phone
       label: Phone
-      required: false
       sort: 3
+      flags: 0x3301
     - type: text
       name: website
       label: Website
-      required: false
       sort: 4
+      flags: 0x3301
       configuration:
         size: 40
         length: 0
     - type: memo # notrans
       name: notes
       label: Internal Notes
-      required: false
       sort: 5
+      flags: 0x3001
       configuration:
         rows: 4
         cols: 40
+- type: A # notrans
+  title: Task Details
+  instructions: Please Describe The Issue
+  notes: |
+      This form is used to create a task.
+  flags: 0
+  fields:
+    - type: text # notrans
+      name: title # notrans
+      flags: 0x470A1
+      sort: 1
+      label: Title
+      configuration:
+        size: 40
+        length: 50
+    - type: thread # notrans
+      name: description # notrans
+      flags: 0x450F3
+      sort: 2
+      label: Description
+      hint: Details on the reason(s) for creating the task.
diff --git a/include/i18n/en_US/group.yaml b/include/i18n/en_US/group.yaml
index bc65037e180922fbf221261ae4a9e2035a5cd681..c48f1c2e577da9a1c5575b122e48bf00a78ec6fc 100644
--- a/include/i18n/en_US/group.yaml
+++ b/include/i18n/en_US/group.yaml
@@ -2,29 +2,11 @@
 # Default groups defined for the system
 #
 # Fields:
-# isactive - (bool:0|1) true or false if the group should be initially
-#       usable
+# id - Primary id for the group
+# role_id - (int)  default role for the group
+# flags - (bit mask) group flags
 # name - (string) descriptive name for the group
 # notes - (string) administrative notes (viewable internally only)
-# can_create_tickets - (bool:0|1) true or false if users of the group can
-#       create new tickets
-# can_edit_tickets - (bool:0|1) true or false if users of the group can
-#       modify and update existing tickets
-# can_delete_tickets - (bool:0|1) true or false if members of the group can
-#       delete tickets (permanently)
-# can_close_tickets - (bool:0|1) true or false if members of the group can
-#       close active tickets
-# can_assign_ticets - (bool:0|1) true or false if members of the group can
-#       assign tickets to staff
-# can_transfer_tickets - (bool:0|1) true or false if members of the group
-#       can change the department tickets are assigne dto
-# can_ban_emails - (bool:0|1) true or false if members of the group can add
-#       emails to the system ban list
-# can_manage_premade - (bool:0|1) true or false if members of the group can
-#       create, modify, and delete canned responses
-# can_manage_faq - (bool:0|1) true or false if members of the group can
-#       manage the customer-facing and internal knowledgebase
-#
 # depts: (list<Department<id>>) id's of the departments to which the group
 #       should initially have access
 #
@@ -32,54 +14,30 @@
 # The very first group listed in this document will be the primary group of
 # the initial staff member -- the administrator.
 ---
-- isactive: 1
+- id: 1
+  role_id: 1
+  flags: 1
   name: Lion Tamers
   notes: |
     System overlords. These folks (initially) have full control to all the
     departments they have access to.
-  can_create_tickets: 1
-  can_edit_tickets: 1
-  can_delete_tickets: 1
-  can_close_tickets: 1
-  can_assign_tickets: 1
-  can_transfer_tickets: 1
-  can_ban_emails: 1
-  can_manage_premade: 1
-  can_manage_faq: 1
-  can_post_ticket_reply: 1
 
   depts: [1, 2, 3]
 
-- isactive: 1
+- id: 2
+  role_id: 2
+  flags: 1
   name: Elephant Walkers
   notes: |
     Inhabitants of the ivory tower
-  can_create_tickets: 1
-  can_edit_tickets: 1
-  can_delete_tickets: 1
-  can_close_tickets: 1
-  can_assign_tickets: 1
-  can_transfer_tickets: 1
-  can_ban_emails: 1
-  can_manage_premade: 1
-  can_manage_faq: 1
-  can_post_ticket_reply: 1
 
   depts: [1, 2, 3]
 
-- isactive: 1
+- id: 3
+  role_id: 2
+  flags: 1
   name: Flea Trainers
   notes: |
     Lowly staff members
-  can_create_tickets: 1
-  can_edit_tickets: 1
-  can_delete_tickets: 0
-  can_close_tickets: 1
-  can_assign_tickets: 1
-  can_transfer_tickets: 1
-  can_ban_emails: 0
-  can_manage_premade: 0
-  can_manage_faq: 0
-  can_post_ticket_reply: 1
 
   depts: [1, 2, 3]
diff --git a/include/i18n/en_US/help/tips/dashboard.my_profile.yaml b/include/i18n/en_US/help/tips/dashboard.my_profile.yaml
index 821fc0bbee69fe5e56bcc5a1d886042a44e35162..b8c40b07c5699ed1b068cf08176b336879f4a6fe 100644
--- a/include/i18n/en_US/help/tips/dashboard.my_profile.yaml
+++ b/include/i18n/en_US/help/tips/dashboard.my_profile.yaml
@@ -54,10 +54,15 @@ default_paper_size:
 show_assigned_tickets:
     title: Show Assigned Tickets
     content: >
-        Enable this to hide your name from the <span class="doc-desc-title">Open 
-        Tickets</span> queue for those tickets which you have been assigned. Upon 
-        hiding, the <span class="doc-desc-title">Department</span> name to which
-        you belong will replace where your name would normally be displayed.
+        <p>
+        Enabling this option will override the global setting to hide
+        assigned tickets from the open queues.
+        </p>
+        <p class="info-banner">
+        <i class="icon-info-sign"></i>
+        This setting is only available when assigned tickets are excluded
+        globally.
+        </p>
 
 password:
     title: Password
diff --git a/include/i18n/en_US/help/tips/org.yaml b/include/i18n/en_US/help/tips/org.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..0cb4219f5afe31dd8357efee506f35718b2c1ad4
--- /dev/null
+++ b/include/i18n/en_US/help/tips/org.yaml
@@ -0,0 +1,36 @@
+#
+# This is the view / management page for an organization in the user
+# directory
+#
+# Fields:
+# title - Shown in bold at the top of the popover window
+# content - The body of the help popover
+# links - List of links shows below the content
+#   title - Link title
+#   href - href of link (links starting with / are translated to the
+#       helpdesk installation path)
+#
+# The key names such as 'helpdesk_name' should not be translated as they
+# must match the HTML #ids put into the page template.
+#
+---
+org_sharing:
+    title: Ticket Sharing
+    content: >
+        <p>
+        Organization ticket sharing allows members access to tickets owned
+        by other members of the organization.
+        </p>
+        <p class="info-banner">
+        <i class="icon-info-sign"></i>
+        Collaborators always have access to tickets.
+        </p>
+
+email_domain:
+    title: Email Domain
+    content: >
+        Users can be automatically added to this organization based on their
+        email domain(s). Use the box below to enter one or more domains
+        separated by commas. For example, enter <code>mycompany.com</code>
+        for users with email addresses ending in @mycompany.com
+
diff --git a/include/i18n/en_US/help/tips/settings.agents.yaml b/include/i18n/en_US/help/tips/settings.agents.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5c699b7efcc46211cb0e5286840eb58100c980c2
--- /dev/null
+++ b/include/i18n/en_US/help/tips/settings.agents.yaml
@@ -0,0 +1,76 @@
+#
+# This is popup help messages for the Admin Panel -> Settings -> Agents
+#
+# Fields:
+# title - Shown in bold at the top of the popover window
+# content - The body of the help popover
+# links - List of links shows below the content
+#   title - Link title
+#   href - href of link (links starting with / are translated to the
+#       helpdesk installation path)
+#
+# The key names such as 'helpdesk_name' should not be translated as they
+# must match the HTML #ids put into the page template.
+#
+---
+
+# General Settings
+agent_name_format:
+    title: Agent Name Formatting
+    content: >
+        Choose a format for Agents names throughout the system. Email templates
+        will use it for names if no other format is specified.
+
+staff_identity_masking:
+    title: Staff Identity Masking
+    content: >
+        If enabled, this will hide the Agent’s name from the Client during any
+        communication.
+
+# Authentication settings
+password_reset:
+    title: Password Expiration Policy
+    content: >
+        Sets how often (in months) Agents will be required to change
+        their password. If disabled (set to "No expiration"), passwords will
+        not expire.
+
+password_expiration_policy:
+    title: Password Expiration Policy
+    content: >
+        Choose how often Agents will be required to change their password. If
+        disabled (i.e., <span class="doc-desc-opt">No Expiration</span>), passwords
+        will not expire.
+
+allow_password_resets:
+    title: Allow Password Resets
+    content: >
+        Enable this feature if you would like to display the
+        <span class="doc-desc-title">Forgot My Password</span> link on the
+        <span class="doc-desc-title">Staff Log-In Page</span>
+        after a failed log in attempt.
+
+reset_token_expiration:
+    title: Password Reset Window
+    content: >
+        Choose the duration (in minutes) for which the <span class="doc-desc-title">
+        Password Reset Tokens</span> will be valid. When an Agent requests a <span
+        class="doc-desc-title">Password Reset</span>, they are emailed a token that
+        will permit the reset to take place.
+
+staff_session_timeout:
+    title: Agent Session Timeout
+    content: >
+        Choose the maximum idle time (in minutes) before an Agent is required to
+        log in again.
+        <br><br>
+        If you would like to disable <span class="doc-desc-title">Agent
+        Session Timeouts</span>, enter 0.
+
+bind_staff_session_to_ip:
+    title: Bind Agent Session to IP
+    content: >
+        Enable this if you want Agent to be remembered by their current IP
+        upon Log In.
+        <br><br>
+        This setting is not recommened for users assigned IP addresses dynamically.
diff --git a/include/i18n/en_US/help/tips/settings.email.yaml b/include/i18n/en_US/help/tips/settings.email.yaml
index 9b257975f3f42bb91d6cf0d96d79b7f937e0498c..8bd3ed3a7b636036013c911b4cf635c5c5f9e97c 100644
--- a/include/i18n/en_US/help/tips/settings.email.yaml
+++ b/include/i18n/en_US/help/tips/settings.email.yaml
@@ -124,6 +124,12 @@ default_mta:
         <span class="doc-desc-title">Default MTA</span> takes care of
         email delivery process for outgoing emails without SMTP setting.
 
+ticket_response_files:
+    title: Ticket Response Files
+    content: >
+        If enabled, any attachments an Agent may attach to a ticket response will
+        be also included in the email to the User.
+
 verify_email_addrs:
     title: Verify Email Addresses
     content: >
diff --git a/include/i18n/en_US/help/tips/settings.kb.yaml b/include/i18n/en_US/help/tips/settings.kb.yaml
index 702dac9d573a5bf3094321069db57c20dc511331..c6147567c1c39c8f220af8da4cee19a854a5cc4d 100644
--- a/include/i18n/en_US/help/tips/settings.kb.yaml
+++ b/include/i18n/en_US/help/tips/settings.kb.yaml
@@ -35,7 +35,7 @@ restrict_kb:
         your knowledge base articles on the client interface.
     links:
       - title: Access Control Settings
-        href: /scp/settings.php?t=access
+        href: /scp/settings.php?t=users
 
 canned_responses:
     title: Canned Responses
diff --git a/include/i18n/en_US/help/tips/settings.pages.yaml b/include/i18n/en_US/help/tips/settings.pages.yaml
index 487d09cf09f792374742136de279d0a338eff98a..13615271e15dd68195e3aea93639d89c4ec7375c 100644
--- a/include/i18n/en_US/help/tips/settings.pages.yaml
+++ b/include/i18n/en_US/help/tips/settings.pages.yaml
@@ -60,3 +60,18 @@ upload_a_new_logo:
         resize the display of your image. We will not, however, resize the image’s
         data. Therefore, to speed load times, it is recommended that you keep your
         image close to the default image size (817px &times; 170px).
+
+backdrops:
+    title: Backdrops
+    content: >
+        You may customize the <span class="doc-desc-title">Backdrop</span> that will be
+        displayed on the staff login page.
+
+upload_a_new_backdrop:
+    title: Upload a New Backdrop
+    content: >
+        Choose an image in the .gif, .jpg or .png formats. We will proportionally
+        resize the display of your image. We will not, however, resize the image’s
+        data. Therefore, to speed load times, it is recommended that you keep your
+        image relatively small (under a megabyte). Note also that the PHP
+        max upload settings apply.
diff --git a/include/i18n/en_US/help/tips/settings.system.yaml b/include/i18n/en_US/help/tips/settings.system.yaml
index fd89cbaea2e01ad4c824537b56e47d4d92caaff9..53ed8c65e029731ba2ec0f9857de8894d479cd53 100644
--- a/include/i18n/en_US/help/tips/settings.system.yaml
+++ b/include/i18n/en_US/help/tips/settings.system.yaml
@@ -66,25 +66,92 @@ purge_logs:
         Determine how long you would like to keep <span
         class="doc-desc-title">System Logs</span> before they are deleted.
 
-default_name_formatting:
-    title: Default Name Formatting
+enable_richtext:
+    title: Enable Rich Text
     content: >
-        Choose a format for names throughout the system. Email templates
-        will use it for names if no other format is specified in the
-        variable.
+        If enabled, this will permit the use of rich text formatting between
+        Clients and Agents.
 
+enable_avatars:
+    title: Enable Avatars on Thread View
+    content: >
+        Enable this to show <span class="doc-desc-title">Avatars</span> on thread correspondence.
+        <br><br>
+        The <span class="doc-desc-title">Avatar Source</span> can be set in Agents' and Users' settings pages.
     links:
-      - title: Supported Email Template Variables
-        href: http://osticket.com/wiki/Email_templates
+      - title: Agents Settings
+        href: /scp/settings.php?t=agents
+
+      - title: Users Settings
+        href: /scp/settings.php?t=users
+
+collision_avoidance:
+    title: Agent Collision Avoidance
+    content: >
+        Enter the maximum length of time an Agent is allowed to hold a lock
+        on a ticket or task without any activity.
+        <br><br>
+        Enter <span class="doc-desc-opt">0</span> to disable the lockout feature.
 
 # Date and time options
 date_time_options:
     title: Date &amp; Time Options
     content: >
-        The following settings define the Date &amp; Time settings for
-        Clients. You can change how these appear by following the PHP date
-        format characters. The dates shown below simply illustrate the
-        result of their corresponding values.
+        The following settings define the default settings for Date &amp;
+        Time settings for the help desk. You can choose to use the locale
+        defaults for the selected locale or use customize the formats to
+        meet your unique requirements. Refer to the ICU format strings as a
+        reference for customization.  The dates shown below simply
+        illustrate the result of their corresponding values.
+    links:
+      - title: See the ICU Date Formatting Table
+        href: http://userguide.icu-project.org/formatparse/datetime
+
+languages:
+    title: System Languages
+    content: >
+        Choose a system primary language and optionally secondary languages
+        to make your interface feel localized for your agents and end-users.
+
+primary_language:
+    title: System Primary Language
+    content: >
+        Content of this language is displayed to agents and end-users if
+        their respective language preference is not currently available.
+        This includes the content of the interface, as well as, custom
+        content such as thank-you pages and email messages.
+        <br/><br/>
+        This is the language in which the untranslated versions of your
+        content should be written.
+
+secondary_language:
+    title: Secondary Languages
+    content: >
+        Select language preference options for your agents and end-users.
+        The interface will be available in these languages, and custom
+        content, such as thank-you pages and help topic names, will be
+        translatable to these languages.
+
+# Attachments
+attachments:
+    title: Attachment Settings and Storage
+    content: >
+        Configure how attachments are stored.
+
+default_storage_bk:
+    title: File Storage Backend
+    content: >
+        Choose how attachments are stored.
+        <br<br>
+        Additional storage backends can be added by installing storage plugins
+
+max_file_size:
+    title: Maximum File Size
+    content: >
+        Choose a maximum file size for attachments uploaded by agents. This
+        includes canned attachments, knowledge base articles, and
+        attachments to ticket and task replies. The upper limit is
+        controlled by PHP's <code>upload_max_filesize</code> setting.
     links:
-      - title: See the PHP Date Formatting Table
-        href: http://www.php.net/manual/en/function.date.php
+      - title: PHP ini settings
+        href: "http://php.net/manual/en/ini.core.php#ini.upload-max-filesize"
diff --git a/include/i18n/en_US/help/tips/settings.tasks.yaml b/include/i18n/en_US/help/tips/settings.tasks.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..6d1f056187ec87bc7e9ce8d42a05bd702f5bf7c0
--- /dev/null
+++ b/include/i18n/en_US/help/tips/settings.tasks.yaml
@@ -0,0 +1,110 @@
+#
+# This is popup help messages for the Admin Panel -> Settings -> Tasks
+#
+# Fields:
+# title - Shown in bold at the top of the popover window
+# content - The body of the help popover
+# links - List of links shows below the content
+#   title - Link title
+#   href - href of link (links starting with / are translated to the
+#       helpdesk installation path)
+#
+# The key names such as 'helpdesk_name' should not be translated as they
+# must match the HTML #ids put into the page template.
+#
+---
+number_format:
+    title: Task Number Format
+    content: >
+        This setting is used to generate task numbers. Use hash signs
+        (`#`) where digits are to be placed. Any other text in the number
+        format will be preserved.
+        <br/><br/>
+        For example, for six-digit numbers, use <code>######</code>.
+
+sequence_id:
+    title: Task Number Sequence
+    content: >
+        Choose a sequence from which to derive new task numbers. The
+        system has a incrementing sequence and a random sequence by default.
+        You may create as many sequences as you wish.
+
+default_sla:
+    title: Default SLA
+    content: >
+        Choose the default Service Level Agreement to manage how long a task
+        can remain Open before it is rendered Overdue.
+    links:
+      - title: Create more SLA Plans
+        href: /scp/slas.php
+
+default_priority:
+    title: Default Priority
+    content: >
+        Choose a default <span class="doc-desc-title">priority</span> for
+        tasks not assigned a priority automatically.
+
+task_attachment_settings:
+    title: Task Thread Attachments
+    content: >
+        Configure settings for files attached to the <span
+        class="doc-desc-title">description</span> field. These settings
+        are used for all new tasks and new messages regardless of the
+        source channel (web portal, email, api, etc.).
+
+page_title:
+    title: Alerts and Notices
+    content: >
+        Alerts and Notices are automated email notifications sent to Agents
+        when various task events are triggered.
+
+task_alert:
+    title: New Task Alert
+    content: >
+        <p>
+        Alert sent out to Agents when a new task is created.
+        </p><p class="info-banner">
+        <i class="icon-info-sign"></i>
+        This alert is not sent out if the task is auto-assigned.
+        </p>
+    links:
+      - title: Default New Task Alert Template
+        href: /scp/templates.php?default_for=task.alert
+
+activity_alert:
+    title: New Activity Alert
+    content: >
+        Alert sent out to Agents when a new message is appended to an
+        existing task.
+    links:
+      - title: Default New Activity Alert Template
+        href: /scp/templates.php?default_for=task.activity.alert
+
+assignment_alert:
+    title: Task Assignment Alert
+    content: >
+        Alert sent out to Agents on task assignment.
+    links:
+      - title: Default Task Assignment Alert Template
+        href: /scp/templates.php?default_for=task.assignment.alert
+
+transfer_alert:
+    title: Task Transfer Alert
+    content: >
+        Alert sent out to Agents on task transfer between Departments.
+    links:
+      - title: Default Task Transfer Alert Template
+        href: /scp/templates.php?default_for=task.transfer.alert
+
+overdue_alert:
+    title: Overdue Task Alert
+    content: >
+        Alert sent out to Agents when a task becomes overdue based on SLA
+        or Due Date.
+    links:
+      - title: Default Stale Task Alert Template
+        href: /scp/templates.php?default_for=task.overdue.alert
+
+      - title: Manage SLAs
+        href: /scp/slas.php
+
diff --git a/include/i18n/en_US/help/tips/settings.ticket.yaml b/include/i18n/en_US/help/tips/settings.ticket.yaml
index 3c7b872046a878bf15c964f6d65882d43deea241..b13bd17c4f3bc9b2739ddb52f02c69c2d3ca497a 100644
--- a/include/i18n/en_US/help/tips/settings.ticket.yaml
+++ b/include/i18n/en_US/help/tips/settings.ticket.yaml
@@ -68,14 +68,6 @@ maximum_open_tickets:
         <br><br>
         Enter <span class="doc-desc-opt">0 </span> if you prefer to disable this limitation.
 
-agent_collision_avoidance:
-    title: Agent Collision Avoidance
-    content: >
-        Enter the maximum length of time an Agent is allowed to hold a lock
-        on a ticket without any activity.
-        <br><br>
-        Enter <span class="doc-desc-opt">0</span> to disable the lockout feature.
-
 email_ticket_priority:
     title: Email Ticket Priority
     content: >
@@ -116,18 +108,6 @@ answered_tickets:
         will be included in the <span class="doc-desc-title">Open Tickets
         Queue</span>.
 
-staff_identity_masking:
-    title: Staff Identity Masking
-    content: >
-        If enabled, this will hide the Agent’s name from the Client during any
-        communication.
-
-enable_html_ticket_thread:
-    title: Enable HTML Ticket Thread
-    content: >
-        If enabled, this will permit the use of rich text formatting between
-        Clients and Agents.
-
 ticket_attachment_settings:
     title: Ticket Thread Attachments
     content: >
@@ -136,15 +116,3 @@ ticket_attachment_settings:
         are used for all new tickets and new messages regardless of the
         source channel (web portal, email, api, etc.).
 
-max_file_size:
-    title: Maximum File Size
-    content: >
-        Choose a maximum file size for attachments uploaded by agents. This
-        includes canned attachments, knowledge base articles, and
-        attachments to ticket replies.
-
-ticket_response_files:
-    title: Ticket Response Files
-    content: >
-        If enabled, any attachments an Agent may attach to a ticket response will
-        be also included in the email to the User.
diff --git a/include/i18n/en_US/help/tips/settings.access.yaml b/include/i18n/en_US/help/tips/settings.users.yaml
similarity index 62%
rename from include/i18n/en_US/help/tips/settings.access.yaml
rename to include/i18n/en_US/help/tips/settings.users.yaml
index 1d88a13bf894cd0878b7a7b2ad90d23b8aa9dd01..b1b48a1c9986aa5377d3ee2d5c8a9dae7b3e6395 100644
--- a/include/i18n/en_US/help/tips/settings.access.yaml
+++ b/include/i18n/en_US/help/tips/settings.users.yaml
@@ -1,5 +1,5 @@
 #
-# This is popup help messages for the Admin Panel -> Settings -> System page
+# This is popup help messages for the Admin Panel -> Settings -> Users
 #
 # Fields:
 # title - Shown in bold at the top of the popover window
@@ -13,46 +13,14 @@
 # must match the HTML #ids put into the page template.
 #
 ---
-# Authentication settings
-password_reset:
-    title: Password Expiration Policy
-    content: >
-        Sets how often (in months) staff members will be required to change
-        their password. If disabled (set to "No expiration"), passwords will
-        not expire.
-
-password_expiration_policy:
-    title: Password Expiration Policy
-    content: >
-        Choose how often Agents will be required to change their password. If
-        disabled (i.e., <span class="doc-desc-opt">No Expiration</span>), passwords
-        will not expire.
-
-allow_password_resets:
-    title: Allow Password Resets
+# General Settings
+client_name_format:
+    title: User Name Formatting
     content: >
-        Enable this feature if you would like to display the
-        <span class="doc-desc-title">Forgot My Password</span> link on the
-        <span class="doc-desc-title">Staff Log In Panel</span>
-        after a failed log in attempt.
-
-reset_token_expiration:
-    title: Password Reset Window
-    content: >
-        Choose the duration (in minutes) for which the <span class="doc-desc-title">
-        Password Reset Tokens</span> will be valid. When an Agent requests a <span
-        class="doc-desc-title">Password Reset</span>, they are emailed a token that
-        will permit the reset to take place.
-
-staff_session_timeout:
-    title: Staff Session Timeout
-    content: >
-        Choose the maximum idle time (in minutes) before an Agent is required to
-        log in again.
-        <br><br>
-        If you would like to disable <span class="doc-desc-title">Staff
-        Session Timeouts</span>, enter 0.
+        Choose a format for Users names throughout the system. Email templates
+        will use it for names if no other format is specified.
 
+# Authentication settings
 client_session_timeout:
     title: User Session Timeout
     content: >
@@ -62,14 +30,6 @@ client_session_timeout:
         If you would like to disable <span
         class="doc-desc-title">User Session Timeouts,</span> enter 0.
 
-bind_staff_session_to_ip:
-    title: Bind Staff Session to IP
-    content: >
-        Enable this if you want Agent to be remembered by their current IP
-        upon Log In.
-        <br><br>
-        This setting is not recommened for users assigned IP addreses dynamically.
-
 registration_method:
     title: Registration Options
     content: >
@@ -111,3 +71,8 @@ client_verify_email:
         <br><br>
         Disabling email verification might allow third-parties (e.g. ticket
         collaborators) to impersonate the ticket owner.
+
+allow_auth_tokens:
+    title: Enable Authentication Tokens
+    content: >
+        Enable this option to allow use of authentication tokens to auto-login users on ticket link click.
diff --git a/include/i18n/en_US/help/tips/staff.agent.yaml b/include/i18n/en_US/help/tips/staff.agent.yaml
index fb946234399b4b6a659759436f46920f4fdc867f..424998e9f1c577ada16de989c9ba0bd5f275bbf9 100644
--- a/include/i18n/en_US/help/tips/staff.agent.yaml
+++ b/include/i18n/en_US/help/tips/staff.agent.yaml
@@ -73,7 +73,8 @@ assigned_group:
 primary_department:
     title: Primary Department
     content: >
-        Choose the primary <span class="doc-desc-title">department</span> to which this Agent belongs.
+        Choose the primary <span class="doc-desc-title">department</span> to
+        which this Agent belongs and an effective <span class="doc-desc-title">Role</span>.
 
     links:
       - title: Manage Departments
@@ -84,6 +85,15 @@ primary_role:
     content: >
         Choose the primary <span class="doc-desc-title">role</span> to which this agent belongs.
 
+primary_role_on_assign:
+    title: Use Primary Role For Assignments
+    content: >
+        Enable this to fallback to the <span class="doc-desc-title">primary
+        role</span>  when this agent is assigned tickets and tasks outside
+        of the <span class="doc-desc-title">primary department</span> and
+        <span class="doc-desc-title">extended access</span> departments.
+        Otherwise the agent will have view only access.
+
 daylight_saving:
     title: Daylight Saving
     content: >
diff --git a/include/i18n/en_US/help/tips/staff.department.yaml b/include/i18n/en_US/help/tips/staff.department.yaml
index 28637faa31a1c7738208b306bf2a9f2c95ae1a38..015c9b1224c3a6a8647e633e3dcb14dd3e2b6f82 100644
--- a/include/i18n/en_US/help/tips/staff.department.yaml
+++ b/include/i18n/en_US/help/tips/staff.department.yaml
@@ -77,6 +77,14 @@ sandboxing:
         if <span class="doc-desc-title">Alerts &amp; Notices
         Recipients</span> includes groups members.
 
+disable_auto_claim:
+    title: Disable Auto Claim
+    content: >
+        Check this to <strong>disable</strong> auto-claim on response/reply for
+        this department.
+        <br><br>
+        Agents can still manually claim unassigned tickets
+
 auto_response_settings:
     title: Autoresponder Settings
     content: >
diff --git a/include/i18n/en_US/help/tips/tasks.queue.yaml b/include/i18n/en_US/help/tips/tasks.queue.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..393447f0b16260e6e1f2166d0453c94091794587
--- /dev/null
+++ b/include/i18n/en_US/help/tips/tasks.queue.yaml
@@ -0,0 +1,28 @@
+#
+# This is popup help messages for the Agents Panel -> Tasks
+#
+# Fields:
+# title - Shown in bold at the top of the popover window
+# content - The body of the help popover
+# links - List of links shows below the content
+#   title - Link title
+#   href - href of link (links starting with / are translated to the
+#       helpdesk installation path)
+#
+# The key names such as 'helpdesk_name' should not be translated as they
+# must match the HTML #ids put into the page template.
+#
+---
+advanced:
+    title: Advanced
+    content: >
+        Narrow down your search parameters. Once you have selected your advanced
+        search criteria and run the search, you can <span class="doc-desc-title">Export
+        </span> the data at the bottom of the page.
+
+export:
+    title: Export
+    content: >
+        Export your data currently in view in a CSV file.
+        CSV files may be opened with any spreadsheet software
+        (i.e., Microsoft Excel, Apple Pages, OpenOffice, etc.).
diff --git a/include/i18n/en_US/help_topic.yaml b/include/i18n/en_US/help_topic.yaml
index d54cdf3d702e391fea43737d8d455595a00eedf1..76faba0c0e3638a0dc4b4b6b8b8f6deb9262dc70 100644
--- a/include/i18n/en_US/help_topic.yaml
+++ b/include/i18n/en_US/help_topic.yaml
@@ -21,16 +21,16 @@
 - topic_id: 1
   isactive: 1
   ispublic: 1
-  dept_id: 1
   priority_id: 2
+  forms: [2]
   topic: General Inquiry
   notes: |
     Questions about products or services
 
 - isactive: 1
   ispublic: 1
-  dept_id: 1
   priority_id: 1
+  forms: [2]
   topic: Feedback
   notes: |
     Tickets that primarily concern the sales and billing departments
@@ -38,8 +38,9 @@
 - topic_id: 10
   isactive: 1
   ispublic: 1
-  dept_id: 1
+  dept_id: 3
   priority_id: 2
+  forms: [2]
   topic: Report a Problem
   notes: |
     Product, service, or equipment related issues
@@ -47,9 +48,9 @@
 - topic_pid: 10
   isactive: 1
   ispublic: 1
-  dept_id: 1
   sla_id: 1
   priority_id: 3
+  forms: [2]
   topic: Access Issue
   notes: |
     Report an inability access a physical or virtual asset
diff --git a/include/i18n/en_US/list.yaml b/include/i18n/en_US/list.yaml
index 613ca4a2f83db4a46048c70069a9569c95791a86..d4e4f6a6c14abce41c8217f42a37de250096c69c 100644
--- a/include/i18n/en_US/list.yaml
+++ b/include/i18n/en_US/list.yaml
@@ -32,22 +32,20 @@
   properties:
     title: Ticket Status Properties
     instructions: Properties that can be set on a ticket status.
-    deletable: false
+    flags: 0
     fields:
       - type: state # notrans
         name: state # notrans
         label: State
-        required: true
         sort: 1
-        edit_mask: 63
+        flags: 0x770F1
         configuration:
             prompt: State of a ticket
       - type: memo # notrans
         name: description # notrans
         label: Description
-        required: false
         sort: 3
-        edit_mask: 15
+        flags: 0x73021
         configuration:
             rows: 2
             cols: 40
diff --git a/include/i18n/en_US/role.yaml b/include/i18n/en_US/role.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..ca76b2650513d4ffcf0d5bb48fa57cb07b203757
--- /dev/null
+++ b/include/i18n/en_US/role.yaml
@@ -0,0 +1,78 @@
+#
+# Default roles defined for the system
+#
+# Fields:
+# id - Primary id for the role
+# flags - (bit mask) role flags
+# name - (string) descriptive name for the role
+# notes - (string) internal notes
+# permissions: (list<keys>)
+#
+# NOTE: ------------------------------------
+# ---
+- id: 1
+  flags: 1
+  name: All Access
+  notes: |
+    Role with unlimited access
+
+  permissions: [
+    ticket.create,
+    ticket.edit,
+    ticket.assign,
+    ticket.transfer,
+    ticket.reply,
+    ticket.close,
+    ticket.delete,
+    task.create,
+    task.edit,
+    task.assign,
+    task.transfer,
+    task.reply,
+    task.close,
+    task.delete,
+    canned.manage,
+    thread.edit]
+
+- id: 2
+  flags: 1
+  name: Expanded Access
+  notes: |
+    Role with expanded access
+
+  permissions: [
+    ticket.create,
+    ticket.edit,
+    ticket.assign,
+    ticket.transfer,
+    ticket.reply,
+    ticket.close,
+    task.create,
+    task.edit,
+    task.assign,
+    task.transfer,
+    task.reply,
+    task.close,
+    canned.manage]
+
+- id: 3
+  flags: 1
+  name: Limited Access
+  notes: |
+    Role with limited access
+
+  permissions: [
+    ticket.create,
+    ticket.assign,
+    ticket.transfer,
+    ticket.reply
+    task.create,
+    task.assign,
+    task.transfer,
+    task.reply]
+
+- id: 4
+  flags: 1
+  name: View only
+  notes: Simple role with no permissions
+  permissions: []
diff --git a/include/i18n/en_US/sequence.yaml b/include/i18n/en_US/sequence.yaml
index bb502c3526c287280a2fe664419989e26c9307ef..f67594aa91269309ddad0a470c3a3c07ac85e151 100644
--- a/include/i18n/en_US/sequence.yaml
+++ b/include/i18n/en_US/sequence.yaml
@@ -21,3 +21,10 @@
   padding: '0'
   increment: 1
   flags: 1
+
+- id: 2
+  name: "Tasks Sequence"
+  next: 1
+  padding: '0'
+  increment: 1
+  flags: 1
diff --git a/include/i18n/en_US/sla.yaml b/include/i18n/en_US/sla.yaml
index 3b91325475edfed4025cfc9e95bdcd9e78fe12c4..4bbcfae98c8921eb014ae3d8e3d58e465efe2617 100644
--- a/include/i18n/en_US/sla.yaml
+++ b/include/i18n/en_US/sla.yaml
@@ -3,20 +3,21 @@
 #
 # Fields:
 # id - (int:optional) id number in the database
-# isactive - (bool:0|1) true of false if the SLA should initially be active
-# enable_priority_escalation - (bool:0|1) true or false if the SLA should
+# flags - (int:bitmask)
+#   isactive - (flag:1) true of false if the SLA should initially be active
+#   enable_priority_escalation - (flag:2) true or false if the SLA should
 #       cause the ticket priority to be escalated when it is marked overdue
-# disable_overdue_alerts - (bool:0|1) - true or false if the overdue alert
+#   disable_overdue_alerts - (flag:4) - true or false if the overdue alert
 #       emails should _not_ go out for tickets assigned to this SLA
+#   transient - (flag:8) - true if the SLA should change when changing
+#       department or help topic.
 # grace_period - (int) number or hours after the ticket is opened before it
 #       is marked overdue
 # name - (string) descriptive name of the SLA
 # notes - (string) administrative notes (viewable internally only)
 ---
 - id: 1
-  isactive: 1
-  enable_priority_escalation: 1
-  disable_overdue_alert: 0
+  flags: 3
   grace_period: 48
   name: Default SLA
   notes: |
diff --git a/include/i18n/en_US/team.yaml b/include/i18n/en_US/team.yaml
index 16ae1244976ab7ed90a88e795a7a3395e986022b..136de6505dc57c689d06142771e897069423f129 100644
--- a/include/i18n/en_US/team.yaml
+++ b/include/i18n/en_US/team.yaml
@@ -2,15 +2,15 @@
 # Initial teams defined for the system.
 #
 # Fields:
-# isenabled - (bool:1|0) true or false if the team should be initially
+# flags - (int)
+#   - isenabled - (0x01) true or false if the team should be initially
 #       enabled
-# noalerts - (bool:1|0)
+#   - noalerts - (0x02)
 # name - Descriptive name for the team
 # notes - Administrative notes (viewable internal only)
 #
 ---
-- isenabled: 1
-  noalerts: 0
+- flags: 0x01
   name: Level I Support
   notes: |
     Tier 1 support, responsible for the initial iteraction with customers
diff --git a/include/i18n/en_US/templates/email/task.activity.alert.yaml b/include/i18n/en_US/templates/email/task.activity.alert.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..5c94582edca104cdacf12fb1b2333933d7b09206
--- /dev/null
+++ b/include/i18n/en_US/templates/email/task.activity.alert.yaml
@@ -0,0 +1,32 @@
+#
+# Email template: task.activity.alert.yaml
+#
+# Sent to agents when a new note/message is posted to a task.
+# This can occur if a collaborator or an agent responds to an email from the
+# system or visits the web portal and posts a new message there.
+#
+#
+---
+notes: |
+    Sent to agents when a new message/note is posted to a task.  This can
+    occur if a collaborator or an agent responds to an email from the system
+    or visits the web portal and posts a new message there.
+
+subject: |
+    Task Activity [#%{task.number}] - %{activity.title}
+body: |
+    <h3><strong>Hi %{recipient.name},</strong></h3>
+    Task <a href="%{task.staff_link}">#%{task.number}</a> updated: %{activity.description}
+    <br>
+    <br>
+    %{message}
+    <br>
+    <br>
+    <hr>
+    <div>To view or respond to the task, please <a
+    href="%{task.staff_link}"><span style="color: rgb(84, 141, 212);"
+    >login</span></a> to the support system</div>
+    <em style="color: rgb(127,127,127); font-size: small; ">Your friendly
+    Customer Support System</em><br>
+    <img src="cid:b56944cb4722cc5cda9d1e23a3ea7fbc"
+    alt="Powered by osTicket" width="126" height="19" style="width: 126px;">
diff --git a/include/i18n/en_US/templates/email/task.activity.notice.yaml b/include/i18n/en_US/templates/email/task.activity.notice.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..750e4a09cd5c892ad1ec5e756bd1fbf368651e7b
--- /dev/null
+++ b/include/i18n/en_US/templates/email/task.activity.notice.yaml
@@ -0,0 +1,25 @@
+#
+# Email template: task.activity.notice.yaml
+#
+# Notice sent to collaborators on task activity e.g reply or message
+#
+---
+notes: |
+    Notice sent to collaborators on task activity e.g reply or message.
+
+subject: |
+    Re: %{task.title} [#%{task.number}]
+body: |
+    <h3><strong>Dear %{recipient.name.first},</strong></h3>
+    <div>
+        <em>%{poster.name}</em> just logged a message to a task in which you participate.
+    </div>
+    <br>
+    %{message}
+    <br>
+    <br>
+    <hr>
+    <div style="color: rgb(127, 127, 127); font-size: small; text-align: center;">
+    <em>You're getting this email because you are a collaborator on task
+    #%{task.number}. To participate, simply reply to this email.</em>
+    </div>
diff --git a/include/i18n/en_US/templates/email/task.alert.yaml b/include/i18n/en_US/templates/email/task.alert.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..ee6760803c6bb1d052603f1feb9ce3675399ffdf
--- /dev/null
+++ b/include/i18n/en_US/templates/email/task.alert.yaml
@@ -0,0 +1,41 @@
+#
+# Email template: task.alert.yaml
+#
+# Sent to an agent when a new task is created in the system.
+#
+#
+---
+notes: |
+    Sent to an agent when a new task is created in the system.
+
+subject: |
+    New Task Alert
+body: |
+    <h2>Hi %{recipient.name},</h2>
+    New task <a href="%{task.staff_link}">#%{task.number}</a> created
+    <br>
+    <br>
+    <table>
+    <tbody>
+    <tr>
+        <td>
+            <strong>Department</strong>:
+        </td>
+        <td>
+            %{task.dept.name}
+        </td>
+    </tr>
+    </tbody>
+    </table>
+    <br>
+    %{task.description}
+    <br>
+    <br>
+    <hr>
+    <div>To view or respond to the ticket, please <a
+    href="%{task.staff_link}">login</a> to the support system</div>
+    <em style="font-size: small">Your friendly Customer Support System</em>
+    <br>
+    <a href="http://osticket.com/"><img width="126" height="19"
+        style="width: 126px; " alt="Powered By osTicket"
+        src="cid:b56944cb4722cc5cda9d1e23a3ea7fbc"/></a>
diff --git a/include/i18n/en_US/templates/email/task.assignment.alert.yaml b/include/i18n/en_US/templates/email/task.assignment.alert.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..2a8684c51d65ceac087757a2778285f1d215ab94
--- /dev/null
+++ b/include/i18n/en_US/templates/email/task.assignment.alert.yaml
@@ -0,0 +1,32 @@
+#
+# Email template: task.assignment.alert.yaml
+#
+# Sent to agents when a task is assigned to them or the team to which
+# they belong.
+# Use %{assigner} to distinguish who made the assignment.
+#
+---
+notes: |
+    Sent to agents when a ticket is assigned to them or the team to which
+    they belong. Use %{assigner} to distinguish who made the assignment.
+
+subject: |
+    Task Assigned to you
+body: |
+    <h3><strong>Hi %{assignee.name.first},</strong></h3>
+    Task <a href="%{task.staff_link}">#%{task.number}</a> has been
+    assigned to you by %{assigner.name.short}
+    <br>
+    <br>
+    %{comments}
+    <br>
+    <br>
+    <hr>
+    <div>To view/respond to the task, please <a
+    href="%{task.staff_link}"><span style="color: rgb(84, 141, 212);"
+    >login</span></a> to the support system</div>
+    <em style="font-size: small; ">Your friendly Customer Support
+    System</em>
+    <br>
+    <img src="cid:b56944cb4722cc5cda9d1e23a3ea7fbc"
+    alt="Powered by osTicket" width="126" height="19" style="width: 126px;">
diff --git a/include/i18n/en_US/templates/email/task.overdue.alert.yaml b/include/i18n/en_US/templates/email/task.overdue.alert.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..05a8573d3a52b88f9b8d153daba9c4ef313e779f
--- /dev/null
+++ b/include/i18n/en_US/templates/email/task.overdue.alert.yaml
@@ -0,0 +1,36 @@
+#
+# Email template: task.overdue.alert.yaml
+#
+# Sent to agents when a tasks transitions to overdue in the system.
+# Overdue tasks occur based on set due-date.
+#
+---
+notes: |
+    Sent to agents when a task transitions to overdue in the system.
+    Overdue tasks occur based on the set due-date.
+
+subject: |
+    Stale Task Alert
+body: |
+    <h3><strong>Hi %{recipient.name}</strong>,</h3>
+    A task, <a href="%{task.staff_link}">#%{task.number}</a> is
+    seriously overdue.
+    <br>
+    <br>
+    We should all work hard to guarantee that all tasks are being
+    addressed in a timely manner.
+    <br>
+    <br>
+    Signed,<br>
+    %{task.dept.manager.name}
+    <hr>
+    <div>To view or respond to the task, please <a
+    href="%{task.staff_link}"><span style="color: rgb(84, 141, 212);"
+    >login</span></a> to the support system. You're receiving this
+    notice because the task is assigned directly to you or to a team or
+    department of which you're a member.</div>
+    <em style="font-size: small">Your friendly <span style="font-size: smaller"
+    >(although with limited patience)</span> Customer Support
+    System</em><br>
+    <img src="cid:b56944cb4722cc5cda9d1e23a3ea7fbc" height="19"
+        alt="Powered by osTicket" width="126" style="width: 126px;">
diff --git a/include/i18n/en_US/templates/email/task.transfer.alert.yaml b/include/i18n/en_US/templates/email/task.transfer.alert.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..6e2d26a26865a03c58495045965d87d24e215137
--- /dev/null
+++ b/include/i18n/en_US/templates/email/task.transfer.alert.yaml
@@ -0,0 +1,31 @@
+#
+# Email template: task.transfer.alert.yaml
+#
+# Sent to agents when a task is transfered to their department.
+#
+---
+notes: |
+    Sent to agents when a task is transfered to a department to which
+    they are a member.
+subject: |
+    Task #%{task.number} transfer - %{task.dept.name}
+body: |
+    <h3>Hi %{recipient.name},</h3>
+    Task <a href="%{task.staff_link}">#%{task.number}</a> has been
+    transferred to the %{task.dept.name} department by
+    <strong>%{staff.name.short}</strong>
+    <br>
+    <br>
+    <blockquote>
+        %{comments}
+    </blockquote>
+    <hr>
+    <div>To view or respond to the task, please <a
+    href="%{task.staff_link}">login</a> to the support system.
+    </div>
+    <em style="font-size: small; ">Your friendly Customer Support
+    System</em>
+    <br>
+    <a href="http://osticket.com/"><img width="126" height="19"
+        alt="Powered By osTicket" style="width: 126px;"
+        src="cid:b56944cb4722cc5cda9d1e23a3ea7fbc"/></a>
diff --git a/include/mpdf/classes/bmp.php b/include/mpdf/classes/bmp.php
index acf7f808549104a003bcf9b180aee41b5b6bcfd8..04b64326d4b9826b6270d8082903be49526b0016 100755
--- a/include/mpdf/classes/bmp.php
+++ b/include/mpdf/classes/bmp.php
@@ -4,7 +4,7 @@ class bmp {
 
 var $mpdf = null;
 
-function bmp(&$mpdf) {
+function __construct(&$mpdf) {
 	$this->mpdf = $mpdf;
 }
 
@@ -245,4 +245,4 @@ function rle4_decode ($str, $width){
 
 }
 
-?>
\ No newline at end of file
+?>
diff --git a/include/mpdf/classes/cssmgr.php b/include/mpdf/classes/cssmgr.php
index aef74542d44b0ab9019f40841162b59464151750..3b030f8c4c0866b54092811bb6091385d0a55b6b 100755
--- a/include/mpdf/classes/cssmgr.php
+++ b/include/mpdf/classes/cssmgr.php
@@ -12,7 +12,7 @@ var $tbCSSlvl;
 var $listCSSlvl;
 
 
-function cssmgr(&$mpdf) {
+function __construct(&$mpdf) {
 	$this->mpdf = $mpdf;
 	$this->tablecascadeCSS = array();
 	$this->listcascadeCSS = array();
@@ -1574,4 +1574,4 @@ function PreviewBlockCSS($tag,$attr) {
 
 }	// end of class
 
-?>
\ No newline at end of file
+?>
diff --git a/include/mpdf/classes/gif.php b/include/mpdf/classes/gif.php
index 582de0d6f49ecf5fa8743b2685ea018c934decb4..2087a97d34ae660acb02b8c0bb8cf69e39906a85 100755
--- a/include/mpdf/classes/gif.php
+++ b/include/mpdf/classes/gif.php
@@ -26,7 +26,7 @@ class CGIFLZW
 	///////////////////////////////////////////////////////////////////////////
 
 	// CONSTRUCTOR
-	function CGIFLZW()
+	function __construct()
 	{
 		$this->MAX_LZW_BITS = 12;
 		unSet($this->Next);
@@ -240,7 +240,7 @@ class CGIFCOLORTABLE
 	///////////////////////////////////////////////////////////////////////////
 
 	// CONSTRUCTOR
-	function CGIFCOLORTABLE()
+	function __construct()
 	{
 		unSet($this->m_nColors);
 		unSet($this->m_arColors);
@@ -327,7 +327,7 @@ class CGIFFILEHEADER
 	///////////////////////////////////////////////////////////////////////////
 
 	// CONSTRUCTOR
-	function CGIFFILEHEADER()
+	function __construct()
 	{
 		unSet($this->m_lpVer);
 		unSet($this->m_nWidth);
@@ -402,20 +402,6 @@ class CGIFIMAGEHEADER
 
 	///////////////////////////////////////////////////////////////////////////
 
-	// CONSTRUCTOR
-	function CGIFIMAGEHEADER()
-	{
-		unSet($this->m_nLeft);
-		unSet($this->m_nTop);
-		unSet($this->m_nWidth);
-		unSet($this->m_nHeight);
-		unSet($this->m_bLocalClr);
-		unSet($this->m_bInterlace);
-		unSet($this->m_bSorted);
-		unSet($this->m_nTableSize);
-		unSet($this->m_colorTable);
-	}
-
 	///////////////////////////////////////////////////////////////////////////
 
 	function load($lpData, &$hdrLen)
@@ -473,15 +459,8 @@ class CGIFIMAGE
 
 	///////////////////////////////////////////////////////////////////////////
 
-	function CGIFIMAGE()
+	function __construct()
 	{
-		unSet($this->m_disp);
-		unSet($this->m_bUser);
-		unSet($this->m_bTrans);
-		unSet($this->m_nDelay);
-		unSet($this->m_nTrans);
-		unSet($this->m_lpComm);
-		unSet($this->m_data);
 		$this->m_gih = new CGIFIMAGEHEADER();
 		$this->m_lzw = new CGIFLZW();
 	}
@@ -647,7 +626,7 @@ class CGIF
 	///////////////////////////////////////////////////////////////////////////
 
 	// CONSTRUCTOR
-	function CGIF()
+	function __construct()
 	{
 		$this->m_gfh     = new CGIFFILEHEADER();
 		$this->m_img     = new CGIFIMAGE();
@@ -697,4 +676,4 @@ class CGIF
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-?>
\ No newline at end of file
+?>
diff --git a/include/mpdf/classes/grad.php b/include/mpdf/classes/grad.php
index b5db60204c705fe2650fd6b3ec0844393a0e468b..c43c3e06fab6da011cbc1139e5449a0175e9b06f 100755
--- a/include/mpdf/classes/grad.php
+++ b/include/mpdf/classes/grad.php
@@ -4,7 +4,7 @@ class grad {
 
 var $mpdf = null;
 
-function grad(&$mpdf) {
+function __construct(&$mpdf) {
 	$this->mpdf = $mpdf;
 }
 
@@ -720,4 +720,4 @@ function parseBackgroundGradient($bg) {
 
 }
 
-?>
\ No newline at end of file
+?>
diff --git a/include/mpdf/classes/indic.php b/include/mpdf/classes/indic.php
index e747dedef69317c2587e09e4813cf5258388825b..c99e6eb4515b3a3a22f01c73204505d56a3cdcab 100755
--- a/include/mpdf/classes/indic.php
+++ b/include/mpdf/classes/indic.php
@@ -2,11 +2,6 @@
 
 class indic {
 
-function indic() {
-
-}
-
-
 function substituteIndic($earr, $lang, $font) {
 	global $voltdata;
 
@@ -430,4 +425,4 @@ function substituteIndic($earr, $lang, $font) {
 
 }
 
-?>
\ No newline at end of file
+?>
diff --git a/include/mpdf/classes/ttfontsuni.php b/include/mpdf/classes/ttfontsuni.php
index f639b005b38e802e6c6ff1531bf3d12e84af2c0f..70be745a641097e7f28ef3d9469a5a010cb54222 100755
--- a/include/mpdf/classes/ttfontsuni.php
+++ b/include/mpdf/classes/ttfontsuni.php
@@ -79,7 +79,7 @@ var $TTCFonts;
 var $maxUniChar;
 var $kerninfo;
 
-	function TTFontFile() {
+	function __construct() {
 		$this->maxStrLenRead = 200000;	// Maximum size of glyf table to read in as string (otherwise reads each glyph from file)
 	}
 
@@ -2062,4 +2062,4 @@ PCLT - not recommended
 }
 
 
-?>
\ No newline at end of file
+?>
diff --git a/include/mpdf/classes/wmf.php b/include/mpdf/classes/wmf.php
index e5f5e3c1b985f047e4b0eed0bacdfd7c94735af3..5182d33979ad020ddfac0db9ca3db915d46359f4 100755
--- a/include/mpdf/classes/wmf.php
+++ b/include/mpdf/classes/wmf.php
@@ -5,7 +5,7 @@ class wmf {
 var $mpdf = null;
 var $gdiObjectArray;
 
-function wmf(&$mpdf) {
+function __construct(&$mpdf) {
 	$this->mpdf = $mpdf;
 }
 
@@ -233,4 +233,4 @@ function _DeleteGDIObject($idx) {
 
 }
 
-?>
\ No newline at end of file
+?>
diff --git a/include/mpdf/mpdf.php b/include/mpdf/mpdf.php
index 9c7fe98ad44cd7a9848a0890d6583bfeefd52710..fa2ff8f152f82ea67265520bf797387fb75dd2f3 100755
--- a/include/mpdf/mpdf.php
+++ b/include/mpdf/mpdf.php
@@ -827,7 +827,7 @@ var $innerblocktags;
 // **********************************
 // **********************************
 
-function mPDF($mode='',$format='A4',$default_font_size=0,$default_font='',$mgl=15,$mgr=15,$mgt=16,$mgb=16,$mgh=9,$mgf=9, $orientation='P') {
+function __construct($mode='',$format='A4',$default_font_size=0,$default_font='',$mgl=15,$mgr=15,$mgt=16,$mgb=16,$mgh=9,$mgf=9, $orientation='P') {
 
 /*-- BACKGROUNDS --*/
 		if (!class_exists('grad', false)) { include(_MPDF_PATH.'classes/grad.php'); }
@@ -1431,7 +1431,6 @@ function _getPageFormat($format) {
 			case 'A': {$format=array(314.65,504.57 );	 break;}		//	'A' format paperback size 111x178mm
 			case 'DEMY': {$format=array(382.68,612.28 );  break;}		//	'Demy' format paperback size 135x216mm
 			case 'ROYAL': {$format=array(433.70,663.30 );  break;}	//	'Royal' format paperback size 153x234mm
-			default: $format = false;
 		}
 	return $format;
 }
diff --git a/include/mysqli.php b/include/mysqli.php
index ed70cd82ef3c5fc8ae0f209759ed9c969f2ceb61..2a79feaa745bd966f596e1830c0151fa2aa7dbb4 100644
--- a/include/mysqli.php
+++ b/include/mysqli.php
@@ -90,6 +90,12 @@ function db_autocommit($enable=true) {
     return $__db->autocommit($enable);
 }
 
+function db_rollback() {
+    global $__db;
+
+    return $__db->rollback();
+}
+
 function db_close() {
     global $__db;
     return @$__db->close();
@@ -108,7 +114,7 @@ function db_version() {
 }
 
 function db_timezone() {
-    return db_get_variable('time_zone');
+    return db_get_variable('system_time_zone', 'global');
 }
 
 function db_get_variable($variable, $type='session') {
diff --git a/include/pear/Crypt/AES.php b/include/pear/Crypt/AES.php
index 84de2d9acf8223294c6ce0f1e5fea7e4748ab900..771d87aee685605af0ce4d65b2552de8fa45a232 100644
--- a/include/pear/Crypt/AES.php
+++ b/include/pear/Crypt/AES.php
@@ -175,7 +175,7 @@ class Crypt_AES extends Crypt_Rijndael {
      * @return Crypt_AES
      * @access public
      */
-    function Crypt_AES($mode = CRYPT_AES_MODE_CBC)
+    function __construct($mode = CRYPT_AES_MODE_CBC)
     {
         if ( !defined('CRYPT_AES_MODE') ) {
             switch (true) {
@@ -237,7 +237,7 @@ class Crypt_AES extends Crypt_Rijndael {
         }
 
         if (CRYPT_AES_MODE == CRYPT_AES_MODE_INTERNAL) {
-            parent::Crypt_Rijndael($this->mode);
+            parent::__construct($this->mode);
         }
 
     }
diff --git a/include/pear/Crypt/Hash.php b/include/pear/Crypt/Hash.php
index 3b506164ea93102292b2073119945ce893911e16..77bf2b53441bfdbc46a772b0c7ae9485b0687a68 100644
--- a/include/pear/Crypt/Hash.php
+++ b/include/pear/Crypt/Hash.php
@@ -143,7 +143,7 @@ class Crypt_Hash {
      * @return Crypt_Hash
      * @access public
      */
-    function Crypt_Hash($hash = 'sha1')
+    function __construct($hash = 'sha1')
     {
         if ( !defined('CRYPT_HASH_MODE') ) {
             switch (true) {
diff --git a/include/pear/Crypt/Rijndael.php b/include/pear/Crypt/Rijndael.php
index a8510007afe1abac110b552c0d4a5fa3ce02c892..e35d96383ad1f0168ae67f86e4b860b5379c6dfe 100644
--- a/include/pear/Crypt/Rijndael.php
+++ b/include/pear/Crypt/Rijndael.php
@@ -448,7 +448,7 @@ class Crypt_Rijndael {
      * @return Crypt_Rijndael
      * @access public
      */
-    function Crypt_Rijndael($mode = CRYPT_RIJNDAEL_MODE_CBC)
+    function __construct($mode = CRYPT_RIJNDAEL_MODE_CBC)
     {
         switch ($mode) {
             case CRYPT_RIJNDAEL_MODE_ECB:
diff --git a/include/pear/Mail.php b/include/pear/Mail.php
index 5d4d3b09dd61c14a501cc52cb8d2f9f1499bb98a..751616debf634aa180b502c5f16a0b0fd5d63418 100644
--- a/include/pear/Mail.php
+++ b/include/pear/Mail.php
@@ -76,14 +76,13 @@ class Mail
         $driver = strtolower($driver);
         $class = 'Mail_' . $driver;
         if (!class_exists($class))
-            include_once PEAR_DIR.'Mail/' . $driver . '.php';
+            include_once 'Mail/' . $driver . '.php';
 
-        if (class_exists($class)) {
-            $mailer = new $class($params);
-            return $mailer;
-        }
+        if (!class_exists($class))
+            return PEAR::raiseError('Unable to find class for driver ' .  $driver);
 
-        return PEAR::raiseError('Unable to find class for driver ' . $driver);
+        $mailer = new $class($params);
+        return $mailer;
     }
 
     /**
diff --git a/include/pear/Mail/RFC822.php b/include/pear/Mail/RFC822.php
index abf1000d9f84293d444053121107d4cfaa20f2d8..0ad24d7d80d31d3c1471804b6a39f21b94b937a1 100644
--- a/include/pear/Mail/RFC822.php
+++ b/include/pear/Mail/RFC822.php
@@ -149,7 +149,7 @@ class Mail_RFC822 {
      *
      * @return object Mail_RFC822 A new Mail_RFC822 object.
      */
-    function Mail_RFC822($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null)
+    function __construct($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null)
     {
         if (isset($address))        $this->address        = $address;
         if (isset($default_domain)) $this->default_domain = $default_domain;
diff --git a/include/pear/Mail/mail.php b/include/pear/Mail/mail.php
index a8b4b5dbeef6c0b194fa9a2036af40c2feb5869a..3c79da7f5e86b2b666b4d281e9631ff75f829002 100644
--- a/include/pear/Mail/mail.php
+++ b/include/pear/Mail/mail.php
@@ -64,7 +64,7 @@ class Mail_mail extends Mail {
      *
      * @param array $params Extra arguments for the mail() function.
      */
-    function Mail_mail($params = null)
+    function __construct($params = null)
     {
         // The other mail implementations accept parameters as arrays.
         // In the interest of being consistent, explode an array into
diff --git a/include/pear/Mail/mimeDecode.php b/include/pear/Mail/mimeDecode.php
index 29095c9605e270af4b885ad0181a1dcd1ab6800f..9b1e208be17859b5380910fd0ce9f77aff1d7ada 100644
--- a/include/pear/Mail/mimeDecode.php
+++ b/include/pear/Mail/mimeDecode.php
@@ -149,7 +149,7 @@ class Mail_mimeDecode extends PEAR
      * @param string The input to decode
      * @access public
      */
-    function Mail_mimeDecode(&$input)
+    function __construct(&$input)
     {
         list($header, $body)   = $this->_splitBodyHeader($input);
 
diff --git a/include/pear/Mail/mock.php b/include/pear/Mail/mock.php
index 61570ba408cdcec0b6cd5814f080d4aa78581a45..a1bf9b803dee25fd232bb86fb15638c8fe26ca48 100644
--- a/include/pear/Mail/mock.php
+++ b/include/pear/Mail/mock.php
@@ -84,7 +84,7 @@ class Mail_mock extends Mail {
      * @param array Hash containing any parameters.
      * @access public
      */
-    function Mail_mock($params)
+    function __construct($params)
     {
         if (isset($params['preSendCallback']) &&
             is_callable($params['preSendCallback'])) {
diff --git a/include/pear/Mail/sendmail.php b/include/pear/Mail/sendmail.php
index b056575e99274f886b1473b5ee35f002d2699329..77dbe1fe7fb2b5b60f4745697f1c1bb12b00c3fe 100644
--- a/include/pear/Mail/sendmail.php
+++ b/include/pear/Mail/sendmail.php
@@ -56,7 +56,7 @@ class Mail_sendmail extends Mail {
      *              defaults.
      * @access public
      */
-    function Mail_sendmail($params)
+    function __construct($params)
     {
         if (isset($params['sendmail_path'])) {
             $this->sendmail_path = $params['sendmail_path'];
diff --git a/include/pear/Mail/smtp.php b/include/pear/Mail/smtp.php
index 75171891ea61351ed3ac3530228b549ad4a14a31..877803d959ffce6c3d01161d99049bdf83212f49 100644
--- a/include/pear/Mail/smtp.php
+++ b/include/pear/Mail/smtp.php
@@ -188,7 +188,7 @@ class Mail_smtp extends Mail {
      *              defaults.
      * @access public
      */
-    function Mail_smtp($params)
+    function __construct($params)
     {
         if (isset($params['host'])) $this->host = $params['host'];
         if (isset($params['port'])) $this->port = $params['port'];
@@ -346,7 +346,7 @@ class Mail_smtp extends Mail {
         }
 
         include_once 'Net/SMTP.php';
-        $this->_smtp = &new Net_SMTP($this->host,
+        $this->_smtp = new Net_SMTP($this->host,
                                      $this->port,
                                      $this->localhost);
 
diff --git a/include/pear/Math/BigInteger.php b/include/pear/Math/BigInteger.php
index 37acb1fe300e23864b627197622d0ddf91e20f15..f91c246b1a43c8f75fb43ad9d0f6d0b8fd2ed623 100644
--- a/include/pear/Math/BigInteger.php
+++ b/include/pear/Math/BigInteger.php
@@ -256,7 +256,7 @@ class Math_BigInteger {
      * @return Math_BigInteger
      * @access public
      */
-    function Math_BigInteger($x = 0, $base = 10)
+    function __construct($x = 0, $base = 10)
     {
         if ( !defined('MATH_BIGINTEGER_MODE') ) {
             switch (true) {
diff --git a/include/pear/PEAR.php b/include/pear/PEAR.php
index 2aa85259d62dc69c0cad3f38320bc82fdcf28af9..9708fcd5ef024dccf126a7c1dc1797c9d6832a60 100644
--- a/include/pear/PEAR.php
+++ b/include/pear/PEAR.php
@@ -146,7 +146,7 @@ class PEAR
      * @access public
      * @return void
      */
-    function PEAR($error_class = null)
+    function __construct($error_class = null)
     {
         $classname = strtolower(get_class($this));
         if ($this->_debug) {
@@ -247,7 +247,7 @@ class PEAR
      * @access  public
      * @return  bool    true if parameter is an error
      */
-    function isError($data, $code = null)
+    static function isError($data, $code = null)
     {
         if (!is_a($data, 'PEAR_Error')) {
             return false;
@@ -469,7 +469,7 @@ class PEAR
      * @see PEAR::setErrorHandling
      * @since PHP 4.0.5
      */
-    function &raiseError($message = null,
+    static function &raiseError($message = null,
                          $code = null,
                          $mode = null,
                          $options = null,
@@ -823,7 +823,7 @@ class PEAR_Error
      * @access public
      *
      */
-    function PEAR_Error($message = 'unknown error', $code = null,
+    function __construct($message = 'unknown error', $code = null,
                         $mode = null, $options = null, $userinfo = null)
     {
         if ($mode === null) {
diff --git a/include/pear/PEAR/FixPHP5PEARWarnings.php b/include/pear/PEAR/FixPHP5PEARWarnings.php
index be5dc3ce707c3e06189b89395819ae49edbab19c..32807c29057d25dcd03a1f5ce8f834aabcef4240 100644
--- a/include/pear/PEAR/FixPHP5PEARWarnings.php
+++ b/include/pear/PEAR/FixPHP5PEARWarnings.php
@@ -1,7 +1,7 @@
 <?php
 if ($skipmsg) {
-    $a = &new $ec($code, $mode, $options, $userinfo);
+    $a = new $ec($code, $mode, $options, $userinfo);
 } else {
-    $a = &new $ec($message, $code, $mode, $options, $userinfo);
+    $a = new $ec($message, $code, $mode, $options, $userinfo);
 }
-?>
\ No newline at end of file
+?>
diff --git a/include/staff/apikey.inc.php b/include/staff/apikey.inc.php
index fdf30da4bc844ed0cbdef37c3902a3916eca2716..9a39b814f82544ef7ea26bb0b002a9e29b409450 100644
--- a/include/staff/apikey.inc.php
+++ b/include/staff/apikey.inc.php
@@ -22,14 +22,16 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
  <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
- <h2><?php echo __('API Key');?>
+ <h2><?php echo $title; ?>
+    <?php if (isset($info['ipaddr'])) { ?><small>
+    — <?php echo $info['ipaddr']; ?></small>
+    <?php } ?>
     <i class="help-tip icon-question-sign" href="#api_key"></i>
     </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 __('API Key is auto-generated. Delete and re-add to change the key.');?></em>
             </th>
         </tr>
@@ -70,7 +72,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
             </td>
             <td>
                 <span>
-                <input type="text" size="30" name="ipaddr" value="<?php echo $info['ipaddr']; ?>">
+                <input type="text" size="30" name="ipaddr" value="<?php echo $info['ipaddr']; ?>"i
+                    autofocus>
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['ipaddr']; ?></span>
                 <i class="help-tip icon-question-sign" href="#ip_addr"></i>
                 </span>
@@ -100,7 +103,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
         </tr>
         <tr>
             <th colspan="2">
-                <em><strong><?php echo __('Admin Notes');?></strong>: <?php echo __('Internal notes.');?>&nbsp;</em>
+                <em><strong><?php echo __('Internal Notes');?></strong>: <?php echo __("be liberal, they're internal");?></em>
             </th>
         </tr>
         <tr>
@@ -111,7 +114,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
         </tr>
     </tbody>
 </table>
-<p style="padding-left:225px;">
+<p style="text-align:center">
     <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
     <input type="reset"  name="reset"  value="<?php echo __('Reset');?>">
     <input type="button" name="cancel" value="<?php echo __('Cancel');?>" onclick='window.location.href="apikeys.php"'>
diff --git a/include/staff/apikeys.inc.php b/include/staff/apikeys.inc.php
index 030bc89f97d4aed9bebb7e8ce865845fbd5e4212..2fd02c9021e2b7f48c1d83eb95f9605626c9cfc1 100644
--- a/include/staff/apikeys.inc.php
+++ b/include/staff/apikeys.inc.php
@@ -40,27 +40,56 @@ else
     $showing=__('No API keys found!');
 
 ?>
-
-<div class="pull-left" style="width:700px;padding-top:5px;">
- <h2><?php echo __('API Keys');?></h2>
-</div>
-<div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;">
- <b><a href="apikeys.php?a=add" class="Icon newapi"><?php echo __('Add New API Key');?></a></b></div>
-<div class="clear"></div>
 <form action="apikeys.php" method="POST" name="keys">
+    <div class="sticky bar opaque">
+        <div class="content">
+            <div class="pull-left flush-left">
+                <h2><?php echo __('API Keys');?></h2>
+            </div>
+            <div class="pull-right">
+                <a href="apikeys.php?a=add" class="green button action-button"><i class="icon-plus-sign"></i> <?php echo __('Add New API Key');?></a>
+                <span class="action-button" data-dropdown="#action-dropdown-more">
+                            <i class="icon-caret-down pull-right"></i>
+                            <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+                </span>
+                <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                    <ul id="actions">
+                        <li>
+                            <a class="confirm" data-name="enable" href="apikeys.php?a=enable">
+                                <i class="icon-ok-sign icon-fixed-width"></i>
+                                <?php echo __( 'Enable'); ?>
+                            </a>
+                        </li>
+                        <li>
+                            <a class="confirm" data-name="disable" href="apikeys.php?a=disable">
+                                <i class="icon-ban-circle icon-fixed-width"></i>
+                                <?php echo __( 'Disable'); ?>
+                            </a>
+                        </li>
+                        <li class="danger">
+                            <a class="confirm" data-name="delete" href="apikeys.php?a=delete">
+                                <i class="icon-trash icon-fixed-width"></i>
+                                <?php echo __( 'Delete'); ?>
+                            </a>
+                        </li>
+                    </ul>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="clear"></div>
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="mass_process" >
 <input type="hidden" id="action" name="a" value="" >
  <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption><?php echo $showing; ?></caption>
     <thead>
         <tr>
-            <th width="7">&nbsp;</th>
-            <th width="320"><a <?php echo $key_sort; ?> href="apikeys.php?<?php echo $qstr; ?>&sort=key"><?php echo __('API Key');?></a></th>
-            <th width="120"><a <?php echo $ip_sort; ?> href="apikeys.php?<?php echo $qstr; ?>&sort=ip"><?php echo __('IP Address');?></a></th>
-            <th width="100"><a  <?php echo $status_sort; ?> href="apikeys.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Status');?></a></th>
-            <th width="150" nowrap><a  <?php echo $date_sort; ?>href="apikeys.php?<?php echo $qstr; ?>&sort=date"><?php echo __('Date Added');?></a></th>
-            <th width="150" nowrap><a  <?php echo $updated_sort; ?>href="apikeys.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated');?></a></th>
+            <th width="4%">&nbsp;</th>
+            <th width="46%"><a <?php echo $key_sort; ?> href="apikeys.php?<?php echo $qstr; ?>&sort=key"><?php echo __('API Key');?></a></th>
+            <th width="12%"><a <?php echo $ip_sort; ?> href="apikeys.php?<?php echo $qstr; ?>&sort=ip"><?php echo __('IP Address');?></a></th>
+            <th width="8%"><a  <?php echo $status_sort; ?> href="apikeys.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Status');?></a></th>
+            <th width="10%" nowrap><a  <?php echo $date_sort; ?>href="apikeys.php?<?php echo $qstr; ?>&sort=date"><?php echo __('Date Added');?></a></th>
+            <th width="20%" nowrap><a  <?php echo $updated_sort; ?>href="apikeys.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated');?></a></th>
         </tr>
     </thead>
     <tbody>
@@ -74,14 +103,14 @@ else
                     $sel=true;
                 ?>
             <tr id="<?php echo $row['id']; ?>">
-                <td width=7px>
+                <td align="center">
                   <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['id']; ?>"
                             <?php echo $sel?'checked="checked"':''; ?>> </td>
                 <td>&nbsp;<a href="apikeys.php?id=<?php echo $row['id']; ?>"><?php echo Format::htmlchars($row['apikey']); ?></a></td>
                 <td><?php echo $row['ipaddr']; ?></td>
                 <td><?php echo $row['isactive']?__('Active'):'<b>'.__('Disabled').'</b>'; ?></td>
-                <td>&nbsp;<?php echo Format::db_date($row['created']); ?></td>
-                <td>&nbsp;<?php echo Format::db_datetime($row['updated']); ?></td>
+                <td>&nbsp;<?php echo Format::date($row['created']); ?></td>
+                <td>&nbsp;<?php echo Format::datetime($row['updated']); ?></td>
             </tr>
             <?php
             } //end of while.
@@ -105,11 +134,7 @@ else
 if($res && $num): //Show options..
     echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
 ?>
-<p class="centered" id="actions">
-    <input class="button" type="submit" name="enable" value="<?php echo __('Enable');?>" >
-    <input class="button" type="submit" name="disable" value="<?php echo __('Disable');?>">
-    <input class="button" type="submit" name="delete" value="<?php echo __('Delete');?>">
-</p>
+
 <?php
 endif;
 ?>
diff --git a/include/staff/banlist.inc.php b/include/staff/banlist.inc.php
index 9e69cf66577ad6ca9c3e8aef5e56406a3719b54a..aef6966ccdfd197bac645a2d97f6872213632e97 100644
--- a/include/staff/banlist.inc.php
+++ b/include/staff/banlist.inc.php
@@ -48,98 +48,118 @@ $qstr.='&amp;order='.($order=='DESC'?'ASC':'DESC');
 $query="$select $from $where ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
 //echo $query;
 ?>
-<h2><?php echo __('Banned Email Addresses');?>
-    <i class="help-tip icon-question-sign" href="#ban_list"></i>
-    </h2>
-<div class="pull-left" style="width:600;padding-top:5px;">
-    <form action="banlist.php" method="GET" name="filter">
-     <input type="hidden" name="a" value="filter" >
-     <div>
-       <?php echo __('Query');?>: <input name="q" type="text" size="20" value="<?php echo Format::htmlchars($_REQUEST['q']); ?>">
-        &nbsp;&nbsp;
-        <input type="submit" name="submit" value="<?php echo __('Search');?>"/>
-     </div>
-    </form>
- </div>
-<div class="pull-right flush-right" style="padding-right:5px;"><b><a href="banlist.php?a=add" class="Icon newstaff"><?php echo __('Ban New Email');?></a></b></div>
+<div id='basic_search'>
+    <div style="height:25px">
+        <form action="banlist.php" method="GET" name="filter">
+            <input type="hidden" name="a" value="filter" >
+            <div class="attached input">
+                <input name="q" type="text" class="basic-search" size="30" autofocus
+                       value="<?php echo Format::htmlchars($_REQUEST['q']); ?>">
+                <button type="submit" class="attached button"><i class="icon-search"></i></button>
+            </div>
+        </form>
+    </div>
+</div>
 <div class="clear"></div>
-<?php
-if(($res=db_query($query)) && ($num=db_num_rows($res)))
-    $showing=$pageNav->showing();
-else
-    $showing=__('No banned emails matching the query found!');
+<form action="banlist.php" method="POST" name="banlist">
+    <div style="margin-bottom:20px; padding-top:5px;">
+        <div class="sticky bar opaque">
+            <div class="content">
+                <div class="pull-left flush-left">
+                    <h2><?php echo __('Banned Email Addresses');?>
+                        <i class="help-tip icon-question-sign" href="#ban_list"></i>
+                    </h2>
+                </div>
+                <div class="pull-right flush-right">
+                    <a href="banlist.php?a=add" class="red button action-button">
+                        <i class="icon-ban-circle"></i> <?php echo __('Ban New Email');?></a>
+                    <span class="action-button" data-dropdown="#action-dropdown-more">
+                        <i class="icon-caret-down pull-right"></i>
+                    <span ><i class="icon-cog"></i> <?php echo __('More');?></span>                        </span>
+                    <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                        <ul id="actions">
+                            <li><a class="confirm" data-name="enable" href="banlist.php?a=enable">
+                                <i class="icon-ok-sign icon-fixed-width"></i>
+                                <?php echo __('Enable'); ?></a></li>
+                            <li><a class="confirm" data-name="disable" href="banlist.php?a=disable">
+                                <i class="icon-ban-circle icon-fixed-width"></i>
+                                <?php echo __('Disable'); ?></a></li>
+                            <li><a class="confirm" data-name="delete" href="banlist.php?a=delete">                                <i class="icon-undo icon-fixed-width"></i>
+                                <?php echo __('Remove'); ?></a></li>
+                        </ul>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="clear"></div>
+    <?php
+    if(($res=db_query($query)) && ($num=db_num_rows($res)))
+        $showing=$pageNav->showing();
+    else
+        $showing=__('No banned emails matching the query found!');
 
-if($search)
-    $showing=__('Search Results').': '.$showing;
+    if($search)
+        $showing=__('Search Results').': '.$showing;
 
-?>
-<form action="banlist.php" method="POST" name="banlist">
- <?php csrf_token(); ?>
- <input type="hidden" name="do" value="mass_process" >
-<input type="hidden" id="action" name="a" value="" >
- <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption><?php echo $showing; ?></caption>
-    <thead>
-        <tr>
-            <th width="7px">&nbsp;</th>
-            <th width="350"><a <?php echo $email_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=email"><?php echo __('Email Address');?></a></th>
-            <th width="200"><a  <?php echo $status_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Ban Status');?></a></th>
-            <th width="120"><a <?php echo $created_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=created"><?php echo __('Date Added');?></a></th>
-            <th width="120"><a <?php echo $updated_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated');?></a></th>
-        </tr>
-    </thead>
-    <tbody>
+    ?>
+    <?php csrf_token(); ?>
+    <input type="hidden" name="do" value="mass_process" >
+    <input type="hidden" id="action" name="a" value="" >
+    <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
+        <thead>
+            <tr>
+                <th width="4%">&nbsp;</th>
+                <th width="56%"><a <?php echo $email_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=email"><?php echo __('Email Address');?></a></th>
+                <th width="10%"><a  <?php echo $status_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Ban Status');?></a></th>
+                <th width="10%"><a <?php echo $created_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=created"><?php echo __('Date Added');?></a></th>
+                <th width="20%"><a <?php echo $updated_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated');?></a></th>
+            </tr>
+        </thead>
+        <tbody>
+        <?php
+            if($res && db_num_rows($res)):
+                $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null;
+                while ($row = db_fetch_array($res)) {
+                    $sel=false;
+                    if($ids && in_array($row['id'],$ids))
+                        $sel=true;
+                    ?>
+                   <tr id="<?php echo $row['id']; ?>">
+                    <td align="center">
+                      <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['id']; ?>" <?php echo $sel?'checked="checked"':''; ?>>
+                    </td>
+                    <td>&nbsp;<a href="banlist.php?id=<?php echo $row['id']; ?>"><?php echo Format::htmlchars($row['val']); ?></a></td>
+                    <td>&nbsp;&nbsp;<?php echo $row['isactive']?__('Active'):'<b>'.__('Disabled').'</b>'; ?></td>
+                    <td><?php echo Format::date($row['created']); ?></td>
+                    <td><?php echo Format::datetime($row['updated']); ?>&nbsp;</td>
+                   </tr>
+                <?php
+                } //end of while.
+            endif; ?>
+        <tfoot>
+         <tr>
+            <td colspan="5">
+                <?php if($res && $num){ ?>
+                <?php echo __('Select');?>:&nbsp;
+                <a id="selectAll" href="#ckb"><?php echo __('All');?></a>&nbsp;&nbsp;
+                <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 banned emails found!');
+                } ?>
+            </td>
+         </tr>
+        </tfoot>
+    </table>
     <?php
-        if($res && db_num_rows($res)):
-            $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null;
-            while ($row = db_fetch_array($res)) {
-                $sel=false;
-                if($ids && in_array($row['id'],$ids))
-                    $sel=true;
-                ?>
-               <tr id="<?php echo $row['id']; ?>">
-                <td width=7px>
-                  <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['id']; ?>" <?php echo $sel?'checked="checked"':''; ?>>
-                </td>
-                <td>&nbsp;<a href="banlist.php?id=<?php echo $row['id']; ?>"><?php echo Format::htmlchars($row['val']); ?></a></td>
-                <td>&nbsp;&nbsp;<?php echo $row['isactive']?__('Active'):'<b>'.__('Disabled').'</b>'; ?></td>
-                <td><?php echo Format::db_date($row['created']); ?></td>
-                <td><?php echo Format::db_datetime($row['updated']); ?>&nbsp;</td>
-               </tr>
-            <?php
-            } //end of while.
-        endif; ?>
-    <tfoot>
-     <tr>
-        <td colspan="5">
-            <?php if($res && $num){ ?>
-            <?php echo __('Select');?>:&nbsp;
-            <a id="selectAll" href="#ckb"><?php echo __('All');?></a>&nbsp;&nbsp;
-            <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 banned emails found!');
-            } ?>
-        </td>
-     </tr>
-    </tfoot>
-</table>
-<?php
-if($res && $num): //Show options..
-    echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
-?>
-<p class="centered" id="actions">
-    <input class="button" type="submit" name="enable" value="<?php echo __('Enable');?>" >
-    &nbsp;&nbsp;
-    <input class="button" type="submit" name="disable" value="<?php echo __('Disable');?>" >
-    &nbsp;&nbsp;
-    <input class="button" type="submit" name="delete" value="<?php echo __('Delete');?>">
-</p>
-<?php
-endif;
-?>
+    if($res && $num): //Show options..
+        echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
+    ?>
+    <?php
+        endif;
+    ?>
 </form>
-
 <div style="display:none;" class="dialog" id="confirm-action">
     <h3><?php echo __('Please Confirm');?></h3>
     <a class="close" href=""><i class="icon-remove-circle"></i></a>
diff --git a/include/staff/banrule.inc.php b/include/staff/banrule.inc.php
index 4f98cd148511d0787873a81f07b75e5ddc6c8275..1e433ee89ade186369e1dafe14a1d7a315ccf365 100644
--- a/include/staff/banrule.inc.php
+++ b/include/staff/banrule.inc.php
@@ -24,14 +24,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 Format::htmlchars($_REQUEST['a']); ?>">
  <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
- <h2><?php echo __('Manage Email Ban Rule');?>
+
+    <h2><?php echo $title; ?>
     <i class="help-tip icon-question-sign" href="#ban_list"></i>
     </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 __('Valid email address required');?></em>
             </th>
         </tr>
diff --git a/include/staff/cannedresponse.inc.php b/include/staff/cannedresponse.inc.php
index b2c851210713a304470f6773e8b4f79ec240f5c0..0dabac6fd3319fd5dca169521e47da16f7693bb6 100644
--- a/include/staff/cannedresponse.inc.php
+++ b/include/staff/cannedresponse.inc.php
@@ -26,14 +26,16 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
  <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
- <h2><?php echo __('Canned Response')?>
- &nbsp;<i class="help-tip icon-question-sign" href="#canned_response"></i></h2>
+ <h2><?php echo $title; ?>
+         <?php if (isset($info['title'])) { ?><small>
+    — <?php echo $info['title']; ?></small>
+     <?php } ?><i class="help-tip icon-question-sign" href="#canned_response"></i>
+</h2>
  <table class="form_table fixed" width="940" border="0" cellspacing="0" cellpadding="2">
     <thead>
         <tr><td></td><td></td></tr> <!-- For fixed table layout -->
         <tr>
             <th colspan="2">
-                <h4><?php echo $title; ?></h4>
                 <em><?php echo __('Canned response settings');?></em>
             </th>
         </tr>
@@ -55,9 +57,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <select name="dept_id">
                     <option value="0">&mdash; <?php echo __('All Departments');?> &mdash;</option>
                     <?php
-                    $sql='SELECT dept_id, dept_name FROM '.DEPT_TABLE.' dept ORDER by dept_name';
-                    if(($res=db_query($sql)) && db_num_rows($res)) {
-                        while(list($id,$name)=db_fetch_row($res)) {
+                    if (($depts=Dept::getDepartments())) {
+                        foreach($depts as $id => $name) {
                             $selected=($info['dept_id'] && $id==$info['dept_id'])?'selected="selected"':'';
                             echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name);
                         }
@@ -81,22 +82,21 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                     <font class="error">*&nbsp;<?php echo $errors['response']; ?></font>
                     &nbsp;&nbsp;&nbsp;(<a class="tip" href="#ticket_variables"><?php echo __('Supported Variables'); ?></a>)
                     </div>
-                <textarea name="response" class="richtext draft draft-delete" cols="21" rows="12"
-                    data-draft-namespace="canned"
-                    data-draft-object-id="<?php if (isset($canned)) echo $canned->getId(); ?>"
-                    style="width:98%;" class="richtext draft"><?php
-                        echo $info['response']; ?></textarea>
+                <textarea name="response" cols="21" rows="12"
+                    data-root-context="cannedresponse"
+                    style="width:98%;" class="richtext draft draft-delete" <?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('canned',
+        is_object($canned) ? $canned->getId() : false, $info['response']);
+    echo $attrs; ?>><?php echo $draft ?: $info['response'];
+                ?></textarea>
                 <div><h3><?php echo __('Canned Attachments'); ?> <?php echo __('(optional)'); ?>
                 &nbsp;<i class="help-tip icon-question-sign" href="#canned_attachments"></i></h3>
                 <div class="error"><?php echo $errors['files']; ?></div>
                 </div>
                 <?php
                 $attachments = $canned_form->getField('attachments');
-                if ($canned && ($files=$canned->attachments->getSeparates())) {
-                    $ids = array();
-                    foreach ($files as $f)
-                        $ids[] = $f['id'];
-                    $attachments->value = $ids;
+                if ($canned && $attachments) {
+                    $attachments->setAttachments($canned->attachments);
                 }
                 print $attachments->render(); ?>
                 <br/>
@@ -120,7 +120,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
     <div id="msg_warning"><?php echo __('Canned response is in use by email filter(s)');?>: <?php
     echo implode(', ', $canned->getFilters()); ?></div>
  <?php } ?>
-<p style="padding-left:225px;">
+<p style="text-align:center;">
     <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
     <input type="reset"  name="reset"  value="<?php echo __('Reset'); ?>" onclick="javascript:
         $(this.form).find('textarea.richtext')
diff --git a/include/staff/cannedresponses.inc.php b/include/staff/cannedresponses.inc.php
index 171f6977a71738bf722a82978a4fa86507f88bb8..1a55d08db8f164a9890fe49f08b181259bfef754 100644
--- a/include/staff/cannedresponses.inc.php
+++ b/include/staff/cannedresponses.inc.php
@@ -2,9 +2,9 @@
 if(!defined('OSTSCPINC') || !$thisstaff) die('Access Denied');
 
 $qs = array();
-$sql='SELECT canned.*, count(attach.file_id) as files, dept.dept_name as department '.
+$sql='SELECT canned.*, count(attach.file_id) as files, dept.name as department '.
      ' FROM '.CANNED_TABLE.' canned '.
-     ' LEFT JOIN '.DEPT_TABLE.' dept ON (dept.dept_id=canned.dept_id) '.
+     ' LEFT JOIN '.DEPT_TABLE.' dept ON (dept.id=canned.dept_id) '.
      ' LEFT JOIN '.ATTACHMENT_TABLE.' attach
             ON (attach.object_id=canned.canned_id AND attach.`type`=\'C\' AND NOT attach.inline)';
 $sql.=' WHERE 1';
@@ -50,25 +50,57 @@ else
     $showing=__('No premade responses found!');
 
 ?>
-<div class="pull-left" style="width:700px;padding-top:5px;">
- <h2><?php echo __('Canned Responses');?></h2>
- </div>
-<div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;">
-    <b><a href="canned.php?a=add" class="Icon newReply"><?php echo __('Add New Response');?></a></b></div>
-<div class="clear"></div>
 <form action="canned.php" method="POST" name="canned">
+
+<div class="sticky bar opaque">
+    <div class="content">
+        <div class="pull-left flush-left">
+            <h2><?php echo __('Canned Responses');?></h2>
+        </div>
+        <div class="pull-right flush-right">
+            <a href="canned.php?a=add" class="green button"><i class="icon-plus-sign"></i> <?php echo __('Add New Response');?></a>
+
+            <span class="action-button" data-dropdown="#action-dropdown-more" style="/*DELME*/ vertical-align:top; margin-bottom:0">
+                    <i class="icon-caret-down pull-right"></i>
+                    <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+            </span>
+            <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                <ul id="actions">
+                    <li>
+                        <a class="confirm" data-name="enable" href="canned.php?a=enable">
+                            <i class="icon-ok-sign icon-fixed-width"></i>
+                            <?php echo __( 'Enable'); ?>
+                        </a>
+                    </li>
+                    <li>
+                        <a class="confirm" data-name="disable" href="canned.php?a=disable">
+                            <i class="icon-ban-circle icon-fixed-width"></i>
+                            <?php echo __( 'Disable'); ?>
+                        </a>
+                    </li>
+                    <li class="danger">
+                        <a class="confirm" data-name="delete" href="canned.php?a=delete">
+                            <i class="icon-trash icon-fixed-width"></i>
+                            <?php echo __( 'Delete'); ?>
+                        </a>
+                    </li>
+                </ul>
+            </div>
+        </div>
+    </div>
+</div>
+<div class="clear"></div>
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="mass_process" >
  <input type="hidden" id="action" name="a" value="" >
  <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption><?php echo $showing; ?></caption>
     <thead>
         <tr>
-            <th width="7">&nbsp;</th>
-            <th width="500"><a <?php echo $title_sort; ?> href="canned.php?<?php echo $qstr; ?>&sort=title"><?php echo __('Title');?></a></th>
-            <th width="80"><a  <?php echo $status_sort; ?> href="canned.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Status');?></a></th>
-            <th width="200"><a  <?php echo $dept_sort; ?> href="canned.php?<?php echo $qstr; ?>&sort=dept"><?php echo __('Department');?></a></th>
-            <th width="150" nowrap><a  <?php echo $updated_sort; ?>href="canned.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated');?></a></th>
+            <th width="4%">&nbsp;</th>
+            <th width="46%"><a <?php echo $title_sort; ?> href="canned.php?<?php echo $qstr; ?>&sort=title"><?php echo __('Title');?></a></th>
+            <th width="10%"><a  <?php echo $status_sort; ?> href="canned.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Status');?></a></th>
+            <th width="20%"><a  <?php echo $dept_sort; ?> href="canned.php?<?php echo $qstr; ?>&sort=dept"><?php echo __('Department');?></a></th>
+            <th width="20%" nowrap><a  <?php echo $updated_sort; ?>href="canned.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated');?></a></th>
         </tr>
     </thead>
     <tbody>
@@ -83,7 +115,7 @@ else
                 $files=$row['files']?'<span class="Icon file">&nbsp;</span>':'';
                 ?>
             <tr id="<?php echo $row['canned_id']; ?>">
-                <td width=7px>
+                <td align="center">
                   <input type="checkbox" name="ids[]" value="<?php echo $row['canned_id']; ?>" class="ckb"
                             <?php echo $sel?'checked="checked"':''; ?> />
                 </td>
@@ -92,7 +124,7 @@ else
                 </td>
                 <td><?php echo $row['isenabled']?__('Active'):'<b>'.__('Disabled').'</b>'; ?></td>
                 <td><?php echo $row['department']?$row['department']:'&mdash; '.__('All Departments').' &mdash;'; ?></td>
-                <td>&nbsp;<?php echo Format::db_datetime($row['updated']); ?></td>
+                <td>&nbsp;<?php echo Format::datetime($row['updated']); ?></td>
             </tr>
             <?php
             } //end of while.
@@ -116,11 +148,7 @@ else
 if($res && $num): //Show options..
     echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
 ?>
-<p class="centered" id="actions">
-    <input class="button" type="submit" name="enable" value="<?php echo __('Enable');?>" >
-    <input class="button" type="submit" name="disable" value="<?php echo __('Disable');?>" >
-    <input class="button" type="submit" name="delete" value="<?php echo __('Delete');?>" >
-</p>
+
 <?php
 endif;
 ?>
diff --git a/include/staff/categories.inc.php b/include/staff/categories.inc.php
index 692443588ab37221e2fe872411aa24c36c3721c8..222079b5274daceb7775467b7a53457775f438e6 100644
--- a/include/staff/categories.inc.php
+++ b/include/staff/categories.inc.php
@@ -2,93 +2,111 @@
 if(!defined('OSTSCPINC') || !$thisstaff) die('Access Denied');
 
 $qs = array();
-$sql='SELECT cat.category_id, cat.name, cat.ispublic, cat.updated, count(faq.faq_id) as faqs '.
-     ' FROM '.FAQ_CATEGORY_TABLE.' cat '.
-     ' LEFT JOIN '.FAQ_TABLE.' faq ON (faq.category_id=cat.category_id) ';
-$sql.=' WHERE 1';
-$sortOptions=array('name'=>'cat.name','type'=>'cat.ispublic','faqs'=>'faqs','updated'=>'cat.updated');
-$orderWays=array('DESC'=>'DESC','ASC'=>'ASC');
+$categories = Category::objects()
+    ->annotate(array('faq_count'=>SqlAggregate::COUNT('faqs')));
+$sortOptions=array('name'=>'name','type'=>'ispublic','faqs'=>'faq_count','updated'=>'updated');
+$orderWays=array('DESC'=>'-','ASC'=>'');
 $sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'name';
 //Sorting options...
 if($sort && $sortOptions[$sort]) {
     $order_column =$sortOptions[$sort];
 }
-$order_column=$order_column?$order_column:'cat.name';
+$order_column=$order_column ?: 'name';
 
 if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) {
     $order=$orderWays[strtoupper($_REQUEST['order'])];
 }
-$order=$order?$order:'ASC';
+$order=$order ?: '';
 
-if($order_column && strpos($order_column,',')){
-    $order_column=str_replace(','," $order,",$order_column);
-}
 $x=$sort.'_sort';
 $$x=' class="'.strtolower($order).'" ';
 $order_by="$order_column $order ";
 
-$total=db_count('SELECT count(*) FROM '.FAQ_CATEGORY_TABLE.' cat ');
+$total=$categories->count();
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 $pageNav=new Pagenate($total, $page, PAGE_LIMIT);
 $qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
 $pageNav->setURL('categories.php', $qs);
 $qstr = '&amp;order='.($order=='DESC'?'ASC':'DESC');
-$query="$sql GROUP BY cat.category_id ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
-$res=db_query($query);
-if($res && ($num=db_num_rows($res)))
+$pageNav->paginate($categories);
+
+if ($total)
     $showing=$pageNav->showing().' '.__('categories');
 else
     $showing=__('No FAQ categories found!');
 
 ?>
-<div class="pull-left" style="width:700px;padding-top:5px;">
- <h2><?php echo __('FAQ Categories');?></h2>
- </div>
-<div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;">
-    <b><a href="categories.php?a=add" class="Icon newCategory"><?php echo __('Add New Category');?></a></b></div>
-<div class="clear"></div>
-<form action="categories.php" method="POST" name="cat">
- <?php csrf_token(); ?>
- <input type="hidden" name="do" value="mass_process" >
- <input type="hidden" id="action" name="a" value="" >
+
+<form action="categories.php" method="POST" id="mass-actions">
+    <div class="sticky bar opaque">
+        <div class="content">
+            <div class="pull-left flush-left">
+                <h2><?php echo __('FAQ Categories');?></h2>
+            </div>
+            <div class="pull-right flush-right">
+                <a href="categories.php?a=add" class="green button">
+                    <i class="icon-plus-sign"></i>
+                    <?php echo __( 'Add New Category');?>
+                </a>
+                <div class="pull-right flush-right">
+
+                    <span class="action-button" data-dropdown="#action-dropdown-more">
+                        <i class="icon-caret-down pull-right"></i>
+                        <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+                    </span>
+                    <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                        <ul id="actions">
+                            <li class="danger">
+                                <a class="confirm" data-form-id="mass-actions" data-name="delete" href="categories.php?a=delete">
+                                    <i class="icon-trash icon-fixed-width"></i>
+                                    <?php echo __( 'Delete'); ?>
+                                </a>
+                            </li>
+                        </ul>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="clear"></div>
+    <?php csrf_token(); ?>
+    <input type="hidden" name="do" value="mass_process" >
+    <input type="hidden" id="action" name="a" value="" >
  <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption><?php echo $showing; ?></caption>
     <thead>
         <tr>
-            <th width="7">&nbsp;</th>
-            <th width="500"><a <?php echo $name_sort; ?> href="categories.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name');?></a></th>
-            <th width="150"><a  <?php echo $type_sort; ?> href="categories.php?<?php echo $qstr; ?>&sort=type"><?php echo __('Type');?></a></th>
-            <th width="80"><a  <?php echo $faqs_sort; ?> href="categories.php?<?php echo $qstr; ?>&sort=faqs"><?php echo __('FAQs');?></a></th>
-            <th width="150" nowrap><a  <?php echo $updated_sort; ?>href="categories.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated');?></a></th>
+            <th width="4%">&nbsp;</th>
+            <th width="56%"><a <?php echo $name_sort; ?> href="categories.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name');?></a></th>
+            <th width="10%"><a  <?php echo $type_sort; ?> href="categories.php?<?php echo $qstr; ?>&sort=type"><?php echo __('Type');?></a></th>
+            <th width="10%"><a  <?php echo $faqs_sort; ?> href="categories.php?<?php echo $qstr; ?>&sort=faqs"><?php echo __('FAQs');?></a></th>
+            <th width="20%" nowrap><a  <?php echo $updated_sort; ?>href="categories.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated');?></a></th>
         </tr>
     </thead>
     <tbody>
     <?php
         $total=0;
         $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null;
-        if($res && db_num_rows($res)):
-            while ($row = db_fetch_array($res)) {
-                $sel=false;
-                if($ids && in_array($row['category_id'],$ids))
-                    $sel=true;
+        foreach ($categories as $C) {
+            $sel=false;
+            if ($ids && in_array($C->getId(), $ids))
+                $sel=true;
 
-                $faqs=0;
-                if($row['faqs'])
-                    $faqs=sprintf('<a href="faq.php?cid=%d">%d</a>',$row['category_id'],$row['faqs']);
-                ?>
-            <tr id="<?php echo $row['category_id']; ?>">
-                <td width=7px>
-                  <input type="checkbox" name="ids[]" value="<?php echo $row['category_id']; ?>" class="ckb"
+            $faqs=0;
+            if ($C->faq_count)
+                $faqs=sprintf('<a href="faq.php?cid=%d">%d</a>',$C->getId(),$C->faq_count);
+            ?>
+            <tr id="<?php echo $C->getId(); ?>">
+                <td align="center">
+                  <input type="checkbox" name="ids[]" value="<?php echo $C->getId(); ?>" class="ckb"
                             <?php echo $sel?'checked="checked"':''; ?>>
                 </td>
-                <td><a href="categories.php?id=<?php echo $row['category_id']; ?>"><?php echo Format::truncate($row['name'],200); ?></a>&nbsp;</td>
-                <td><?php echo $row['ispublic']?'<b>'.__('Public').'</b>':__('Internal'); ?></td>
+                <td><a class="truncate" style="width:500px" href="categories.php?id=<?php echo $C->getId(); ?>"><?php
+                    echo $C->getLocalName(); ?></a></td>
+                <td><?php echo $C->getVisibilityDescription(); ?></td>
                 <td style="text-align:right;padding-right:25px;"><?php echo $faqs; ?></td>
-                <td>&nbsp;<?php echo Format::db_datetime($row['updated']); ?></td>
-            </tr>
-            <?php
-            } //end of while.
-        endif; ?>
+                <td>&nbsp;<?php echo Format::datetime($C->updated); ?></td>
+            </tr><?php
+        } // end of foreach ?>
     <tfoot>
      <tr>
         <td colspan="5">
@@ -98,7 +116,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 e67bfd4dc14779d0172a6e44b241777c3e2445cb..5ac65cc2131e550dd05bf61f60e602c7fd981829 100644
--- a/include/staff/category.inc.php
+++ b/include/staff/category.inc.php
@@ -1,15 +1,32 @@
 <?php
-if(!defined('OSTSCPINC') || !$thisstaff || !$thisstaff->canManageFAQ()) die('Access Denied');
+if (!defined('OSTSCPINC') || !$thisstaff
+        || !$thisstaff->hasPerm(FAQ::PERM_MANAGE))
+    die('Access Denied');
+
 $info=array();
 $qs = array();
 if($category && $_REQUEST['a']!='add'){
-    $title=__('Update Category').': '.$category->getName();
+    $title=__('Update Category');
     $action='update';
     $submit_text=__('Save Changes');
     $info=$category->getHashtable();
     $info['id']=$category->getId();
     $info['notes'] = Format::viewableImages($category->getNotes());
     $qs += array('id' => $category->getId());
+    $langs = $cfg->getSecondaryLanguages();
+    $translations = $category->getAllTranslations();
+    foreach ($langs as $tag) {
+        foreach ($translations as $t) {
+            if (strcasecmp($t->lang, $tag) === 0) {
+                $trans = $t->getComplex();
+                $info['trans'][$tag] = array(
+                    'name' => $trans['name'],
+                    'description' => Format::viewableImages($trans['description']),
+                );
+                break;
+            }
+        }
+    }
 }else {
     $title=__('Add New Category');
     $action='create';
@@ -24,58 +41,104 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
  <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
- <h2><?php echo __('FAQ Category');?></h2>
- <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
-    <thead>
-        <tr>
-            <th colspan="2">
-                <h4><?php echo $title; ?></h4>
-            </th>
-        </tr>
-    </thead>
-    <tbody>
-        <tr>
-            <th colspan="2">
-                <em><?php echo __('Category information'); ?>
-                <i class="help-tip icon-question-sign" href="#category_information"></i></em>
-            </th>
-        </tr>
-        <tr>
-            <td width="180" class="required"><?php echo __('Category Type');?>:</td>
-            <td>
-                <input type="radio" name="ispublic" value="1" <?php echo $info['ispublic']?'checked="checked"':''; ?>><b><?php echo __('Public');?></b> <?php echo __('(publish)');?>
-                &nbsp;&nbsp;&nbsp;&nbsp;
-                <input type="radio" name="ispublic" value="0" <?php echo !$info['ispublic']?'checked="checked"':''; ?>><?php echo __('Private');?> <?php echo __('(internal)');?>
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['ispublic']; ?></span>
-            </td>
-        </tr>
-        <tr>
-            <td colspan=2>
-                <div style="padding-top:3px;"><b><?php echo __('Category Name');?></b>:&nbsp;<span class="faded"><?php echo __('Short descriptive name.');?></span></div>
-                    <input type="text" size="70" name="name" value="<?php echo $info['name']; ?>">
-                    &nbsp;<span class="error">*&nbsp;<?php echo $errors['name']; ?></span>
-                <br>
-                <div style="padding-top:5px;">
-                    <b><?php echo __('Category Description');?></b>:&nbsp;<span class="faded"><?php echo __('Summary of the category.');?></span>
-                    &nbsp;
-                    <font class="error">*&nbsp;<?php echo $errors['description']; ?></font></div>
-                    <textarea class="richtext" name="description" cols="21" rows="12" style="width:98%;"><?php echo $info['description']; ?></textarea>
-            </td>
-        </tr>
-        <tr>
-            <th colspan="2">
-                <em><?php echo __('Internal Notes');?>&nbsp;</em>
-            </th>
-        </tr>
-        <tr>
-            <td colspan=2>
-                <textarea class="richtext no-bar" name="notes" cols="21"
-                    rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea>
-            </td>
-        </tr>
-    </tbody>
-</table>
-<p style="padding-left:225px;">
+ <h2><?php echo $title; ?>
+     <?php if (isset($info['name'])) { ?><small>
+    — <?php echo $info['name']; ?></small>
+     <?php } ?>
+    </h2>
+
+
+    <div style="margin:8px 0"><strong><?php echo __('Category Type');?>:</strong>
+        <span class="error">*</span></div>
+    <div style="margin-left:5px">
+    <input type="radio" name="ispublic" value="2" <?php echo $info['ispublic']?'checked="checked"':''; ?>><b><?php echo __('Featured');?></b> <?php echo __('(on front-page sidebar)');?>
+    <br/>
+    <input type="radio" name="ispublic" value="1" <?php echo $info['ispublic']?'checked="checked"':''; ?>><b><?php echo __('Public');?></b> <?php echo __('(publish)');?>
+    <br/>
+    <input type="radio" name="ispublic" value="0" <?php echo !$info['ispublic']?'checked="checked"':''; ?>><?php echo __('Private');?> <?php echo __('(internal)');?>
+    <br/>
+    <div class="error"><?php echo $errors['ispublic']; ?></div>
+    </div>
+
+<div style="margin-top:20px"></div>
+
+<ul class="tabs clean" style="margin-top:9px;">
+    <li class="active"><a href="#info"><?php echo __('Category Information'); ?></a></li>
+    <li><a href="#notes"><?php echo __('Internal Notes'); ?></a></li>
+</ul>
+
+<div class="tab_content" id="info">
+
+<?php
+$langs = Internationalization::getConfiguredSystemLanguages();
+if (count($langs) > 1) { ?>
+    <ul class="alt tabs clean" id="trans">
+        <li class="empty"><i class="icon-globe" title="This content is translatable"></i></li>
+<?php foreach ($langs as $tag=>$i) {
+    list($lang, $locale) = explode('_', $tag);
+ ?>
+    <li class="<?php if ($tag == $cfg->getPrimaryLanguage()) echo "active";
+        ?>"><a href="#lang-<?php echo $tag; ?>" title="<?php
+        echo Internationalization::getLanguageDescription($tag);
+    ?>"><span class="flag flag-<?php echo strtolower($i['flag'] ?: $locale ?: $lang); ?>"></span>
+    </a></li>
+<?php } ?>
+    </ul>
+<?php
+} ?>
+
+
+<?php foreach ($langs as $tag=>$i) {
+    $code = $i['code'];
+    $cname = 'name';
+    $dname = 'description';
+    if ($tag == $cfg->getPrimaryLanguage()) {
+        $category = $info[$cname];
+        $desc = $info[$dname];
+    }
+    else {
+        $category = $info['trans'][$code][$cname];
+        $desc = $info['trans'][$code][$dname];
+        $cname = "trans[$code][$cname]";
+        $dname = "trans[$code][$dname]";
+    } ?>
+    <div class="tab_content <?php
+        if ($code != $cfg->getPrimaryLanguage()) echo "hidden";
+      ?>" id="lang-<?php echo $tag; ?>"
+      <?php if ($i['direction'] == 'rtl') echo 'dir="rtl" class="rtl"'; ?>
+    >
+    <div style="padding-bottom:8px;">
+        <b><?php echo __('Category Name');?></b>:
+        <span class="error">*</span>
+        <div class="faded"><?php echo __('Short descriptive name.');?></div>
+    </div>
+    <input type="text" size="70" style="font-size:110%;width:100%;box-sizing:border-box"
+        name="<?php echo $cname; ?>" value="<?php echo $category; ?>">
+    <div class="error"><?php echo $errors['name']; ?></div>
+
+    <div style="padding:8px 0;">
+        <b><?php echo __('Category Description');?></b>:
+        <span class="error">*</span>
+        <div class="faded"><?php echo __('Summary of the category.');?></div>
+        <div class="error"><?php echo $errors['description']; ?></div>
+    </div>
+    <textarea class="richtext" name="<?php echo $dname; ?>" cols="21" rows="12"
+        style="width:100%;"><?php
+        echo $desc; ?></textarea>
+    </div>
+<?php } ?>
+</div>
+
+
+<div class="tab_content" id="notes" style="display:none;">
+    <b><?php echo __('Internal Notes');?></b>:
+    <span class="faded"><?php echo __("Be liberal, they're internal");?></span>
+    <textarea class="richtext no-bar" name="notes" cols="21"
+        rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea>
+</div>
+
+
+<p style="text-align:center">
     <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
     <input type="reset"  name="reset"  value="<?php echo __('Reset');?>">
     <input type="button" name="cancel" value="<?php echo __('Cancel');?>" onclick='window.location.href="categories.php"'>
diff --git a/include/staff/dashboard.inc.php b/include/staff/dashboard.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..d7b35af5ee0485a9b352ad8e2224d2b3c7c24a07
--- /dev/null
+++ b/include/staff/dashboard.inc.php
@@ -0,0 +1,125 @@
+<?php
+$report = new OverviewReport($_POST['start'], $_POST['period']);
+$plots = $report->getPlotData();
+
+?>
+<script type="text/javascript" src="js/raphael-min.js"></script>
+<script type="text/javascript" src="js/g.raphael.js"></script>
+<script type="text/javascript" src="js/g.line-min.js"></script>
+<script type="text/javascript" src="js/g.dot-min.js"></script>
+<script type="text/javascript" src="js/dashboard.inc.js"></script>
+
+<link rel="stylesheet" type="text/css" href="css/dashboard.css"/>
+
+<form method="post" action="dashboard.php">
+<div id="basic_search">
+    <div style="min-height:25px;">
+        <!--<p><?php //echo __('Select the starting time and period for the system activity graph');?></p>-->
+            <?php echo csrf_token(); ?>
+            <label>
+                <?php echo __( 'Report timeframe'); ?>:
+                <input type="text" class="dp input-medium search-query"
+                    name="start" placeholder="<?php echo __('Last month');?>"
+                    value="<?php
+                        echo Format::htmlchars($report->getStartDate());
+                    ?>" />
+            </label>
+            <label>
+                <?php echo __( 'period');?>:
+                <select name="period">
+                    <option value="now" selected="selected">
+                        <?php echo __( 'Up to today');?>
+                    </option>
+                    <option value="+7 days">
+                        <?php echo __( 'One Week');?>
+                    </option>
+                    <option value="+14 days">
+                        <?php echo __( 'Two Weeks');?>
+                    </option>
+                    <option value="+1 month">
+                        <?php echo __( 'One Month');?>
+                    </option>
+                    <option value="+3 months">
+                        <?php echo __( 'One Quarter');?>
+                    </option>
+                </select>
+            </label>
+            <button class="green button action-button muted" type="submit">
+                <?php echo __( 'Refresh');?>
+            </button>
+            <i class="help-tip icon-question-sign" href="#report_timeframe"></i>
+    </div>
+</div>
+<div class="clear"></div>
+<div style="margin-bottom:20px; padding-top:5px;">
+    <div class="pull-left flush-left">
+        <h2><?php echo __('Ticket Activity');
+            ?>&nbsp;<i class="help-tip icon-question-sign" href="#ticket_activity"></i></h2>
+    </div>
+</div>
+<div class="clear"></div>
+<!-- Create a graph and fetch some data to create pretty dashboard -->
+<div style="position:relative">
+    <div id="line-chart-here" style="height:300px"></div>
+    <div style="position:absolute;right:0;top:0" id="line-chart-legend"></div>
+</div>
+
+<hr/>
+<h2><?php echo __('Statistics'); ?>&nbsp;<i class="help-tip icon-question-sign" href="#statistics"></i></h2>
+<p><?php echo __('Statistics of tickets organized by department, help topic, and agent.');?></p>
+
+<ul class="clean tabs">
+<?php
+$first = true;
+$groups = $report->enumTabularGroups();
+foreach ($groups as $g=>$desc) { ?>
+    <li class="<?php echo $first ? 'active' : ''; ?>"><a href="#<?php echo Format::slugify($g); ?>"
+        ><?php echo Format::htmlchars($desc); ?></a></li>
+<?php
+    $first = false;
+} ?>
+</ul>
+
+<?php
+$first = true;
+foreach ($groups as $g=>$desc) {
+    $data = $report->getTabularData($g); ?>
+    <div class="tab_content <?php echo (!$first) ? 'hidden' : ''; ?>" id="<?php echo Format::slugify($g); ?>">
+    <table class="dashboard-stats table"><tbody><tr>
+<?php
+    foreach ($data['columns'] as $j=>$c) { ?>
+        <th <?php if ($j === 0) echo 'width="30%" class="flush-left"'; ?>><?php echo Format::htmlchars($c); ?></th>
+<?php
+    } ?>
+    </tr></tbody>
+    <tbody>
+<?php
+    foreach ($data['data'] as $i=>$row) {
+        echo '<tr>';
+        foreach ($row as $j=>$td) {
+            if ($j === 0) { ?>
+                <th class="flush-left"><?php echo Format::htmlchars($td); ?></th>
+<?php       }
+            else { ?>
+                <td><?php echo Format::htmlchars($td);
+                if ($td) { // TODO Add head map
+                }
+                echo '</td>';
+            }
+        }
+        echo '</tr>';
+    }
+    $first = false; ?>
+    </tbody></table>
+    <div style="margin-top: 5px"><button type="submit" class="link button" name="export"
+        value="<?php echo Format::htmlchars($g); ?>">
+        <i class="icon-download"></i>
+        <?php echo __('Export'); ?></a></div>
+    </div>
+<?php
+}
+?>
+</form>
+<script>
+    $.drawPlots(<?php echo JsonDataEncoder::encode($report->getPlotData()); ?>);
+</script>
diff --git a/include/staff/department.inc.php b/include/staff/department.inc.php
index 3e5aa6c9406d3e40840a8e0bfa04b5a5f71ab92d..7374f4b7f74f7407901b0644e86f9ebcf6e01146 100644
--- a/include/staff/department.inc.php
+++ b/include/staff/department.inc.php
@@ -6,11 +6,12 @@ if($dept && $_REQUEST['a']!='add') {
     $title=__('Update Department');
     $action='update';
     $submit_text=__('Save Changes');
-    $info=$dept->getInfo();
-    $info['id']=$dept->getId();
-    $info['groups'] = $dept->getAllowedGroups();
+    $info = $dept->getInfo();
+    $info['id'] = $dept->getId();
     $qs += array('id' => $dept->getId());
 } else {
+    if (!$dept)
+        $dept = Dept::create();
     $title=__('Add New Department');
     $action='create';
     $submit_text=__('Create Dept');
@@ -22,30 +23,60 @@ if($dept && $_REQUEST['a']!='add') {
 
     $qs += array('a' => $_REQUEST['a']);
 }
-$info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
+
+$info = Format::htmlchars(($errors && $_POST) ? $_POST : $info);
 ?>
 <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']); ?>">
  <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
- <h2><?php echo __('Department');?></h2>
+<h2><?php echo $title; ?>
+    <?php if (isset($info['name'])) { ?><small>
+    — <?php echo $info['name']; ?></small>
+    <?php } ?>
+</h2>
+<ul class="clean tabs">
+    <li class="active"><a href="#settings">
+        <i class="icon-file"></i> <?php echo __('Settings'); ?></a></li>
+    <li><a href="#access">
+      <i class="icon-user"></i> <?php echo __('Access'); ?></a></li>
+</ul>
+<div id="settings" class="tab_content">
  <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 __('Department Information');?></em>
             </th>
         </tr>
     </thead>
     <tbody>
+        <tr>
+            <td width="180">
+                <?php echo __('Parent');?>:
+            </td>
+            <td>
+                <select name="pid">
+                    <option value="">&mdash; <?php echo __('Top-Level Deptartment'); ?> &mdash;</option>
+<?php foreach (Dept::getDepartments() as $id=>$name) {
+    if ($info['id'] && $id == $info['id'])
+        continue; ?>
+                    <option value="<?php echo $id; ?>" <?php
+                    if ($info['pid'] == $id) echo 'selected="selected"';
+                    ?>><?php echo $name; ?></option>
+<?php } ?>
+                </select>
+            </td>
+        </tr>
         <tr>
             <td width="180" class="required">
                 <?php echo __('Name');?>:
             </td>
             <td>
-                <input type="text" size="30" name="name" value="<?php echo $info['name']; ?>">
+                <input data-translate-tag="<?php echo $dept ? $dept->getTranslateTag() : '';
+                ?>" type="text" size="30" name="name" value="<?php echo $info['name']; ?>"
+                autofocus>
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['name']; ?></span>
             </td>
         </tr>
@@ -54,9 +85,13 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <?php echo __('Type');?>:
             </td>
             <td>
+                <label>
                 <input type="radio" name="ispublic" value="1" <?php echo $info['ispublic']?'checked="checked"':''; ?>><strong><?php echo __('Public');?></strong>
+                </label>
                 &nbsp;
+                <label>
                 <input type="radio" name="ispublic" value="0" <?php echo !$info['ispublic']?'checked="checked"':''; ?>><strong><?php echo __('Private');?></strong> <?php echo mb_convert_case(__('(internal)'), MB_CASE_TITLE);?>
+                </label>
                 &nbsp;<i class="help-tip icon-question-sign" href="#type"></i>
             </td>
         </tr>
@@ -107,14 +142,30 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
         <tr>
             <td><?php echo __('Ticket Assignment'); ?>:</td>
             <td>
-                <span>
+                <label>
                 <input type="checkbox" name="assign_members_only" <?php echo
                 $info['assign_members_only']?'checked="checked"':''; ?>>
                 <?php echo __('Restrict ticket assignment to department members'); ?>
+                </label>
                 <i class="help-tip icon-question-sign" href="#sandboxing"></i>
-                </span>
             </td>
         </tr>
+
+        <tr>
+            <td><?php echo __('Claim on Response'); ?>:</td>
+            <td>
+                <label>
+                <input type="checkbox" name="disable_auto_claim" <?php echo
+                 $info['disable_auto_claim'] ? 'checked="checked"' : ''; ?>>
+                <?php echo sprintf('<strong>%s</strong> %s',
+                        __('Disable'),
+                        __('auto claim')); ?>
+                </label>
+                <i class="help-tip icon-question-sign"
+                href="#disable_auto_claim"></i>
+            </td>
+        </tr>
+
         <tr>
             <th colspan="2">
                 <em><strong><?php echo __('Outgoing Email Settings'); ?></strong>:</em>
@@ -173,12 +224,12 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <?php echo __('New Ticket');?>:
             </td>
             <td>
-                <span>
+                <label>
                 <input type="checkbox" name="ticket_auto_response" value="0" <?php echo !$info['ticket_auto_response']?'checked="checked"':''; ?> >
 
                 <?php echo __('<strong>Disable</strong> for this Department'); ?>
+                </label>
                 <i class="help-tip icon-question-sign" href="#new_ticket"></i>
-                </span>
             </td>
         </tr>
         <tr>
@@ -186,11 +237,11 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <?php echo __('New Message');?>:
             </td>
             <td>
-                <span>
+                <label>
                 <input type="checkbox" name="message_auto_response" value="0" <?php echo !$info['message_auto_response']?'checked="checked"':''; ?> >
                 <?php echo __('<strong>Disable</strong> for this Department'); ?>
+                </label>
                 <i class="help-tip icon-question-sign" href="#new_message"></i>
-                </span>
             </td>
         </tr>
         <tr>
@@ -236,7 +287,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 <?php foreach (array(
     Dept::ALERTS_DISABLED =>        __("No one (disable Alerts and Notices)"),
     Dept::ALERTS_DEPT_ONLY =>       __("Department members only"),
-    Dept::ALERTS_DEPT_AND_GROUPS => __("Department and Group members"),
+    Dept::ALERTS_DEPT_AND_EXTENDED => __("Department and extended access members"),
 ) as $mode=>$desc) { ?>
     <option value="<?php echo $mode; ?>" <?php
         if ($info['group_membership'] == $mode) echo 'selected="selected"';
@@ -247,30 +298,6 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 </span>
             </td>
         </tr>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Group Access'); ?></strong>:
-                <?php echo __('Check all groups allowed to access this department.'); ?>
-                <i class="help-tip icon-question-sign" href="#department_access"></i></em>
-            </th>
-        </tr>
-        <?php
-         $sql='SELECT group_id, group_name, count(staff.staff_id) as members '
-             .' FROM '.GROUP_TABLE.' grp '
-             .' LEFT JOIN '.STAFF_TABLE. ' staff USING(group_id) '
-             .' GROUP by grp.group_id '
-             .' ORDER BY group_name';
-         if(($res=db_query($sql)) && db_num_rows($res)){
-            while(list($id, $name, $members) = db_fetch_row($res)) {
-                if($members>0)
-                    $members=sprintf('<a href="staff.php?a=filter&gid=%d">%d</a>', $id, $members);
-
-                $ck=($info['groups'] && in_array($id,$info['groups']))?'checked="checked"':'';
-                echo sprintf('<tr><td colspan=2>&nbsp;&nbsp;<label><input type="checkbox" name="groups[]" value="%d" %s>&nbsp;%s</label> (%s)</td></tr>',
-                        $id, $ck, $name, $members);
-            }
-         }
-        ?>
         <tr>
             <th colspan="2">
                 <em><strong><?php echo __('Department Signature'); ?></strong>:
@@ -286,9 +313,148 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
         </tr>
     </tbody>
 </table>
+</div>
+
+<div id="access" class="hidden tab_content">
+  <table class="two-column table" width="100%">
+    <tbody>
+        <tr class="header">
+            <td colspan="2">
+                <?php echo __('Department Members'); ?>
+                <div><small>
+                <?php echo __('Agents who are primary members of this department'); ?>
+                </small></div>
+            </td>
+        </tr>
+<?php
+$agents = Staff::getStaffMembers();
+foreach ($dept->getMembers() as $member) {
+    unset($agents[$member->getId()]);
+} ?>
+      <tr id="add_extended_access">
+        <td colspan="2">
+          <i class="icon-plus-sign"></i>
+          <select id="add_access" data-quick-add="staff">
+            <option value="0">&mdash; <?php echo __('Select Agent');?> &mdash;</option>
+            <?php
+            foreach ($agents as $id=>$name) {
+              echo sprintf('<option value="%d">%s</option>',$id,Format::htmlchars($name));
+            }
+            ?>
+            <option value="0" data-quick-add>&mdash; <?php echo __('Add New');?> &mdash;</option>
+          </select>
+          <button type="button" class="action-button">
+            <?php echo __('Add'); ?>
+          </button>
+        </td>
+      </tr>
+    </tbody>
+    <tbody>
+      <tr id="member_template" class="hidden">
+        <td>
+          <input type="hidden" data-name="members[]" value="" />
+        </td>
+        <td>
+          <select data-name="member_role" data-quick-add="role">
+            <option value="0">&mdash; <?php echo __('Select Role');?> &mdash;</option>
+            <?php
+            foreach (Role::getRoles() as $id=>$name) {
+              echo sprintf('<option value="%d" %s>%s</option>',$id,$sel,$name);
+            }
+            ?>
+            <option value="0" data-quick-add>&mdash; <?php echo __('Add New');?> &mdash;</option>
+          </select>
+          <span style="display:inline-block;width:60px"> </span>
+          <label class="inline checkbox">
+            <input type="checkbox" data-name="member_alerts" value="1" />
+            <?php echo __('Alerts'); ?>
+          </label>
+          <a href="#" class="pull-right drop-membership" title="<?php echo __('Delete');
+            ?>"><i class="icon-trash"></i></a>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+</div>
+
 <p style="text-align:center">
     <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
     <input type="reset"  name="reset"  value="<?php echo __('Reset');?>">
-    <input type="button" name="cancel" value="<?php echo __('Cancel');?>" onclick='window.location.href="departments.php"'>
+    <input type="button" name="cancel" value="<?php echo __('Cancel');?>"
+        onclick='window.location.href="?"'>
 </p>
 </form>
+
+<script type="text/javascript">
+var addAccess = function(staffid, name, role, alerts, primary, error) {
+  if (!staffid) return;
+  var copy = $('#member_template').clone();
+
+  copy.find('td:first').append(document.createTextNode(name));
+  if (primary) {
+    copy.find('td:first').append($('<span class="faded">').text(primary));
+    copy.find('td:last').empty();
+  }
+  else {
+    copy.find('[data-name^=member_alerts]')
+      .attr('name', 'member_alerts['+staffid+']')
+      .prop('checked', alerts);
+    copy.find('[data-name^=member_role]')
+      .attr('name', 'member_role['+staffid+']')
+      .val(role || 0);
+    copy.find('[data-name=members\\[\\]]')
+      .attr('name', 'members[]')
+      .val(staffid);
+  }
+  copy.attr('id', '').show().insertBefore($('#add_extended_access'));
+  copy.removeClass('hidden')
+  if (error)
+      $('<div class="error">').text(error).appendTo(copy.find('td:last'));
+  copy.find('.drop-membership').click(function() {
+    $('#add_access').append(
+      $('<option>')
+      .attr('value', copy.find('input[name^=members][type=hidden]').val())
+      .text(copy.find('td:first').text())
+    );
+    copy.fadeOut(function() { $(this).remove(); });
+    return false;
+  });
+};
+
+$('#add_extended_access').find('button').on('click', function() {
+  var selected = $('#add_access').find(':selected'),
+      id = parseInt(selected.val());
+  if (!id)
+    return;
+  addAccess(id, selected.text(), 0, true);
+  selected.remove();
+  return false;
+});
+
+<?php
+if ($dept) {
+    $members = $dept->members->all();
+    foreach ($dept->extended as $x) {
+        if (!$x->staff)
+            continue;
+        $members[] = new AnnotatedModel($x->staff, array(
+            'alerts' => $x->isAlertsEnabled(),
+            'role_id' => $x->role_id,
+        ));
+    }
+    usort($members, function($a, $b) { return strcmp($a->getName(), $b->getName()); });
+
+    foreach ($members as $member) {
+        $primary = $member->dept_id == $info['id'];
+        echo sprintf('addAccess(%d, %s, %d, %d, %s, %s);',
+            $member->getId(),
+            JsonDataEncoder::encode((string) $member->getName()),
+            $member->role_id,
+            $member->get('alerts', 0),
+            JsonDataEncoder::encode($primary ? ' — '.__('Primary') : ''),
+            JsonDataEncoder::encode($errors['members'][$member->staff_id])
+        );
+    }
+}
+?>
+</script>
diff --git a/include/staff/departments.inc.php b/include/staff/departments.inc.php
index 267b728a005019027b904c54a7c58422c4b88b5c..2924eea68f11acb94209187472eef9b1892c03e6 100644
--- a/include/staff/departments.inc.php
+++ b/include/staff/departments.inc.php
@@ -1,141 +1,163 @@
 <?php
-if(!defined('OSTADMININC') || !$thisstaff->isAdmin()) die('Access Denied');
+if (!defined('OSTADMININC') || !$thisstaff->isAdmin())
+    die('Access Denied');
 
 $qs = array();
-$sql='SELECT dept.dept_id,dept_name,email.email_id,email.email,email.name as email_name,ispublic,count(staff.staff_id) as users '.
-     ',CONCAT_WS(" ",mgr.firstname,mgr.lastname) as manager,mgr.staff_id as manager_id,dept.created,dept.updated  FROM '.DEPT_TABLE.' dept '.
-     ' LEFT JOIN '.STAFF_TABLE.' mgr ON dept.manager_id=mgr.staff_id '.
-     ' LEFT JOIN '.EMAIL_TABLE.' email ON dept.email_id=email.email_id '.
-     ' LEFT JOIN '.STAFF_TABLE.' staff ON dept.dept_id=staff.dept_id ';
+$sortOptions=array(
+    'name' => 'name',
+    'type' => 'ispublic',
+    'members'=> 'members_count',
+    'email'=> 'email__name',
+    'manager'=>'manager__lastname'
+    );
 
-$sql.=' WHERE 1';
-$sortOptions=array('name'=>'dept.dept_name','type'=>'ispublic','users'=>'users','email'=>'email_name, email.email','manager'=>'manager');
-$orderWays=array('DESC'=>'DESC','ASC'=>'ASC');
-$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'name';
-//Sorting options...
-if($sort && $sortOptions[$sort]) {
-    $order_column =$sortOptions[$sort];
+$orderWays = array('DESC'=>'DESC', 'ASC'=>'ASC');
+$sort = ($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])]) ? strtolower($_REQUEST['sort']) : 'name';
+if ($sort && $sortOptions[$sort]) {
+    $order_column = $sortOptions[$sort];
 }
-$order_column=$order_column?$order_column:'dept.dept_name';
 
-if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) {
-    $order=$orderWays[strtoupper($_REQUEST['order'])];
+$order_column = $order_column ? $order_column : 'name';
+
+if ($_REQUEST['order'] && isset($orderWays[strtoupper($_REQUEST['order'])])) {
+    $order = $orderWays[strtoupper($_REQUEST['order'])];
+} else {
+    $order = 'ASC';
 }
-$order=$order?$order:'ASC';
 
-if($order_column && strpos($order_column,',')){
+if ($order_column && strpos($order_column,',')) {
     $order_column=str_replace(','," $order,",$order_column);
 }
 $x=$sort.'_sort';
 $$x=' class="'.strtolower($order).'" ';
-$order_by="$order_column $order ";
-
-$qs += array('order' => ($order=='DESC' ? 'ASC' : 'DESC'));
+$page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
+$count = Dept::objects()->count();
+$pageNav = new Pagenate($count, $page, PAGE_LIMIT);
 $qstr = '&amp;'. Http::build_query($qs);
-
-$query="$sql GROUP BY dept.dept_id ORDER BY $order_by";
-$res=db_query($query);
-if($res && ($num=db_num_rows($res)))
-    $showing=sprintf(_N('Showing %d department', 'Showing %d departments',
-        $num),$num);
-else
-    $showing=__('No departments found!');
-
+$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);
 ?>
-<div class="pull-left" style="width:700px;padding-top:5px;">
- <h2><?php echo __('Departments');?></h2>
- </div>
-<div class="pull-left flush-right" style="padding-top:5px;padding-right:5px;">
-    <b><a href="departments.php?a=add" class="Icon newDepartment"><?php echo __('Add New Department');?></a></b></div>
-<div class="clear"></div>
 <form action="departments.php" method="POST" name="depts">
+<div class="sticky bar">
+    <div class="content">
+        <div class="pull-left">
+            <h2><?php echo __('Departments');?></h2>
+        </div>
+        <div class="pull-right flush-right">
+            <a href="departments.php?a=add" class="green button action-button"><i class="icon-plus-sign"></i> <?php echo __('Add New Department');?></a>
+            <span class="action-button" data-dropdown="#action-dropdown-more">
+                <i class="icon-caret-down pull-right"></i>
+                <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+            </span>
+            <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                <ul id="actions">
+                    <li class="danger"><a class="confirm" data-name="delete" href="departments.php?a=delete">
+                        <i class="icon-trash icon-fixed-width"></i>
+                        <?php echo __('Delete'); ?></a></li>
+                </ul>
+            </div>
+        </div>
+        <div class="clear"></div>
+    </div>
+</div>
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="mass_process" >
  <input type="hidden" id="action" name="a" value="" >
  <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption><?php echo $showing; ?></caption>
     <thead>
         <tr>
-            <th width="7px">&nbsp;</th>
-            <th width="180"><a <?php echo $name_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name');?></a></th>
-            <th width="80"><a  <?php echo $type_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=type"><?php echo __('Type');?></a></th>
-            <th width="70"><a  <?php echo $users_sort; ?>href="departments.php?<?php echo $qstr; ?>&sort=users"><?php echo __('Agents');?></a></th>
-            <th width="300"><a  <?php echo $email_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=email"><?php echo __('Email Address');?></a></th>
-            <th width="200"><a  <?php echo $manager_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=manager"><?php echo __('Department Manager');?></a></th>
+            <th width="4%">&nbsp;</th>
+            <th width="28%"><a <?php echo $name_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name');?></a></th>
+            <th width="8%"><a  <?php echo $type_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=type"><?php echo __('Type');?></a></th>
+            <th width="8%"><a  <?php echo $users_sort; ?>href="departments.php?<?php echo $qstr; ?>&sort=users"><?php echo __('Agents');?></a></th>
+            <th width="30%"><a  <?php echo $email_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=email"><?php echo __('Email Address');?></a></th>
+            <th width="22%"><a  <?php echo $manager_sort; ?> href="departments.php?<?php echo $qstr; ?>&sort=manager"><?php echo __('Manager');?></a></th>
         </tr>
     </thead>
     <tbody>
     <?php
-        $total=0;
-        $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null;
-        if($res && db_num_rows($res)):
+        $ids= ($errors && is_array($_POST['ids'])) ? $_POST['ids'] : null;
+        if ($count) {
+            $depts = Dept::objects()
+                ->annotate(array(
+                        'members_count' => SqlAggregate::COUNT('members', true),
+                ))
+                ->order_by(sprintf('%s%s',
+                            strcasecmp($order, 'DESC') ? '' : '-',
+                            $order_column))
+                ->limit($pageNav->getLimit())
+                ->offset($pageNav->getStart());
             $defaultId=$cfg->getDefaultDeptId();
-            $defaultEmailId = $cfg->getDefaultEmail()->getId();
+            $defaultEmailId = $cfg->getDefaultEmailId();
             $defaultEmailAddress = (string) $cfg->getDefaultEmail();
-            while ($row = db_fetch_array($res)) {
+            foreach ($depts as $dept) {
+                $id = $dept->getId();
                 $sel=false;
-                if($ids && in_array($row['dept_id'],$ids))
+                if($ids && in_array($dept->getId(), $ids))
                     $sel=true;
 
-                if ($row['email_id'])
-                    $row['email']=$row['email_name']?($row['email_name'].' <'.$row['email'].'>'):$row['email'];
-                elseif($defaultEmailId) {
-                    $row['email_id'] = $defaultEmailId;
-                    $row['email'] = $defaultEmailAddress;
+                if ($dept->email) {
+                    $email = (string) $dept->email;
+                    $emailId = $dept->email->getId();
+                } else {
+                    $emailId = $defaultEmailId;
+                    $email = $defaultEmailAddress;
                 }
 
-                $default=($defaultId==$row['dept_id'])?' <small>'.__('(Default)').'</small>':'';
+                $default= ($defaultId == $dept->getId()) ?' <small>'.__('(Default)').'</small>' : '';
                 ?>
-            <tr id="<?php echo $row['dept_id']; ?>">
-                <td width=7px>
-                  <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['dept_id']; ?>"
-                            <?php echo $sel?'checked="checked"':''; ?>  <?php echo $default?'disabled="disabled"':''; ?> >
+            <tr id="<?php echo $id; ?>">
+                <td align="center">
+                  <input type="checkbox" class="ckb" name="ids[]"
+                  value="<?php echo $id; ?>"
+                  <?php echo $sel? 'checked="checked"' : ''; ?>
+                  <?php echo $default? 'disabled="disabled"' : ''; ?> >
                 </td>
-                <td><a href="departments.php?id=<?php echo $row['dept_id']; ?>"><?php echo $row['dept_name']; ?></a>&nbsp;<?php echo $default; ?></td>
-                <td><?php echo $row['ispublic']?__('Public'):'<b>'.__('Private').'</b>'; ?></td>
+                <td><a href="departments.php?id=<?php echo $id; ?>"><?php
+                echo Dept::getNameById($id); ?></a>&nbsp;<?php echo $default; ?></td>
+                <td><?php echo $dept->isPublic() ? __('Public') :'<b>'.__('Private').'</b>'; ?></td>
                 <td>&nbsp;&nbsp;
                     <b>
-                    <?php if($row['users']>0) { ?>
-                        <a href="staff.php?did=<?php echo $row['dept_id']; ?>"><?php echo $row['users']; ?></a>
+                    <?php if ($dept->members_count) { ?>
+                        <a href="staff.php?did=<?php echo $id; ?>"><?php echo $dept->members_count; ?></a>
                     <?php }else{ ?> 0
                     <?php } ?>
                     </b>
                 </td>
-                <td><span class="ltr"><a href="emails.php?id=<?php echo $row['email_id']; ?>"><?php
-                    echo Format::htmlchars($row['email']); ?></a></span></td>
-                <td><a href="staff.php?id=<?php echo $row['manager_id']; ?>"><?php echo $row['manager']; ?>&nbsp;</a></td>
+                <td><span class="ltr"><a href="emails.php?id=<?php echo $emailId; ?>"><?php
+                    echo Format::htmlchars($email); ?></a></span></td>
+                <td><a href="staff.php?id=<?php echo $dept->manager_id; ?>"><?php
+                    echo $dept->manager_id ? $dept->manager : ''; ?>&nbsp;</a></td>
             </tr>
             <?php
-            } //end of while.
-        endif; ?>
+            } //end of foreach.
+        } ?>
     <tfoot>
      <tr>
         <td colspan="6">
-            <?php if($res && $num){ ?>
+            <?php
+            if ($count) { ?>
             <?php echo __('Select');?>:&nbsp;
             <a id="selectAll" href="#ckb"><?php echo __('All');?></a>&nbsp;&nbsp;
             <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>
     </tfoot>
 </table>
 <?php
-if($res && $num): //Show options..
+if ($count):
+    echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
 ?>
-<p class="centered" id="actions">
-    <input class="button" type="submit" name="make_public" value="<?php echo __('Make Public');?>" >
-    <input class="button" type="submit" name="make_private" value="<?php echo __('Make Private');?>" >
-    <input class="button" type="submit" name="delete" value="<?php echo __('Delete Dept(s)');?>" >
-</p>
 <?php
 endif;
 ?>
 </form>
-
 <div style="display:none;" class="dialog" id="confirm-action">
     <h3><?php echo __('Please Confirm');?></h3>
     <a class="close" href=""><i class="icon-remove-circle"></i></a>
diff --git a/include/staff/directory.inc.php b/include/staff/directory.inc.php
index 15b717c141fbc5f39ec4c04964a6cde896f3e0e6..9eb4926dd6825ace920ad39536e8521ae422102f 100644
--- a/include/staff/directory.inc.php
+++ b/include/staff/directory.inc.php
@@ -1,95 +1,91 @@
 <?php
 if(!defined('OSTSTAFFINC') || !$thisstaff || !$thisstaff->isStaff()) die('Access Denied');
 $qs = array();
-$select='SELECT staff.*,dept.dept_name as dept ';
-$from='FROM '.STAFF_TABLE.' staff '.
-      'LEFT JOIN '.DEPT_TABLE.' dept ON(staff.dept_id=dept.dept_id) ';
-$where='WHERE staff.isvisible=1 ';
+
+$agents = Staff::objects()
+    ->select_related('dept');
 
 if($_REQUEST['q']) {
     $searchTerm=$_REQUEST['q'];
     if($searchTerm){
-        $query=db_real_escape($searchTerm,false); //escape the term ONLY...no quotes.
         if(is_numeric($searchTerm)){
-            $where.=" AND (staff.phone LIKE '%$query%' OR staff.phone_ext LIKE '%$query%' OR staff.mobile LIKE '%$query%') ";
+            $agents->filter(Q::any(array(
+                'phone__contains'=>$searchTerm,
+                'phone_ext__contains'=>$searchTerm,
+                'mobile__contains'=>$searchTerm,
+            )));
         }elseif(strpos($searchTerm,'@') && Validator::is_email($searchTerm)){
-            $where.=" AND staff.email='$query'";
+            $agents->filter(array('email'=>$searchTerm));
         }else{
-            $where.=" AND ( staff.email LIKE '%$query%'".
-                         " OR staff.lastname LIKE '%$query%'".
-                         " OR staff.firstname LIKE '%$query%'".
-                        ' ) ';
+            $agents->filter(Q::any(array(
+                'email__contains'=>$searchTerm,
+                'lastname__contains'=>$searchTerm,
+                'firstname__contains'=>$searchTerm,
+            )));
         }
     }
 }
 
 if($_REQUEST['did'] && is_numeric($_REQUEST['did'])) {
-    $where.=' AND staff.dept_id='.db_input($_REQUEST['did']);
+    $agents->filter(array('dept'=>$_REQUEST['did']));
     $qs += array('did' => $_REQUEST['did']);
 }
 
-$sortOptions=array('name'=>'staff.firstname,staff.lastname','email'=>'staff.email','dept'=>'dept.dept_name',
-                   'phone'=>'staff.phone','mobile'=>'staff.mobile','ext'=>'phone_ext',
-                   'created'=>'staff.created','login'=>'staff.lastlogin');
+$sortOptions=array('name'=>array('firstname','lastname'),'email'=>'email','dept'=>'dept__name',
+                   'phone'=>'phone','mobile'=>'mobile','ext'=>'phone_ext',
+                   'created'=>'created','login'=>'lastlogin');
+$orderWays=array('DESC'=>'-','ASC'=>'');
 
-switch ($cfg->getDefaultNameFormat()) {
+switch ($cfg->getAgentNameFormat()) {
 case 'last':
 case 'lastfirst':
 case 'legal':
-    $sortOptions['name'] = 'staff.lastname, staff.firstname';
+    $sortOptions['name'] = array('lastname', 'firstname');
     break;
 // Otherwise leave unchanged
 }
 
-$orderWays=array('DESC'=>'DESC','ASC'=>'ASC');
 $sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'name';
 //Sorting options...
 if($sort && $sortOptions[$sort]) {
     $order_column =$sortOptions[$sort];
 }
-$order_column=$order_column?$order_column:'staff.firstname,staff.lastname';
+$order_column = $order_column ?: 'firstname,lastname';
 
 if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) {
     $order=$orderWays[strtoupper($_REQUEST['order'])];
 }
 
-$order=$order?$order:'ASC';
-if($order_column && strpos($order_column,',')){
-    $order_column=str_replace(','," $order,",$order_column);
-}
 $x=$sort.'_sort';
-$$x=' class="'.strtolower($order).'" ';
-$order_by="$order_column $order ";
+$$x=' class="'.strtolower($_REQUEST['order'] ?: 'desc').'" ';
+foreach ((array) $order_column as $C) {
+    $agents->order_by($order.$C);
+}
 
-$total=db_count('SELECT count(DISTINCT staff.staff_id) '.$from.' '.$where);
+$total=$agents->count();
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 $pageNav=new Pagenate($total, $page, PAGE_LIMIT);
 $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.='&amp;order='.($order=='DESC' ? 'ASC' : 'DESC');
-$query="$select $from $where GROUP BY staff.staff_id ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
-//echo $query;
+
 ?>
-<h2><?php echo __('Agents');?>
-&nbsp;<i class="help-tip icon-question-sign" href="#staff_members"></i></h2>
-<div class="pull-left" style="width:700px">
+
+<div id="basic_search">
+    <div style="min-height:25px;">
     <form action="directory.php" method="GET" name="filter">
        <input type="text" name="q" value="<?php echo Format::htmlchars($_REQUEST['q']); ?>" >
         <select name="did" id="did">
              <option value="0">&mdash; <?php echo __('All Departments');?> &mdash;</option>
              <?php
-             $sql='SELECT dept.dept_id, dept.dept_name,count(staff.staff_id) as users  '.
-                  'FROM '.DEPT_TABLE.' dept '.
-                  'INNER JOIN '.STAFF_TABLE.' staff ON(staff.dept_id=dept.dept_id AND staff.isvisible=1) '.
-                  'GROUP By dept.dept_id HAVING users>0 ORDER BY dept_name';
-             if(($res=db_query($sql)) && db_num_rows($res)){
-                 while(list($id,$name, $users)=db_fetch_row($res)){
-                     $sel=($_REQUEST['did'] && $_REQUEST['did']==$id)?'selected="selected"':'';
-                     echo sprintf('<option value="%d" %s>%s (%s)</option>',$id,$sel,$name,$users);
-                 }
-             }
+                foreach (Dept::getDepartments(array('nonempty'=>1)) as $id=>$name) {
+                    $sel=($_REQUEST['did'] && $_REQUEST['did']==$id)?'selected="selected"':'';
+                    echo sprintf('<option value="%d" %s>%s</option>',$id,$sel,$name);
+                }
              ?>
         </select>
         &nbsp;&nbsp;
@@ -97,48 +93,51 @@ $query="$select $from $where GROUP BY staff.staff_id ORDER BY $order_by LIMIT ".
         &nbsp;<i class="help-tip icon-question-sign" href="#apply_filtering_criteria"></i>
     </form>
  </div>
+</div>
 <div class="clear"></div>
-<?php
-$res=db_query($query);
-if($res && ($num=db_num_rows($res)))
-    $showing=$pageNav->showing();
-else
-    $showing=__('No agents found!');
-?>
+<div style="margin-bottom:20px; padding-top:5px;">
+    <div class="pull-left flush-left">
+        <h2><?php echo __('Agents');?>
+            &nbsp;<i class="help-tip icon-question-sign" href="#staff_members"></i>
+        </h2>
+    </div>
+</div>
+    <?php
+    if ($agents->exists(true))
+        $showing=$pageNav->showing();
+    else
+        $showing=__('No agents found!');
+    ?>
 <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption><?php echo $showing; ?></caption>
     <thead>
         <tr>
-            <th width="160"><a <?php echo $name_sort; ?> href="directory.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name');?></a></th>
-            <th width="150"><a  <?php echo $dept_sort; ?>href="directory.php?<?php echo $qstr; ?>&sort=dept"><?php echo __('Department');?></a></th>
-            <th width="180"><a  <?php echo $email_sort; ?>href="directory.php?<?php echo $qstr; ?>&sort=email"><?php echo __('Email Address');?></a></th>
-            <th width="120"><a <?php echo $phone_sort; ?> href="directory.php?<?php echo $qstr; ?>&sort=phone"><?php echo __('Phone Number');?></a></th>
-            <th width="80"><a <?php echo $ext_sort; ?> href="directory.php?<?php echo $qstr; ?>&sort=ext"><?php echo __(/* As in a phone number `extension` */ 'Extension');?></a></th>
-            <th width="120"><a <?php echo $mobile_sort; ?> href="directory.php?<?php echo $qstr; ?>&sort=mobile"><?php echo __('Mobile Number');?></a></th>
+            <th width="20%"><a <?php echo $name_sort; ?> href="directory.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name');?></a></th>
+            <th width="15%"><a  <?php echo $dept_sort; ?>href="directory.php?<?php echo $qstr; ?>&sort=dept"><?php echo __('Department');?></a></th>
+            <th width="25%"><a  <?php echo $email_sort; ?>href="directory.php?<?php echo $qstr; ?>&sort=email"><?php echo __('Email Address');?></a></th>
+            <th width="15%"><a <?php echo $phone_sort; ?> href="directory.php?<?php echo $qstr; ?>&sort=phone"><?php echo __('Phone Number');?></a></th>
+            <th width="10%"><a <?php echo $ext_sort; ?> href="directory.php?<?php echo $qstr; ?>&sort=ext"><?php echo __(/* As in a phone number `extension` */ 'Extension');?></a></th>
+            <th width="15%"><a <?php echo $mobile_sort; ?> href="directory.php?<?php echo $qstr; ?>&sort=mobile"><?php echo __('Mobile Number');?></a></th>
         </tr>
     </thead>
     <tbody>
     <?php
-        if($res && db_num_rows($res)):
-            $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null;
-            while ($row = db_fetch_array($res)) {
-            $name = new PersonsName(array('first' => $row['firstname'], 'last' => $row['lastname']));
-?>
-               <tr id="<?php echo $row['staff_id']; ?>">
-                <td>&nbsp;<?php echo Format::htmlchars($name); ?></td>
-                <td>&nbsp;<?php echo Format::htmlchars($row['dept']); ?></td>
-                <td>&nbsp;<?php echo Format::htmlchars($row['email']); ?></td>
-                <td>&nbsp;<?php echo Format::phone($row['phone']); ?></td>
-                <td>&nbsp;<?php echo $row['phone_ext']; ?></td>
-                <td>&nbsp;<?php echo Format::phone($row['mobile']); ?></td>
-               </tr>
+        $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null;
+        foreach ($agents as $A) { ?>
+           <tr id="<?php echo $A->staff_id; ?>">
+                <td>&nbsp;<?php echo Format::htmlchars($A->getName()); ?></td>
+                <td>&nbsp;<?php echo Format::htmlchars((string) $A->dept); ?></td>
+                <td>&nbsp;<?php echo Format::htmlchars($A->email); ?></td>
+                <td>&nbsp;<?php echo Format::phone($A->phone); ?></td>
+                <td>&nbsp;<?php echo $A->phone_ext; ?></td>
+                <td>&nbsp;<?php echo Format::phone($A->mobile); ?></td>
+           </tr>
             <?php
-            } //end of while.
-        endif; ?>
+            } // end of foreach
+        ?>
     <tfoot>
      <tr>
         <td colspan="6">
-            <?php if($res && $num) {
+            <?php if ($agents->exists(true)) {
                 echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
                 ?>
             <?php } else {
diff --git a/include/staff/dynamic-form.inc.php b/include/staff/dynamic-form.inc.php
index f5518ee0245b84c336b8d395c8b622faf8095983..24361af87eeefb20e3dc3dec8d8f2504fc1855c4 100644
--- a/include/staff/dynamic-form.inc.php
+++ b/include/staff/dynamic-form.inc.php
@@ -6,8 +6,21 @@ if($form && $_REQUEST['a']!='add') {
     $action = 'update';
     $url = "?id=".urlencode($_REQUEST['id']);
     $submit_text=__('Save Changes');
-    $info = $form->ht;
+    $info = $form->getInfo();
+    $trans = array(
+        'title' => $form->getTranslateTag('title'),
+        'instructions' => $form->getTranslateTag('instructions'),
+    );
     $newcount=2;
+    $translations = CustomDataTranslation::allTranslations($trans, 'phrase');
+    $_keys = array_flip($trans);
+    foreach ($translations as $t) {
+        if (!Internationalization::isLanguageEnabled($t->lang))
+            continue;
+        // Create keys of [trans][de_DE][title] for instance
+        $info['trans'][$t->lang][$_keys[$t->object_hash]]
+            = Format::viewableImages($t->text);
+    }
 } else {
     $title = __('Add new custom form section');
     $action = 'add';
@@ -23,12 +36,16 @@ $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 $form ? Format::htmlchars($form->getTitle()) : __('Custom Form'); ?></h2>
+
+    <h2><?php echo $title; ?>
+    <?php if (isset($info['title'])) { ?><small>
+    — <?php echo $info['title']; ?></small>
+        <?php } ?>
+    </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 __(
                 'Forms are used to allow for collection of custom data'
                 ); ?></em>
@@ -36,22 +53,64 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
         </tr>
     </thead>
     <tbody style="vertical-align:top">
-        <tr>
-            <td width="180" class="required"><?php echo __('Title'); ?>:</td>
-            <td><input type="text" name="title" size="40" value="<?php
-                echo $info['title']; ?>"/>
+      <tr>
+        <td colspan="2">
+<?php
+$langs = Internationalization::getConfiguredSystemLanguages();
+if ($form && count($langs) > 1) { ?>
+    <ul class="alt tabs clean" id="translations">
+        <li class="empty"><i class="icon-globe" title="This content is translatable"></i></li>
+<?php foreach ($langs as $tag=>$nfo) { ?>
+    <li class="<?php if ($tag == $cfg->getPrimaryLanguage()) echo "active";
+        ?>"><a href="#translation-<?php echo $tag; ?>" title="<?php
+        echo Internationalization::getLanguageDescription($tag);
+    ?>"><span class="flag flag-<?php echo strtolower($nfo['flag']); ?>"></span>
+    </a></li>
+<?php } ?>
+    </ul>
+<?php
+} ?>
+    <div id="translations_container">
+        <div id="translation-<?php echo $cfg->getPrimaryLanguage(); ?>" class="tab_content"
+            lang="<?php echo $cfg->getPrimaryLanguage(); ?>">
+            <div class="required"><?php echo __('Title'); ?>:</div>
+            <div>
+            <input type="text" name="title" size="60" autofocus
+                value="<?php echo $info['title']; ?>"/>
                 <i class="help-tip icon-question-sign" href="#form_title"></i>
-                <font class="error"><?php
-                    if ($errors['title']) echo '<br/>'; echo $errors['title']; ?></font>
-            </td>
-        </tr>
-        <tr>
-            <td width="180"><?php echo __('Instructions'); ?>:</td>
-            <td><textarea name="instructions" rows="3" cols="40"><?php
-                echo $info['instructions']; ?></textarea>
+                <div class="error"><?php
+                    if ($errors['title']) echo '<br/>'; echo $errors['title']; ?></div>
+            </div>
+            <div style="margin-top: 8px"><?php echo __('Instructions'); ?>:
                 <i class="help-tip icon-question-sign" href="#form_instructions"></i>
-            </td>
-        </tr>
+                </div>
+            <textarea name="instructions" rows="3" cols="40" class="richtext small"><?php
+                echo $info['instructions']; ?></textarea>
+        </div>
+
+<?php if ($langs && $form) {
+    foreach ($langs as $tag=>$nfo) {
+        if ($tag == $cfg->getPrimaryLanguage())
+            continue; ?>
+        <div id="translation-<?php echo $tag; ?>" class="tab_content"
+            style="display:none;" lang="<?php echo $tag; ?>">
+        <div>
+            <div class="required"><?php echo __('Title'); ?>:</div>
+            <input type="text" name="trans[<?php echo $tag; ?>][title]" size="60"
+                value="<?php echo $info['trans'][$tag]['title']; ?>"/>
+                <i class="help-tip icon-question-sign" href="#form_title"></i>
+        </div>
+        <div style="margin-top: 8px"><?php echo __('Instructions'); ?>:
+            <i class="help-tip icon-question-sign" href="#form_instructions"></i>
+            </div>
+        <textarea name="trans[<?php echo $tag; ?>][instructions]" cols="21" rows="12"
+            style="width:100%" class="richtext small"><?php
+            echo $info['trans'][$tag]['instructions']; ?></textarea>
+        </div>
+<?php }
+} ?>
+        </td>
+      </tr>
     </tbody>
     </table>
     <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
@@ -79,17 +138,13 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
         $uform = UserForm::objects()->all();
         $ftypes = FormField::allTypes();
         foreach ($uform[0]->getFields() as $f) {
-            if ($f->get('private')) continue;
+            if (!$f->isVisibleToUsers()) continue;
         ?>
         <tr>
             <td></td>
             <td><?php echo $f->get('label'); ?></td>
             <td><?php $t=FormField::getFieldType($f->get('type')); echo __($t[0]); ?></td>
-            <td><?php
-                $rmode = $f->getRequirementMode();
-                $modes = $f->getAllRequirementModes();
-                echo $modes[$rmode]['desc'];
-            ?></td>
+            <td><?php echo $f->getVisibilityDescription(); ?></td>
             <td><?php echo $f->get('name'); ?></td>
             <td><input type="checkbox" disabled="disabled"/></td></tr>
 
@@ -104,7 +159,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
             </th>
         </tr>
         <tr>
-            <th nowrap
+            <th nowrap width="4%"
                 ><i class="help-tip icon-question-sign" href="#field_sort"></i></th>
             <th nowrap><?php echo __('Label'); ?>
                 <i class="help-tip icon-question-sign" href="#field_label"></i></th>
@@ -123,15 +178,16 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
         $id = $f->get('id');
         $deletable = !$f->isDeletable() ? 'disabled="disabled"' : '';
         $force_name = $f->isNameForced() ? 'disabled="disabled"' : '';
-        $rmode = $f->getRequirementMode();
         $fi = $f->getImpl();
         $ferrors = $f->errors(); ?>
         <tr>
-            <td><i class="icon-sort"></i></td>
+            <td align="center"><i class="icon-sort"></i></td>
             <td><input type="text" size="32" name="label-<?php echo $id; ?>"
+                data-translate-tag="<?php echo $f->getTranslateTag('label'); ?>"
                 value="<?php echo Format::htmlchars($f->get('label')); ?>"/>
                 <font class="error"><?php
                     if ($ferrors['label']) echo '<br/>'; echo $ferrors['label']; ?>
+                </font>
             </td>
             <td nowrap><select style="max-width:150px" name="type-<?php echo $id; ?>" <?php
                 if (!$fi->isChangeable()) echo 'disabled="disabled"'; ?>>
@@ -157,12 +213,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                     "><i class="icon-edit"></i> <?php echo __('Config'); ?></a>
             <?php } ?></td>
             <td>
-                <select name="visibility-<?php echo $id; ?>">
-<?php foreach ($f->getAllRequirementModes() as $m=>$I) { ?>
-    <option value="<?php echo $m; ?>" <?php if ($rmode == $m)
-         echo 'selected="selected"'; ?>><?php echo $I['desc']; ?></option>
-<?php } ?>
-                <select>
+                <?php echo $f->getVisibilityDescription(); ?>
             </td>
             <td>
                 <input type="text" size="20" name="name-<?php echo $id; ?>"
@@ -172,18 +223,20 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                     if ($ferrors['name']) echo '<br/>'; echo $ferrors['name'];
                 ?></font>
                 </td>
-            <td><input class="delete-box" type="checkbox" name="delete-<?php echo $id; ?>"
+            <td align="center">
+                <input class="delete-box" type="checkbox" name="delete-<?php echo $id; ?>"
                     data-field-label="<?php echo $f->get('label'); ?>"
                     data-field-id="<?php echo $id; ?>"
                     <?php echo $deletable; ?>/>
                 <input type="hidden" name="sort-<?php echo $id; ?>"
                     value="<?php echo $f->get('sort'); ?>"/>
-                </td>
+            </td>
         </tr>
+    <tr>
     <?php
     }
     for ($i=0; $i<$newcount; $i++) { ?>
-            <td><em>+</em>
+            <td align="center"><em>+</em>
                 <input type="hidden" name="sort-new-<?php echo $i; ?>"
                     value="<?php echo $info["sort-new-$i"]; ?>"/></td>
             <td><input type="text" size="32" name="label-new-<?php echo $i; ?>"
@@ -200,9 +253,10 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                     <?php } ?>
                 </optgroup>
                 <?php } ?>
-            </select></td>
-            <td>
-                <select name="visibility-new-<?php echo $i; ?>">
+            </select>
+        </td>
+        <td>
+            <select name="visibility-new-<?php echo $i; ?>">
 <?php
     $rmode = $info['visibility-new-'.$i];
     foreach (DynamicFormField::allRequirementModes() as $m=>$I) { ?>
@@ -210,12 +264,15 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
          echo 'selected="selected"'; ?>><?php echo $I['desc']; ?></option>
 <?php } ?>
                 <select>
-            <td><input type="text" size="20" name="name-new-<?php echo $i; ?>"
-                value="<?php echo $info["name-new-$i"]; ?>"/>
-                <font class="error"><?php
-                    if ($errors["new-$i"]['name']) echo '<br/>'; echo $errors["new-$i"]['name'];
-                ?></font>
-            <td></td>
+                    <td><input type="text" size="20" name="name-new-<?php echo $i; ?>"
+                        value="<?php echo $info["name-new-$i"]; ?>"/>
+                        <font class="error"><?php
+                            if ($errors["new-$i"]['name']) echo '<br/>'; echo $errors["new-$i"]['name'];
+                            ?>
+                        </font>
+                    </td>
+                </select>
+            </select>
         </tr>
     <?php } ?>
     </tbody>
@@ -241,7 +298,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 </p>
 
 <div style="display:none;" class="draggable dialog" id="delete-confirm">
-    <h3><i class="icon-trash"></i> <?php echo __('Remove Existing Data?'); ?></h3>
+    <h3 class="drag-handle"><i class="icon-trash"></i> <?php echo __('Remove Existing Data?'); ?></h3>
     <a class="close" href=""><i class="icon-remove-circle"></i></a>
     <hr/>
     <p>
diff --git a/include/staff/dynamic-forms.inc.php b/include/staff/dynamic-forms.inc.php
index bfa399490f858aebbc46bdf25354d010be718a2e..7dafb2c6b4c959aee7bb70ae7fc5ca988eda8a3f 100644
--- a/include/staff/dynamic-forms.inc.php
+++ b/include/staff/dynamic-forms.inc.php
@@ -1,28 +1,52 @@
-<div class="pull-left" style="width:700;padding-top:5px;">
- <h2><?php echo __('Custom Forms'); ?></h2>
+<form action="forms.php" method="POST" name="forms">
+
+<div class="sticky bar opaque">
+    <div class="content">
+        <div class="pull-left flush-left">
+            <h2><?php echo __('Custom Forms'); ?></h2>
+        </div>
+        <div class="pull-right flush-right">
+            <a href="forms.php?a=add" class="green button action-button"><i class="icon-plus-sign"></i> <?php
+                    echo __('Add New Custom Form'); ?></a>
+            <span class="action-button" data-dropdown="#action-dropdown-more">
+                    <i class="icon-caret-down pull-right"></i>
+                    <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+            </span>
+            <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                <ul id="actions">
+                    <li class="danger">
+                        <a class="confirm" data-name="delete" href="forms.php?a=delete">
+                            <i class="icon-trash icon-fixed-width"></i>
+                            <?php echo __( 'Delete'); ?>
+                        </a>
+                    </li>
+                </ul>
+            </div>
+        </div>
+    </div>
 </div>
-<div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;">
-<b><a href="forms.php?a=add" class="Icon form-add"><?php
-    echo __('Add New Custom Form'); ?></a></b></div>
 <div class="clear"></div>
 
 <?php
+$other_forms = DynamicForm::objects()
+    ->filter(array('type'=>'G'))
+    ->exclude(array('flags__hasbit' => DynamicForm::FLAG_DELETED));
+
 $page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
-$count = DynamicForm::objects()->filter(array('type__in'=>array('G')))->count();
+$count = $other_forms->count();
 $pageNav = new Pagenate($count, $page, PAGE_LIMIT);
 $pageNav->setURL('forms.php');
 $showing=$pageNav->showing().' '._N('form','forms',$count);
 ?>
 
-<form action="forms.php" method="POST" name="forms">
 <?php csrf_token(); ?>
 <input type="hidden" name="do" value="mass_process" >
 <input type="hidden" id="action" name="a" value="" >
 <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
     <thead>
         <tr>
-            <th width="7">&nbsp;</th>
-            <th><?php echo __('Built-in Forms'); ?></th>
+            <th width="4%">&nbsp;</th>
+            <th width="50%"><?php echo __('Built-in Forms'); ?></th>
             <th><?php echo __('Last Updated'); ?></th>
         </tr>
     </thead>
@@ -31,6 +55,7 @@ $showing=$pageNav->showing().' '._N('form','forms',$count);
     $forms = array(
         'U' => 'icon-user',
         'T' => 'icon-ticket',
+        'A' => 'icon-tasks',
         'C' => 'icon-building',
         'O' => 'icon-group',
     );
@@ -38,7 +63,7 @@ $showing=$pageNav->showing().' '._N('form','forms',$count);
             ->filter(array('type__in'=>array_keys($forms)))
             ->order_by('type', 'title') as $form) { ?>
         <tr>
-        <td><i class="<?php echo $forms[$form->get('type')]; ?>"></i></td>
+        <td align="center"><i class="<?php echo $forms[$form->get('type')]; ?>"></i></td>
             <td><a href="?id=<?php echo $form->get('id'); ?>">
                 <?php echo $form->get('title'); ?></a>
             <td><?php echo $form->get('updated'); ?></td>
@@ -46,24 +71,22 @@ $showing=$pageNav->showing().' '._N('form','forms',$count);
     <?php } ?>
     </tbody>
     <tbody>
-    <caption><?php echo $showing; ?></caption>
     <thead>
         <tr>
-            <th width="7">&nbsp;</th>
+            <th width="4%">&nbsp;</th>
             <th><?php echo __('Custom Forms'); ?></th>
             <th><?php echo __('Last Updated'); ?></th>
         </tr>
     </thead>
     <tbody>
-    <?php foreach (DynamicForm::objects()->filter(array('type'=>'G'))
-                ->order_by('title')
+<?php foreach ($other_forms->order_by('title')
                 ->limit($pageNav->getLimit())
                 ->offset($pageNav->getStart()) as $form) {
             $sel=false;
             if($ids && in_array($form->get('id'),$ids))
                 $sel=true; ?>
         <tr>
-            <td><?php if ($form->isDeletable()) { ?>
+            <td align="center"><?php if ($form->isDeletable()) { ?>
                 <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $form->get('id'); ?>"
                     <?php echo $sel?'checked="checked"':''; ?>>
             <?php } ?></td>
@@ -94,9 +117,7 @@ $showing=$pageNav->showing().' '._N('form','forms',$count);
 if ($count) //Show options..
     echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
 ?>
-<p class="centered" id="actions">
-    <input class="button" type="submit" name="delete" value="<?php echo __('Delete'); ?>">
-</p>
+
 </form>
 
 <div style="display:none;" class="dialog" id="confirm-action">
diff --git a/include/staff/dynamic-list.inc.php b/include/staff/dynamic-list.inc.php
index 853c1078375aa77a41ae205862af6ccead620204..4e4b0b7766040afefae49cbd7b47708566c9cf28 100644
--- a/include/staff/dynamic-list.inc.php
+++ b/include/staff/dynamic-list.inc.php
@@ -6,6 +6,8 @@ if ($list) {
     $action = 'update';
     $submit_text = __('Save Changes');
     $info = $list->getInfo();
+    $trans['name'] = $list->getTranslateTag('name');
+    $trans['plural'] = $list->getTranslateTag('plural');
     $newcount=2;
 } else {
     $title = __('Add New Custom List');
@@ -22,24 +24,27 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
     <input type="hidden" name="do" value="<?php echo $action; ?>">
     <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
     <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
-    <h2><?php echo __('Custom List'); ?>
-    <?php echo $list ? $list->getName() : __('Add new list'); ?></h2>
-
-<ul class="tabs">
-    <li><a href="#definition" class="active">
+    <h2><?php echo $title; ?>
+        <?php if (isset($info['name'])) { ?><small>
+        — <?php echo $info['name']; ?></small>
+        <?php } ?>
+    </h2>
+<ul class="clean tabs" id="list-tabs">
+    <li <?php if (!$list) echo 'class="active"'; ?>><a href="#definition">
         <i class="icon-plus"></i> <?php echo __('Definition'); ?></a></li>
-    <li><a href="#items">
-        <i class="icon-list"></i> <?php echo __('Items'); ?></a></li>
+<?php if ($list) { ?>
+    <li class="active"><a href="#items">
+        <i class="icon-list"></i> <?php echo sprintf(__('Items (%d)'), $list->getItems()->count()); ?></a></li>
+<?php } ?>
     <li><a href="#properties">
         <i class="icon-asterisk"></i> <?php echo __('Properties'); ?></a></li>
 </ul>
-
-<div id="definition" class="tab_content">
+<div id="list-tabs_container">
+<div id="definition" class="tab_content <?php if ($list) echo 'hidden'; ?>">
     <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 lists are used to provide drop-down lists for custom forms.'
                 ); ?>&nbsp;<i class="help-tip icon-question-sign" href="#custom_lists"></i></em>
@@ -55,9 +60,10 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
                     echo $list->getName();
                 else {
                     echo sprintf('<input size="50" type="text" name="name"
+                            data-translate-tag="%s" autofocus
                             value="%s"/> <span
                             class="error">*<br/>%s</span>',
-                            $info['name'], $errors['name']);
+                            $trans['name'], $info['name'], $errors['name']);
                 }
                 ?>
             </td>
@@ -70,8 +76,9 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
                         echo $list->getPluralName();
                     else
                         echo sprintf('<input size="50" type="text"
+                                data-translate-tag="%s"
                                 name="name_plural" value="%s"/>',
-                                $info['name_plural']);
+                                $trans['plural'], $info['name_plural']);
                 ?>
             </td>
         </tr>
@@ -104,7 +111,7 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
     </tbody>
     </table>
 </div>
-<div id="properties" class="tab_content" style="display:none">
+<div id="properties" class="hidden tab_content">
     <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
     <thead>
         <tr>
@@ -117,6 +124,7 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
             <th nowrap></th>
             <th nowrap><?php echo __('Label'); ?></th>
             <th nowrap><?php echo __('Type'); ?></th>
+            <th nowrap><?php echo __('Visibility'); ?></th>
             <th nowrap><?php echo __('Variable'); ?></th>
             <th nowrap><?php echo __('Delete'); ?></th>
         </tr>
@@ -131,6 +139,7 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
         <tr>
             <td><i class="icon-sort"></i></td>
             <td><input type="text" size="32" name="prop-label-<?php echo $id; ?>"
+                data-translate-tag="<?php echo $f->getTranslateTag('label'); ?>"
                 value="<?php echo Format::htmlchars($f->get('label')); ?>"/>
                 <font class="error"><?php
                     if ($ferrors['label']) echo '<br/>'; echo $ferrors['label']; ?>
@@ -155,6 +164,8 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
                     href="#form/field-config/<?php
                         echo $f->get('id'); ?>"><i
                         class="icon-cog"></i> <?php echo __('Config'); ?></a> <?php } ?></td>
+            <td>
+                <?php echo $f->getVisibilityDescription(); ?></td>
             <td>
                 <input type="text" size="20" name="name-<?php echo $id; ?>"
                     value="<?php echo Format::htmlchars($f->get('name'));
@@ -195,6 +206,7 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
                 </optgroup>
                 <?php } ?>
             </select></td>
+            <td></td>
             <td><input type="text" size="20" name="name-new-<?php echo $i; ?>"
                 value="<?php echo $info["name-new-$i"]; ?>"/>
                 <font class="error"><?php
@@ -206,125 +218,15 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
     </tbody>
 </table>
 </div>
-<div id="items" class="tab_content" style="display:none">
-    <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
-    <thead>
-    <?php if ($list) {
-        $page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
-        $count = $list->getNumItems();
-        $pageNav = new Pagenate($count, $page, PAGE_LIMIT);
-        $pageNav->setURL('list.php', array('id' => $list->getId()));
-        $showing=$pageNav->showing().' '.__('list items');
-        ?>
-    <?php }
-        else $showing = __('Add a few initial items to the list');
-    ?>
-        <tr>
-            <th colspan="5">
-                <em><?php echo $showing; ?></em>
-            </th>
-        </tr>
-        <tr>
-            <th></th>
-            <th><?php echo __('Value'); ?></th>
-            <?php
-            if (!$list || $list->hasAbbrev()) { ?>
-            <th><?php echo __(/* Short for 'abbreviation' */ 'Abbrev'); ?> <em style="display:inline">&mdash;
-                <?php echo __('abbreviations and such'); ?></em></th>
-            <?php
-            } ?>
-            <th><?php echo __('Disabled'); ?></th>
-            <th><?php echo __('Delete'); ?></th>
-        </tr>
-    </thead>
-
-    <tbody <?php if ($info['sort_mode'] == 'SortCol') { ?>
-            class="sortable-rows" data-sort="sort-"<?php } ?>>
-        <?php
-        if ($list) {
-            $icon = ($info['sort_mode'] == 'SortCol')
-                ? '<i class="icon-sort"></i>&nbsp;' : '';
-        foreach ($list->getAllItems() as $i) {
-            $id = $i->getId(); ?>
-        <tr class="<?php if (!$i->isEnabled()) echo 'disabled'; ?>">
-            <td><?php echo $icon; ?>
-                <input type="hidden" name="sort-<?php echo $id; ?>"
-                value="<?php echo $i->getSortOrder(); ?>"/></td>
-            <td><input type="text" size="40" name="value-<?php echo $id; ?>"
-                value="<?php echo $i->getValue(); ?>"/>
-                <?php if ($list->hasProperties()) { ?>
-                   <a class="action-button field-config"
-                       style="overflow:inherit"
-                       href="#list/<?php
-                        echo $list->getId(); ?>/item/<?php
-                        echo $id ?>/properties"
-                       id="item-<?php echo $id; ?>"
-                    ><?php
-                        echo sprintf('<i class="icon-edit" %s></i> ',
-                                $i->getConfiguration()
-                                ? '': 'style="color:red; font-weight:bold;"');
-                        echo __('Properties');
-                   ?></a>
-                <?php
-                }
-
-                if ($errors["value-$id"])
-                    echo sprintf('<br><span class="error">%s</span>',
-                            $errors["value-$id"]);
-                ?>
-            </td>
-            <?php
-            if ($list->hasAbbrev()) { ?>
-            <td><input type="text" size="30" name="abbrev-<?php echo $id; ?>"
-                value="<?php echo $i->getAbbrev(); ?>"/></td>
-            <?php
-            } ?>
-            <td>
-                <?php
-                if (!$i->isDisableable())
-                     echo '<i class="icon-ban-circle"></i>';
-                else
-                    echo sprintf('<input type="checkbox" name="disable-%s"
-                            %s %s />',
-                            $id,
-                            !$i->isEnabled() ? ' checked="checked" ' : '',
-                            (!$i->isEnabled() && !$i->isEnableable()) ? ' disabled="disabled" ' : ''
-                            );
-                ?>
-            </td>
-            <td>
-                <?php
-                if (!$i->isDeletable())
-                    echo '<i class="icon-ban-circle"></i>';
-                else
-                    echo sprintf('<input type="checkbox" name="delete-item-%s">', $id);
 
-                ?>
-            </td>
-        </tr>
-    <?php }
-    }
-
-    if (!$list || $list->allowAdd()) {
-       for ($i=0; $i<$newcount; $i++) { ?>
-        <tr>
-            <td><?php echo $icon; ?> <em>+</em>
-                <input type="hidden" name="sort-new-<?php echo $i; ?>"/></td>
-            <td><input type="text" size="40" name="value-new-<?php echo $i; ?>"/></td>
-            <?php
-            if (!$list || $list->hasAbbrev()) { ?>
-            <td><input type="text" size="30" name="abbrev-new-<?php echo $i; ?>"/></td>
-            <?php
-            } ?>
-            <td>&nbsp;</td>
-            <td>&nbsp;</td>
-        </tr>
-    <?php
-       }
-    }?>
-    </tbody>
-    </table>
+<?php if ($list) { ?>
+<div id="items" class="tab_content">
+<?php
+    $pjax_container = '#items';
+    include STAFFINC_DIR . 'templates/list-items.tmpl.php'; ?>
 </div>
+<?php } ?>
+
 <p class="centered">
     <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
     <input type="reset"  name="reset"  value="<?php echo __('Reset'); ?>">
@@ -335,14 +237,46 @@ $info=Format::htmlchars(($errors && $_POST) ? array_merge($info,$_POST) : $info)
 
 <script type="text/javascript">
 $(function() {
-    $('a.field-config').click( function(e) {
+    $('#properties, #items').on('click', 'a.field-config', function(e) {
         e.preventDefault();
         var $id = $(this).attr('id');
         var url = 'ajax.php/'+$(this).attr('href').substr(1);
-        $.dialog(url, [201], function (xhr) {
-            $('a#'+$id+' i').removeAttr('style');
+        $.dialog(url, [201], function (xhr, resp) {
+          var json = $.parseJSON(resp);
+          if (json && json.success) {
+            if (json.row) {
+              if (json.id)
+                $('#list-item-' + json.id).replaceWith(json.row);
+              else
+                $('#list-items').append(json.row);
+            }
+          }
         });
         return false;
     });
+    $('#items').on('click', 'a.items-action', function(e) {
+        e.preventDefault();
+        var ids = [];
+        $('form#save :checkbox.mass:checked').each(function() {
+            ids.push($(this).val());
+        });
+        if (ids.length && confirm(__('You sure?'))) {
+            $.ajax({
+              url: 'ajax.php/' + $(this).attr('href').substr(1),
+              type: 'POST',
+              data: {count:ids.length, ids:ids},
+              dataType: 'json',
+              success: function(json) {
+                if (json.success) {
+                  if (window.location.search.indexOf('a=items') != -1)
+                    $.pjax.reload('#items');
+                  else
+                    $.pjax.reload('#pjax-container');
+                }
+              }
+            });
+        }
+        return false;
+    });
 });
 </script>
diff --git a/include/staff/dynamic-lists.inc.php b/include/staff/dynamic-lists.inc.php
index 6d294a587fca5c19e3cf66441ac331de7eb7467b..9f810d4a8f1653e586a469015089ef96caf549c2 100644
--- a/include/staff/dynamic-lists.inc.php
+++ b/include/staff/dynamic-lists.inc.php
@@ -1,9 +1,31 @@
-<div class="pull-left" style="width:700;padding-top:5px;">
- <h2><?php echo __('Custom Lists'); ?></h2>
+<form action="lists.php" method="POST" name="lists">
+
+<div class="sticky bar opaque">
+    <div class="content">
+        <div class="pull-left flush-left">
+            <h2><?php echo __('Custom Lists'); ?></h2>
+        </div>
+        <div class="pull-right flush-right">
+            <a href="lists.php?a=add" class="green button action-button"><i class="icon-plus-sign"></i> <?php
+                    echo __('Add New Custom List'); ?></a>
+
+            <span class="action-button" data-dropdown="#action-dropdown-more">
+                    <i class="icon-caret-down pull-right"></i>
+                    <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+            </span>
+            <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                <ul id="actions">
+                    <li class="danger">
+                        <a class="confirm" data-name="delete" href="lists.php?a=delete">
+                            <i class="icon-trash icon-fixed-width"></i>
+                            <?php echo __( 'Delete'); ?>
+                        </a>
+                    </li>
+                </ul>
+            </div>
+        </div>
+    </div>
 </div>
-<div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;">
- <b><a href="lists.php?a=add" class="Icon list-add"><?php
- echo __('Add New Custom List'); ?></a></b></div>
 <div class="clear"></div>
 
 <?php
@@ -14,18 +36,16 @@ $pageNav->setURL('lists.php');
 $showing=$pageNav->showing().' '._N('custom list', 'custom lists', $count);
 
 ?>
-<form action="lists.php" method="POST" name="lists">
 <?php csrf_token(); ?>
 <input type="hidden" name="do" value="mass_process" >
 <input type="hidden" id="action" name="a" value="" >
 <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption>Custom Lists</caption>
     <thead>
         <tr>
-            <th width="7">&nbsp;</th>
-            <th><?php echo __('List Name'); ?></th>
-            <th><?php echo __('Created') ?></th>
-            <th><?php echo __('Last Updated'); ?></th>
+            <th width="4%">&nbsp;</th>
+            <th width="32%"><?php echo __('List Name'); ?></th>
+            <th width="32%"><?php echo __('Created') ?></th>
+            <th width="32%"><?php echo __('Last Updated'); ?></th>
         </tr>
     </thead>
     <tbody>
@@ -36,7 +56,7 @@ $showing=$pageNav->showing().' '._N('custom list', 'custom lists', $count);
             if ($ids && in_array($form->get('id'),$ids))
                 $sel = true; ?>
         <tr>
-            <td>
+            <td align="center">
                 <?php
                 if ($list->isDeleteable()) { ?>
                 <input width="7" type="checkbox" class="ckb" name="ids[]"
@@ -77,9 +97,6 @@ if ($count) //Show options..
     echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
 ?>
 
-<p class="centered" id="actions">
-    <input class="button" type="submit" name="delete" value="<?php echo __('Delete'); ?>">
-</p>
 </form>
 
 <div style="display:none;" class="dialog" id="confirm-action">
diff --git a/include/staff/email.inc.php b/include/staff/email.inc.php
index 2ee0222fa15589f83a3b5caa0931b00bd12c1bc8..08225cb95e1175634a5e03b3527e9f2717dc417e 100644
--- a/include/staff/email.inc.php
+++ b/include/staff/email.inc.php
@@ -2,7 +2,7 @@
 if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
 $info = $qs = array();
 if($email && $_REQUEST['a']!='add'){
-    $title=__('Update Email');
+    $title=__('Update Email Address');
     $action='update';
     $submit_text=__('Save Changes');
     $info=$email->getInfo();
@@ -18,7 +18,7 @@ if($email && $_REQUEST['a']!='add'){
 
     $qs += array('id' => $email->getId());
 }else {
-    $title=__('Add New Email');
+    $title=__('Add New Email Address');
     $action='create';
     $submit_text=__('Submit');
     $info['ispublic']=isset($info['ispublic'])?$info['ispublic']:1;
@@ -34,7 +34,11 @@ if($email && $_REQUEST['a']!='add'){
 }
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 ?>
-<h2><?php echo __('Email Address');?></h2>
+<h2><?php echo $title; ?>
+    <?php if (isset($info['email'])) { ?><small>
+    — <?php echo $info['email']; ?></small>
+    <?php } ?>
+</h2>
 <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; ?>">
@@ -44,7 +48,6 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
     <thead>
         <tr>
             <th colspan="2">
-                <h4><?php echo $title; ?></h4>
                 <em><strong><?php echo __('Email Information and Settings');?></strong></em>
             </th>
         </tr>
@@ -55,7 +58,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <?php echo __('Email Address');?>
             </td>
             <td>
-                <input type="text" size="35" name="email" value="<?php echo $info['email']; ?>">
+                <input type="text" size="35" name="email" value="<?php echo $info['email']; ?>"
+                    autofocus>
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['email']; ?></span>
             </td>
         </tr>
@@ -83,12 +87,11 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 			    <option value="0" selected="selected">&mdash; <?php
                 echo __('System Default'); ?> &mdash;</option>
 			    <?php
-			    $sql='SELECT dept_id, dept_name FROM '.DEPT_TABLE.' dept ORDER by dept_name';
-			    if(($res=db_query($sql)) && db_num_rows($res)){
-				while(list($id,$name)=db_fetch_row($res)){
-				    $selected=($info['dept_id'] && $id==$info['dept_id'])?'selected="selected"':'';
-				    echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name);
-				}
+                if (($depts=Dept::getDepartments())) {
+                    foreach ($depts as $id => $name) {
+				        $selected=($info['dept_id'] && $id==$info['dept_id'])?'selected="selected"':'';
+				        echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name);
+				    }
 			    }
 			    ?>
 			</select>
diff --git a/include/staff/emails.inc.php b/include/staff/emails.inc.php
index e02d178d266db868683105ae7188a34dc340f40c..78c296ce27e5a23928c41300e1e4759286d39c11 100644
--- a/include/staff/emails.inc.php
+++ b/include/staff/emails.inc.php
@@ -2,98 +2,118 @@
 if(!defined('OSTADMININC') || !$thisstaff->isAdmin()) die('Access Denied');
 
 $qs = array();
-$sql='SELECT email.*,dept.dept_name as department,priority_desc as priority '.
-     ' FROM '.EMAIL_TABLE.' email '.
-     ' LEFT JOIN '.DEPT_TABLE.' dept ON (dept.dept_id=email.dept_id) '.
-     ' LEFT JOIN '.TICKET_PRIORITY_TABLE.' pri ON (pri.priority_id=email.priority_id) ';
-$sql.=' WHERE 1';
-$sortOptions=array('email'=>'email.email','dept'=>'department','priority'=>'priority','created'=>'email.created','updated'=>'email.updated');
-$orderWays=array('DESC'=>'DESC','ASC'=>'ASC');
-$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'email';
-//Sorting options...
-if($sort && $sortOptions[$sort]) {
-    $order_column =$sortOptions[$sort];
-}
-$order_column=$order_column?$order_column:'email.email';
+$sortOptions = array(
+        'email' => 'email',
+        'dept' => 'dept__name',
+        'priority' => 'priority__priority_desc',
+        'created' => 'created',
+        'updated' => 'updated');
+
 
-if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) {
-    $order=$orderWays[strtoupper($_REQUEST['order'])];
+$orderWays = array('DESC'=>'DESC', 'ASC'=>'ASC');
+$sort = ($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])]) ?  strtolower($_REQUEST['sort']) : 'email';
+if ($sort && $sortOptions[$sort]) {
+        $order_column = $sortOptions[$sort];
 }
-$order=$order?$order:'ASC';
 
-if($order_column && strpos($order_column,',')){
-    $order_column=str_replace(','," $order,",$order_column);
+$order_column = $order_column ? $order_column : 'email';
+
+if ($_REQUEST['order'] && isset($orderWays[strtoupper($_REQUEST['order'])]))
+{
+        $order = $orderWays[strtoupper($_REQUEST['order'])];
+} else {
+        $order = 'ASC';
 }
+
 $x=$sort.'_sort';
 $$x=' class="'.strtolower($order).'" ';
-$order_by="$order_column $order ";
-
-$total=db_count('SELECT count(*) FROM '.EMAIL_TABLE.' email ');
-$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
-$pageNav=new Pagenate($total, $page, PAGE_LIMIT);
+$page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
+$count = Email::objects()->count();
+$pageNav = new Pagenate($count, $page, PAGE_LIMIT);
 $qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
 $pageNav->setURL('emails.php', $qs);
+$showing = $pageNav->showing().' '._N('email', 'emails', $count);
 $qstr = '&amp;order='.($order=='DESC' ? 'ASC' : 'DESC');
-$query="$sql GROUP BY email.email_id ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
-$res=db_query($query);
-if($res && ($num=db_num_rows($res)))
-    $showing=$pageNav->showing().' '.__('emails');
-else
-    $showing=__('No emails found!');
 
 $def_dept_id = $cfg->getDefaultDeptId();
 $def_dept_name = $cfg->getDefaultDept()->getName();
 $def_priority = $cfg->getDefaultPriority()->getDesc();
-
 ?>
-<div class="pull-left" style="width:700px;padding-top:5px;">
- <h2><?php echo __('Email Addresses');?></h2>
- </div>
-<div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;">
-    <b><a href="emails.php?a=add" class="Icon newEmail"><?php echo __('Add New Email');?></a></b></div>
-<div class="clear"></div>
 <form action="emails.php" method="POST" name="emails">
+    <div class="sticky bar opaque">
+        <div class="content">
+            <div class="pull-left flush-left">
+                <h2><?php echo __('Email Addresses');?></h2>
+            </div>
+            <div class="pull-right flush-right">
+                <a href="emails.php?a=add" class="green button action-button"><i class="icon-plus-sign"></i> <?php echo __('Add New Email');?></a>
+                <span class="action-button" data-dropdown="#action-dropdown-more">
+                            <i class="icon-caret-down pull-right"></i>
+                            <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+                </span>
+                <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                    <ul id="actions">
+                        <li class="danger">
+                            <a class="confirm" data-name="delete" href="emails.php?a=delete">
+                                <i class="icon-trash icon-fixed-width"></i>
+                                <?php echo __( 'Delete'); ?>
+                            </a>
+                        </li>
+                    </ul>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="clear"></div>
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="mass_process" >
  <input type="hidden" id="action" name="a" value="" >
  <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption><?php echo $showing; ?></caption>
     <thead>
         <tr>
-            <th width="7">&nbsp;</th>
-            <th width="400"><a <?php echo $email_sort; ?> href="emails.php?<?php echo $qstr; ?>&sort=email"><?php echo __('Email');?></a></th>
-            <th width="120"><a  <?php echo $priority_sort; ?> href="emails.php?<?php echo $qstr; ?>&sort=priority"><?php echo __('Priority');?></a></th>
-            <th width="250"><a  <?php echo $dept_sort; ?> href="emails.php?<?php echo $qstr; ?>&sort=dept"><?php echo __('Department');?></a></th>
-            <th width="110" nowrap><a  <?php echo $created_sort; ?>href="emails.php?<?php echo $qstr; ?>&sort=created"><?php echo __('Created');?></a></th>
-            <th width="150" nowrap><a  <?php echo $updated_sort; ?>href="emails.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated');?></a></th>
+            <th width="4%">&nbsp;</th>
+            <th width="38%"><a <?php echo $email_sort; ?> href="emails.php?<?php echo $qstr; ?>&sort=email"><?php echo __('Email');?></a></th>
+            <th width="8%"><a  <?php echo $priority_sort; ?> href="emails.php?<?php echo $qstr; ?>&sort=priority"><?php echo __('Priority');?></a></th>
+            <th width="15%"><a  <?php echo $dept_sort; ?> href="emails.php?<?php echo $qstr; ?>&sort=dept"><?php echo __('Department');?></a></th>
+            <th width="15%" nowrap><a  <?php echo $created_sort; ?>href="emails.php?<?php echo $qstr; ?>&sort=created"><?php echo __('Created');?></a></th>
+            <th width="20%" nowrap><a  <?php echo $updated_sort; ?>href="emails.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated');?></a></th>
         </tr>
     </thead>
     <tbody>
     <?php
-        $total=0;
-        $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null;
-        if($res && db_num_rows($res)):
+        $ids = ($errors && is_array($_POST['ids'])) ? $_POST['ids'] : null;
+        if ($count):
             $defaultId=$cfg->getDefaultEmailId();
-            while ($row = db_fetch_array($res)) {
+            $emails = Email::objects()
+                ->order_by(sprintf('%s%s',
+                            strcasecmp($order, 'DESC') ? '' : '-',
+                            $order_column))
+                ->limit($pageNav->getLimit())
+                ->offset($pageNav->getStart());
+
+            foreach ($emails as $email) {
+                $id = $email->getId();
                 $sel=false;
-                if($ids && in_array($row['email_id'],$ids))
+                if ($ids && in_array($id, $ids))
                     $sel=true;
-                $default=($row['email_id']==$defaultId);
-                $email=$row['email'];
-                if($row['name'])
-                    $email=$row['name'].' <'.$row['email'].'>';
+                $default=($id==$defaultId);
                 ?>
-            <tr id="<?php echo $row['email_id']; ?>">
-                <td width=7px>
-                  <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['email_id']; ?>"
-                            <?php echo $sel?'checked="checked"':''; ?>  <?php echo $default?'disabled="disabled"':''; ?>>
+            <tr id="<?php echo $id; ?>">
+                <td align="center">
+                  <input type="checkbox" class="ckb" name="ids[]"
+                    value="<?php echo $id; ?>"
+                    <?php echo $sel ? 'checked="checked" ' : ''; ?>
+                    <?php echo $default?'disabled="disabled" ':''; ?>>
                 </td>
-                <td><span class="ltr"><a href="emails.php?id=<?php echo $row['email_id']; ?>"><?php echo Format::htmlchars($email); ?></a></span></td>
-                <td><?php echo $row['priority'] ?: $def_priority; ?></td>
-                <td><a href="departments.php?id=<?php $row['dept_id'] ?: $def_dept_id; ?>"><?php
-                    echo $row['department'] ?: $def_dept_name; ?></a></td>
-                <td>&nbsp;<?php echo Format::db_date($row['created']); ?></td>
-                <td>&nbsp;<?php echo Format::db_datetime($row['updated']); ?></td>
+                <td><span class="ltr"><a href="emails.php?id=<?php echo $id; ?>"><?php
+                    echo Format::htmlchars((string) $email); ?></a></span>
+                <?php echo ($default) ?' <small>'.__('(Default)').'</small>' : ''; ?>
+                </td>
+                <td><?php echo $email->priority ?: $def_priority; ?></td>
+                <td><a href="departments.php?id=<?php $email->dept_id ?: $def_dept_id; ?>"><?php
+                    echo $email->dept ?: $def_dept_name; ?></a></td>
+                <td>&nbsp;<?php echo Format::date($email->created); ?></td>
+                <td>&nbsp;<?php echo Format::datetime($email->updated); ?></td>
             </tr>
             <?php
             } //end of while.
@@ -101,25 +121,23 @@ $def_priority = $cfg->getDefaultPriority()->getDesc();
     <tfoot>
      <tr>
         <td colspan="6">
-            <?php if($res && $num){ ?>
+            <?php if ($count){ ?>
             <?php echo __('Select');?>:&nbsp;
             <a id="selectAll" href="#ckb"><?php echo __('All');?></a>&nbsp;&nbsp;
             <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>
     </tfoot>
 </table>
 <?php
-if($res && $num): //Show options..
+if ($count):
     echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
 ?>
-<p class="centered" id="actions">
-    <input class="button" type="submit" name="delete" value="<?php echo __('Delete Email(s)');?>" >
-</p>
+
 <?php
 endif;
 ?>
diff --git a/include/staff/faq-categories.inc.php b/include/staff/faq-categories.inc.php
index 9d9efa32ba994d56730d3b49f8fa62e343455fdb..d2aeb0cb7bc2da4d6c0a5cd19115a9eba5a3e479 100644
--- a/include/staff/faq-categories.inc.php
+++ b/include/staff/faq-categories.inc.php
@@ -2,91 +2,136 @@
 if(!defined('OSTSTAFFINC') || !$thisstaff) die('Access Denied');
 
 ?>
-<h2><?php echo __('Frequently Asked Questions');?></h2>
 <form id="kbSearch" action="kb.php" method="get">
     <input type="hidden" name="a" value="search">
-    <div>
-        <input id="query" type="text" size="20" name="q" value="<?php echo Format::htmlchars($_REQUEST['q']); ?>">
-        <select name="cid" id="cid">
-            <option value="">&mdash; <?php echo __('All Categories');?> &mdash;</option>
-            <?php
-            $sql='SELECT category_id, name, count(faq.category_id) as faqs '
-                .' FROM '.FAQ_CATEGORY_TABLE.' cat '
-                .' LEFT JOIN '.FAQ_TABLE.' faq USING(category_id) '
-                .' GROUP BY cat.category_id '
-                .' HAVING faqs>0 '
-                .' ORDER BY cat.name DESC ';
-            if(($res=db_query($sql)) && db_num_rows($res)) {
-                while($row=db_fetch_array($res))
-                    echo sprintf('<option value="%d" %s>%s (%d)</option>',
-                            $row['category_id'],
-                            ($_REQUEST['cid'] && $row['category_id']==$_REQUEST['cid']?'selected="selected"':''),
-                            $row['name'],
-                            $row['faqs']);
-            }
-            ?>
-        </select>
-        <input id="searchSubmit" type="submit" value="<?php echo __('Search');?>">
-    </div>
-    <div>
-        <select name="topicId" style="width:350px;" id="topic-id">
-            <option value="">&mdash; <?php echo __('All Help Topics');?> &mdash;</option>
-            <?php
-            $sql='SELECT ht.topic_id, CONCAT_WS(" / ", pht.topic, ht.topic) as helptopic, count(faq.topic_id) as faqs '
-                .' FROM '.TOPIC_TABLE.' ht '
-                .' LEFT JOIN '.TOPIC_TABLE.' pht ON (pht.topic_id=ht.topic_pid) '
-                .' LEFT JOIN '.FAQ_TOPIC_TABLE.' faq ON(faq.topic_id=ht.topic_id) '
-                .' GROUP BY ht.topic_id '
-                .' HAVING faqs>0 '
-                .' ORDER BY helptopic';
-            if(($res=db_query($sql)) && db_num_rows($res)) {
-                while($row=db_fetch_array($res))
-                    echo sprintf('<option value="%d" %s>%s (%d)</option>',
-                            $row['topic_id'],
-                            ($_REQUEST['topicId'] && $row['topic_id']==$_REQUEST['topicId']?'selected="selected"':''),
-                            $row['helptopic'], $row['faqs']);
-            }
-            ?>
-        </select>
+    <input type="hidden" name="cid" value="<?php echo Format::htmlchars($_REQUEST['cid']); ?>"/>
+    <input type="hidden" name="topicId" value="<?php echo Format::htmlchars($_REQUEST['topicId']); ?>"/>
+
+    <div id="basic_search">
+        <div class="attached input">
+            <input id="query" type="text" size="20" name="q" autofocus
+                value="<?php echo Format::htmlchars($_REQUEST['q']); ?>">
+            <button class="attached button" id="searchSubmit" type="submit">
+                <i class="icon icon-search"></i>
+            </button>
+        </div>
+
+        <div class="pull-right">
+            <span class="action-button muted" data-dropdown="#category-dropdown">
+                <i class="icon-caret-down pull-right"></i>
+                <span>
+                    <i class="icon-filter"></i>
+                    <?php echo __('Category'); ?>
+                </span>
+            </span>
+            <span class="action-button muted" data-dropdown="#topic-dropdown">
+                <i class="icon-caret-down pull-right"></i>
+                <span>
+                    <i class="icon-filter"></i>
+                    <?php echo __('Help Topic'); ?>
+                </span>
+            </span>
+        </div>
+
+        <div id="category-dropdown" class="action-dropdown anchor-right"
+            onclick="javascript:
+                var form = $(this).closest('form');
+                form.find('[name=cid]').val($(event.target).data('cid'));
+                form.submit();">
+            <ul class="bleed-left">
+<?php
+$total = FAQ::objects()->count();
+
+$categories = Category::objects()
+    ->annotate(array('faq_count' => SqlAggregate::COUNT('faqs')))
+    ->filter(array('faq_count__gt' => 0))
+    ->order_by('name')
+    ->all();
+array_unshift($categories, new Category(array('id' => 0, 'name' => __('All Categories'), 'faq_count' => $total)));
+foreach ($categories as $C) {
+        $active = $_REQUEST['cid'] == $C->getId(); ?>
+        <li <?php if ($active) echo 'class="active"'; ?>>
+            <a href="#" data-cid="<?php echo $C->getId(); ?>">
+                <i class="icon-fixed-width <?php
+                if ($active) echo 'icon-hand-right'; ?>"></i>
+                <?php echo sprintf('%s (%d)',
+                    Format::htmlchars($C->getLocalName()),
+                    $C->faq_count); ?></a>
+        </li> <?php
+} ?>
+            </ul>
+        </div>
+
+        <div id="topic-dropdown" class="action-dropdown anchor-right"
+            onclick="javascript:
+                var form = $(this).closest('form');
+                form.find('[name=topicId]').val($(event.target).data('topicId'));
+                form.submit();">
+            <ul class="bleed-left">
+<?php
+$topics = Topic::objects()
+    ->annotate(array('faq_count'=>SqlAggregate::COUNT('faqs')))
+    ->filter(array('faq_count__gt'=>0))
+    ->all();
+usort($topics, function($a, $b) {
+    return strcmp($a->getFullName(), $b->getFullName());
+});
+array_unshift($topics, new Topic(array('id' => 0, 'topic' => __('All Topics'), 'faq_count' => $total)));
+foreach ($topics as $T) {
+        $active = $_REQUEST['topicId'] == $T->getId(); ?>
+        <li <?php if ($active) echo 'class="active"'; ?>>
+            <a href="#" data-topic-id="<?php echo $T->getId(); ?>">
+                <i class="icon-fixed-width <?php
+                if ($active) echo 'icon-hand-right'; ?>"></i>
+                <?php echo sprintf('%s (%d)',
+                    Format::htmlchars($T->getFullName()),
+                    $T->faq_count); ?></a>
+        </li> <?php
+} ?>
+            </ul>
+        </div>
+
     </div>
 </form>
-<hr>
+    <div class="has_bottom_border" style="margin-bottom:5px; padding-top:5px;">
+        <div class="pull-left">
+            <h2><?php echo __('Frequently Asked Questions');?></h2>
+        </div>
+        <div class="clear"></div>
+    </div>
 <div>
 <?php
 if($_REQUEST['q'] || $_REQUEST['cid'] || $_REQUEST['topicId']) { //Search.
-    $sql='SELECT faq.faq_id, question, ispublished, count(attach.file_id) as attachments, count(ft.topic_id) as topics '
-        .' FROM '.FAQ_TABLE.' faq '
-        .' LEFT JOIN '.FAQ_CATEGORY_TABLE.' cat ON(cat.category_id=faq.category_id) '
-        .' LEFT JOIN '.FAQ_TOPIC_TABLE.' ft ON(ft.faq_id=faq.faq_id) '
-        .' LEFT JOIN '.ATTACHMENT_TABLE.' attach
-             ON(attach.object_id=faq.faq_id AND attach.type=\'F\' AND attach.inline = 0) '
-        .' WHERE 1 ';
+    $faqs = FAQ::objects()
+        ->annotate(array(
+            'attachment_count'=>SqlAggregate::COUNT('attachments'),
+            'topic_count'=>SqlAggregate::COUNT('topics')
+        ))
+        ->order_by('question');
 
-    if($_REQUEST['cid'])
-        $sql.=' AND faq.category_id='.db_input($_REQUEST['cid']);
+    if ($_REQUEST['cid'])
+        $faqs->filter(array('category_id'=>$_REQUEST['cid']));
 
-    if($_REQUEST['topicId'])
-        $sql.=' AND ft.topic_id='.db_input($_REQUEST['topicId']);
-
-    if($_REQUEST['q']) {
-        $sql.=" AND (question LIKE ('%".db_input($_REQUEST['q'],false)."%')
-                 OR answer LIKE ('%".db_input($_REQUEST['q'],false)."%')
-                 OR keywords LIKE ('%".db_input($_REQUEST['q'],false)."%')
-                 OR cat.name LIKE ('%".db_input($_REQUEST['q'],false)."%')
-                 OR cat.description LIKE ('%".db_input($_REQUEST['q'],false)."%')
-                 )";
-    }
+    if ($_REQUEST['topicId'])
+        $faqs->filter(array('topics__topic_id'=>$_REQUEST['topicId']));
 
-    $sql.=' GROUP BY faq.faq_id ORDER BY question';
+    if ($_REQUEST['q'])
+        $faqs->filter(Q::ANY(array(
+            'question__contains'=>$_REQUEST['q'],
+            'answer__contains'=>$_REQUEST['q'],
+            'keywords__contains'=>$_REQUEST['q'],
+            'category__name__contains'=>$_REQUEST['q'],
+            'category__description__contains'=>$_REQUEST['q'],
+        )));
 
     echo "<div><strong>".__('Search Results')."</strong></div><div class='clear'></div>";
-    if(($res=db_query($sql)) && db_num_rows($res)) {
+    if ($faqs->exists(true)) {
         echo '<div id="faq">
                 <ol>';
-        while($row=db_fetch_array($res)) {
-            echo sprintf('
-                <li><a href="faq.php?id=%d" class="previewfaq">%s</a> - <span>%s</span></li>',
-                $row['faq_id'],$row['question'],$row['ispublished']?__('Published'):__('Internal'));
+        foreach ($faqs as $F) {
+            echo sprintf(
+                '<li><a href="faq.php?id=%d" class="previewfaq">%s</a> - <span>%s</span></li>',
+                $F->getId(), $F->getLocalQuestion(), $F->getVisibilityDescription());
         }
         echo '  </ol>
              </div>';
@@ -94,23 +139,25 @@ if($_REQUEST['q'] || $_REQUEST['cid'] || $_REQUEST['topicId']) { //Search.
         echo '<strong class="faded">'.__('The search did not match any FAQs.').'</strong>';
     }
 } else { //Category Listing.
-    $sql='SELECT cat.category_id, cat.name, cat.description, cat.ispublic, count(faq.faq_id) as faqs '
-        .' FROM '.FAQ_CATEGORY_TABLE.' cat '
-        .' LEFT JOIN '.FAQ_TABLE.' faq ON(faq.category_id=cat.category_id) '
-        .' GROUP BY cat.category_id '
-        .' ORDER BY cat.name';
-    if(($res=db_query($sql)) && db_num_rows($res)) {
+    $categories = Category::objects()
+        ->annotate(array('faq_count'=>SqlAggregate::COUNT('faqs')))
+        ->all();
+
+    if (count($categories)) {
+        usort($categories, function($a, $b) {
+            return strcmp($a->getLocalName(), $b->getLocalName());
+        });
         echo '<div>'.__('Click on the category to browse FAQs or manage its existing FAQs.').'</div>
                 <ul id="kb">';
-        while($row=db_fetch_array($res)) {
-
+        foreach ($categories as $C) {
             echo sprintf('
                 <li>
-                    <h4><a href="kb.php?cid=%d">%s (%d)</a> - <span>%s</span></h4>
+                    <h4><a class="truncate" style="max-width:600px" href="kb.php?cid=%d">%s (%d)</a> - <span>%s</span></h4>
                     %s
-                </li>',$row['category_id'],$row['name'],$row['faqs'],
-                ($row['ispublic']?__('Public'):__('Internal')),
-                Format::safe_html($row['description']));
+                </li>',$C->getId(),$C->getLocalName(),$C->faq_count,
+                $C->getVisibilityDescription(),
+                Format::safe_html($C->getLocalDescriptionWithImages())
+            );
         }
         echo '</ul>';
     } else {
diff --git a/include/staff/faq-category.inc.php b/include/staff/faq-category.inc.php
index 8622a44abb8d6b9e7559f049d74a246fd2ba6c11..d037f9f7bc5fadfc4f54c26c7f1d013ff5841274 100644
--- a/include/staff/faq-category.inc.php
+++ b/include/staff/faq-category.inc.php
@@ -2,46 +2,62 @@
 if(!defined('OSTSTAFFINC') || !$category || !$thisstaff) die('Access Denied');
 
 ?>
-<div class="pull-left" style="width:700px;padding-top:10px;">
+<div class="has_bottom_border" style="margin-bottom:5px; padding-top:5px;">
+<div class="pull-left">
   <h2><?php echo __('Frequently Asked Questions');?></h2>
 </div>
-<div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;">&nbsp;</div>
-<div class="clear"></div>
-<br>
-<div>
-    <strong><?php echo $category->getName() ?></strong>
-    <span>(<?php echo $category->isPublic()?__('Public'):__('Internal'); ?>)</span>
-    <time> <?php echo __('Last updated').' '. Format::db_daydatetime($category->getUpdateDate()); ?></time>
+<?php if ($thisstaff->hasPerm(FAQ::PERM_MANAGE)) {
+echo sprintf('<div class="pull-right flush-right">
+    <a class="green action-button" href="faq.php?cid=%d&a=add">'.__('Add New FAQ').'</a>
+    <span class="action-button" data-dropdown="#action-dropdown-more"
+          style="/*DELME*/ vertical-align:top; margin-bottom:0">
+        <i class="icon-caret-down pull-right"></i>
+        <span ><i class="icon-cog"></i>'. __('More').'</span>
+    </span>
+    <div id="action-dropdown-more" class="action-dropdown anchor-right">
+        <ul>
+            <li><a class="user-action" href="categories.php?id=%d">
+                <i class="icon-pencil icon-fixed-width"></i>'
+                .__('Edit Category').'</a>
+            </li>
+            <li class="danger">
+                <a class="user-action" href="categories.php">
+                    <i class="icon-trash icon-fixed-width"></i>'
+                    .__('Delete Category').'</a>
+            </li>
+        </ul>
+    </div>
+</div>', $category->getId(), $category->getId());
+} else {
+?><?php
+} ?>
+    <div class="clear"></div>
+
 </div>
-<div class="cat-desc">
-<?php echo Format::display($category->getDescription()); ?>
+<div class="faq-category">
+    <div style="margin-bottom:10px;">
+        <div class="faq-title pull-left"><?php echo $category->getName() ?></div>
+        <div class="faq-status inline">(<?php echo $category->isPublic()?__('Public'):__('Internal'); ?>)</div>
+        <div class="clear"><time class="faq"> <?php echo __('Last updated').' '. Format::daydatetime($category->getUpdateDate()); ?></time></div>
+    </div>
+    <div class="cat-desc has_bottom_border">
+    <?php echo Format::display($category->getDescription()); ?>
 </div>
 <?php
-if($thisstaff->canManageFAQ()) {
-    echo sprintf('<div class="cat-manage-bar"><a href="categories.php?id=%d" class="Icon editCategory">'.__('Edit Category').'</a>
-             <a href="categories.php" class="Icon deleteCategory">'.__('Delete Category').'</a>
-             <a href="faq.php?cid=%d&a=add" class="Icon newFAQ">'.__('Add New FAQ').'</a></div>',
-            $category->getId(),
-            $category->getId());
-} else {
-?>
-<hr>
-<?php
-}
 
-$sql='SELECT faq.faq_id, question, ispublished, count(attach.file_id) as attachments '
-    .' FROM '.FAQ_TABLE.' faq '
-    .' LEFT JOIN '.ATTACHMENT_TABLE.' attach
-         ON(attach.object_id=faq.faq_id AND attach.type=\'F\' AND attach.inline = 0) '
-    .' WHERE faq.category_id='.db_input($category->getId())
-    .' GROUP BY faq.faq_id ORDER BY question';
-if(($res=db_query($sql)) && db_num_rows($res)) {
+
+$faqs = $category->faqs
+    ->constrain(array('attachments__inline' => 0))
+    ->annotate(array('attachments' => SqlAggregate::COUNT('attachments')));
+if ($faqs->exists(true)) {
     echo '<div id="faq">
             <ol>';
-    while($row=db_fetch_array($res)) {
+    foreach ($faqs as $faq) {
         echo sprintf('
-            <li><a href="faq.php?id=%d" class="previewfaq">%s <span>- %s</span></a></li>',
-            $row['faq_id'],$row['question'],$row['ispublished']?__('Published'):__('Internal'));
+            <li><strong><a href="faq.php?id=%d" class="previewfaq">%s <span>- %s</span></a> %s</strong></li>',
+            $faq->getId(),$faq->getQuestion(),$faq->isPublished() ? __('Published'):__('Internal'),
+            $faq->attachments ? '<i class="icon-paperclip"></i>' : ''
+        );
     }
     echo '  </ol>
          </div>';
@@ -49,3 +65,4 @@ if(($res=db_query($sql)) && db_num_rows($res)) {
     echo '<strong>'.__('Category does not have FAQs').'</strong>';
 }
 ?>
+</div>
diff --git a/include/staff/faq-view.inc.php b/include/staff/faq-view.inc.php
index 267104a6476efae27134ec37cf31b0fccc50a7b3..8f8daf77cef0bb913f8883e29d5e46296f39ccd0 100644
--- a/include/staff/faq-view.inc.php
+++ b/include/staff/faq-view.inc.php
@@ -3,66 +3,116 @@ if(!defined('OSTSTAFFINC') || !$faq || !$thisstaff) die('Access Denied');
 
 $category=$faq->getCategory();
 
-?>
-<h2><?php echo __('Frequently Asked Questions');?></h2>
+?><div class="has_bottom_border" style="padding-top:5px;">
+<div class="pull-left"><h2><?php echo __('Frequently Asked Questions');?></h2></div>
+<div class="pull-right flush-right">
+<?php
+$query = array();
+parse_str($_SERVER['QUERY_STRING'], $query);
+$query['a'] = 'print';
+$query['id'] = $faq->getId();
+$query = http_build_query($query); ?>
+    <a href="faq.php?<?php echo $query; ?>" class="no-pjax action-button">
+    <i class="icon-print"></i>
+        <?php echo __('Print'); ?>
+    </a>
+<?php
+if ($thisstaff->hasPerm(FAQ::PERM_MANAGE)) { ?>
+    <a href="faq.php?id=<?php echo $faq->getId(); ?>&a=edit" class="action-button">
+    <i class="icon-edit"></i>
+        <?php echo __('Edit FAQ'); ?>
+    </a>
+<?php } ?>
+</div><div class="clear"></div>
+
+</div>
+
 <div id="breadcrumbs">
     <a href="kb.php"><?php echo __('All Categories');?></a>
     &raquo; <a href="kb.php?cid=<?php echo $category->getId(); ?>"><?php echo $category->getName(); ?></a>
     <span class="faded">(<?php echo $category->isPublic()?__('Public'):__('Internal'); ?>)</span>
 </div>
-<div class="pull-left" style="width:700px;padding-top:2px;">
-<strong style="font-size:16px;"><?php echo $faq->getQuestion() ?></strong>&nbsp;&nbsp;<span class="faded"><?php echo $faq->isPublished() ? ('('.__('Published').')'):''; ?></span>
+
+<div class="pull-right sidebar faq-meta">
+<?php if ($attachments = $faq->getLocalAttachments()->all()) { ?>
+<section>
+    <header><?php echo __('Attachments');?>:</header>
+<?php foreach ($attachments as $att) { ?>
+<div>
+    <i class="icon-paperclip pull-left"></i>
+    <a target="_blank" href="<?php echo $att->file->getDownloadUrl(); ?>"
+        class="attachment no-pjax">
+        <?php echo Format::htmlchars($att->getFilename()); ?>
+    </a>
 </div>
-<div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;">
+<?php } ?>
+</section>
+<?php } ?>
+
+<?php if ($faq->getHelpTopics()->count()) { ?>
+<section>
+    <header><?php echo __('Help Topics'); ?></header>
+<?php foreach ($faq->getHelpTopics() as $T) { ?>
+    <div><?php echo $T->topic->getFullName(); ?></div>
+<?php } ?>
+</section>
+<?php } ?>
+
 <?php
-if($thisstaff->canManageFAQ()) {
-    echo sprintf('<a href="faq.php?id=%d&a=edit" class="Icon newHelpTopic">'.__('Edit FAQ').'</a>',
-            $faq->getId());
+$displayLang = $faq->getDisplayLang();
+$otherLangs = array();
+if ($cfg->getPrimaryLanguage() != $displayLang)
+    $otherLangs[] = $cfg->getPrimaryLanguage();
+foreach ($faq->getAllTranslations() as $T) {
+    if ($T->lang != $displayLang)
+        $otherLangs[] = $T->lang;
 }
-?>
-&nbsp;
+if ($otherLangs) { ?>
+<section>
+    <div><strong><?php echo __('Other Languages'); ?></strong></div>
+<?php
+    foreach ($otherLangs as $lang) { ?>
+    <div><a href="faq.php?kblang=<?php echo $lang; ?>&id=<?php echo $faq->getId(); ?>">
+        <?php echo Internationalization::getLanguageDescription($lang); ?>
+    </a></div>
+    <?php } ?>
+</section>
+<?php } ?>
+
+<section>
+<div>
+    <strong><?php echo $faq->isPublished()?__('Published'):__('Internal'); ?></strong>
 </div>
-<div class="clear"></div>
-<div class="thread-body">
-<?php echo $faq->getAnswerWithImages(); ?>
+<a data-dialog="ajax.php/kb/faq/<?php echo $faq->getId(); ?>/access" href="#"><?php echo __('manage access'); ?></a>
+</section>
+
+</div>
+
+<div class="faq-content">
+
+
+<div class="faq-title flush-left"><?php echo $faq->getLocalQuestion() ?>
+</div>
+
+<div class="faded"><?php echo __('Last updated');?>
+    <?php echo Format::relativeTime(Misc::db2gmtime($faq->getUpdateDate())); ?>
+</div>
+<br/>
+<div class="thread-body bleed">
+<?php echo $faq->getLocalAnswerWithImages(); ?>
+</div>
+
 </div>
 <div class="clear"></div>
-<p>
- <div><span class="faded"><b><?php echo __('Attachments');?>:</b></span> <?php echo $faq->getAttachmentsLinks(); ?></div>
- <div><span class="faded"><b><?php echo __('Help Topics');?>:</b></span>
-    <?php echo ($topics=$faq->getHelpTopics())?implode(', ',$topics):' '; ?>
-    </div>
-</p>
-<div class="faded">&nbsp;<?php echo __('Last updated');?> <?php echo Format::db_daydatetime($faq->getUpdateDate()); ?></div>
 <hr>
+
 <?php
-if($thisstaff->canManageFAQ()) {
-    //TODO: add js confirmation....
-    ?>
-   <div>
-    <form action="faq.php?id=<?php echo  $faq->getId(); ?>" method="post">
-	 <?php csrf_token(); ?>
-        <input type="hidden" name="id" value="<?php echo  $faq->getId(); ?>">
-        <input type="hidden" name="do" value="manage-faq">
-        <div>
-            <strong><?php echo __('Options');?>: </strong>
-            <select name="a" style="width:200px;">
-                <option value=""><?php echo __('Select Action');?></option>
-                <?php
-                if($faq->isPublished()) { ?>
-                <option value="unpublish"><?php echo __('Unpublish FAQ');?></option>
-                <?php
-                }else{ ?>
-                <option value="publish"><?php echo __('Publish FAQ');?></option>
-                <?php
-                } ?>
-                <option value="edit"><?php echo __('Edit FAQ');?></option>
-                <option value="delete"><?php echo __('Delete FAQ');?></option>
-            </select>
-            &nbsp;&nbsp;<input type="submit" name="submit" value="<?php echo __('Go');?>">
-        </div>
-    </form>
-   </div>
-<?php
-}
+if ($thisstaff->hasPerm(FAQ::PERM_MANAGE)) { ?>
+<form action="faq.php?id=<?php echo  $faq->getId(); ?>" method="post">
+    <?php csrf_token(); ?>
+    <input type="hidden" name="do" value="manage-faq">
+    <input type="hidden" name="id" value="<?php echo  $faq->getId(); ?>">
+    <button name="a" class="red button" value="delete"><?php echo __('Delete FAQ'); ?></button>
+</form>
+<?php }
 ?>
diff --git a/include/staff/faq.inc.php b/include/staff/faq.inc.php
index de62e9682ed29d1897d7f0094e85a20916cfba63..e96d302228e693d6aa507a94eecd7ef74b3ebaf0 100644
--- a/include/staff/faq.inc.php
+++ b/include/staff/faq.inc.php
@@ -1,5 +1,8 @@
 <?php
-if(!defined('OSTSCPINC') || !$thisstaff || !$thisstaff->canManageFAQ()) die('Access Denied');
+if (!defined('OSTSCPINC') || !$thisstaff
+        || !$thisstaff->hasPerm(FAQ::PERM_MANAGE))
+    die('Access Denied');
+
 $info = $qs = array();
 if($faq){
     $title=__('Update FAQ').': '.$faq->getQuestion();
@@ -11,6 +14,20 @@ if($faq){
     $info['answer']=Format::viewableImages($faq->getAnswer());
     $info['notes']=Format::viewableImages($faq->getNotes());
     $qs += array('id' => $faq->getId());
+    $langs = $cfg->getSecondaryLanguages();
+    $translations = $faq->getAllTranslations();
+    foreach ($langs as $tag) {
+        foreach ($translations as $t) {
+            if (strcasecmp($t->lang, $tag) === 0) {
+                $trans = $t->getComplex();
+                $info['trans'][$tag] = array(
+                    'question' => $trans['question'],
+                    'answer' => Format::viewableImages($trans['answer']),
+                );
+                break;
+            }
+        }
+    }
 }else {
     $title=__('Add New FAQ');
     $action='create';
@@ -29,122 +46,213 @@ $qstr = Http::build_query($qs);
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
  <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
- <h2><?php echo __('FAQ');?></h2>
- <table class="form_table fixed" width="940" border="0" cellspacing="0" cellpadding="2">
-    <thead>
-        <tr><td></td><td></td></tr> <!-- For fixed table layout -->
-        <tr>
-            <th colspan="2">
-                <h4><?php echo $title; ?></h4>
-            </th>
-        </tr>
-    </thead>
-    <tbody>
-        <tr>
-            <th colspan="2">
-                <em><?php echo __('FAQ Information');?></em>
-            </th>
-        </tr>
-        <tr>
-            <td colspan=2>
-                <div style="padding-top:3px;"><b><?php echo __('Question');?></b>&nbsp;<span class="error">*&nbsp;<?php echo $errors['question']; ?></span></div>
-                    <input type="text" size="70" name="question" value="<?php echo $info['question']; ?>">
-            </td>
-        </tr>
-        <tr>
-            <td colspan=2>
-                <div><b><?php echo __('Category Listing');?></b>:&nbsp;<span class="faded"><?php echo __('FAQ category the question belongs to.');?></span></div>
-                <select name="category_id" style="width:350px;">
-                    <option value="0"><?php echo __('Select FAQ Category');?> </option>
-                    <?php
-                    $sql='SELECT category_id, name, ispublic FROM '.FAQ_CATEGORY_TABLE;
-                    if(($res=db_query($sql)) && db_num_rows($res)) {
-                        while($row=db_fetch_array($res)) {
-                            echo sprintf('<option value="%d" %s>%s (%s)</option>',
-                                    $row['category_id'],
-                                    (($info['category_id']==$row['category_id'])?'selected="selected"':''),
-                                    $row['name'],
-                                    ($row['ispublic']?__('Public'):__('Internal')));
-                        }
-                    }
-                   ?>
-                </select>
-                <span class="error">*&nbsp;<?php echo $errors['category_id']; ?></span>
-            </td>
-        </tr>
-        <tr>
-            <td colspan=2>
-                <div><b><?php echo __('Listing Type');?></b>:
-                &nbsp;<i class="help-tip icon-question-sign" href="#listing_type"></i></div>
-                <input type="radio" name="ispublished" value="1" <?php echo $info['ispublished']?'checked="checked"':''; ?>><?php echo __('Public (publish)');?>
-                &nbsp;&nbsp;&nbsp;&nbsp;
-                <input type="radio" name="ispublished" value="0" <?php echo !$info['ispublished']?'checked="checked"':''; ?>><?php echo __('Internal (private)');?>
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['ispublished']; ?></span>
-            </td>
-        </tr>
-        <tr>
-            <td colspan=2>
-                <div style="margin-bottom:0.5em;margin-top:0.5em">
-                    <b><?php echo __('Answer');?></b>&nbsp;<font class="error">*&nbsp;<?php echo $errors['answer']; ?></font></div>
-                </div>
-                <textarea name="answer" cols="21" rows="12"
-                    style="width:98%;" class="richtext draft"
-                    data-draft-namespace="faq"
-                    data-draft-object-id="<?php if (is_object($faq)) echo $faq->getId(); ?>"
-                    ><?php echo $info['answer']; ?></textarea>
-            </td>
-        </tr>
-        <tr>
-            <td colspan=2>
-                <div><h3><?php echo __('Attachments');?>
-                    <span class="faded">(<?php echo __('optional');?>)</span></h3>
-                    <div class="error"><?php echo $errors['files']; ?></div>
-                </div>
-                <?php
-                $attachments = $faq_form->getField('attachments');
-                if ($faq && ($files=$faq->attachments->getSeparates())) {
-                    $ids = array();
-                    foreach ($files as $f)
-                        $ids[] = $f['id'];
-                    $attachments->value = $ids;
-                }
-                print $attachments->render(); ?>
-                <br/>
-            </td>
-        </tr>
-        <?php
-        if ($topics = Topic::getAllHelpTopics()) { ?>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Help Topics');?></strong>: <?php echo __('Check all help topics related to this FAQ.');?></em>
-            </th>
-        </tr>
-        <tr><td colspan="2">
-            <?php
-            while (list($topicId,$topic) = each($topics)) {
-                echo sprintf('<input type="checkbox" name="topics[]" value="%d" %s>%s<br>',
-                        $topicId,
-                        (($info['topics'] && in_array($topicId,$info['topics']))?'checked="checked"':''),
-                        $topic);
-            }
-             ?>
-            </td>
-        </tr>
-        <?php
-        } ?>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Internal Notes');?></strong>: &nbsp;</em>
-            </th>
-        </tr>
-        <tr>
-            <td colspan=2>
-                <textarea class="richtext no-bar" name="notes" cols="21"
-                    rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea>
-            </td>
-        </tr>
-    </tbody>
-</table>
+ <h2><?php echo __('Frequently Asked Questions');?></h2>
+<?php if ($info['question']) { ?>
+     <div class="faq-title" style="margin:5px 0 15px"><?php echo $info['question']; ?></div>
+<?php } ?>
+<div>
+ <div style="display:inline-block;width:49%">
+    <div>
+        <b><?php echo __('Category Listing');?></b>:
+        <span class="error">*</span>
+        <div class="faded"><?php echo __('FAQ category the question belongs to.');?></div>
+    </div>
+    <select name="category_id" style="width:350px;">
+        <option value="0"><?php echo __('Select FAQ Category');?> </option>
+<?php foreach (Category::objects() as $C) { ?>
+        <option value="<?php echo $C->getId(); ?>" <?php
+            if ($C->getId() == $info['category_id']) echo 'selected="selected"';
+            ?>><?php echo sprintf('%s (%s)',
+                $C->getName(),
+                $C->isPublic() ? __('Public') : __('Private')
+            ); ?></option>
+<?php } ?>
+    </select>
+    <div class="error"><?php echo $errors['category_id']; ?></div>
+
+<?php
+if ($topics = Topic::getAllHelpTopics()) {
+    if (!is_array(@$info['topics']))
+        $info['topics'] = array();
+?>
+    <div style="padding-top:9px">
+        <strong><?php echo __('Help Topics');?></strong>:
+        <div class="faded"><?php echo __('Check all help topics related to this FAQ.');?></div>
+    </div>
+    <select multiple="multiple" name="topics[]" class="multiselect"
+        data-placeholder="<?php echo __('Help Topics'); ?>"
+        id="help-topic-selection" style="width:350px;">
+    <?php while (list($topicId,$topic) = each($topics)) { ?>
+        <option value="<?php echo $topicId; ?>" <?php
+            if (in_array($topicId, $info['topics'])) echo 'selected="selected"';
+        ?>><?php echo $topic; ?></option>
+    <?php } ?>
+    </select>
+    <script type="text/javascript">
+        $(function() { $("#help-topic-selection").select2(); });
+    </script>
+<?php } ?>
+    </div>
+ <div style="display:inline-block;width:49%;margin-left:1%;vertical-align:top">
+    <div style="padding-top:9px;">
+        <b><?php echo __('Listing Type');?></b>:
+        <span class="error">*</span>
+        <i class="help-tip icon-question-sign" href="#listing_type"></i>
+    </div>
+    <select name="ispublished">
+        <option value="2" <?php echo $info['ispublished'] == 2 ? 'selected="selected"' : ''; ?>>
+            <?php echo __('Featured (promote to front page)'); ?>
+        </option>
+        <option value="1" <?php echo $info['ispublished'] == 1 ? 'selected="selected"' : ''; ?>>
+            <?php echo __('Public').' '.__('(publish)'); ?>
+        </option>
+        <option value="0" <?php echo !$info['ispublished'] ? 'selected="selected"' : ''; ?>>
+            <?php echo __('Internal').' '.('(private)'); ?>
+        </option>
+    </select>
+    <div class="error"><?php echo $errors['ispublished']; ?></div>
+  </div>
+</div>
+
+<div style="margin-top:20px"></div>
+
+<ul class="tabs clean" style="margin-top:9px;">
+    <li class="active"><a href="#article"><?php echo __('Article Content'); ?></a></li>
+    <li><a href="#attachments"><?php echo __('Attachments') . sprintf(' (%d)',
+        $faq ? count($faq->attachments->getSeparates('')) : 0); ?></a></li>
+    <li><a href="#notes"><?php echo __('Internal Notes'); ?></a></li>
+</ul>
+
+<div class="tab_content" id="article">
+<strong><?php echo __('Knowledgebase Article Content'); ?></strong><br/>
+<?php echo __('Here you can manage the question and answer for the article. Multiple languages are available if enabled in the admin panel.'); ?>
+<div class="clear"></div>
+
+<?php
+$langs = Internationalization::getConfiguredSystemLanguages();
+if ($faq && count($langs) > 1) { ?>
+    <ul class="tabs alt clean" id="trans" style="margin-top:10px;">
+        <li class="empty"><i class="icon-globe" title="This content is translatable"></i></li>
+<?php foreach ($langs as $tag=>$i) {
+    list($lang, $locale) = explode('_', $tag);
+ ?>
+    <li class="<?php if ($tag == $cfg->getPrimaryLanguage()) echo "active";
+        ?>"><a href="#lang-<?php echo $tag; ?>" title="<?php
+        echo Internationalization::getLanguageDescription($tag);
+    ?>"><span class="flag flag-<?php echo strtolower($i['flag'] ?: $locale ?: $lang); ?>"></span>
+    </a></li>
+<?php } ?>
+    </ul>
+<?php
+} ?>
+
+<div id="trans_container">
+<?php foreach ($langs as $tag=>$i) {
+    $code = $i['code'];
+    if ($tag == $cfg->getPrimaryLanguage()) {
+        $namespace = $faq ? $faq->getId() : false;
+        $answer = $info['answer'];
+        $question = $info['question'];
+        $qname = 'question';
+        $aname = 'answer';
+    }
+    else {
+        $namespace = $faq ? $faq->getId() . $code : $code;
+        $answer = $info['trans'][$code]['answer'];
+        $question = $info['trans'][$code]['question'];
+        $qname = 'trans['.$code.'][question]';
+        $aname = 'trans['.$code.'][answer]';
+    }
+?>
+    <div class="tab_content <?php
+        if ($code != $cfg->getPrimaryLanguage()) echo "hidden";
+     ?>" id="lang-<?php echo $tag; ?>"
+<?php if ($i['direction'] == 'rtl') echo 'dir="rtl" class="rtl"'; ?>
+    >
+    <div style="margin-bottom:0.5em;margin-top:9px">
+        <b><?php echo __('Question');?>
+            <span class="error">*</span>
+        </b>
+        <div class="error"><?php echo $errors['question']; ?></div>
+    </div>
+    <input type="text" size="70" name="<?php echo $qname; ?>"
+        style="font-size:110%;width:100%;box-sizing:border-box;"
+        value="<?php echo $question; ?>">
+    <div style="margin-bottom:0.5em;margin-top:9px">
+        <b><?php echo __('Answer');?></b>
+        <span class="error">*</span>
+        <div class="error"><?php echo $errors['answer']; ?></div>
+    </div>
+    <div>
+    <textarea name="<?php echo $aname; ?>" cols="21" rows="12"
+        data-width="670px"
+        class="richtext draft" <?php
+list($draft, $attrs) = Draft::getDraftAndDataAttrs('faq', $namespace, $answer);
+echo $attrs; ?>><?php echo $draft ?: $answer;
+        ?></textarea>
+
+    </div>
+    </div>
+<?php } ?>
+    </div>
+</div>
+
+<div class="tab_content" id="attachments" style="display:none">
+    <div>
+        <strong><?php echo __('Common Attachments'); ?></strong>
+        <div><?php echo __(
+            'These attachments are always available, regardless of the language in which the article is rendered'
+        ); ?></div>
+        <div class="error"><?php echo $errors['files']; ?></div>
+        <div style="margin-top:15px"></div>
+    </div>
+    <?php
+    print $faq_form->getField('attachments')->render(); ?>
+
+<?php if (count($langs) > 1) { ?>
+    <div style="margin-top:15px"></div>
+    <strong><?php echo __('Language-Specific Attachments'); ?></strong>
+    <div><?php echo __(
+        'These attachments are only available when article is rendered in one of the following languages.'
+    ); ?></div>
+    <div class="error"><?php echo $errors['files']; ?></div>
+    <div style="margin-top:15px"></div>
+
+    <ul class="tabs alt clean">
+        <li class="empty"><i class="icon-globe" title="This content is translatable"></i></li>
+<?php foreach ($langs as $lang=>$i) { ?>
+        <li class="<?php if ($i['code'] == $cfg->getPrimaryLanguage()) echo 'active';
+?>"><a href="#attachments-<?php echo $i['code']; ?>">
+    <span class="flag flag-<?php echo $i['flag']; ?>"></span>
+    </a></li>
+<?php } ?>
+    </ul>
+<?php foreach ($langs as $lang=>$i) {
+    $code = $i['code']; ?>
+    <div class="tab_content" id="attachments-<?php echo $i['code']; ?>" <?php if ($i['code'] != $cfg->getPrimaryLanguage()) echo 'style="display:none;"'; ?>>
+    <div style="padding:0 0 9px">
+        <strong><?php echo sprintf(__(
+            /* %s is the name of a language */ 'Attachments for %s'),
+            Internationalization::getLanguageDescription($lang));
+        ?></strong>
+    </div>
+    <?php
+    print $faq_form->getField('attachments.'.$code)->render();
+    ?></div><?php
+    }
+} ?>
+<div class="clear"></div>
+</div>
+
+<div class="tab_content" style="display:none;" id="notes">
+    <div>
+        <b><?php echo __('Internal Notes');?></b>:<span class="faded"><?php echo __("Be libergsdfgal, they're internal");?></span>
+    </div>
+    <div style="margin-top:10px"></div>
+    <textarea class="richtext no-bar" name="notes" cols="21"
+        rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea>
+</div>
+
 <p style="text-align:center;">
     <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
     <input type="reset"  name="reset"  value="<?php echo __('Reset'); ?>" onclick="javascript:
diff --git a/include/staff/filter.inc.php b/include/staff/filter.inc.php
index 0273f2bc22b3ab9f8c7acd95f11c1a667c4743a7..817d20b6b82b6f9b10e18e78cd3b96a3c853aa42 100644
--- a/include/staff/filter.inc.php
+++ b/include/staff/filter.inc.php
@@ -9,391 +9,350 @@ if($filter && $_REQUEST['a']!='add'){
     $title=__('Update Filter');
     $action='update';
     $submit_text=__('Save Changes');
-    $info=array_merge($filter->getInfo(),$filter->getFlatRules());
+    $info=array_merge($filter->getInfo());
     $info['id']=$filter->getId();
+    $info['rules'] = $filter->getRules();
     $qs += array('id' => $filter->getId());
 }else {
     $title=__('Add New Filter');
     $action='add';
     $submit_text=__('Add Filter');
     $info['isactive']=isset($info['isactive'])?$info['isactive']:0;
+    $info['rules'] = array();
     $qs += array('a' => $_REQUEST['a']);
 }
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 ?>
 <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']); ?>">
- <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
- <h2><?php echo __('Ticket Filter');?></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 __('Filters are executed based on execution order. Filter can target specific ticket source.');?></em>
-            </th>
-        </tr>
-    </thead>
-    <tbody>
-        <tr>
-            <td width="180" class="required">
-              <?php echo __('Filter Name');?>:
-            </td>
-            <td>
-                <input type="text" size="30" name="name" value="<?php echo $info['name']; ?>">
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['name']; ?></span>
-            </td>
-        </tr>
-        <tr>
-            <td width="180" class="required">
-              <?php echo __('Execution Order');?>:
-            </td>
-            <td>
-                <input type="text" size="6" name="execorder" value="<?php echo $info['execorder']; ?>">
-                <em>(1...99)</em>
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['execorder']; ?></span>
-                &nbsp;&nbsp;&nbsp;
-                <input type="checkbox" name="stop_onmatch" value="1" <?php echo $info['stop_onmatch']?'checked="checked"':''; ?> >
-                <?php echo __('<strong>Stop</strong> processing further on match!');?>
-                &nbsp;<i class="help-tip icon-question-sign" href="#execution_order"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180" class="required">
-                <?php echo __('Filter Status');?>:
-            </td>
-            <td>
-                <input type="radio" name="isactive" value="1" <?php echo
-                $info['isactive']?'checked="checked"':''; ?>> <?php echo __('Active'); ?>
-                <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked="checked"':''; ?>
-                > <?php echo __('Disabled'); ?>
-                &nbsp;<span class="error">*&nbsp;</span>
-            </td>
-        </tr>
-        <tr>
-            <td width="180" class="required">
-                <?php echo __('Target Channel');?>:
-            </td>
-            <td>
-                <select name="target">
-                   <option value="">&mdash; <?php echo __('Select a Channel');?> &mdash;</option>
-                   <?php
-                   foreach(Filter::getTargets() as $k => $v) {
-                       echo sprintf('<option value="%s" %s>%s</option>',
-                               $k, (($k==$info['target'])?'selected="selected"':''), $v);
-                    }
-                    $sql='SELECT email_id,email,name FROM '.EMAIL_TABLE.' email ORDER by name';
-                    if(($res=db_query($sql)) && db_num_rows($res)) {
-                        echo sprintf('<OPTGROUP label="%s">', __('System Emails'));
-                        while(list($id,$email,$name)=db_fetch_row($res)) {
-                            $selected=($info['email_id'] && $id==$info['email_id'])?'selected="selected"':'';
-                            if($name)
-                                $email=Format::htmlchars("$name <$email>");
-                            echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$email);
+    <?php csrf_token(); ?>
+    <input type="hidden" name="do" value="<?php echo $action; ?>">
+    <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
+    <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
+    <h2><?php echo $title; ?>
+        <?php if (isset($info['name'])) { ?><small>
+        — <?php echo $info['name']; ?></small>
+        <?php } ?>
+    </h2>
+    <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
+        <thead>
+            <tr>
+                <th colspan="2">
+                    <em><?php echo __('Filters are executed based on execution order. Filter can target specific ticket source.');?></em>
+                </th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td width="180" class="required">
+                  <?php echo __('Filter Name');?>:
+                </td>
+                <td>
+                    <input type="text" size="30" name="name" value="<?php echo $info['name']; ?>"
+                        autofocus>
+                    &nbsp;<span class="error">*&nbsp;<?php echo $errors['name']; ?></span>
+                </td>
+            </tr>
+            <tr>
+                <td width="180" class="required">
+                  <?php echo __('Execution Order');?>:
+                </td>
+                <td>
+                    <input type="text" size="6" name="execorder" value="<?php echo $info['execorder']; ?>">
+                    <em>(1...99)</em>
+                    &nbsp;<span class="error">*&nbsp;<?php echo $errors['execorder']; ?></span>
+                    &nbsp;&nbsp;&nbsp;
+                    <label class="inline checkbox">
+                    <input type="checkbox" name="stop_onmatch" value="1" <?php echo $info['stop_onmatch']?'checked="checked"':''; ?> >
+                    <?php echo __('<strong>Stop</strong> processing further on match!');?>
+                    </label>
+                    &nbsp;<i class="help-tip icon-question-sign" href="#execution_order"></i>
+                </td>
+            </tr>
+            <tr>
+                <td width="180" class="required">
+                    <?php echo __('Filter Status');?>:
+                </td>
+                <td>
+                    <input type="radio" name="isactive" value="1" <?php echo
+                    $info['isactive']?'checked="checked"':''; ?>> <?php echo __('Active'); ?>
+                    <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked="checked"':''; ?>
+                    > <?php echo __('Disabled'); ?>
+                    &nbsp;<span class="error">*&nbsp;</span>
+                </td>
+            </tr>
+            <tr>
+                <td width="180" class="required">
+                    <?php echo __('Target Channel');?>:
+                </td>
+                <td>
+                    <select name="target">
+                       <option value="">&mdash; <?php echo __('Select a Channel');?> &mdash;</option>
+                       <?php
+                       foreach(Filter::getTargets() as $k => $v) {
+                           echo sprintf('<option value="%s" %s>%s</option>',
+                                   $k, (($k==$info['target'])?'selected="selected"':''), $v);
                         }
-                        echo '</OPTGROUP>';
-                    }
-                    ?>
-                </select>
-                &nbsp;
-                <span class="error">*&nbsp;<?php echo $errors['target']; ?></span>&nbsp;
-                <i class="help-tip icon-question-sign" href="#target_channel"></i>
-            </td>
-        </tr>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Filter Rules');?></strong>: <?php
-                echo __('Rules are applied based on the criteria.');?>&nbsp;<span class="error">*&nbsp;<?php echo
-                $errors['rules']; ?></span></em>
-            </th>
-        </tr>
-        <tr>
-            <td colspan=2>
-               <em><?php echo __('Rules Matching Criteria');?>:</em>
-                &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
-                <input type="radio" name="match_all_rules" value="1" <?php echo $info['match_all_rules']?'checked="checked"':''; ?>><?php echo __('Match All');?>
-                &nbsp;&nbsp;&nbsp;
-                <input type="radio" name="match_all_rules" value="0" <?php echo !$info['match_all_rules']?'checked="checked"':''; ?>><?php echo __('Match Any');?>
-                &nbsp;<span class="error">*&nbsp;</span>
-                <em>(<?php echo __('case-insensitive comparison');?>)</em>
-                &nbsp;<i class="help-tip icon-question-sign" href="#rules_matching_criteria"></i>
-
-            </td>
-        </tr>
-        <?php
-        $n=($filter?$filter->getNumRules():0)+2; //2 extra rules of unlimited.
-        for($i=1; $i<=$n; $i++){ ?>
-        <tr id="r<?php echo $i; ?>">
-            <td colspan="2">
-                <div>
-                    <select style="max-width: 200px;" name="rule_w<?php echo $i; ?>">
-                        <option value="">&mdash; <?php echo __('Select One');?> &mdash;</option>
-                        <?php
-                        foreach ($matches as $group=>$ms) { ?>
-                            <optgroup label="<?php echo __($group); ?>"><?php
-                            foreach ($ms as $k=>$v) {
-                                $sel=($info["rule_w$i"]==$k)?'selected="selected"':'';
-                                echo sprintf('<option value="%s" %s>%s</option>',
-                                    $k,$sel,__($v));
-                            } ?>
-                        </optgroup>
-                        <?php } ?>
-                    </select>
-                    <select name="rule_h<?php echo $i; ?>">
-                        <option value="0">&mdash; <?php echo __('Select One');?> &mdash;</option>
-                        <?php
-                        foreach($match_types as $k=>$v){
-                            $sel=($info["rule_h$i"]==$k)?'selected="selected"':'';
-                            echo sprintf('<option value="%s" %s>%s</option>',
-                                $k,$sel,$v);
+                        $sql='SELECT email_id,email,name FROM '.EMAIL_TABLE.' email ORDER by name';
+                        if(($res=db_query($sql)) && db_num_rows($res)) {
+                            echo sprintf('<OPTGROUP label="%s">', __('System Emails'));
+                            while(list($id,$email,$name)=db_fetch_row($res)) {
+                                $selected=($info['email_id'] && $id==$info['email_id'])?'selected="selected"':'';
+                                if($name)
+                                    $email=Format::htmlchars("$name <$email>");
+                                echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$email);
+                            }
+                            echo '</OPTGROUP>';
                         }
                         ?>
-                    </select>&nbsp;
-                    <input class="ltr" type="text" size="60" name="rule_v<?php echo $i; ?>" value="<?php echo $info["rule_v$i"]; ?>">
-                    &nbsp;<span class="error">&nbsp;<?php echo $errors["rule_$i"]; ?></span>
-                <?php
-                if($info["rule_w$i"] || $info["rule_h$i"] || $info["rule_v$i"]){ ?>
-                <div class="pull-right" style="padding-right:20px;"><a href="#" class="clearrule">(<?php echo __('clear');?>)</a></div>
+                    </select>
+                    &nbsp;
+                    <span class="error">*&nbsp;<?php echo $errors['target']; ?></span>&nbsp;
+                    <i class="help-tip icon-question-sign" href="#target_channel"></i>
+                </td>
+            </tr>
+        </tbody>
+    </table>
+    <ul class="clean tabs" style="margin-top:20px;" id="filter-tabs">
+        <li class="active"><a href="#filter_rules"><i class="icon-filter"></i> <?php echo __('Filter Rules'); ?></a></li>
+        <li><a href="#filter_actions"><i class="icon-bolt"></i> <?php echo __('Filter Actions'); ?></a></li>
+        <li><a href="#internal_notes"><i class="icon-file-text-alt"></i> <?php echo __('Internal Notes'); ?></a></li>
+    </ul>
+    <!-- ====================== FILTER RULES ========================== -->
+    <div class="tab_content" id="filter_rules">
+        <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
+            <thead>
+                <tr>
+                    <th colspan="2" style="text-align:left;">
+                        <em><strong><?php echo __('Filter Rules');?></strong>: <?php
+                        echo __('Rules are applied based on the criteria.');?>&nbsp;<span class="error">*&nbsp;<?php echo
+                        $errors['rules']; ?></span></em>
+                    </th>
+                </tr>
+            </thead>
+            <tbody id="rules">
+                <tr>
+                    <td colspan=2>
+                       <em><?php echo __('Rules Matching Criteria');?>:</em>
+                        &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
+                        <label>
+                        <input type="radio" name="match_all_rules" value="1" <?php echo $info['match_all_rules']?'checked="checked"':''; ?>>
+                            <?php echo __('Match All');?>
+                        </label>
+                        <span style="display:inline-block;width:10px"> </span>
+                        <label>
+                        <input type="radio" name="match_all_rules" value="0" <?php echo !$info['match_all_rules']?'checked="checked"':''; ?>>
+                            <?php echo __('Match Any');?>
+                        </label>
+                        <span class="error">*</span>
+                        <em>(<?php echo __('case-insensitive comparison');?>)</em>
+                        &nbsp;<i class="help-tip icon-question-sign" href="#rules_matching_criteria"></i>
+                    </td>
+                </tr>
                 <?php
+                foreach ($info['rules'] as $i=>$rule) { ?>
+                <tr>
+                    <td colspan="2">
+                        <select style="max-width: 200px;" name="rules[<?php echo $i; ?>][w]">
+                            <option value="">&mdash; <?php echo __('Select One');?> &mdash;</option>
+                                <?php
+                                foreach ($matches as $group=>$ms) { ?>
+                                    <optgroup label="<?php echo __($group); ?>"><?php
+                                    foreach ($ms as $k=>$v) {
+                                        $sel=($rule["w"]==$k)?'selected="selected"':'';
+                                        echo sprintf('<option value="%s" %s>%s</option>',
+                                            $k,$sel,__($v));
+                                    } ?>
+                                </optgroup>
+                                <?php } ?>
+                            </select>
+                            <select name="rules[<?php echo $i; ?>][h]">
+                                <option value="0">&mdash; <?php echo __('Select One');?> &mdash;</option>
+                                <?php
+                                    foreach($match_types as $k=>$v){
+                                    $sel=($rule["h"]==$k)?'selected="selected"':'';
+                                    echo sprintf('<option value="%s" %s>%s</option>',
+                                        $k,$sel,$v);
+                                }
+                                ?>
+                            </select>&nbsp;
+                            <input type="text" size="60" name="rules[<?php echo $i; ?>][v]" value="<?php echo $rule["v"]; ?>">
+                        <div class="pull-right" style="padding-right:20px;"><a href="#" class="clearrule"
+                            onclick="javascript: $(this).closest('tr').remove();">(<?php echo __('clear');?>)</a></div>
+                        <div class="error"><?php echo $errors["rule_$i"]; ?></div>
+                    </td>
+                </tr>
+<?php           $maxi = max($maxi ?: 0, $i+1);
                 } ?>
-                </div>
-            </td>
-        </tr>
-        <?php
-            if($i>=25) //Hardcoded limit of 25 rules...also see class.filter.php
-               break;
-        } ?>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Filter Actions');?></strong>: <?php
-                echo __('Can be overwridden by other filters depending on processing order.');?>&nbsp;</em>
-            </th>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Reject Ticket');?>:
-            </td>
-            <td>
-                <input type="checkbox" name="reject_ticket" value="1" <?php echo $info['reject_ticket']?'checked="checked"':''; ?> >
-                    <strong><font class="error"><?php echo __('Reject Ticket');?></font></strong>
-                    &nbsp;<i class="help-tip icon-question-sign" href="#reject_ticket"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Reply-To Email');?>:
-            </td>
-            <td>
-                <input type="checkbox" name="use_replyto_email" value="1" <?php echo $info['use_replyto_email']?'checked="checked"':''; ?> >
-                    <?php echo __('<strong>Use</strong> Reply-To Email');?> <em>(<?php echo __('if available');?>)</em>
-                    &nbsp;<i class="help-tip icon-question-sign" href="#reply_to_email"></i></em>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Ticket auto-response');?>:
-            </td>
-            <td>
-                <input type="checkbox" name="disable_autoresponder" value="1" <?php echo $info['disable_autoresponder']?'checked="checked"':''; ?> >
-                    <?php echo __('<strong>Disable</strong> auto-response.');?>
-                    &nbsp;<i class="help-tip icon-question-sign" href="#ticket_auto_response"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Canned Response');?>:
-            </td>
-                <td>
-                <select name="canned_response_id">
-                    <option value="">&mdash; <?php echo __('None');?> &mdash;</option>
-                    <?php
-                    $sql='SELECT canned_id, title, isenabled FROM '.CANNED_TABLE .' ORDER by title';
-                    if ($res=db_query($sql)) {
-                        while (list($id, $title, $isenabled)=db_fetch_row($res)) {
-                            $selected=($info['canned_response_id'] &&
-                                    $id==$info['canned_response_id'])
-                                ? 'selected="selected"' : '';
-
-                            if (!$isenabled)
-                                $title .= ' ' . __('(disabled)');
-
-                            echo sprintf('<option value="%d" %s>%s</option>',
-                                $id, $selected, $title);
-                        }
-                    }
-                    ?>
-                </select>
-                &nbsp;<i class="help-tip icon-question-sign" href="#canned_response"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Department');?>:
-            </td>
-            <td>
-                <select name="dept_id">
-                    <option value="">&mdash; <?php echo __('Default');?> &mdash;</option>
-                    <?php
-                    $sql='SELECT dept_id,dept_name FROM '.DEPT_TABLE.' dept ORDER by dept_name';
-                    if(($res=db_query($sql)) && db_num_rows($res)){
-                        while(list($id,$name)=db_fetch_row($res)){
-                            $selected=($info['dept_id'] && $id==$info['dept_id'])?'selected="selected"':'';
-                            echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name);
-                        }
-                    }
-                    ?>
-                </select>
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['dept_id']; ?></span>&nbsp;<i class="help-tip icon-question-sign" href="#department"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Status'); ?>:
-            </td>
-            <td>
-                <span>
-                <select name="status_id">
-                    <option value="">&mdash; <?php echo __('Default'); ?> &mdash;</option>
-                    <?php
-                    foreach (TicketStatusList::getStatuses() as $status) {
-                        $name = $status->getName();
-                        if (!($isenabled = $status->isEnabled()))
-                            $name.=' '.__('(disabled)');
-
-                        echo sprintf('<option value="%d" %s %s>%s</option>',
-                                $status->getId(),
-                                ($info['status_id'] == $status->getId())
-                                 ? 'selected="selected"' : '',
-                                 $isenabled ? '' : 'disabled="disabled"',
-                                 $name
-                                );
-                    }
-                    ?>
-                </select>
-                &nbsp;
-                <span class="error"><?php echo $errors['status_id']; ?></span>
-                <i class="help-tip icon-question-sign" href="#status"></i>
-                </span>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Priority');?>:
-            </td>
-            <td>
-                <select name="priority_id">
-                    <option value="">&mdash; <?php echo __('Default');?> &mdash;</option>
-                    <?php
-                    $sql='SELECT priority_id,priority_desc FROM '.PRIORITY_TABLE.' pri ORDER by priority_urgency DESC';
-                    if(($res=db_query($sql)) && db_num_rows($res)){
-                        while(list($id,$name)=db_fetch_row($res)){
-                            $selected=($info['priority_id'] && $id==$info['priority_id'])?'selected="selected"':'';
-                            echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name);
-                        }
-                    }
-                    ?>
-                </select>
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['priority_id']; ?></span>
-                &nbsp;<i class="help-tip icon-question-sign" href="#priority"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('SLA Plan');?>:
-            </td>
-            <td>
-                <select name="sla_id">
-                    <option value="0">&mdash; <?php echo __('System Default');?> &mdash;</option>
-                    <?php
-                    if($slas=SLA::getSLAs()) {
-                        foreach($slas as $id =>$name) {
-                            echo sprintf('<option value="%d" %s>%s</option>',
-                                    $id, ($info['sla_id']==$id)?'selected="selected"':'',$name);
-                        }
-                    }
-                    ?>
-                </select>
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['sla_id']; ?></span>
-                &nbsp;<i class="help-tip icon-question-sign" href="#sla_plan"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Auto-assign To');?>:
-            </td>
-            <td>
-                <select name="assign">
-                    <option value="0">&mdash; <?php echo __('Unassigned');?> &mdash;</option>
-                    <?php
-                    if (($users=Staff::getStaffMembers())) {
-                        echo '<OPTGROUP label="'.__('Agents').'">';
-                        foreach($users as $id => $name) {
-                            $name = new PersonsName($name);
-                            $k="s$id";
-                            $selected = ($info['assign']==$k || $info['staff_id']==$id)?'selected="selected"':'';
+            </tbody>
+            <tbody class="hidden" id="new-rule-template">
+                <tr>
+                    <td colspan="2">
+                        <select style="max-width: 200px;" data-name="rulew">
+                            <option value="">&mdash; <?php echo __('Select One');?> &mdash;</option>
+                            <?php
+                            foreach ($matches as $group=>$ms) { ?>
+                                <optgroup label="<?php echo __($group); ?>"><?php
+                                foreach ($ms as $k=>$v) {
+                                    echo sprintf('<option value="%s">%s</option>',
+                                        $k,__($v));
+                                } ?>
+                            </optgroup>
+                            <?php } ?>
+                        </select>
+                        <select data-name="ruleh">
+                            <option value="0">&mdash; <?php echo __('Select One');?> &mdash;</option>
+                            <?php
+                                foreach($match_types as $k=>$v){
+                                echo sprintf('<option value="%s">%s</option>',
+                                    $k,$v);
+                            }
                             ?>
-                            <option value="<?php echo $k; ?>"<?php echo $selected; ?>><?php echo $name; ?></option>
-                        <?php
-                        }
-                        echo '</OPTGROUP>';
-                    }
-                    $sql='SELECT team_id, isenabled, name FROM '.TEAM_TABLE .' ORDER BY name';
-                    if(($res=db_query($sql)) && db_num_rows($res)){
-                        echo '<OPTGROUP label="'.__('Teams').'">';
-                        while (list($id, $isenabled, $name) = db_fetch_row($res)){
-                            $k="t$id";
-                            $selected = ($info['assign']==$k || $info['team_id']==$id)?'selected="selected"':'';
-                            if (!$isenabled)
-                                $name .= ' (disabled)';
-                            ?>
-                            <option value="<?php echo $k; ?>"<?php echo $selected; ?>><?php echo $name; ?></option>
-                        <?php
-                        }
-                        echo '</OPTGROUP>';
-                    }
-                    ?>
-                </select>
-                &nbsp;<span class="error">&nbsp;<?php echo
-                $errors['assign']; ?></span><i class="help-tip icon-question-sign" href="#auto_assign"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Help Topic'); ?>
-            </td>
-            <td>
-                <select name="topic_id">
-                    <option value="0" selected="selected">&mdash; <?php
-                        echo __('Unchanged'); ?> &mdash;</option>
+                        </select>&nbsp;
+                        <input type="text" size="60" data-name="rulev">
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+        <div style="padding: 5px">
+            <button class="green button" type="button" id="add-rule">
+                <i class="icon-plus-sign"></i> <?php echo __('Add Rule'); ?>
+            </button>
+        </div>
+    </div>
+    <!-- ======================= FILTER ACTIONS ========================= -->
+    <div class="tab_content hidden" id="filter_actions">
+        <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
+            <thead>
+                <tr>
+                    <th colspan="2">
+                        <em><strong><?php echo __('Filter Actions');?></strong>:
+                        <div><?php
+                            echo __('Can be overwridden by other filters depending on processing order.');
+                        ?><br/><?php
+                            echo __('Actions are executed in the order declared below');
+                            ?></div></em>
+                    </th>
+                </tr>
+            </thead>
+            <tbody id="dynamic-actions" class="sortable-rows">
+                <?php
+                $existing = array();
+                if ($filter) { foreach ($filter->getActions() as $A) {
+                    $existing[] = $A->type;
+                ?>
+                <tr style="background-color:white"><td><i class="icon-sort icon-large icon-muted"></i>
+                    <?php echo $A->getImpl()->getName(); ?>:</td>
+                    <td>
+                        <div style="position:relative"><?php
+                        $form = $A->getImpl()->getConfigurationForm($_POST ?: false);
+                        // XXX: Drop this when the ORM supports proper caching
+                        $form->isValid();
+                        include STAFFINC_DIR . 'templates/dynamic-form-simple.tmpl.php';
+                        ?>
+                        <input type="hidden" name="actions[]" value="I<?php echo $A->getId(); ?>"/>
+                        <div class="pull-right" style="position:absolute;top:2px;right:2px;">
+                            <a href="#" title="<?php echo __('clear'); ?>" onclick="javascript:
+                                if (!confirm(__('You sure?')))
+                                return false;
+                                $(this).closest('td').find('input[name=\'actions[]\']')
+                                    .val(function(i,v) { return 'D' + v.substring(1); });
+                                $(this).closest('tr').fadeOut(400, function() { $(this).hide(); });
+                                return false;"><i class="icon-trash"></i></a>
+                            </div>
+                        </div>
+                    </td>
+                </tr>
+                <?php } } ?>
+                </tbody>
+            </table>
+            <div style="padding: 5px">
+                <i class="icon-plus-sign"></i>
+                <select name="new-action" id="new-action-select"
+                        onchange="javascript: $('#new-action-btn').trigger('click');">
+                    <option value=""><?php echo __('— Select an Action —'); ?></option>
                     <?php
-                    foreach (Topic::getHelpTopics(false, Topic::DISPLAY_DISABLED) as $id=>$name) {
-                        $selected=($info['topic_id'] && $id==$info['topic_id'])?'selected="selected"':'';
-                        echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name);
-                    }
+                    $current_group = '';
+                    foreach (FilterAction::allRegistered() as $group=>$actions) {
+                        if ($group && $current_group != $group) {
+                            if ($current_group) echo '</optgroup>';
+                            $current_group = $group;
+                            ?><optgroup label="<?php echo Format::htmlchars($group); ?>"><?php
+                        }
+                        foreach ($actions as $type=>$name) {
                     ?>
+                    <option data-title="<?php echo $name; ?>" value="<?php echo $type; ?>"
+                            data-multi-use="<?php echo $mu = FilterAction::lookupByType($type)->hasFlag(TriggerAction::FLAG_MULTI_USE); ?> " <?php
+                            if (in_array($type, $existing) && !$mu) echo 'disabled="disabled"';
+                            ?>><?php echo $name; ?></option>
+                    <?php }
+                    } ?>
                 </select>
-                &nbsp;<span class="error"><?php echo $errors['topic_id']; ?></span><i class="help-tip icon-question-sign" href="#help_topic"></i>
-            </td>
-        </tr>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Internal Notes');?></strong>: <?php
-                    echo __("be liberal, they're internal");?></em>
-            </th>
-        </tr>
-        <tr>
-            <td colspan=2>
-                <textarea class="richtext no-bar" name="notes" cols="21"
-                    rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea>
-            </td>
-        </tr>
-    </tbody>
-</table>
-<p style="text-align:center;">
-    <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
-    <input type="reset"  name="reset"  value="<?php echo __('Reset');?>">
-    <input type="button" name="cancel" value="<?php echo __('Cancel');?>" onclick='window.location.href="filters.php"'>
-</p>
+                <button id="new-action-btn" type="button" class="inline green button" onclick="javascript:
+                    var dropdown = $('#new-action-select'), selected = dropdown.find(':selected');
+                    dropdown.val('');
+                    $('#dynamic-actions')
+                      .append($('<tr></tr>')
+                        .append($('<td></td>')
+                          .text(selected.data('title') + ':')
+                        ).append($('<td></td>')
+                          .append($('<em></em>').text(__('Loading ...')))
+                          .load('ajax.php/filter/action/' + selected.val() + '/config', function() {
+                            if (!selected.data('multiUse')) selected.prop('disabled', true);
+                          })
+                        )
+                      ).append(
+                        $('<input>').attr({type:'hidden',name:'actions[]',value:'N'+selected.val()})
+                      );"><?php echo __('Add'); ?>
+                </button>
+            </div>
+    </div>
+    <!-- ======================== INTERNAL NOTES ======================== -->
+    <div class="tab_content hidden" id="internal_notes">
+        <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
+            <thead>
+                <tr>
+                    <th colspan="2">
+                        <em><strong><?php echo __('Internal Notes');?></strong>: <?php
+                            echo __("be liberal, they're internal");?></em>
+                    </th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr>
+                    <td colspan=2>
+                        <textarea class="richtext no-bar" name="notes" cols="21"
+                            rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea>
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+    </div>
+    <p style="text-align:center;">
+        <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
+        <input type="reset"  name="reset"  value="<?php echo __('Reset');?>">
+        <input type="button" name="cancel" value="<?php echo __('Cancel');?>" onclick='window.location.href="filters.php"'>
+    </p>
 </form>
+<script type="text/javascript">
+   var fixHelper = function(e, ui) {
+      ui.children().each(function() {
+          $(this).width($(this).width());
+      });
+      return ui;
+   };
+   $(function() {
+     $('#dynamic-actions').sortable({helper: fixHelper, opacity: 0.5});
+     var next = <?php echo $maxi ?: 0; ?>;
+     $('#add-rule').click(function() {
+       var clone = $('#new-rule-template tr').clone();
+       clone.find('[data-name=rulew]').attr('name', 'rules['+next+'][w]');
+       clone.find('[data-name=ruleh]').attr('name', 'rules['+next+'][h]');
+       clone.find('[data-name=rulev]').attr('name', 'rules['+next+'][v]');
+       clone.appendTo('#rules');
+       next++;
+     });
+<?php if (!$info['rules']) { ?>
+        $('#add-rule').trigger('click').trigger('click');
+<?php } ?>
+   });
+</script>
diff --git a/include/staff/filters.inc.php b/include/staff/filters.inc.php
index e3eac064393983561377441d9fefe92a8d75bd6c..25a753302064dc9ebf177eca8e83535cd58d45f2 100644
--- a/include/staff/filters.inc.php
+++ b/include/staff/filters.inc.php
@@ -44,29 +44,58 @@ else
     $showing=__('No filters found!');
 
 ?>
-
-<div class="pull-left" style="width:700px;padding-top:5px;">
- <h2><?php echo __('Ticket Filters');?></h2>
+<form action="filters.php" method="POST" name="filters">
+<div class="sticky bar opaque">
+    <div class="content">
+        <div class="pull-left flush-left">
+            <h2><?php echo __('Ticket Filters');?></h2>
+        </div>
+        <div class="pull-right flush-right">
+            <a href="filters.php?a=add" class="green button action-button"><i class="icon-plus-sign"></i> <?php echo __('Add New Filter');?></a>
+            <span class="action-button" data-dropdown="#action-dropdown-more">
+                <i class="icon-caret-down pull-right"></i>
+                <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+            </span>
+            <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                <ul id="actions">
+                    <li>
+                        <a class="confirm" data-name="enable" href="filters.php?a=enable">
+                            <i class="icon-ok-sign icon-fixed-width"></i>
+                            <?php echo __( 'Enable'); ?>
+                        </a>
+                    </li>
+                    <li>
+                        <a class="confirm" data-name="disable" href="filters.php?a=disable">
+                            <i class="icon-ban-circle icon-fixed-width"></i>
+                            <?php echo __( 'Disable'); ?>
+                        </a>
+                    </li>
+                    <li class="danger">
+                        <a class="confirm" data-name="delete" href="filters.php?a=delete">
+                            <i class="icon-trash icon-fixed-width"></i>
+                            <?php echo __( 'Delete'); ?>
+                        </a>
+                    </li>
+                </ul>
+            </div>
+        </div>
+    </div>
 </div>
-<div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;">
- <b><a href="filters.php?a=add" class="Icon newTicketFilter"><?php echo __('Add New Filter');?></a></b></div>
 <div class="clear"></div>
-<form action="filters.php" method="POST" name="filters">
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="mass_process" >
 <input type="hidden" id="action" name="a" value="" >
  <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption><?php echo $showing; ?></caption>
     <thead>
         <tr>
-            <th width="7">&nbsp;</th>
-            <th width="320"><a <?php echo $name_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name');?></a></th>
-            <th width="80"><a  <?php echo $status_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Status');?></a></th>
-            <th width="80" style="text-align:center;"><a  <?php echo $order_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=order"><?php echo __('Order');?></a></th>
-            <th width="80" style="text-align:center;"><a  <?php echo $rules_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=rules"><?php echo __('Rules');?></a></th>
-            <th width="100"><a  <?php echo $target_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=target"><?php echo __('Target');?></a></th>
-            <th width="120" nowrap><a  <?php echo $created_sort; ?>href="filters.php?<?php echo $qstr; ?>&sort=created"><?php echo __('Date Added');?></a></th>
-            <th width="150" nowrap><a  <?php echo $updated_sort; ?>href="filters.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated');?></a></th>
+            <th width="4%">&nbsp;</th>
+            <th width="32%"><a <?php echo $name_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name');?></a></th>
+            <th width="8%"><a  <?php echo $status_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Status');?></a></th>
+            <th width="8%" style="text-align:center;"><a  <?php echo $order_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=order"><?php echo __('Order');?></a></th>
+            <th width="8%" style="text-align:center;"><a  <?php echo $rules_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=rules"><?php echo __('Rules');?></a></th>
+            <th width="10%"><a  <?php echo $target_sort; ?> href="filters.php?<?php echo $qstr; ?>&sort=target"><?php echo __('Target');?></a></th>
+            <th width="12%" nowrap><a  <?php echo $created_sort; ?>href="filters.php?<?php echo $qstr; ?>&sort=created"><?php echo __('Date Added');?></a></th>
+            <th width="18%" nowrap><a  <?php echo $updated_sort; ?>href="filters.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated');?></a></th>
         </tr>
     </thead>
     <tbody>
@@ -80,7 +109,7 @@ else
                     $sel=true;
                 ?>
             <tr id="<?php echo $row['id']; ?>">
-                <td width=7px>
+                <td align="center">
                   <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['id']; ?>"
                             <?php echo $sel?'checked="checked"':''; ?>>
                 </td>
@@ -89,8 +118,8 @@ else
                 <td style="text-align:right;padding-right:25px;"><?php echo $row['execorder']; ?>&nbsp;</td>
                 <td style="text-align:right;padding-right:25px;"><?php echo $row['rules']; ?>&nbsp;</td>
                 <td>&nbsp;<?php echo Format::htmlchars($targets[$row['target']]); ?></td>
-                <td>&nbsp;<?php echo Format::db_date($row['created']); ?></td>
-                <td>&nbsp;<?php echo Format::db_datetime($row['updated']); ?></td>
+                <td>&nbsp;<?php echo Format::date($row['created']); ?></td>
+                <td>&nbsp;<?php echo Format::datetime($row['updated']); ?></td>
             </tr>
             <?php
             } //end of while.
@@ -114,11 +143,7 @@ else
 if($res && $num): //Show options..
     echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
 ?>
-<p class="centered" id="actions">
-    <input class="button" type="submit" name="enable" value="<?php echo __('Enable');?>">
-    <input class="button" type="submit" name="disable" value="<?php echo __('Disable');?>">
-    <input class="button" type="submit" name="delete" value="<?php echo __('Delete');?>">
-</p>
+
 <?php
 endif;
 ?>
diff --git a/include/staff/footer.inc.php b/include/staff/footer.inc.php
index 12c9b034dfe457bd9ff7dbff974b754250858de8..5d0cc65aef0a245665d6921d8f7a2bb14b4be8be 100644
--- a/include/staff/footer.inc.php
+++ b/include/staff/footer.inc.php
@@ -8,7 +8,7 @@
 if(is_object($thisstaff) && $thisstaff->isStaff()) { ?>
     <div>
         <!-- Do not remove <img src="autocron.php" alt="" width="1" height="1" border="0" /> or your auto cron will cease to function -->
-        <img src="autocron.php" alt="" width="1" height="1" border="0" />
+        <img src="<?php echo ROOT_PATH; ?>scp/autocron.php" alt="" width="1" height="1" border="0" />
         <!-- Do not remove <img src="autocron.php" alt="" width="1" height="1" border="0" /> or your auto cron will cease to function -->
     </div>
 <?php
@@ -19,7 +19,7 @@ if(is_object($thisstaff) && $thisstaff->isStaff()) { ?>
     <i class="icon-spinner icon-spin icon-3x pull-left icon-light"></i>
     <h1><?php echo __('Loading ...');?></h1>
 </div>
-<div class="dialog draggable" style="display:none;width:650px;" id="popup">
+<div class="dialog draggable" style="display:none;" id="popup">
     <div id="popup-loading">
         <h1 style="margin-bottom: 20px;"><i class="icon-spinner icon-spin icon-large"></i>
         <?php echo __('Loading ...');?></h1>
@@ -34,24 +34,38 @@ if(is_object($thisstaff) && $thisstaff->isStaff()) { ?>
     <hr style="margin-top:3em"/>
     <p class="full-width">
         <span class="buttons pull-right">
-            <input type="button" value="<?php echo __('OK');?>" class="close">
+            <input type="button" value="<?php echo __('OK');?>" class="close ok">
         </span>
      </p>
     <div class="clear"></div>
 </div>
 
+<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery.pjax.js"></script>
+<script type="text/javascript" src="<?php echo ROOT_PATH; ?>scp/js/bootstrap-typeahead.js"></script>
+<script type="text/javascript" src="<?php echo ROOT_PATH; ?>scp/js/scp.js"></script>
+<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-1.10.3.custom.min.js"></script>
+<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/filedrop.field.js"></script>
+<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/select2.min.js"></script>
+<script type="text/javascript" src="<?php echo ROOT_PATH; ?>scp/js/tips.js"></script>
+<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor.min.js"></script>
+<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor-osticket.js"></script>
+<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor-plugins.js"></script>
+<script type="text/javascript" src="<?php echo ROOT_PATH; ?>scp/js/jquery.translatable.js"></script>
+<script type="text/javascript" src="<?php echo ROOT_PATH; ?>scp/js/jquery.dropdown.js"></script>
+<script type="text/javascript" src="<?php echo ROOT_PATH; ?>scp/js/bootstrap-tooltip.js"></script>
+<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/fabric.min.js"></script>
+<link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>scp/css/tooltip.css">
 <script type="text/javascript">
-if ($.support.pjax) {
-  $(document).on('click', 'a', function(event) {
-    if (!$(this).hasClass('no-pjax')
-        && !$(this).closest('.no-pjax').length
-        && $(this).attr('href')[0] != '#')
-      $.pjax.click(event, {container: $('#pjax-container'), timeout: 2000});
-  })
-}
+    getConfig().resolve(<?php
+        include INCLUDE_DIR . 'ajax.config.php';
+        $api = new ConfigAjaxAPI();
+        print $api->scp(false);
+    ?>);
 </script>
 <?php
-if ($thisstaff && $thisstaff->getLanguage() != 'en_US') { ?>
+if ($thisstaff
+        && ($lang = $thisstaff->getLanguage())
+        && 0 !== strcasecmp($lang, 'en_US')) { ?>
     <script type="text/javascript" src="ajax.php/i18n/<?php
         echo $thisstaff->getLanguage(); ?>/js"></script>
 <?php } ?>
diff --git a/include/staff/group.inc.php b/include/staff/group.inc.php
deleted file mode 100644
index bbadcc066a8b9635e4df61ecf16840d43927af02..0000000000000000000000000000000000000000
--- a/include/staff/group.inc.php
+++ /dev/null
@@ -1,188 +0,0 @@
-<?php
-if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
-$info = $qs = array();
-if($group && $_REQUEST['a']!='add'){
-    $title=__('Update Group');
-    $action='update';
-    $submit_text=__('Save Changes');
-    $info=$group->getInfo();
-    $info['id']=$group->getId();
-    $info['depts']=$group->getDepartments();
-    $qs += array('id' => $group->getId());
-}else {
-    $title=__('Add New Group');
-    $action='create';
-    $submit_text=__('Create Group');
-    $info['isactive']=isset($info['isactive'])?$info['isactive']:1;
-    $info['can_create_tickets']=isset($info['can_create_tickets'])?$info['can_create_tickets']:1;
-    $qs += array('a' => $_REQUEST['a']);
-}
-$info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
-?>
-<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']); ?>">
- <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
- <h2><?php echo __('Group Access and Permissions');?></h2>
- <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
-    <thead>
-        <tr>
-            <th colspan="2">
-                <h4><?php echo $title; ?></h4>
-                <em><strong><?php echo __('Group Information');?></strong>: <?php echo __("Disabled group will limit agents' access. Admins are exempted.");?></em>
-            </th>
-        </tr>
-    </thead>
-    <tbody>
-        <tr>
-            <td width="180" class="required">
-                <?php echo __('Name');?>:
-            </td>
-            <td>
-                <input type="text" size="30" name="name" value="<?php echo $info['name']; ?>">
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['name']; ?></span>
-            </td>
-        </tr>
-        <tr>
-            <td width="180" class="required">
-                <?php echo __('Status');?>:
-            </td>
-            <td>
-                <input type="radio" name="isactive" value="1" <?php echo $info['isactive']?'checked="checked"':''; ?>><strong><?php echo __('Active');?></strong>
-                &nbsp;
-                <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked="checked"':''; ?>><strong><?php echo __('Disabled');?></strong>
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['status']; ?></span>
-                <i class="help-tip icon-question-sign" href="#status"></i>
-            </td>
-        </tr>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Group Permissions');?></strong>: <?php echo __('Applies to all group members');?>&nbsp;</em>
-            </th>
-        </tr>
-        <tr><td><?php echo __('Can <b>Create</b> Tickets');?></td>
-            <td>
-                <input type="radio" name="can_create_tickets"  value="1"   <?php echo $info['can_create_tickets']?'checked="checked"':''; ?> /><?php echo __('Yes');?>
-                &nbsp;&nbsp;
-                <input type="radio" name="can_create_tickets"  value="0"   <?php echo !$info['can_create_tickets']?'checked="checked"':''; ?> /><?php echo __('No');?>
-                &nbsp;&nbsp;<i><?php echo __('Ability to open tickets on behalf of users.');?></i>
-            </td>
-        </tr>
-        <tr><td><?php echo __('Can <b>Edit</b> Tickets</td>');?>
-            <td>
-                <input type="radio" name="can_edit_tickets"  value="1"   <?php echo $info['can_edit_tickets']?'checked="checked"':''; ?> /><?php echo __('Yes');?>
-                &nbsp;&nbsp;
-                <input type="radio" name="can_edit_tickets"  value="0"   <?php echo !$info['can_edit_tickets']?'checked="checked"':''; ?> /><?php echo __('No');?>
-                &nbsp;&nbsp;<i><?php echo __('Ability to edit tickets.');?></i>
-            </td>
-        </tr>
-        <tr><td><?php echo __('Can <b>Post Reply</b>');?></td>
-            <td>
-                <input type="radio" name="can_post_ticket_reply"  value="1"   <?php echo $info['can_post_ticket_reply']?'checked="checked"':''; ?> /><?php echo __('Yes');?>
-                &nbsp;&nbsp;
-                <input type="radio" name="can_post_ticket_reply"  value="0"   <?php echo !$info['can_post_ticket_reply']?'checked="checked"':''; ?> /><?php echo __('No');?>
-                &nbsp;&nbsp;<i><?php echo __('Ability to post a ticket reply.');?></i>
-            </td>
-        </tr>
-        <tr><td><?php echo __('Can <b>Close</b> Tickets');?></td>
-            <td>
-                <input type="radio" name="can_close_tickets"  value="1" <?php echo $info['can_close_tickets']?'checked="checked"':''; ?> /><?php echo __('Yes');?>
-                &nbsp;&nbsp;
-                <input type="radio" name="can_close_tickets"  value="0" <?php echo !$info['can_close_tickets']?'checked="checked"':''; ?> /><?php echo __('No');?>
-                &nbsp;&nbsp;<i><?php echo __('Ability to close tickets.  Agents can still post a response.');?></i>
-            </td>
-        </tr>
-        <tr><td><?php echo __('Can <b>Assign</b> Tickets');?></td>
-            <td>
-                <input type="radio" name="can_assign_tickets"  value="1" <?php echo $info['can_assign_tickets']?'checked="checked"':''; ?> /><?php echo __('Yes');?>
-                &nbsp;&nbsp;
-                <input type="radio" name="can_assign_tickets"  value="0" <?php echo !$info['can_assign_tickets']?'checked="checked"':''; ?> /><?php echo __('No');?>
-                &nbsp;&nbsp;<i><?php echo __('Ability to assign tickets to agents.');?></i>
-            </td>
-        </tr>
-        <tr><td><?php echo __('Can <b>Transfer</b> Tickets');?></td>
-            <td>
-                <input type="radio" name="can_transfer_tickets"  value="1" <?php echo $info['can_transfer_tickets']?'checked="checked"':''; ?> /><?php echo __('Yes');?>
-                &nbsp;&nbsp;
-                <input type="radio" name="can_transfer_tickets"  value="0" <?php echo !$info['can_transfer_tickets']?'checked="checked"':''; ?> /><?php echo __('No');?>
-                &nbsp;&nbsp;<i><?php echo __('Ability to transfer tickets between departments.');?></i>
-            </td>
-        </tr>
-        <tr><td><?php echo __('Can <b>Delete</b> Tickets');?></td>
-            <td>
-                <input type="radio" name="can_delete_tickets"  value="1"   <?php echo $info['can_delete_tickets']?'checked="checked"':''; ?> /><?php echo __('Yes');?>
-                &nbsp;&nbsp;
-                <input type="radio" name="can_delete_tickets"  value="0"   <?php echo !$info['can_delete_tickets']?'checked="checked"':''; ?> /><?php echo __('No');?>
-                &nbsp;&nbsp;<i><?php echo __("Ability to delete tickets (Deleted tickets can't be recovered!)");?></i>
-            </td>
-        </tr>
-        <tr><td><?php echo __('Can Ban Emails');?></td>
-            <td>
-                <input type="radio" name="can_ban_emails"  value="1" <?php echo $info['can_ban_emails']?'checked="checked"':''; ?> /><?php echo __('Yes');?>
-                &nbsp;&nbsp;
-                <input type="radio" name="can_ban_emails"  value="0" <?php echo !$info['can_ban_emails']?'checked="checked"':''; ?> /><?php echo __('No');?>
-                &nbsp;&nbsp;<i><?php echo __('Ability to add/remove emails from banlist via ticket interface.');?></i>
-            </td>
-        </tr>
-        <tr><td><?php echo __('Can Manage Premade');?></td>
-            <td>
-                <input type="radio" name="can_manage_premade"  value="1" <?php echo $info['can_manage_premade']?'checked="checked"':''; ?> /><?php echo __('Yes');?>
-                &nbsp;&nbsp;
-                <input type="radio" name="can_manage_premade"  value="0" <?php echo !$info['can_manage_premade']?'checked="checked"':''; ?> /><?php echo __('No');?>
-                &nbsp;&nbsp;<i><?php echo __('Ability to add/update/disable/delete canned responses and attachments.');?></i>
-            </td>
-        </tr>
-        <tr><td><?php echo __('Can Manage FAQ');?></td>
-            <td>
-                <input type="radio" name="can_manage_faq"  value="1" <?php echo $info['can_manage_faq']?'checked="checked"':''; ?> /><?php echo __('Yes');?>
-                &nbsp;&nbsp;
-                <input type="radio" name="can_manage_faq"  value="0" <?php echo !$info['can_manage_faq']?'checked="checked"':''; ?> /><?php echo __('No');?>
-                &nbsp;&nbsp;<i><?php echo __('Ability to add/update/disable/delete knowledgebase categories and FAQs.');?></i>
-            </td>
-        </tr>
-        <tr><td><?php echo __('Can View Agent Stats');?></td>
-            <td>
-                <input type="radio" name="can_view_staff_stats"  value="1" <?php echo $info['can_view_staff_stats']?'checked="checked"':''; ?> /><?php echo __('Yes');?>
-                &nbsp;&nbsp;
-                <input type="radio" name="can_view_staff_stats"  value="0" <?php echo !$info['can_view_staff_stats']?'checked="checked"':''; ?> /><?php echo __('No');?>
-                &nbsp;&nbsp;<i><?php echo __('Ability to view stats of other agents in allowed departments.');?></i>
-            </td>
-        </tr>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Department Access');?></strong>:
-                <i class="help-tip icon-question-sign" href="#department_access"></i>
-                &nbsp;<a id="selectAll" href="#deptckb"><?php echo __('Select All');?></a>
-                &nbsp;&nbsp;
-                <a id="selectNone" href="#deptckb"><?php echo __('Select None');?></a></em>
-            </th>
-        </tr>
-        <?php
-         $sql='SELECT dept_id,dept_name FROM '.DEPT_TABLE.' ORDER BY dept_name';
-         if(($res=db_query($sql)) && db_num_rows($res)){
-            while(list($id,$name) = db_fetch_row($res)){
-                $ck=($info['depts'] && in_array($id,$info['depts']))?'checked="checked"':'';
-                echo sprintf('<tr><td colspan=2>&nbsp;&nbsp;<input type="checkbox" class="deptckb" name="depts[]" value="%d" %s>%s</td></tr>',$id,$ck,$name);
-            }
-         }
-        ?>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Admin Notes');?></strong>: <?php echo __('Internal notes viewable by all admins.');?>&nbsp;</em>
-            </th>
-        </tr>
-        <tr>
-            <td colspan=2>
-                <textarea class="richtext no-bar" name="notes" cols="21"
-                    rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea>
-            </td>
-        </tr>
-    </tbody>
-</table>
-<p style="text-align:center">
-    <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
-    <input type="reset"  name="reset"  value="<?php echo __('Reset');?>">
-    <input type="button" name="cancel" value="<?php echo __('Cancel');?>" onclick='window.location.href="groups.php"'>
-</p>
-</form>
diff --git a/include/staff/groups.inc.php b/include/staff/groups.inc.php
deleted file mode 100644
index d7f5d55cf058167a51592e6189023318d1977127..0000000000000000000000000000000000000000
--- a/include/staff/groups.inc.php
+++ /dev/null
@@ -1,158 +0,0 @@
-<?php
-if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
-
-$qs = array();
-$sql='SELECT grp.*,count(DISTINCT staff.staff_id) as users, count(DISTINCT dept.dept_id) as depts '
-     .' FROM '.GROUP_TABLE.' grp '
-     .' LEFT JOIN '.STAFF_TABLE.' staff ON(staff.group_id=grp.group_id) '
-     .' LEFT JOIN '.GROUP_DEPT_TABLE.' dept ON(dept.group_id=grp.group_id) '
-     .' WHERE 1';
-$sortOptions=array('name'=>'grp.group_name','status'=>'grp.group_enabled',
-                   'users'=>'users', 'depts'=>'depts', 'created'=>'grp.created','updated'=>'grp.updated');
-$orderWays=array('DESC'=>'DESC','ASC'=>'ASC');
-$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'name';
-//Sorting options...
-if($sort && $sortOptions[$sort]) {
-    $order_column =$sortOptions[$sort];
-}
-$order_column=$order_column?$order_column:'grp.group_name';
-
-if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) {
-    $order=$orderWays[strtoupper($_REQUEST['order'])];
-}
-$order=$order?$order:'ASC';
-
-if($order_column && strpos($order_column,',')){
-    $order_column=str_replace(','," $order,",$order_column);
-}
-$x=$sort.'_sort';
-$$x=' class="'.strtolower($order).'" ';
-$order_by="$order_column $order ";
-
-$qs += array('order' => ($order=='DESC' ? 'ASC' : 'DESC'));
-$query="$sql GROUP BY grp.group_id ORDER BY $order_by";
-$res=db_query($query);
-if($res && ($num=db_num_rows($res)))
-    $showing=sprintf(__('Showing 1-%1$d of %2$d groups'), $num, $num);
-else
-    $showing=__('No groups found!');
-
-
-$qstr = '&amp;'.Http::build_query($qs);
-
-?>
-<div class="pull-left" style="width:700px;padding-top:5px;">
- <h2><?php echo __('Agent Groups');?>
-    <i class="help-tip icon-question-sign" href="#groups"></i>
-    </h2>
- </div>
-<div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;">
-    <b><a href="groups.php?a=add" class="Icon newgroup"><?php echo __('Add New Group');?></a></b></div>
-<div class="clear"></div>
-<form action="groups.php" method="POST" name="groups">
- <?php csrf_token(); ?>
- <input type="hidden" name="do" value="mass_process" >
- <input type="hidden" id="action" name="a" value="" >
- <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption><?php echo $showing; ?></caption>
-    <thead>
-        <tr>
-            <th width="7px">&nbsp;</th>
-            <th width="200"><a <?php echo $name_sort; ?> href="groups.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Group Name');?></a></th>
-            <th width="80"><a  <?php echo $status_sort; ?> href="groups.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Status');?></a></th>
-            <th width="80" style="text-align:center;"><a  <?php echo $users_sort; ?>href="groups.php?<?php echo $qstr; ?>&sort=users"><?php echo __('Members');?></a></th>
-            <th width="80" style="text-align:center;"><a  <?php echo $depts_sort; ?>href="groups.php?<?php echo $qstr; ?>&sort=depts"><?php echo __('Departments');?></a></th>
-            <th width="100"><a  <?php echo $created_sort; ?> href="groups.php?<?php echo $qstr; ?>&sort=created"><?php echo __('Created On');?></a></th>
-            <th width="120"><a  <?php echo $updated_sort; ?> href="groups.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated');?></a></th>
-        </tr>
-    </thead>
-    <tbody>
-    <?php
-        $total=0;
-        $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null;
-        if($res && db_num_rows($res)) {
-            while ($row = db_fetch_array($res)) {
-                $sel=false;
-                if($ids && in_array($row['group_id'],$ids))
-                    $sel=true;
-                ?>
-            <tr id="<?php echo $row['group_id']; ?>">
-                <td width=7px>
-                  <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['group_id']; ?>"
-                            <?php echo $sel?'checked="checked"':''; ?>> </td>
-                <td><a href="groups.php?id=<?php echo $row['group_id']; ?>"><?php echo $row['group_name']; ?></a> &nbsp;</td>
-                <td>&nbsp;<?php echo $row['group_enabled']?__('Active'):'<b>'.__('Disabled').'</b>'; ?></td>
-                <td style="text-align:right;padding-right:30px">&nbsp;&nbsp;
-                    <?php if($row['users']>0) { ?>
-                        <a href="staff.php?gid=<?php echo $row['group_id']; ?>"><?php echo $row['users']; ?></a>
-                    <?php }else{ ?> 0
-                    <?php } ?>
-                    &nbsp;
-                </td>
-                <td style="text-align:right;padding-right:30px">&nbsp;&nbsp;
-                    <?php echo $row['depts']; ?>
-                </td>
-                <td><?php echo Format::db_date($row['created']); ?>&nbsp;</td>
-                <td><?php echo Format::db_datetime($row['updated']); ?>&nbsp;</td>
-            </tr>
-            <?php
-            } //end of while.
-        } ?>
-    <tfoot>
-     <tr>
-        <td colspan="7">
-            <?php if($res && $num){ ?>
-            <?php echo __('Select');?>:&nbsp;
-            <a id="selectAll" href="#ckb"><?php echo __('All');?></a>&nbsp;&nbsp;
-            <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 groups found!');
-            } ?>
-        </td>
-     </tr>
-    </tfoot>
-</table>
-<?php
-if($res && $num): //Show options..
-?>
-<p class="centered" id="actions">
-    <input class="button" type="submit" name="enable" value="<?php echo __('Enable');?>" >
-    <input class="button" type="submit" name="disable" value="<?php echo __('Disable');?>" >
-    <input class="button" type="submit" name="delete" value="<?php echo __('Delete');?>">
-</p>
-<?php
-endif;
-?>
-</form>
-
-<div style="display:none;" class="dialog" id="confirm-action">
-    <h3><?php echo __('Please Confirm');?></h3>
-    <a class="close" href=""><i class="icon-remove-circle"></i></a>
-    <hr/>
-    <p class="confirm-action" style="display:none;" id="enable-confirm">
-        <?php echo sprintf(__('Are you sure you want to <b>enable</b> %s?'),
-            _N('selected group', 'selected groups', 2));?>
-    </p>
-    <p class="confirm-action" style="display:none;" id="disable-confirm">
-        <?php echo sprintf(__('Are you sure you want to <b>disable</b> %s?'),
-            _N('selected group', 'selected groups', 2));?>
-    </p>
-    <p class="confirm-action" style="display:none;" id="delete-confirm">
-        <font color="red"><strong><?php echo sprintf(__('Are you sure you want to DELETE %s?'),
-            _N('selected group', 'selected groups', 2));?></strong></font>
-        <br><br><?php echo __("Deleted data CANNOT be recovered and might affect agents' access.");?>
-    </p>
-    <div><?php echo __('Please confirm to continue.');?></div>
-    <hr style="margin-top:1em"/>
-    <p class="full-width">
-        <span class="buttons pull-left">
-            <input type="button" value="<?php echo __('No, Cancel');?>" class="close">
-        </span>
-        <span class="buttons pull-right">
-            <input type="button" value="<?php echo __('Yes, Do it!');?>" class="confirm">
-        </span>
-     </p>
-    <div class="clear"></div>
-</div>
-
diff --git a/include/staff/header.inc.php b/include/staff/header.inc.php
index 7604e259c213c1037dd922984c6c90b13a4cfa5a..13a122c54df8a1fb40a03435e431b068d9fd05bd 100644
--- a/include/staff/header.inc.php
+++ b/include/staff/header.inc.php
@@ -2,11 +2,14 @@
 header("Content-Type: text/html; charset=UTF-8");
 if (!isset($_SERVER['HTTP_X_PJAX'])) { ?>
 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
-<html <?php
+<html<?php
 if (($lang = Internationalization::getCurrentLanguage())
         && ($info = Internationalization::getLanguageInfo($lang))
         && (@$info['direction'] == 'rtl'))
-    echo 'dir="rtl" class="rtl"';
+    echo ' dir="rtl" class="rtl"';
+if ($lang) {
+    echo ' lang="' . Internationalization::rfc1766($lang) . '"';
+}
 ?>>
 <head>
     <meta http-equiv="content-type" content="text/html; charset=UTF-8">
@@ -20,31 +23,23 @@ if (($lang = Internationalization::getCurrentLanguage())
         .tip_shadow { display:block !important; }
     </style>
     <![endif]-->
-    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-1.8.3.min.js"></script>
-    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-1.10.3.custom.min.js"></script>
-    <script type="text/javascript" src="./js/scp.js"></script>
-    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery.pjax.js"></script>
-    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/filedrop.field.js"></script>
-    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery.multiselect.min.js"></script>
-    <script type="text/javascript" src="./js/tips.js"></script>
-    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor.min.js"></script>
-    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor-osticket.js"></script>
-    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor-fonts.js"></script>
-    <script type="text/javascript" src="./js/bootstrap-typeahead.js"></script>
+    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-1.11.2.min.js"></script>
     <link rel="stylesheet" href="<?php echo ROOT_PATH ?>css/thread.css" media="all">
-    <link rel="stylesheet" href="./css/scp.css" media="all">
+    <link rel="stylesheet" href="<?php echo ROOT_PATH ?>scp/css/scp.css" media="all">
     <link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/redactor.css" media="screen">
-    <link rel="stylesheet" href="./css/typeahead.css" media="screen">
+    <link rel="stylesheet" href="<?php echo ROOT_PATH ?>scp/css/typeahead.css" media="screen">
     <link type="text/css" href="<?php echo ROOT_PATH; ?>css/ui-lightness/jquery-ui-1.10.3.custom.min.css"
          rel="stylesheet" media="screen" />
      <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/font-awesome.min.css">
     <!--[if IE 7]>
     <link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/font-awesome-ie7.min.css">
     <![endif]-->
-    <link type="text/css" rel="stylesheet" href="./css/dropdown.css">
+    <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH ?>scp/css/dropdown.css">
     <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/loadingbar.css"/>
+    <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/flags.css">
+    <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/select2.min.css">
     <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/rtl.css"/>
-    <script type="text/javascript" src="./js/jquery.dropdown.js"></script>
+    <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH ?>scp/css/translatable.css"/>
 
     <?php
     if($ost && ($headers=$ost->getExtraHeaders())) {
@@ -66,16 +61,16 @@ if (($lang = Internationalization::getCurrentLanguage())
         <p id="info" class="pull-right no-pjax"><?php echo sprintf(__('Welcome, %s.'), '<strong>'.$thisstaff->getFirstName().'</strong>'); ?>
            <?php
             if($thisstaff->isAdmin() && !defined('ADMINPAGE')) { ?>
-            | <a href="admin.php" class="no-pjax"><?php echo __('Admin Panel'); ?></a>
+            | <a href="<?php echo ROOT_PATH ?>scp/admin.php" class="no-pjax"><?php echo __('Admin Panel'); ?></a>
             <?php }else{ ?>
-            | <a href="index.php" class="no-pjax"><?php echo __('Agent Panel'); ?></a>
+            | <a href="<?php echo ROOT_PATH ?>scp/index.php" class="no-pjax"><?php echo __('Agent Panel'); ?></a>
             <?php } ?>
-            | <a href="profile.php"><?php echo __('My Preferences'); ?></a>
-            | <a href="logout.php?auth=<?php echo $ost->getLinkToken(); ?>" class="no-pjax"><?php echo __('Log Out'); ?></a>
+            | <a href="<?php echo ROOT_PATH ?>scp/profile.php"><?php echo __('Profile'); ?></a>
+            | <a href="<?php echo ROOT_PATH ?>scp/logout.php?auth=<?php echo $ost->getLinkToken(); ?>" class="no-pjax"><?php echo __('Log Out'); ?></a>
         </p>
-        <a href="index.php" class="no-pjax" id="logo">
+        <a href="<?php echo ROOT_PATH ?>scp/index.php" class="no-pjax" id="logo">
             <span class="valign-helper"></span>
-            <img src="logo.php" alt="osTicket &mdash; <?php echo __('Customer Support System'); ?>"/>
+            <img src="<?php echo ROOT_PATH ?>scp/logo.php?<?php echo strtotime($cfg->lastModified('staff_logo_id')); ?>" alt="osTicket &mdash; <?php echo __('Customer Support System'); ?>"/>
         </a>
     </div>
     <div id="pjax-container" class="<?php if ($_POST) echo 'no-pjax'; ?>">
@@ -105,4 +100,8 @@ if (($lang = Internationalization::getCurrentLanguage())
             <div id="msg_notice"><?php echo $msg; ?></div>
         <?php }elseif($warn) { ?>
             <div id="msg_warning"><?php echo $warn; ?></div>
-        <?php } ?>
+        <?php }
+        foreach (Messages::getMessages() as $M) { ?>
+            <div class="<?php echo strtolower($M->getLevel()); ?>-banner"><?php
+                echo (string) $M; ?></div>
+<?php   } ?>
diff --git a/include/staff/helptopic.inc.php b/include/staff/helptopic.inc.php
index ca3090443c7d5c75d61dcecbb57094557a2c1d0a..28b01152fcf608bdfccc7ed1340b7ba61bcf62d1 100644
--- a/include/staff/helptopic.inc.php
+++ b/include/staff/helptopic.inc.php
@@ -1,6 +1,6 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
-$info = $qs = array();
+$info = $qs = $forms = array();
 if($topic && $_REQUEST['a']!='add') {
     $title=__('Update Help Topic');
     $action='update';
@@ -8,41 +8,50 @@ if($topic && $_REQUEST['a']!='add') {
     $info=$topic->getInfo();
     $info['id']=$topic->getId();
     $info['pid']=$topic->getPid();
+    $trans['name'] = $topic->getTranslateTag('name');
     $qs += array('id' => $topic->getId());
+    $forms = $topic->getForms();
 } else {
     $title=__('Add New Help Topic');
     $action='create';
     $submit_text=__('Add Topic');
     $info['isactive']=isset($info['isactive'])?$info['isactive']:1;
     $info['ispublic']=isset($info['ispublic'])?$info['ispublic']:1;
-    $info['form_id'] = Topic::FORM_USE_PARENT;
     $qs += array('a' => $_REQUEST['a']);
+    $forms = TicketForm::objects();
 }
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 ?>
+
+<h2><?php echo $title; ?>
+    <?php if (isset($info['topic'])) { ?><small>
+    — <?php echo $info['topic']; ?></small>
+<?php } ?>
+ <i class="help-tip icon-question-sign" href="#help_topic_information"></i></h2>
+
+<ul class="clean tabs" id="topic-tabs">
+    <li class="active"><a href="#info"><i class="icon-info-sign"></i> <?php echo __('Help Topic Information'); ?></a></li>
+    <li><a href="#routing"><i class="icon-ticket"></i> <?php echo __('New ticket options'); ?></a></li>
+    <li><a href="#forms"><i class="icon-paste"></i> <?php echo __('Forms'); ?></a></li>
+</ul>
+
 <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']); ?>">
  <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
- <h2><?php echo __('Help Topic');?></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 __('Help Topic Information');?>
-                &nbsp;<i class="help-tip icon-question-sign" href="#help_topic_information"></i></em>
-            </th>
-        </tr>
-    </thead>
+
+<div id="topic-tabs_container">
+<div class="tab_content" id="info">
+ <table class="table" border="0" cellspacing="0" cellpadding="2">
     <tbody>
         <tr>
             <td width="180" class="required">
                <?php echo __('Topic');?>:
             </td>
             <td>
-                <input type="text" size="30" name="topic" value="<?php echo $info['topic']; ?>">
+                <input type="text" size="30" name="topic" value="<?php echo $info['topic']; ?>"
+                autofocus data-translate-tag="<?php echo $trans['name']; ?>"/>
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['topic']; ?></span> <i class="help-tip icon-question-sign" href="#topic"></i>
             </td>
         </tr>
@@ -51,8 +60,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <?php echo __('Status');?>:
             </td>
             <td>
-                <input type="radio" name="isactive" value="1" <?php echo $info['isactive']?'checked="checked"':''; ?>><?php echo __('Active'); ?>
-                <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked="checked"':''; ?>><?php echo __('Disabled'); ?>
+                <input type="radio" name="isactive" value="1" <?php echo $info['isactive']?'checked="checked"':''; ?>> <?php echo __('Active'); ?>
+                <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked="checked"':''; ?>> <?php echo __('Disabled'); ?>
                 &nbsp;<span class="error">*&nbsp;</span> <i class="help-tip icon-question-sign" href="#status"></i>
             </td>
         </tr>
@@ -61,8 +70,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <?php echo __('Type');?>:
             </td>
             <td>
-                <input type="radio" name="ispublic" value="1" <?php echo $info['ispublic']?'checked="checked"':''; ?>><?php echo __('Public'); ?>
-                <input type="radio" name="ispublic" value="0" <?php echo !$info['ispublic']?'checked="checked"':''; ?>><?php echo __('Private/Internal'); ?>
+                <input type="radio" name="ispublic" value="1" <?php echo $info['ispublic']?'checked="checked"':''; ?>> <?php echo __('Public'); ?>
+                <input type="radio" name="ispublic" value="0" <?php echo !$info['ispublic']?'checked="checked"':''; ?>> <?php echo __('Private/Internal'); ?>
                 &nbsp;<span class="error">*&nbsp;</span> <i class="help-tip icon-question-sign" href="#type"></i>
             </td>
         </tr>
@@ -85,49 +94,100 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
             </td>
         </tr>
 
-        <tr><th colspan="2"><em><?php echo __('New ticket options');?></em></th></tr>
-        <tr>
-            <td><strong><?php echo __('Custom Form'); ?></strong>:</td>
-           <td><select name="form_id">
-                <option value="0" <?php
-if ($info['form_id'] == '0') echo 'selected="selected"';
-                    ?>>&mdash; <?php echo __('None'); ?> &mdash;</option>
-                <option value="<?php echo Topic::FORM_USE_PARENT; ?>"  <?php
-if ($info['form_id'] == Topic::FORM_USE_PARENT) echo 'selected="selected"';
-                    ?>>&mdash; <?php echo __('Use Parent Form'); ?> &mdash;</option>
-               <?php foreach (DynamicForm::objects()->filter(array('type'=>'G')) as $group) { ?>
-                <option value="<?php echo $group->get('id'); ?>"
-                       <?php if ($group->get('id') == $info['form_id'])
-                            echo 'selected="selected"'; ?>>
-                       <?php echo $group->get('title'); ?>
-                   </option>
-               <?php } ?>
-               </select>
-               &nbsp;<span class="error">&nbsp;<?php echo $errors['form_id']; ?></span>
-               <i class="help-tip icon-question-sign" href="#custom_form"></i>
-           </td>
-        </tr>
+    </tbody>
+    </table>
+
+        <div style="padding:8px 3px;border-bottom: 2px dotted #ddd;">
+            <strong><?php echo __('Internal Notes');?>:</strong>
+            <?php echo __("be liberal, they're internal.");?>
+        </div>
+
+        <textarea class="richtext no-bar" name="notes" cols="21"
+            rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea>
+
+</div>
+
+<div class="hidden tab_content" id="routing">
+<div style="padding:8px 0;border-bottom: 2px dotted #ddd;">
+<div><b class="big"><?php echo __('New ticket options');?></b></div>
+</div>
+
+ <table class="table" border="0" cellspacing="0" cellpadding="2">
+        <tbody>
         <tr>
             <td width="180" class="required">
                 <?php echo __('Department'); ?>:
             </td>
             <td>
-                <select name="dept_id">
+                <select name="dept_id" data-quick-add="department">
                     <option value="0">&mdash; <?php echo __('System Default'); ?> &mdash;</option>
                     <?php
-                    $sql='SELECT dept_id,dept_name FROM '.DEPT_TABLE.' dept ORDER by dept_name';
-                    if(($res=db_query($sql)) && db_num_rows($res)){
-                        while(list($id,$name)=db_fetch_row($res)){
-                            $selected=($info['dept_id'] && $id==$info['dept_id'])?'selected="selected"':'';
-                            echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name);
-                        }
-                    }
-                    ?>
+                    foreach (Dept::getDepartments() as $id=>$name) {
+                        $selected=($info['dept_id'] && $id==$info['dept_id'])?'selected="selected"':'';
+                        echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name);
+                    } ?>
+                    <option value="0" data-quick-add>&mdash; <?php echo __('Add New');?> &mdash;</option>
                 </select>
                 &nbsp;<span class="error">&nbsp;<?php echo $errors['dept_id']; ?></span>
                 <i class="help-tip icon-question-sign" href="#department"></i>
             </td>
         </tr>
+        <tr class="border">
+            <td>
+                <?php echo __('Ticket Number Format'); ?>:
+            </td>
+            <td>
+                <label>
+                <input type="radio" name="custom-numbers" value="0" <?php echo !$info['custom-numbers']?'checked="checked"':''; ?>
+                    onchange="javascript:$('#custom-numbers').hide();"> <?php echo __('System Default'); ?>
+                </label>&nbsp;<label>
+                <input type="radio" name="custom-numbers" value="1" <?php echo $info['custom-numbers']?'checked="checked"':''; ?>
+                    onchange="javascript:$('#custom-numbers').show(200);"> <?php echo __('Custom'); ?>
+                </label>&nbsp; <i class="help-tip icon-question-sign" href="#custom_numbers"></i>
+            </td>
+        </tr>
+    </tbody>
+    <tbody id="custom-numbers" style="<?php if (!$info['custom-numbers']) echo 'display:none'; ?>">
+        <tr>
+            <td style="padding-left:20px">
+                <?php echo __('Format'); ?>:
+            </td>
+            <td>
+                <input type="text" name="number_format" value="<?php echo $info['number_format']; ?>"/>
+                <span class="faded"><?php echo __('e.g.'); ?> <span id="format-example"><?php
+                    if ($info['custom-numbers']) {
+                        if ($info['sequence_id'])
+                            $seq = Sequence::lookup($info['sequence_id']);
+                        if (!isset($seq))
+                            $seq = new RandomSequence();
+                        echo $seq->current($info['number_format']);
+                    } ?></span></span>
+                <div class="error"><?php echo $errors['number_format']; ?></div>
+            </td>
+        </tr>
+        <tr>
+<?php $selected = 'selected="selected"'; ?>
+            <td style="padding-left:20px">
+                <?php echo __('Sequence'); ?>:
+            </td>
+            <td>
+                <select name="sequence_id">
+                <option value="0" <?php if ($info['sequence_id'] == 0) echo $selected;
+                    ?>>&mdash; <?php echo __('Random'); ?> &mdash;</option>
+<?php foreach (Sequence::objects() as $s) { ?>
+                <option value="<?php echo $s->id; ?>" <?php
+                    if ($info['sequence_id'] == $s->id) echo $selected;
+                    ?>><?php echo $s->name; ?></option>
+<?php } ?>
+                </select>
+                <button class="action-button pull-right" onclick="javascript:
+                $.dialog('ajax.php/sequence/manage', 205);
+                return false;
+                "><i class="icon-gear"></i> <?php echo __('Manage'); ?></button>
+            </td>
+        </tr>
+    </tbody>
+    <tbody>
         <tr>
             <td width="180">
                 <?php echo __('Status'); ?>:
@@ -166,11 +226,10 @@ if ($info['form_id'] == Topic::FORM_USE_PARENT) echo 'selected="selected"';
                 <select name="priority_id">
                     <option value="">&mdash; <?php echo __('System Default'); ?> &mdash;</option>
                     <?php
-                    $sql='SELECT priority_id,priority_desc FROM '.PRIORITY_TABLE.' pri ORDER by priority_urgency DESC';
-                    if(($res=db_query($sql)) && db_num_rows($res)){
-                        while(list($id,$name)=db_fetch_row($res)){
+                    if (($priorities=Priority::getPriorities())) {
+                        foreach ($priorities as $id => $name) {
                             $selected=($info['priority_id'] && $id==$info['priority_id'])?'selected="selected"':'';
-                            echo sprintf('<option value="%d" %s>%s</option>',$id,$selected,$name);
+                            echo sprintf('<option value="%d" %s>%s</option>', $id, $selected, $name);
                         }
                     }
                     ?>
@@ -224,13 +283,13 @@ if ($info['form_id'] == Topic::FORM_USE_PARENT) echo 'selected="selected"';
                 <?php echo __('Auto-assign To');?>:
             </td>
             <td>
-                <select name="assign">
+                <select name="assign" data-quick-add>
                     <option value="0">&mdash; <?php echo __('Unassigned'); ?> &mdash;</option>
                     <?php
                     if (($users=Staff::getStaffMembers())) {
-                        echo sprintf('<OPTGROUP label="%s">', sprintf(__('Agents (%d)'), count($user)));
+                        echo sprintf('<OPTGROUP label="%s">',
+                                sprintf(__('Agents (%d)'), count($users)));
                         foreach ($users as $id => $name) {
-                            $name = new PersonsName($name);
                             $k="s$id";
                             $selected = ($info['assign']==$k || $info['staff_id']==$id)?'selected="selected"':'';
                             ?>
@@ -240,22 +299,20 @@ if ($info['form_id'] == Topic::FORM_USE_PARENT) echo 'selected="selected"';
                         }
                         echo '</OPTGROUP>';
                     }
-                    $sql='SELECT team_id, name, isenabled FROM '.TEAM_TABLE.' ORDER BY name';
-                    if(($res=db_query($sql)) && ($cteams = db_num_rows($res))) {
-                        echo sprintf('<OPTGROUP label="%s">', sprintf(__('Teams (%d)'), $cteams));
-                        while (list($id, $name, $isenabled) = db_fetch_row($res)){
+                    if ($teams = Team::getTeams()) { ?>
+                      <optgroup data-quick-add="team" label="<?php
+                        echo sprintf(__('Teams (%d)'), count($teams)); ?>"><?php
+                        foreach ($teams as $id => $name) {
                             $k="t$id";
-                            $selected = ($info['assign']==$k || $info['team_id']==$id)?'selected="selected"':'';
-
-                            if (!$isenabled)
-                                $name .= ' '.__('(disabled)');
+                            $selected = ($info['assign']==$k || $info['team_id']==$id) ? 'selected="selected"' : '';
                             ?>
                             <option value="<?php echo $k; ?>"<?php echo $selected; ?>><?php echo $name; ?></option>
                         <?php
-                        }
-                        echo '</OPTGROUP>';
-                    }
-                    ?>
+                        } ?>
+                        <option value="0" data-quick-add data-id-prefix="t">— <?php echo __('Add New Team'); ?> —</option>
+                      </optgroup>
+                    <?php
+                    } ?>
                 </select>
                 &nbsp;<span class="error">&nbsp;<?php echo $errors['assign']; ?></span>
                 <i class="help-tip icon-question-sign" href="#auto_assign_to"></i>
@@ -271,75 +328,84 @@ if ($info['form_id'] == Topic::FORM_USE_PARENT) echo 'selected="selected"';
                     <i class="help-tip icon-question-sign" href="#ticket_auto_response"></i>
             </td>
         </tr>
-        <tr>
-            <td>
-                <?php echo __('Ticket Number Format'); ?>:
-            </td>
-            <td>
-                <label>
-                <input type="radio" name="custom-numbers" value="0" <?php echo !$info['custom-numbers']?'checked="checked"':''; ?>
-                    onchange="javascript:$('#custom-numbers').hide();"> <?php echo __('System Default'); ?>
-                </label>&nbsp;<label>
-                <input type="radio" name="custom-numbers" value="1" <?php echo $info['custom-numbers']?'checked="checked"':''; ?>
-                    onchange="javascript:$('#custom-numbers').show(200);"> <?php echo __('Custom'); ?>
-                </label>&nbsp; <i class="help-tip icon-question-sign" href="#custom_numbers"></i>
-            </td>
-        </tr>
     </tbody>
-    <tbody id="custom-numbers" style="<?php if (!$info['custom-numbers']) echo 'display:none'; ?>">
-        <tr>
-            <td style="padding-left:20px">
-                <?php echo __('Format'); ?>:
-            </td>
-            <td>
-                <input type="text" name="number_format" value="<?php echo $info['number_format']; ?>"/>
-                <span class="faded"><?php echo __('e.g.'); ?> <span id="format-example"><?php
-                    if ($info['custom-numbers']) {
-                        if ($info['sequence_id'])
-                            $seq = Sequence::lookup($info['sequence_id']);
-                        if (!isset($seq))
-                            $seq = new RandomSequence();
-                        echo $seq->current($info['number_format']);
-                    } ?></span></span>
-                <div class="error"><?php echo $errors['number_format']; ?></div>
-            </td>
-        </tr>
+ </table>
+</div>
+
+<div class="hidden tab_content" id="forms">
+ <table id="topic-forms" class="table" border="0" cellspacing="0" cellpadding="2">
+
+<?php
+$current_forms = array();
+foreach ($forms as $F) {
+    $current_forms[] = $F->id; ?>
+    <tbody data-form-id="<?php echo $F->get('id'); ?>">
         <tr>
-<?php $selected = 'selected="selected"'; ?>
-            <td style="padding-left:20px">
-                <?php echo __('Sequence'); ?>:
-            </td>
-            <td>
-                <select name="sequence_id">
-                <option value="0" <?php if ($info['sequence_id'] == 0) echo $selected;
-                    ?>>&mdash; <?php echo __('Random'); ?> &mdash;</option>
-<?php foreach (Sequence::objects() as $s) { ?>
-                <option value="<?php echo $s->id; ?>" <?php
-                    if ($info['sequence_id'] == $s->id) echo $selected;
-                    ?>><?php echo $s->name; ?></option>
+            <td class="handle" colspan="6">
+                <input type="hidden" name="forms[]" value="<?php echo $F->get('id'); ?>" />
+                <div class="pull-right">
+                <i class="icon-large icon-move icon-muted"></i>
+<?php if ($F->get('type') != 'T') { ?>
+                <a href="#" title="<?php echo __('Delete'); ?>" onclick="javascript:
+                if (confirm(__('You sure?')))
+                    var tbody = $(this).closest('tbody');
+                    tbody.fadeOut(function(){this.remove()});
+                    $(this).closest('form')
+                        .find('[name=form_id] [value=' + tbody.data('formId') + ']')
+                        .prop('disabled', false);
+                return false;"><i class="icon-large icon-trash"></i></a>
 <?php } ?>
-                </select>
-                <button class="action-button pull-right" onclick="javascript:
-                $.dialog('ajax.php/sequence/manage', 205);
-                return false;
-                "><i class="icon-gear"></i> <?php echo __('Manage'); ?></button>
+                </div>
+                <div><strong><?php echo Format::htmlchars($F->getLocal('title')); ?></strong></div>
+                <div><?php echo Format::display($F->getLocal('instructions')); ?></div>
             </td>
         </tr>
-    </tbody>
-    <tbody>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Internal Notes');?></strong>: <?php echo __("be liberal, they're internal.");?></em>
-            </th>
+        <tr style="text-align:left">
+            <th><?php echo __('Enable'); ?></th>
+            <th><?php echo __('Label'); ?></th>
+            <th><?php echo __('Type'); ?></th>
+            <th><?php echo __('Visibility'); ?></th>
+            <th><?php echo __('Variable'); ?></th>
         </tr>
+    <?php
+        foreach ($F->getDynamicFields() as $f) { ?>
         <tr>
-            <td colspan=2>
-                <textarea class="richtext no-bar" name="notes" cols="21"
-                    rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea>
-            </td>
+            <td><input type="checkbox" name="fields[]" value="<?php
+                echo $f->get('id'); ?>" <?php
+                if ($f->isEnabled()) echo 'checked="checked"'; ?>/></td>
+            <td><?php echo $f->get('label'); ?></td>
+            <td><?php $t=FormField::getFieldType($f->get('type')); echo __($t[0]); ?></td>
+            <td><?php echo $f->getVisibilityDescription(); ?></td>
+            <td><?php echo $f->get('name'); ?></td>
         </tr>
+        <?php } ?>
     </tbody>
-</table>
+    <?php } ?>
+ </table>
+
+   <br/>
+   <strong><?php echo __('Add Custom Form'); ?></strong>:
+   <select name="form_id" id="newform">
+    <option value=""><?php echo '— '.__('Add a custom form') . ' —'; ?></option>
+    <?php foreach (DynamicForm::objects()
+        ->filter(array('type'=>'G'))
+        ->exclude(array('flags__hasbit' => DynamicForm::FLAG_DELETED))
+    as $F) { ?>
+        <option value="<?php echo $F->get('id'); ?>"
+           <?php if (in_array($F->id, $current_forms))
+               echo 'disabled="disabled"'; ?>
+           <?php if ($F->get('id') == $info['form_id'])
+                echo 'selected="selected"'; ?>>
+           <?php echo $F->getLocal('title'); ?>
+        </option>
+    <?php } ?>
+   </select>
+   &nbsp;<span class="error">&nbsp;<?php echo $errors['form_id']; ?></span>
+   <i class="help-tip icon-question-sign" href="#custom_form"></i>
+</div>
+
+</div>
+
 <p style="text-align:center;">
     <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
     <input type="reset"  name="reset"  value="<?php echo __('Reset');?>">
@@ -359,5 +425,36 @@ $(function() {
     };
     $('[name=sequence_id]').on('change', update_example);
     $('[name=number_format]').on('keyup', update_example);
+
+    $('form select#newform').change(function() {
+        var $this = $(this),
+            val = $this.val();
+        if (!val) return;
+        $.ajax({
+            url: 'ajax.php/form/' + val + '/fields/view',
+            dataType: 'json',
+            success: function(json) {
+                if (json.success) {
+                    $(json.html).appendTo('#topic-forms').effect('highlight');
+                    $this.find(':selected').prop('disabled', true);
+                }
+            }
+        });
+    });
+    $('table#topic-forms').sortable({
+      items: 'tbody',
+      handle: 'td.handle',
+      tolerance: 'pointer',
+      forcePlaceholderSize: true,
+      helper: function(e, ui) {
+        ui.children().each(function() {
+          $(this).children().each(function() {
+            $(this).width($(this).width());
+          });
+        });
+        ui=ui.clone().css({'background-color':'white', 'opacity':0.8});
+        return ui;
+      }
+    }).disableSelection();
 });
 </script>
diff --git a/include/staff/helptopics.inc.php b/include/staff/helptopics.inc.php
index 2a7c8f53d1855a9bcea52659563f9f8e24e51561..3116e5016be5744958619a0e3240fcc80c0c3a8c 100644
--- a/include/staff/helptopics.inc.php
+++ b/include/staff/helptopics.inc.php
@@ -1,51 +1,67 @@
 <?php
-if(!defined('OSTADMININC') || !$thisstaff->isAdmin()) die('Access Denied');
+if (!defined('OSTADMININC') || !$thisstaff->isAdmin()) die('Access Denied');
 
-$sql='SELECT topic.* '
-    .', dept.dept_name as department '
-    .', priority_desc as priority '
-    .' FROM '.TOPIC_TABLE.' topic '
-    .' LEFT JOIN '.DEPT_TABLE.' dept ON (dept.dept_id=topic.dept_id) '
-    .' LEFT JOIN '.TICKET_PRIORITY_TABLE.' pri ON (pri.priority_id=topic.priority_id) ';
-$sql.=' WHERE 1';
-$order_by = ($cfg->getTopicSortMode() == 'm' ? '`sort`' : '`topic_id`');
 
-$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
-//Ok..lets roll...create the actual query
-$query="$sql ORDER BY $order_by";
-$res=db_query($query);
-if($res && ($num=db_num_rows($res)))
-    $showing=sprintf(_N('Showing %d help topic', 'Showing %d help topics', $num), $num);
-else
-    $showing=__('No help topics found!');
+$page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
+$count = Topic::objects()->count();
+$pageNav = new Pagenate($count, $page, PAGE_LIMIT);
+$pageNav->setURL('helptopics.php', $_qstr);
+$showing = $pageNav->showing().' '._N('help topic', 'help topics', $count);
 
-// Get the full names and filter for this page
-$topics = array();
-while ($row = db_fetch_array($res))
-    $topics[] = $row;
-
-foreach ($topics as &$t)
-    $t['name'] = Topic::getTopicName($t['topic_id']);
-
-if ($cfg->getTopicSortMode() == 'a')
-    usort($topics, function($a, $b) { return strcasecmp($a['name'], $b['name']); });
+$order_by = 'sort';
 
 ?>
-<div class="pull-left" style="width:700px;padding-top:5px;">
- <h2><?php echo __('Help Topics');?></h2>
- </div>
-<div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;">
-    <b><a href="helptopics.php?a=add" class="Icon newHelpTopic"><?php echo __('Add New Help Topic');?></a></b></div>
-<div class="clear"></div>
 <form action="helptopics.php" method="POST" name="topics">
+    <div class="sticky bar opaque">
+        <div class="content">
+            <div class="pull-left flush-left">
+                <h2><?php echo __('Help Topics');?></h2>
+            </div>
+            <div class="pull-right flush-right">
+                <?php if ($cfg->getTopicSortMode() != 'a') { ?>
+                <button class="button no-confirm" type="submit" name="sort"><i class="icon-save"></i>
+                <?php echo __('Save'); ?></button>
+                <?php } ?>
+                <a href="helptopics.php?a=add" class="green button action-button"><i class="icon-plus-sign"></i> <?php echo __('Add New Help Topic');?></a>
+                <span class="action-button" data-dropdown="#action-dropdown-more">
+           <i class="icon-caret-down pull-right"></i>
+            <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+                </span>
+                <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                    <ul id="actions">
+                        <li>
+                            <a class="confirm" data-name="enable" href="helptopics.php?a=enable">
+                                <i class="icon-ok-sign icon-fixed-width"></i>
+                                <?php echo __( 'Enable'); ?>
+                            </a>
+                        </li>
+                        <li>
+                            <a class="confirm" data-name="disable" href="helptopics.php?a=disable">
+                                <i class="icon-ban-circle icon-fixed-width"></i>
+                                <?php echo __( 'Disable'); ?>
+                            </a>
+                        </li>
+                        <li class="danger">
+                            <a class="confirm" data-name="delete" href="helptopics.php?a=delete">
+                                <i class="icon-trash icon-fixed-width"></i>
+                                <?php echo __( 'Delete'); ?>
+                            </a>
+                        </li>
+                    </ul>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="clear"></div>
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="mass_process" >
 <input type="hidden" id="action" name="a" value="sort" >
  <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption><span class="pull-left" style="display:inline-block;vertical-align:middle"><?php
-         echo $showing; ?></span>
-         <div class="pull-right"><?php echo __('Sorting Mode'); ?>:
-        <select name="help_topic_sort_mode" onchange="javascript:
+
+    <thead>
+<tr><td colspan="7">
+    <div style="padding:3px" class="pull-right"><?php echo __('Sorting Mode'); ?>:
+    <select name="help_topic_sort_mode" onchange="javascript:
     var $form = $(this).closest('form');
     $form.find('input[name=a]').val('sort');
     $form.submit();
@@ -55,67 +71,90 @@ if ($cfg->getTopicSortMode() == 'a')
         $i, $i == $cfg->getTopicSortMode() ? ' selected="selected"' : '', $m); ?>
         </select>
     </div>
-    </caption>
-    <thead>
+</td></tr>
         <tr>
-            <th width="7" style="height:20px;">&nbsp;</th>
-            <th style="padding-left:4px;vertical-align:middle" width="360"><?php echo __('Help Topic'); ?></th>
-            <th style="padding-left:4px;vertical-align:middle" width="80"><?php echo __('Status'); ?></th>
-            <th style="padding-left:4px;vertical-align:middle" width="100"><?php echo __('Type'); ?></th>
-            <th style="padding-left:4px;vertical-align:middle" width="100"><?php echo __('Priority'); ?></th>
-            <th style="padding-left:4px;vertical-align:middle" width="160"><?php echo __('Department'); ?></th>
-            <th style="padding-left:4px;vertical-align:middle" width="150" nowrap><?php echo __('Last Updated'); ?></th>
+            <th width="4%" style="height:20px;">&nbsp;</th>
+            <th style="padding-left:4px;vertical-align:middle" width="36%"><?php echo __('Help Topic'); ?></th>
+            <th style="padding-left:4px;vertical-align:middle" width="8%"><?php echo __('Status'); ?></th>
+            <th style="padding-left:4px;vertical-align:middle" width="8%"><?php echo __('Type'); ?></th>
+            <th style="padding-left:4px;vertical-align:middle" width="10%"><?php echo __('Priority'); ?></th>
+            <th style="padding-left:4px;vertical-align:middle" width="14%"><?php echo __('Department'); ?></th>
+            <th style="padding-left:4px;vertical-align:middle" width="20%" nowrap><?php echo __('Last Updated'); ?></th>
         </tr>
     </thead>
     <tbody class="<?php if ($cfg->getTopicSortMode() == 'm') echo 'sortable-rows'; ?>"
         data-sort="sort-">
     <?php
-        $total=0;
-        $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null;
-        if (count($topics)):
+        $ids= ($errors && is_array($_POST['ids'])) ? $_POST['ids'] : null;
+        if ($count) {
+            $topics = Topic::objects()
+                ->order_by(sprintf('%s%s',
+                            strcasecmp($order, 'DESC') ? '' : '-',
+                            $order_by))
+                ->limit($pageNav->getLimit())
+                ->offset($pageNav->getStart());
+
+            $T = $topics;
+            $names = $topics = array();
+            foreach ($T as $topic) {
+                $names[$topic->getId()] = $topic->getFullName();
+                $topics[$topic->getId()] = $topic;
+            }
+            $names = Internationalization::sortKeyedList($names);
+
             $defaultDept = $cfg->getDefaultDept();
             $defaultPriority = $cfg->getDefaultPriority();
             $sort = 0;
-            foreach($topics as $row) {
+            foreach($names as $topic_id=>$name) {
+                $topic = $topics[$topic_id];
+                $id = $topic->getId();
                 $sort++; // Track initial order for transition
                 $sel=false;
-                if($ids && in_array($row['topic_id'],$ids))
+                if ($ids && in_array($id, $ids))
                     $sel=true;
 
-                if (!$row['dept_id'] && $defaultDept) {
-                    $row['dept_id'] = $defaultDept->getId();
-                    $row['department'] = (string) $defaultDept;
+                if ($topic->dept_id) {
+                    $deptId = $topic->dept_id;
+                    $dept = (string) $topic->dept;
+                } elseif ($defaultDept) {
+                    $deptId = $defaultDept->getId();
+                    $dept = (string) $defaultDept;
+                } else {
+                    $deptId = 0;
+                    $dept = '';
                 }
-
-                if (!$row['priority'] && $defaultPriority)
-                    $row['priority'] = (string) $defaultPriority;
-
+                $priority = $topic->priority ?: $defaultPriority;
                 ?>
-            <tr id="<?php echo $row['topic_id']; ?>">
-                <td width=7px>
-                  <input type="hidden" name="sort-<?php echo $row['topic_id']; ?>" value="<?php
-                        echo $row['sort'] ?: $sort; ?>"/>
-                  <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['topic_id']; ?>"
-                            <?php echo $sel?'checked="checked"':''; ?>>
+            <tr id="<?php echo $id; ?>">
+                <td align="center">
+                  <input type="hidden" name="sort-<?php echo $id; ?>" value="<?php
+                        echo $topic->sort ?: $sort; ?>"/>
+                  <input type="checkbox" class="ckb" name="ids[]"
+                    value="<?php echo $id; ?>" <?php
+                    echo $sel ? 'checked="checked"' : ''; ?>>
                 </td>
                 <td>
-<?php if ($cfg->getTopicSortMode() == 'm') { ?>
-                    <i class="icon-sort"></i>
-<?php } ?>
-<a href="helptopics.php?id=<?php echo $row['topic_id']; ?>"><?php echo $row['name']; ?></a>&nbsp;</td>
-                <td><?php echo $row['isactive']?__('Active'):'<b>'.__('Disabled').'</b>'; ?></td>
-                <td><?php echo $row['ispublic']?__('Public'):'<b>'.__('Private').'</b>'; ?></td>
-                <td><?php echo $row['priority']; ?></td>
-                <td><a href="departments.php?id=<?php echo $row['dept_id']; ?>"><?php echo $row['department']; ?></a></td>
-                <td>&nbsp;<?php echo Format::db_datetime($row['updated']); ?></td>
+                    <?php
+                    if ($cfg->getTopicSortMode() == 'm') { ?>
+                        <i class="icon-sort faded"></i>
+                    <?php } ?>
+                    <a href="helptopics.php?id=<?php echo $id; ?>"><?php
+                    echo Topic::getTopicName($id); ?></a>&nbsp;
+                </td>
+                <td><?php echo $topic->isactive ? __('Active') : '<b>'.__('Disabled').'</b>'; ?></td>
+                <td><?php echo $topic->ispublic ? __('Public') : '<b>'.__('Private').'</b>'; ?></td>
+                <td><?php echo $priority; ?></td>
+                <td><a href="departments.php?id=<?php echo $deptId;
+                ?>"><?php echo $dept; ?></a></td>
+                <td>&nbsp;<?php echo Format::datetime($team->updated); ?></td>
             </tr>
             <?php
-            } //end of while.
-        endif; ?>
+            } //end of foreach.
+        }?>
     <tfoot>
      <tr>
         <td colspan="7">
-            <?php if($res && $num){ ?>
+            <?php if ($count) { ?>
             <?php echo __('Select');?>:&nbsp;
             <a id="selectAll" href="#ckb"><?php echo __('All');?></a>&nbsp;&nbsp;
             <a id="selectNone" href="#ckb"><?php echo __('None');?></a>&nbsp;&nbsp;
@@ -128,16 +167,10 @@ if ($cfg->getTopicSortMode() == 'a')
     </tfoot>
 </table>
 <?php
-if($res && $num): //Show options..
+if ($count): //Show options..
+     echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
 ?>
-<p class="centered" id="actions">
-<?php if ($cfg->getTopicSortMode() != 'a') { ?>
-    <input class="button no-confirm" type="submit" name="sort" value="Save"/>
-<?php } ?>
-    <button class="button" type="submit" name="enable" value="Enable" ><?php echo __('Enable'); ?></button>
-    <button class="button" type="submit" name="disable" value="Disable"><?php echo __('Disable'); ?></button>
-    <button class="button" type="submit" name="delete" value="Delete"><?php echo __('Delete'); ?></button>
-</p>
+
 <?php
 endif;
 ?>
@@ -172,4 +205,3 @@ endif;
      </p>
     <div class="clear"></div>
 </div>
-
diff --git a/include/staff/login.header.php b/include/staff/login.header.php
index 7cf18d895239f5a9425428cb112d6a1aba1827a1..d4068027840b811e2641f4870e6cd3927603e4f9 100644
--- a/include/staff/login.header.php
+++ b/include/staff/login.header.php
@@ -13,7 +13,7 @@ defined('OSTSCPINC') or die('Invalid path');
     <meta http-equiv="cache-control" content="no-cache" />
     <meta http-equiv="pragma" content="no-cache" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
-    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-1.8.3.min.js"></script>
+    <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-1.11.2.min.js"></script>
     <script type="text/javascript">
         $(document).ready(function() {
             $("input:not(.dp):visible:enabled:first").focus();
diff --git a/include/staff/login.tpl.php b/include/staff/login.tpl.php
index b8a4a86016e84961114ab81db1104f90a606b633..c81c46615d7ac0d42c4b64882663a6ce633ceb66 100644
--- a/include/staff/login.tpl.php
+++ b/include/staff/login.tpl.php
@@ -2,23 +2,31 @@
 include_once(INCLUDE_DIR.'staff/login.header.php');
 $info = ($_POST && $errors)?Format::htmlchars($_POST):array();
 ?>
+<div id="brickwall"></div>
 <div id="loginBox">
+    <div id="blur">
+        <div id="background"></div>
+    </div>
     <h1 id="logo"><a href="index.php">
         <span class="valign-helper"></span>
         <img src="logo.php?login" alt="osTicket :: <?php echo __('Staff Control Panel');?>" />
     </a></h1>
     <h3><?php echo Format::htmlchars($msg); ?></h3>
-    <div class="banner"><small><?php echo ($content) ? Format::display($content->getBody()) : ''; ?></small></div>
-    <form action="login.php" method="post">
+    <div class="banner"><small><?php echo ($content) ? Format::display($content->getLocalBody()) : ''; ?></small></div>
+    <form action="login.php" method="post" id="login">
         <?php csrf_token(); ?>
         <input type="hidden" name="do" value="scplogin">
         <fieldset>
-        <input type="text" name="userid" id="name" value="<?php echo $info['userid']; ?>" placeholder="<?php echo __('Email or Username'); ?>" autocorrect="off" autocapitalize="off">
+        <input type="text" name="userid" id="name" value="<?php
+            echo $info['userid']; ?>" placeholder="<?php echo __('Email or Username'); ?>"
+            autofocus autocorrect="off" autocapitalize="off">
         <input type="password" name="passwd" id="pass" placeholder="<?php echo __('Password'); ?>" autocorrect="off" autocapitalize="off">
             <?php if ($show_reset && $cfg->allowPasswordReset()) { ?>
             <h3 style="display:inline"><a href="pwreset.php"><?php echo __('Forgot my password'); ?></a></h3>
             <?php } ?>
-            <input class="submit" type="submit" name="submit" value="<?php echo __('Log In'); ?>">
+            <button class="submit button pull-right" type="submit" name="submit"><i class="icon-signin"></i>
+                <?php echo __('Log In'); ?>
+            </button>
         </fieldset>
     </form>
 <?php
@@ -35,8 +43,29 @@ if (count($ext_bks)) { ?>
 <div class="external-auth"><?php $bk->renderExternalLink(); ?></div><?php
     }
 } ?>
+
+    <div id="company">
+        <div class="content">
+            <?php echo __('Copyright'); ?> &copy; <?php echo Format::htmlchars($ost->company) ?: date('Y'); ?>
+        </div>
+    </div>
+</div>
+<div id="poweredBy"><?php echo __('Powered by'); ?>
+    <a href="http://www.osticket.com" target="_blank">
+        <img alt="osTicket" src="images/osticket-grey.png" class="osticket-logo">
+    </a>
 </div>
-<div id="copyRights"><?php echo __('Copyright'); ?> &copy;
-<a href='http://www.osticket.com' target="_blank">osTicket.com</a></div>
+    <script>
+    document.addEventListener('DOMContentLoaded', function() {
+        if (undefined === window.getComputedStyle(document.documentElement).backgroundBlendMode) {
+            document.getElementById('loginBox').style.backgroundColor = 'white';
+        }
+    });
+    </script>
+    <!--[if IE]>
+    <style>
+        #loginBox:after { background-color: white !important; }
+    </style>
+    <![endif]-->
 </body>
 </html>
diff --git a/include/staff/org-view.inc.php b/include/staff/org-view.inc.php
index c5cea44eb18989a13f3a3060f9c704183e383a64..1c20cb92a07e9b172565a9334eb9d695beb1b445 100644
--- a/include/staff/org-view.inc.php
+++ b/include/staff/org-view.inc.php
@@ -9,21 +9,27 @@ if(!defined('OSTSCPINC') || !$thisstaff || !is_object($org)) die('Invalid path')
              title="Reload"><i class="icon-refresh"></i> <?php echo $org->getName(); ?></a></h2>
         </td>
         <td width="50%" class="right_align has_bottom_border">
-            <span class="action-button pull-right" data-dropdown="#action-dropdown-more">
+<?php if ($thisstaff->hasPerm(Organization::PERM_DELETE)) { ?>
+            <a id="org-delete" class="red button action-button org-action"
+            href="#orgs/<?php echo $org->getId(); ?>/delete"><i class="icon-trash"></i>
+            <?php echo __('Delete Organization'); ?></a>
+<?php } ?>
+<?php if ($thisstaff->hasPerm(Organization::PERM_EDIT)) { ?>
+            <span class="action-button" data-dropdown="#action-dropdown-more">
                 <i class="icon-caret-down pull-right"></i>
                 <span ><i class="icon-cog"></i> <?php echo __('More'); ?></span>
             </span>
-            <a id="org-delete" class="action-button pull-right org-action"
-            href="#orgs/<?php echo $org->getId(); ?>/delete"><i class="icon-trash"></i>
-            <?php echo __('Delete Organization'); ?></a>
+<?php } ?>
             <div id="action-dropdown-more" class="action-dropdown anchor-right">
               <ul>
+<?php if ($thisstaff->hasPerm(Organization::PERM_EDIT)) { ?>
                 <li><a href="#ajax.php/orgs/<?php echo $org->getId();
                     ?>/forms/manage" onclick="javascript:
                     $.dialog($(this).attr('href').substr(1), 201);
                     return false"
                     ><i class="icon-paste"></i>
                     <?php echo __('Manage Forms'); ?></a></li>
+<?php } ?>
               </ul>
             </div>
         </td>
@@ -35,11 +41,17 @@ if(!defined('OSTSCPINC') || !$thisstaff || !is_object($org)) die('Invalid path')
             <table border="0" cellspacing="" cellpadding="4" width="100%">
                 <tr>
                     <th width="150"><?php echo __('Name'); ?>:</th>
-                    <td><b><a href="#orgs/<?php echo $org->getId();
+                    <td>
+<?php if ($thisstaff->hasPerm(Organization::PERM_EDIT)) { ?>
+                    <b><a href="#orgs/<?php echo $org->getId();
                     ?>/edit" class="org-action"><i
-                    class="icon-edit"></i>&nbsp;<?php echo
-                    $org->getName();
-                    ?></a></td>
+                        class="icon-edit"></i>
+<?php }
+                    echo $org->getName();
+    if ($thisstaff->hasPerm(Organization::PERM_EDIT)) { ?>
+                    </a></b>
+<?php } ?>
+                    </td>
                 </tr>
                 <tr>
                     <th><?php echo __('Account Manager'); ?>:</th>
@@ -51,11 +63,11 @@ if(!defined('OSTSCPINC') || !$thisstaff || !is_object($org)) die('Invalid path')
             <table border="0" cellspacing="" cellpadding="4" width="100%">
                 <tr>
                     <th width="150"><?php echo __('Created'); ?>:</th>
-                    <td><?php echo Format::db_datetime($org->getCreateDate()); ?></td>
+                    <td><?php echo Format::datetime($org->getCreateDate()); ?></td>
                 </tr>
                 <tr>
                     <th><?php echo __('Last Updated'); ?>:</th>
-                    <td><?php echo Format::db_datetime($org->getUpdateDate()); ?></td>
+                    <td><?php echo Format::datetime($org->getUpdateDate()); ?></td>
                 </tr>
             </table>
         </td>
@@ -63,32 +75,34 @@ if(!defined('OSTSCPINC') || !$thisstaff || !is_object($org)) die('Invalid path')
 </table>
 <br>
 <div class="clear"></div>
-<ul class="tabs">
-    <li><a class="active" id="users_tab" href="#users"><i
+<ul class="clean tabs" id="orgtabs">
+    <li class="active"><a href="#users"><i
     class="icon-user"></i>&nbsp;<?php echo __('Users'); ?></a></li>
-    <li><a id="tickets_tab" href="#tickets"><i
+    <li><a href="#tickets"><i
     class="icon-list-alt"></i>&nbsp;<?php echo __('Tickets'); ?></a></li>
-    <li><a id="notes_tab" href="#notes"><i
+    <li><a href="#notes"><i
     class="icon-pushpin"></i>&nbsp;<?php echo __('Notes'); ?></a></li>
 </ul>
+<div id="orgtabs_container">
 <div class="tab_content" id="users">
 <?php
 include STAFFINC_DIR . 'templates/users.tmpl.php';
 ?>
 </div>
-<div class="tab_content" id="tickets"  style="display:none;">
+<div class="hidden tab_content" id="tickets">
 <?php
 include STAFFINC_DIR . 'templates/tickets.tmpl.php';
 ?>
 </div>
 
-<div class="tab_content" id="notes" style="display:none">
+<div class="hidden tab_content" id="notes">
 <?php
 $notes = QuickNote::forOrganization($org);
 $create_note_url = 'orgs/'.$org->getId().'/note';
 include STAFFINC_DIR . 'templates/notes.tmpl.php';
 ?>
 </div>
+</div>
 
 <script type="text/javascript">
 $(function() {
diff --git a/include/staff/orgs.inc.php b/include/staff/orgs.inc.php
index 3f500a6f1edf5cbe41ed7f3b2a57eb076f5832f0..feb53acb399b5e934cd49bf9c1eedb5420f3e856 100644
--- a/include/staff/orgs.inc.php
+++ b/include/staff/orgs.inc.php
@@ -1,120 +1,116 @@
 <?php
 if(!defined('OSTSCPINC') || !$thisstaff) die('Access Denied');
 
-$qs = array();
-
-$select = 'SELECT org.*
-            ,COALESCE(team.name,
-                    IF(staff.staff_id, CONCAT_WS(" ", staff.firstname, staff.lastname), NULL)
-                    ) as account_manager ';
-$from = 'FROM '.ORGANIZATION_TABLE.' org '
-       .'LEFT JOIN '.STAFF_TABLE.' staff ON (
-           LEFT(org.manager, 1) = "s" AND staff.staff_id = SUBSTR(org.manager, 2)) '
-       .'LEFT JOIN '.TEAM_TABLE.' team ON (
-           LEFT(org.manager, 1) = "t" AND team.team_id = SUBSTR(org.manager, 2)) ';
+OrganizationForm::ensureDynamicDataView();
 
-$where = ' WHERE 1 ';
+$qs = array();
+$orgs = Organization::objects()
+    ->annotate(array('user_count'=>SqlAggregate::COUNT('users')));
 
 if ($_REQUEST['query']) {
-
-    $from .=' LEFT JOIN '.FORM_ENTRY_TABLE.' entry
-                ON (entry.object_type=\'O\' AND entry.object_id = org.id)
-              LEFT JOIN '.FORM_ANSWER_TABLE.' value
-                ON (value.entry_id=entry.id) ';
-
-    $search = db_input(strtolower($_REQUEST['query']), false);
-    $where .= ' AND (
-                    org.name LIKE \'%'.$search.'%\' OR value.value LIKE \'%'.$search.'%\'
-                )';
-
+    $search = $_REQUEST['query'];
+    $orgs->filter(Q::any(array(
+        'name__contains' => $search,
+        // TODO: Add search for cdata
+    )));
     $qs += array('query' => $_REQUEST['query']);
 }
 
-$sortOptions = array('name' => 'org.name',
-                     'users' => 'users',
-                     'create' => 'org.created',
-                     'update' => 'org.updated');
-$orderWays = array('DESC'=>'DESC','ASC'=>'ASC');
+$sortOptions = array(
+        'name' => 'name',
+        'users' => 'users',
+        'create' => 'created',
+        'update' => 'updated'
+        );
+
+$orderWays = array('DESC' => '-', 'ASC' => '');
 $sort= ($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])]) ? strtolower($_REQUEST['sort']) : 'name';
 //Sorting options...
 if ($sort && $sortOptions[$sort])
-    $order_column =$sortOptions[$sort];
+    $order_column = $sortOptions[$sort];
 
-$order_column = $order_column ?: 'org.name';
+$order_column = $order_column ?: 'name';
 
 if ($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])])
     $order = $orderWays[strtoupper($_REQUEST['order'])];
 
-$order=$order ?: 'ASC';
 if ($order_column && strpos($order_column,','))
     $order_column = str_replace(','," $order,",$order_column);
 
 $x=$sort.'_sort';
-$$x=' class="'.strtolower($order).'" ';
+$$x=' class="'.($order == '' ? 'asc' : 'desc').'" ';
 $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);
+$total = $orgs->count();
+$page=($_GET['p'] && is_numeric($_GET['p']))? $_GET['p'] : 1;
+$pageNav=new Pagenate($total, $page, PAGE_LIMIT);
+$pageNav->paginate($orgs);
+
 $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 ';
+$qstr.='&amp;order='.($order=='-' ? 'ASC' : 'DESC');
 
-$from .= ' LEFT JOIN '.USER_TABLE.' user ON (user.org_id = org.id) ';
-
-
-$query="$select $from $where GROUP BY org.id ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
 //echo $query;
-$qhash = md5($query);
-$_SESSION['orgs_qs_'.$qhash] = $query;
+$_SESSION[':Q:orgs'] = $orgs;
+
+$orgs->values('id', 'name', 'created', 'updated');
+$orgs->order_by($order . $order_column);
 ?>
-<h2><?php echo __('Organizations'); ?></h2>
-<div class="pull-left">
-    <form action="orgs.php" method="get">
-        <?php csrf_token(); ?>
-        <input type="hidden" name="a" value="search">
-        <table>
-            <tr>
-                <td><input type="text" id="basic-org-search" name="query" size=30 value="<?php echo Format::htmlchars($_REQUEST['query']); ?>"
-                autocomplete="off" autocorrect="off" autocapitalize="off"></td>
-                <td><input type="submit" name="basic_search" class="button" value="<?php echo __('Search'); ?>"></td>
-                <!-- <td>&nbsp;&nbsp;<a href="" id="advanced-user-search">[advanced]</a></td> -->
-            </tr>
-        </table>
-    </form>
- </div>
-
-<div class="pull-right">
-    <a class="action-button add-org"
-        href="#">
-        <i class="icon-plus-sign"></i>
-        <?php echo __('Add Organization'); ?>
-    </a>
-    <span class="action-button" data-dropdown="#action-dropdown-more"
-        style="/*DELME*/ vertical-align:top; margin-bottom:0">
-        <i class="icon-caret-down pull-right"></i>
-        <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
-    </span>
-    <div id="action-dropdown-more" class="action-dropdown anchor-right">
-        <ul>
-            <li><a class="orgs-action" href="#delete">
-                <i class="icon-trash icon-fixed-width"></i>
-                <?php echo __('Delete'); ?></a></li>
-        </ul>
+<div id="basic_search">
+    <div style="min-height:25px;">
+        <form action="orgs.php" method="get">
+            <?php csrf_token(); ?>
+            <div class="attached input">
+            <input type="hidden" name="a" value="search">
+            <input type="search" class="basic-search" id="basic-org-search" name="query" autofocus size="30" value="<?php echo Format::htmlchars($_REQUEST['query']); ?>" autocomplete="off" autocorrect="off" autocapitalize="off">
+                <button type="submit" class="attached button"><i class="icon-search"></i>
+                </button>
+            <!-- <td>&nbsp;&nbsp;<a href="" id="advanced-user-search">[advanced]</a></td> -->
+            </div>
+        </form>
+    </div>
+</div>
+<div style="margin-bottom:20px; padding-top:5px;">
+    <div class="sticky bar opaque">
+        <div class="content">
+            <div class="pull-left flush-left">
+                <h2><?php echo __('Organizations'); ?></h2>
+            </div>
+            <div class="pull-right">
+                <?php if ($thisstaff->hasPerm(Organization::PERM_CREATE)) { ?>
+                <a class="green button action-button add-org"
+                   href="#">
+                    <i class="icon-plus-sign"></i>
+                    <?php echo __('Add Organization'); ?>
+                </a>
+                <?php }
+            if ($thisstaff->hasPerm(Organization::PERM_DELETE)) { ?>
+                <span class="action-button" data-dropdown="#action-dropdown-more"
+                      style="/*DELME*/ vertical-align:top; margin-bottom:0">
+                    <i class="icon-caret-down pull-right"></i>
+                    <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+                </span>
+                <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                    <ul>
+                        <li class="danger"><a class="orgs-action" href="#delete">
+                            <i class="icon-trash icon-fixed-width"></i>
+                            <?php echo __('Delete'); ?></a></li>
+                    </ul>
+                </div>
+                <?php } ?>
+            </div>
+        </div>
     </div>
 </div>
-
 <div class="clear"></div>
 <?php
 $showing = $search ? __('Search Results').': ' : '';
-$res = db_query($query);
-if($res && ($num=db_num_rows($res)))
+if ($orgs->exists(true))
     $showing .= $pageNav->showing();
 else
     $showing .= __('No organizations found!');
+
 ?>
 <form id="orgs-list" action="orgs.php" method="POST" name="staff" >
  <?php csrf_token(); ?>
@@ -122,43 +118,42 @@ else
  <input type="hidden" id="action" name="do" value="" >
  <input type="hidden" id="selected-count" name="count" value="" >
  <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption><?php echo $showing; ?></caption>
     <thead>
         <tr>
-            <th nowrap width="12"> </th>
-            <th width="400"><a <?php echo $name_sort; ?> href="orgs.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name'); ?></a></th>
-            <th width="100"><a <?php echo $users_sort; ?> href="orgs.php?<?php echo $qstr; ?>&sort=users"><?php echo __('Users'); ?></a></th>
-            <th width="150"><a <?php echo $create_sort; ?> href="orgs.php?<?php echo $qstr; ?>&sort=create"><?php echo __('Created'); ?></a></th>
-            <th width="145"><a <?php echo $update_sort; ?> href="orgs.php?<?php echo $qstr; ?>&sort=update"><?php echo __('Last Updated'); ?></a></th>
+            <th nowrap width="4%">&nbsp;</th>
+            <th width="45%"><a <?php echo $name_sort; ?> href="orgs.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name'); ?></a></th>
+            <th width="11%"><a <?php echo $users_sort; ?> href="orgs.php?<?php echo $qstr; ?>&sort=users"><?php echo __('Users'); ?></a></th>
+            <th width="20%"><a <?php echo $create_sort; ?> href="orgs.php?<?php echo $qstr; ?>&sort=create"><?php echo __('Created'); ?></a></th>
+            <th width="20%"><a <?php echo $update_sort; ?> href="orgs.php?<?php echo $qstr; ?>&sort=update"><?php echo __('Last Updated'); ?></a></th>
         </tr>
     </thead>
     <tbody>
     <?php
-        if($res && db_num_rows($res)):
-            $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null;
-            while ($row = db_fetch_array($res)) {
-
-                $sel=false;
-                if($ids && in_array($row['id'], $ids))
-                    $sel=true;
-                ?>
-               <tr id="<?php echo $row['id']; ?>">
-                <td nowrap>
-                    <input type="checkbox" value="<?php echo $row['id']; ?>" class="ckb mass nowarn"/>
-                </td>
-                <td>&nbsp; <a href="orgs.php?id=<?php echo $row['id']; ?>"><?php echo $row['name']; ?></a> </td>
-                <td>&nbsp;<?php echo $row['users']; ?></td>
-                <td><?php echo Format::db_date($row['created']); ?></td>
-                <td><?php echo Format::db_datetime($row['updated']); ?>&nbsp;</td>
-               </tr>
-            <?php
-            } //end of while.
-        endif; ?>
+        $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null;
+        foreach ($orgs as $org) {
+
+            $sel=false;
+            if($ids && in_array($org['id'], $ids))
+                $sel=true;
+            ?>
+           <tr id="<?php echo $org['id']; ?>">
+            <td nowrap align="center">
+                <input type="checkbox" value="<?php echo $org['id']; ?>" class="ckb mass nowarn"/>
+            </td>
+            <td>&nbsp; <a href="orgs.php?id=<?php echo $org['id']; ?>"><?php
+            echo $org['name']; ?></a> </td>
+            <td>&nbsp;<?php echo $org['user_count']; ?></td>
+            <td><?php echo Format::date($org['created']); ?></td>
+            <td><?php echo Format::datetime($org['updated']); ?>&nbsp;</td>
+           </tr>
+        <?php
+        }
+        ?>
     </tbody>
     <tfoot>
      <tr>
         <td colspan="7">
-            <?php if ($res && $num) { ?>
+            <?php if ($total) { ?>
             <?php echo __('Select');?>:&nbsp;
             <a id="selectAll" href="#ckb"><?php echo __('All');?></a>&nbsp;&nbsp;
             <a id="selectNone" href="#ckb"><?php echo __('None');?></a>&nbsp;&nbsp;
@@ -173,12 +168,11 @@ else
     </tfoot>
 </table>
 <?php
-if($res && $num): //Show options..
+if ($total): //Show options..
     echo sprintf('<div>&nbsp;%s: %s &nbsp; <a class="no-pjax"
-            href="orgs.php?a=export&qh=%s">%s</a></div>',
+            href="orgs.php?a=export">%s</a></div>',
             __('Page'),
             $pageNav->getPageLinks(),
-            $qhash,
             __('Export'));
 endif;
 ?>
diff --git a/include/staff/page.inc.php b/include/staff/page.inc.php
index c98dc4188f0522962a113713afddd00bf65bef30..63d4fee2c02c1eab07f06d52db5bb2ac403b584e 100644
--- a/include/staff/page.inc.php
+++ b/include/staff/page.inc.php
@@ -14,8 +14,20 @@ if($page && $_REQUEST['a']!='add'){
     $info=$page->getHashtable();
     $info['body'] = Format::viewableImages($page->getBody());
     $info['notes'] = Format::viewableImages($info['notes']);
+    $trans['name'] = $page->getTranslateTag('name');
     $slug = Format::slugify($info['name']);
     $qs += array('id' => $page->getId());
+    $translations = CustomDataTranslation::allTranslations(
+        $page->getTranslateTag('name:body'), 'article');
+    foreach ($cfg->getSecondaryLanguages() as $tag) {
+        foreach ($translations as $t) {
+            if (strcasecmp($t->lang, $tag) === 0) {
+                $C = $t->getComplex();
+                $info['trans'][$tag] = Format::viewableImages($C['body']);
+                break;
+            }
+        }
+    }
 }else {
     $title=__('Add New Page');
     $action='add';
@@ -30,15 +42,17 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
  <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
- <h2><?php echo __('Site Pages'); ?>
+ <h2><?php echo $title; ?>
+    <?php if (isset($info['name'])) { ?><small>
+    — <?php echo $info['name']; ?></small>
+     <?php } ?>
     <i class="help-tip icon-question-sign" href="#site_pages"></i>
     </h2>
  <table class="form_table fixed" width="940" border="0" cellspacing="0" cellpadding="2">
     <thead>
-        <tr><td></td><td></td></tr> <!-- For fixed table layout -->
+        <tr><td style="padding:0"></td><td style="padding:0;"></td></tr> <!-- For fixed table layout -->
         <tr>
             <th colspan="2">
-                <h4><?php echo $title; ?></h4>
                 <em><?php echo __('Page information'); ?></em>
             </th>
         </tr>
@@ -49,7 +63,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
               <?php echo __('Name'); ?>:
             </td>
             <td>
-                <input type="text" size="40" name="name" value="<?php echo $info['name']; ?>">
+                <input type="text" size="40" name="name" value="<?php echo $info['name']; ?>"
+                    autofocus data-translate-tag="<?php echo $trans['name']; ?>"/>
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['name']; ?></span>
             </td>
         </tr>
@@ -89,40 +104,91 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <?php echo __('Status'); ?>:
             </td>
             <td>
-                <input type="radio" name="isactive" value="1" <?php echo $info['isactive']?'checked="checked"':''; ?>><strong><?php echo __('Active'); ?></strong>
-                <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked="checked"':''; ?>><?php echo __('Disabled'); ?>
+                <input type="radio" name="isactive" value="1" <?php echo $info['isactive']?'checked="checked"':''; ?>>
+                <strong><?php echo __('Active'); ?></strong>
+                <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked="checked"':''; ?>>
+                <?php echo __('Disabled'); ?>
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['isactive']; ?></span>
             </td>
         </tr>
-        <tr>
-            <th colspan="2">
-                <em><?php echo __(
-                '<b>Page body</b>: Ticket variables are only supported in thank-you pages.'
-                ); ?><font class="error">*&nbsp;<?php echo $errors['body']; ?></font></em>
-            </th>
-        </tr>
-         <tr>
-            <td colspan=2 style="padding-left:3px;">
-                <textarea name="body" cols="21" rows="12" style="width:98%;" class="richtext draft"
-                    data-draft-namespace="page" data-draft-object-id="<?php echo $info['id']; ?>"
-                    ><?php echo $info['body']; ?></textarea>
-            </td>
-        </tr>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Internal Notes'); ?></strong>:
-                <?php echo __("be liberal, they're internal"); ?></em>
-            </th>
-        </tr>
-        <tr>
-            <td colspan=2>
-                <textarea class="richtext no-bar" name="notes" cols="21"
-                    rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea>
-            </td>
-        </tr>
     </tbody>
 </table>
-<p style="padding-left:225px;">
+<div style="margin-top: 10px">
+  <ul class="tabs clean">
+    <li class="active"><a href="#page-content"><?php echo __('Page Content'); ?></a></li>
+    <li><a href="#notes"><?php echo __('Internal Notes'); ?></a></li>
+  </ul>
+  <div class="tab_content active" id="page-content">
+
+<?php
+$langs = Internationalization::getConfiguredSystemLanguages();
+if ($page && count($langs) > 1) { ?>
+    <ul class="tabs alt clean" id="translations">
+       <li class="empty"><i class="icon-globe" title="This content is translatable"></i></li>
+<?php foreach ($langs as $tag=>$nfo) { ?>
+       <li class="<?php if ($tag == $cfg->getPrimaryLanguage()) echo "active";
+         ?>"><a href="#translation-<?php echo $tag; ?>" title="<?php
+         echo Internationalization::getLanguageDescription($tag);
+         ?>"><span class="flag flag-<?php echo strtolower($nfo['flag']); ?>"></span>
+       </a></li>
+<?php } ?>
+    </ul>
+<?php
+}
+
+// For landing page, constrain to the diplayed width of 565px;
+if ($info['type'] == 'landing')
+    $width = '565px';
+else
+    $width = '100%';
+?>
+    <div id="translations_container">
+      <div id="translation-<?php echo $cfg->getPrimaryLanguage(); ?>" class="tab_content"
+        lang="<?php echo $cfg->getPrimaryLanguage(); ?>">
+        <textarea name="body" cols="21" rows="12" class="richtext draft"
+          data-width="<?php echo $width; ?>"
+<?php
+    if (!$info['type'] || $info['type'] == 'thank-you') echo 'data-root-context="thank-you"';
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('page', $info['id'], $info['body']);
+    echo $attrs; ?>><?php echo $info['body'] ?: $draft; ?></textarea>
+      </div>
+
+<?php if ($langs && $page) {
+    foreach ($langs as $tag=>$nfo) {
+        if ($tag == $cfg->getPrimaryLanguage())
+          continue; ?>
+      <div id="translation-<?php echo $tag; ?>" class="tab_content hidden"
+        dir="<?php echo $nfo['direction']; ?>" lang="<?php echo $tag; ?>">
+        <textarea name="trans[<?php echo $tag; ?>][body]" cols="21" rows="12"
+<?php if ($info['type'] == 'thank-you') echo 'data-root-context="thank-you"'; ?>
+          style="width:100%" class="richtext draft" data-width="<?php echo $width; ?>"
+<?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('page', $info['id'].'.'.$tag, $info['trans'][$tag]);
+    echo $attrs; ?>><?php echo $info['trans'][$tag] ?: $draft; ?></textarea>
+      </div>
+<?php }
+} ?>
+
+      <div id="msg_info">
+        <em><i class="icon-info-sign"></i> <?php
+          echo __(
+            'Ticket variables are only supported in thank-you pages.'
+          ); ?></em>
+      </div>
+
+      <div class="error" style="margin: 5px 0"><?php echo $errors['body']; ?></div>
+      <div class="clear"></div>
+    </div>
+  </div>
+  <div class="tab_content" style="display:none" id="notes">
+    <em><strong><?php echo __('Internal Notes'); ?></strong>:
+      <?php echo __("be liberal, they're internal"); ?></em>
+    <textarea class="richtext no-bar" name="notes" cols="21"
+      rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea>
+  </div>
+</div>
+
+<p style="text-align:center">
     <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
     <input type="reset"  name="reset"  value="<?php echo __('Reset'); ?>">
     <input type="button" name="cancel" value="<?php echo __('Cancel'); ?>" onclick='window.location.href="pages.php"'>
diff --git a/include/staff/pages.inc.php b/include/staff/pages.inc.php
index 4ee3646e3b86c494bd324b217db61ea0f95a1946..76f7a4c3a1ff048e4f2925578d3c0dd9b38db554 100644
--- a/include/staff/pages.inc.php
+++ b/include/staff/pages.inc.php
@@ -1,111 +1,126 @@
 <?php
 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')));
 $qs = array();
-$sql='SELECT page.id, page.isactive, page.name, page.created, page.updated, '
-     .'page.type, count(topic.topic_id) as topics '
-     .' FROM '.PAGE_TABLE.' page '
-     .' LEFT JOIN '.TOPIC_TABLE.' topic ON(topic.page_id=page.id) ';
-$where = ' WHERE type in ("other","landing","thank-you","offline") ';
 $sortOptions=array(
-        'name'=>'page.name', 'status'=>'page.isactive',
-        'created'=>'page.created', 'updated'=>'page.updated',
-        'type'=>'page.type');
+        'name'=>'name', 'status'=>'isactive',
+        'created'=>'created', 'updated'=>'updated',
+        'type'=>'type');
 
-$orderWays=array('DESC'=>'DESC','ASC'=>'ASC');
+$orderWays=array('DESC'=>'-','ASC'=>'');
 $sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'name';
 //Sorting options...
 if($sort && $sortOptions[$sort]) {
-    $order_column =$sortOptions[$sort];
+    $pages = $pages->order_by(
+        $orderWays[strtoupper($_REQUEST['order'])] ?: ''
+        . $sortOptions[$sort]);
 }
 
-$order_column=$order_column?$order_column:'page.name';
-
-if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) {
-    $order=$orderWays[strtoupper($_REQUEST['order'])];
-}
-$order=$order?$order:'ASC';
-
-if($order_column && strpos($order_column,',')){
-    $order_column=str_replace(','," $order,",$order_column);
-}
 $x=$sort.'_sort';
 $$x=' class="'.strtolower($order).'" ';
-$order_by="$order_column $order ";
 
-$total=db_count('SELECT count(*) FROM '.PAGE_TABLE.' page '.$where);
+$total = $pages->count();
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 $pageNav=new Pagenate($total, $page, PAGE_LIMIT);
 $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);
-$qstr .= '&amp;order='.($order=='DESC' ? 'ASC' : 'DESC');
-$query="$sql $where GROUP BY page.id ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
-$res=db_query($query);
-if($res && ($num=db_num_rows($res)))
-    $showing=$pageNav->showing()._N('site page','site pages', $num);
+//Ok..lets roll...create the actual query
+if ($total)
+    $showing=$pageNav->showing().' '._N('site page','site pages', $num);
 else
     $showing=__('No pages found!');
 
 ?>
-
-<div class="pull-left" style="width:700px;padding-top:5px;">
- <h2><?php echo __('Site Pages'); ?>
-    <i class="help-tip icon-question-sign" href="#site_pages"></i>
-    </h2>
-</div>
-<div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;">
- <b><a href="pages.php?a=add" class="Icon newPage"><?php echo __('Add New Page'); ?></a></b></div>
-<div class="clear"></div>
+<form action="pages.php" method="POST" name="tpls">
+    <div class="sticky bar opaque">
+        <div class="content">
+            <div class="pull-left flush-left">
+                <h2><?php echo __('Site Pages'); ?>
+        <i class="help-tip icon-question-sign notsticky" href="#site_pages"></i>
+        </h2>
+            </div>
+            <div class="pull-right flush-right">
+                <a href="pages.php?a=add" class="green button action-button"><i class="icon-plus-sign"></i> <?php echo __('Add New Page'); ?></a>
+                <span class="action-button" data-dropdown="#action-dropdown-more">
+           <i class="icon-caret-down pull-right"></i>
+            <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+                </span>
+                <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                    <ul id="actions">
+                        <li>
+                            <a class="confirm" data-name="enable" href="pages.php?a=enable">
+                                <i class="icon-ok-sign icon-fixed-width"></i>
+                                <?php echo __( 'Enable'); ?>
+                            </a>
+                        </li>
+                        <li>
+                            <a class="confirm" data-name="disable" href="pages.php?a=disable">
+                                <i class="icon-ban-circle icon-fixed-width"></i>
+                                <?php echo __( 'Disable'); ?>
+                            </a>
+                        </li>
+                        <li class="danger">
+                            <a class="confirm" data-name="delete" href="pages.php?a=delete">
+                                <i class="icon-trash icon-fixed-width"></i>
+                                <?php echo __( 'Delete'); ?>
+                            </a>
+                        </li>
+                    </ul>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="clear"></div>
 <form action="pages.php" method="POST" name="tpls">
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="mass_process" >
 <input type="hidden" id="action" name="a" value="" >
  <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption><?php echo $showing; ?></caption>
     <thead>
         <tr>
-            <th width="7">&nbsp;</th>
-            <th width="300"><a <?php echo $name_sort; ?> href="pages.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name'); ?></a></th>
-            <th width="90"><a  <?php echo $type_sort; ?> href="pages.php?<?php echo $qstr; ?>&sort=type"><?php echo __('Type'); ?></a></th>
-            <th width="110"><a  <?php echo $status_sort; ?> href="pages.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Status'); ?></a></th>
-            <th width="150" nowrap><a  <?php echo $created_sort; ?>href="pages.php?<?php echo $qstr; ?>&sort=created"><?php echo __('Date Added'); ?></a></th>
-            <th width="150" nowrap><a  <?php echo $updated_sort; ?>href="pages.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated'); ?></a></th>
+            <th width="4%">&nbsp;</th>
+            <th width="35%"><a <?php echo $name_sort; ?> href="pages.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name'); ?></a></th>
+            <th width="10%"><a  <?php echo $type_sort; ?> href="pages.php?<?php echo $qstr; ?>&sort=type"><?php echo __('Type'); ?></a></th>
+            <th width="16%"><a  <?php echo $status_sort; ?> href="pages.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Status'); ?></a></th>
+            <th width="15%" nowrap><a  <?php echo $created_sort; ?>href="pages.php?<?php echo $qstr; ?>&sort=created"><?php echo __('Date Added'); ?></a></th>
+            <th width="20%" nowrap><a  <?php echo $updated_sort; ?>href="pages.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated'); ?></a></th>
         </tr>
     </thead>
     <tbody>
     <?php
-        $total=0;
         $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null;
-        if($res && db_num_rows($res)):
-            $defaultPages=$cfg->getDefaultPages();
-            while ($row = db_fetch_array($res)) {
+        $defaultPages=$cfg->getDefaultPages();
+        foreach ($pages as $page) {
                 $sel=false;
                 if($ids && in_array($row['id'], $ids))
                     $sel=true;
-                $inuse = ($row['topics'] || in_array($row['id'], $defaultPages));
+                $inuse = ($page->topics || in_array($page->id, $defaultPages));
                 ?>
-            <tr id="<?php echo $row['id']; ?>">
-                <td width=7px>
-                  <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['id']; ?>"
+            <tr id="<?php echo $page->id; ?>">
+                <td align="center">
+                  <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $page->id; ?>"
                             <?php echo $sel?'checked="checked"':''; ?>>
                 </td>
-                <td>&nbsp;<a href="pages.php?id=<?php echo $row['id']; ?>"><?php echo Format::htmlchars($row['name']); ?></a></td>
-                <td class="faded"><?php echo $row['type']; ?></td>
+                <td>&nbsp;<a href="pages.php?id=<?php echo $page->id; ?>"><?php echo Format::htmlchars($page->getLocalName() ?: $page->getName()); ?></a></td>
+                <td class="faded"><?php echo $page->type; ?></td>
                 <td>
-                    &nbsp;<?php echo $row['isactive']?__('Active'):'<b>'.__('Disabled').'</b>'; ?>
+                    &nbsp;<?php echo $page->isActive()?__('Active'):'<b>'.__('Disabled').'</b>'; ?>
                     &nbsp;&nbsp;<?php echo $inuse?'<em>'.__('(in-use)').'</em>':''; ?>
                 </td>
-                <td>&nbsp;<?php echo Format::db_date($row['created']); ?></td>
-                <td>&nbsp;<?php echo Format::db_datetime($row['updated']); ?></td>
+                <td>&nbsp;<?php echo Format::date($page->created); ?></td>
+                <td>&nbsp;<?php echo Format::datetime($page->updated); ?></td>
             </tr>
             <?php
-            } //end of while.
-        endif; ?>
+        } //end of foreach. ?>
     <tfoot>
      <tr>
         <td colspan="6">
-            <?php if($res && $num){ ?>
+            <?php if($total){ ?>
             <?php echo __('Select'); ?>:&nbsp;
             <a id="selectAll" href="#ckb"><?php echo __('All'); ?></a>&nbsp;&nbsp;
             <a id="selectNone" href="#ckb"><?php echo __('None'); ?></a>&nbsp;&nbsp;
@@ -118,14 +133,10 @@ else
     </tfoot>
 </table>
 <?php
-if($res && $num): //Show options..
+if($total): //Show options..
     echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
 ?>
-<p class="centered" id="actions">
-    <input class="button" type="submit" name="enable" value="<?php echo __('Enable'); ?>" >
-    <input class="button" type="submit" name="disable" value="<?php echo __('Disable'); ?>" >
-    <input class="button" type="submit" name="delete" value="<?php echo __('Delete'); ?>" >
-</p>
+
 <?php
 endif;
 ?>
diff --git a/include/staff/plugin-add.inc.php b/include/staff/plugin-add.inc.php
index a5aae19d0581e1dce739ef3d05096f7d49028016..ac4bfb375497fa3499cc131f49cb8af92fb3e6c7 100644
--- a/include/staff/plugin-add.inc.php
+++ b/include/staff/plugin-add.inc.php
@@ -17,7 +17,7 @@ foreach ($ost->plugins->allInfos() as $info) {
     if (isset($installed[$info['install_path']]))
         continue;
     ?>
-        <tr><td><button type="submit" name="install_path"
+        <tr><td><button class="button action-button" type="submit" name="install_path"
             value="<?php echo $info['install_path'];
             ?>"><?php echo __('Install'); ?></button></td>
         <td>
diff --git a/include/staff/plugin.inc.php b/include/staff/plugin.inc.php
index f27e21333aabc78535863ccb2e9c403851ac8a01..6ab5b3c284d7c1eedf88a477a17cc931c9f05a70 100644
--- a/include/staff/plugin.inc.php
+++ b/include/staff/plugin.inc.php
@@ -1,9 +1,9 @@
 <?php
-
-$info=array();
+$info = array();
+$page = null;
 if($plugin && $_REQUEST['a']!='add') {
     $config = $plugin->getConfig();
-    if (!($page = $config->hasCustomConfig())) {
+    if ($config && !($page = $config->hasCustomConfig())) {
         if ($config)
             $form = $config->getForm();
         if ($form && $_POST)
@@ -23,18 +23,14 @@ $info = Format::htmlchars(($errors && $_POST) ? $_POST : $info);
     <input type="hidden" name="do" value="<?php echo $action; ?>">
     <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
     <h2><?php echo __('Manage Plugin'); ?>
-        <br/><small><?php echo $plugin->getName(); ?></small></h2>
+       — <small><?php echo $plugin->getName(); ?></small></h2>
 
     <h3><?php echo __('Configuration'); ?></h3>
 <?php
 if ($page)
     $config->renderCustomConfig();
-elseif ($form) { ?>
-    <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
-    <tbody>
-<?php $form->render(); ?>
-    </tbody></table>
-<?php
+elseif ($form) {
+    include STAFFINC_DIR . 'templates/simple-form.tmpl.php';
 }
 else { ?>
     <tr><th><?php echo __('This plugin has no configurable settings'); ?><br>
diff --git a/include/staff/plugins.inc.php b/include/staff/plugins.inc.php
index ecca0e6ac93d7a3c86032cd3ce74d0496ff1182e..7f210246b55ec349fbd3c75c9c315d1c50cc46b1 100644
--- a/include/staff/plugins.inc.php
+++ b/include/staff/plugins.inc.php
@@ -1,9 +1,42 @@
-<div class="pull-left" style="width:700;padding-top:5px;">
- <h2><?php echo __('Currently Installed Plugins'); ?></h2>
+<form action="plugins.php" method="POST" name="forms">
+
+<div class="sticky bar opaque">
+    <div class="content">
+        <div class="pull-left flush-left">
+            <h2><?php echo __('Currently Installed Plugins'); ?></h2>
+        </div>
+        <div class="pull-right flush-right">
+            <a href="plugins.php?a=add" class="green button action-button"><i class="icon-plus-sign"></i> <?php
+                echo __('Add New Plugin'); ?></a>
+            <span class="action-button" data-dropdown="#action-dropdown-more">
+                <i class="icon-caret-down pull-right"></i>
+                <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+            </span>
+            <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                <ul id="actions">
+                    <li>
+                        <a class="confirm" data-name="enable" href="plugins.php?a=enable">
+                            <i class="icon-ok-sign icon-fixed-width"></i>
+                            <?php echo __( 'Enable'); ?>
+                        </a>
+                    </li>
+                    <li>
+                        <a class="confirm" data-name="disable" href="plugins.php?a=disable">
+                            <i class="icon-ban-circle icon-fixed-width"></i>
+                            <?php echo __( 'Disable'); ?>
+                        </a>
+                    </li>
+                    <li class="danger">
+                        <a class="confirm" data-name="delete" href="plugins.php?a=delete">
+                            <i class="icon-trash icon-fixed-width"></i>
+                            <?php echo __( 'Delete'); ?>
+                        </a>
+                    </li>
+                </ul>
+            </div>
+        </div>
+    </div>
 </div>
-<div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;">
- <b><a href="plugins.php?a=add" class="Icon form-add"><?php
- echo __('Add New Plugin'); ?></a></b></div>
 <div class="clear"></div>
 
 <?php
@@ -21,10 +54,10 @@ $showing=$pageNav->showing().' '._N('plugin', 'plugins', $count);
 <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
     <thead>
         <tr>
-            <th width="7">&nbsp;</th>
-            <th><?php echo __('Plugin Name'); ?></th>
-            <th><?php echo __('Status'); ?></td>
-            <th><?php echo __('Date Installed'); ?></th>
+            <th width="4%">&nbsp;</th>
+            <th width="66%"><?php echo __('Plugin Name'); ?></th>
+            <th width="10%"><?php echo __('Status'); ?></th>
+            <th width="20%"><?php echo __('Date Installed'); ?></th>
         </tr>
     </thead>
     <tbody>
@@ -32,13 +65,13 @@ $showing=$pageNav->showing().' '._N('plugin', 'plugins', $count);
 foreach ($ost->plugins->allInstalled() as $p) {
     if ($p instanceof Plugin) { ?>
     <tr>
-        <td><input type="checkbox" class="ckb" name="ids[]" value="<?php echo $p->getId(); ?>"
+        <td align="center"><input type="checkbox" class="ckb" name="ids[]" value="<?php echo $p->getId(); ?>"
                 <?php echo $sel?'checked="checked"':''; ?>></td>
         <td><a href="plugins.php?id=<?php echo $p->getId(); ?>"
             ><?php echo $p->getName(); ?></a></td>
         <td><?php echo ($p->isActive())
             ? 'Enabled' : '<strong>Disabled</strong>'; ?></td>
-        <td><?php echo Format::db_datetime($p->getInstallDate()); ?></td>
+        <td><?php echo Format::datetime($p->getInstallDate()); ?></td>
     </tr>
     <?php } else {} ?>
 <?php } ?>
@@ -63,11 +96,7 @@ foreach ($ost->plugins->allInstalled() as $p) {
 if ($count) //Show options..
     echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
 ?>
-<p class="centered" id="actions">
-    <input class="button" type="submit" name="delete" value="<?php echo __('Delete'); ?>">
-    <input class="button" type="submit" name="enable" value="<?php echo __('Enable'); ?>">
-    <input class="button" type="submit" name="disable" value="<?php echo __('Disable'); ?>">
-</p>
+
 </form>
 
 <div style="display:none;" class="dialog" id="confirm-action">
diff --git a/include/staff/profile.inc.php b/include/staff/profile.inc.php
index f94f2b47212d6a55bb110e82a580e74f305d9098..e71a1d1e0a10383b297b54e5cb4cc8c262654956 100644
--- a/include/staff/profile.inc.php
+++ b/include/staff/profile.inc.php
@@ -1,132 +1,167 @@
 <?php
 if(!defined('OSTSTAFFINC') || !$staff || !$thisstaff) die('Access Denied');
-
-$info=$staff->getInfo();
-$info['signature'] = Format::viewableImages($info['signature']);
-$info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
-$info['id']=$staff->getId();
 ?>
+
 <form action="profile.php" method="post" id="save" autocomplete="off">
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="update">
- <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
- <h2><?php echo __('My Account Profile');?></h2>
- <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
-    <thead>
-        <tr>
-            <th colspan="2">
-                <h4><?php echo __('Account Information');?></h4>
-                <em><?php echo __('Contact information');?></em>
-            </th>
-        </tr>
-    </thead>
-    <tbody>
-        <tr>
-            <td width="180" class="required">
-                <?php echo __('Username');?>:
-            </td>
-            <td><b><?php echo $staff->getUserName(); ?></b>&nbsp;<i class="help-tip icon-question-sign" href="#username"></i></td>
-        </tr>
+ <input type="hidden" name="id" value="<?php echo $staff->getId(); ?>">
+<h2><?php echo __('My Account Profile');?></h2>
+  <ul class="clean tabs">
+    <li class="active"><a href="#account"><i class="icon-user"></i> <?php echo __('Account'); ?></a></li>
+    <li><a href="#preferences"><?php echo __('Preferences'); ?></a></li>
+    <li><a href="#signature"><?php echo __('Signature'); ?></a></li>
+  </ul>
 
+  <div class="tab_content" id="account">
+    <table class="table two-column" width="940" border="0" cellspacing="0" cellpadding="2">
+      <tbody>
+        <tr><td colspan="2"><div>
+        <div class="avatar pull-left" style="margin: 10px 15px; width: 100px; height: 100px;">
+<?php       $avatar = $staff->getAvatar();
+            echo $avatar;
+if ($avatar->isChangeable()) { ?>
+          <div style="text-align: center">
+            <a class="button no-pjax"
+                href="#ajax.php/staff/<?php echo $staff->getId(); ?>/avatar/change"
+                onclick="javascript:
+    event.preventDefault();
+    var $a = $(this),
+        form = $a.closest('form');
+    $.ajax({
+      url: $a.attr('href').substr(1),
+      dataType: 'json',
+      success: function(json) {
+        if (!json || !json.code)
+          return;
+        var code = form.find('[name=avatar_code]');
+        if (!code.length)
+          code = form.append($('<input>').attr({type: 'hidden', name: 'avatar_code'}));
+        code.val(json.code).trigger('change');
+        $a.closest('.avatar').find('img').replaceWith($(json.img));
+      }
+    });
+    return false;"><i class="icon-retweet"></i></a>
+          </div>
+<?php
+} ?>
+        </div>
+        <table class="table two-column" border="0" cellspacing="2" cellpadding="2" style="width:760px">
         <tr>
-            <td width="180" class="required">
-                <?php echo __('First Name');?>:
-            </td>
-            <td>
-                <input type="text" size="34" name="firstname" value="<?php echo $info['firstname']; ?>">
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['firstname']; ?></span>
-            </td>
+          <td class="required"><?php echo __('Name'); ?>:</td>
+          <td>
+            <input type="text" size="20" maxlength="64" style="width: 145px" name="firstname"
+              autofocus value="<?php echo Format::htmlchars($staff->firstname); ?>"
+              placeholder="<?php echo __("First Name"); ?>" />
+            <input type="text" size="20" maxlength="64" style="width: 145px" name="lastname"
+              value="<?php echo Format::htmlchars($staff->lastname); ?>"
+              placeholder="<?php echo __("Last Name"); ?>" />
+            <div class="error"><?php echo $errors['firstname']; ?></div>
+            <div class="error"><?php echo $errors['lastname']; ?></div>
+          </td>
         </tr>
         <tr>
-            <td width="180" class="required">
-                <?php echo __('Last Name');?>:
-            </td>
-            <td>
-                <input type="text" size="34" name="lastname" value="<?php echo $info['lastname']; ?>">
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['lastname']; ?></span>
-            </td>
+          <td class="required"><?php echo __('Email Address'); ?>:</td>
+          <td>
+            <input type="email" size="40" maxlength="64" style="width: 300px" name="email"
+              value="<?php echo Format::htmlchars($staff->email); ?>"
+              placeholder="<?php echo __('e.g. me@mycompany.com'); ?>" />
+            <div class="error"><?php echo $errors['email']; ?></div>
+          </td>
         </tr>
         <tr>
-            <td width="180" class="required">
-                <?php echo __('Email Address');?>:
-            </td>
-            <td>
-                <input type="text" size="34" name="email" value="<?php echo $info['email']; ?>">
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['email']; ?></span>
-            </td>
+          <td><?php echo __('Phone Number');?>:</td>
+          <td>
+            <input type="tel" size="18" name="phone" class="auto phone"
+              value="<?php echo Format::htmlchars($staff->phone); ?>" />
+            <?php echo __('Ext');?>
+            <input type="text" size="5" name="phone_ext"
+              value="<?php echo Format::htmlchars($staff->phone_ext); ?>">
+            <div class="error"><?php echo $errors['phone']; ?></div>
+            <div class="error"><?php echo $errors['phone_ext']; ?></div>
+          </td>
         </tr>
         <tr>
-            <td width="180">
-                <?php echo __('Phone Number');?>:
-            </td>
-            <td>
-                <input type="text" size="22" name="phone" value="<?php echo $info['phone']; ?>">
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['phone']; ?></span>
-                Ext <input type="text" size="5" name="phone_ext" value="<?php echo $info['phone_ext']; ?>">
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['phone_ext']; ?></span>
-            </td>
+          <td><?php echo __('Mobile Number');?>:</td>
+          <td>
+            <input type="tel" size="18" name="mobile" class="auto phone"
+              value="<?php echo Format::htmlchars($staff->mobile); ?>" />
+            <div class="error"><?php echo $errors['mobile']; ?></div>
+          </td>
         </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Mobile Number');?>:
-            </td>
-            <td>
-                <input type="text" size="22" name="mobile" value="<?php echo $info['mobile']; ?>">
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['mobile']; ?></span>
-            </td>
+        </table></div></td></tr>
+      </tbody>
+      <!-- ================================================ -->
+      <tbody>
+        <tr class="header">
+          <th colspan="2">
+            <?php echo __('Authentication'); ?>
+          </th>
         </tr>
+        <?php if ($bk = $staff->getAuthBackend()) { ?>
         <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Preferences');?></strong>: <?php echo __('Profile preferences and settings.');?></em>
-            </th>
-        </tr>
-        <tr>
-            <td width="180" class="required">
-                <?php echo __('Time Zone');?>:
-            </td>
-            <td>
-                <select name="timezone_id" id="timezone_id">
-                    <option value="0">&mdash; <?php echo __('Select Time Zone');?> &mdash;</option>
-                    <?php
-                    $sql='SELECT id, offset,timezone FROM '.TIMEZONE_TABLE.' ORDER BY id';
-                    if(($res=db_query($sql)) && db_num_rows($res)){
-                        while(list($id,$offset, $tz)=db_fetch_row($res)){
-                            $sel=($info['timezone_id']==$id)?'selected="selected"':'';
-                            echo sprintf('<option value="%d" %s>GMT %s - %s</option>',$id,$sel,$offset,$tz);
-                        }
-                    }
-                    ?>
-                </select>
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['timezone_id']; ?></span>
-            </td>
+          <td><?php echo __("Backend"); ?></td>
+          <td><?php echo $bk->getName(); ?></td>
         </tr>
+        <?php } ?>
         <tr>
-            <td width="180">
-                <?php echo __('Preferred Language'); ?>:
-            </td>
-            <td>
-        <?php
-        $langs = Internationalization::availableLanguages(); ?>
-                <select name="lang">
-                    <option value="">&mdash; <?php echo __('Use Browser Preference'); ?> &mdash;</option>
-<?php foreach($langs as $l) {
-    $selected = ($info['lang'] == $l['code']) ? 'selected="selected"' : ''; ?>
-                    <option value="<?php echo $l['code']; ?>" <?php echo $selected;
-                        ?>><?php echo Internationalization::getLanguageDescription($l['code']); ?></option>
+          <td class="required"><?php echo __('Username'); ?>:
+            <span class="error">*</span></td>
+          <td>
+            <input type="text" size="40" style="width:300px"
+              class="staff-username typeahead"
+              name="username" disabled value="<?php echo Format::htmlchars($staff->username); ?>" />
+<?php if (!$bk || $bk->supportsPasswordChange()) { ?>
+            <button type="button" id="change-pw-button" class="action-button" onclick="javascript:
+            $.dialog('ajax.php/staff/'+<?php echo $staff->getId(); ?>+'/change-password', 201);">
+              <i class="icon-refresh"></i> <?php echo __('Change Password'); ?>
+            </button>
 <?php } ?>
-                </select>
-                <span class="error">&nbsp;<?php echo $errors['lang']; ?></span>
-            </td>
+            <i class="offset help-tip icon-question-sign" href="#username"></i>
+            <div class="error"><?php echo $errors['username']; ?></div>
+          </td>
+        </tr>
+      </tbody>
+      <!-- ================================================ -->
+      <tbody>
+        <tr class="header">
+          <th colspan="2">
+            <?php echo __('Status and Settings'); ?>
+          </th>
         </tr>
         <tr>
-            <td width="180">
-               <?php echo __('Daylight Saving');?>:
-            </td>
-            <td>
-                <input type="checkbox" name="daylight_saving" value="1" <?php echo $info['daylight_saving']?'checked="checked"':''; ?>>
-                <?php echo __('Observe daylight saving');?>
-                <em>(<?php echo __('Current Time');?>: <strong><?php echo Format::date($cfg->getDateTimeFormat(),Misc::gmtime(),$info['tz_offset'],$info['daylight_saving']); ?></strong>)</em>
-            </td>
+          <td colspan="2">
+            <label class="checkbox">
+            <input type="checkbox" name="show_assigned_tickets"
+              <?php echo $cfg->showAssignedTickets() ? 'disabled="disabled" ' : ''; ?>
+              <?php echo $staff->show_assigned_tickets ? 'checked="checked"' : ''; ?> />
+              <?php echo __('Show assigned tickets on open queue.'); ?>
+            <i class="help-tip icon-question-sign" href="#show_assigned_tickets"></i>
+            </label>
+            <label class="checkbox">
+            <input type="checkbox" name="onvacation"
+              <?php echo ($staff->onvacation) ? 'checked="checked"' : ''; ?> />
+              <?php echo __('Vacation Mode'); ?>
+            </label>
+            <br/>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+
+  <!-- =================== PREFERENCES ======================== -->
+
+  <div class="hidden tab_content" id="preferences">
+    <table class="table two-column" width="100%">
+      <tbody>
+        <tr class="header">
+          <th colspan="2">
+            <?php echo __('Preferences'); ?>
+            <div><small><?php echo __(
+            "Profile preferences and settings"
+          ); ?>
+            </small></div>
+          </th>
         </tr>
         <tr>
             <td width="180"><?php echo __('Maximum Page size');?>:</td>
@@ -134,7 +169,7 @@ $info['id']=$staff->getId();
                 <select name="max_page_size">
                     <option value="0">&mdash; <?php echo __('system default');?> &mdash;</option>
                     <?php
-                    $pagelimit=$info['max_page_size']?$info['max_page_size']:$cfg->getPageSize();
+                    $pagelimit = $staff->max_page_size ?: $cfg->getPageSize();
                     for ($i = 5; $i <= 50; $i += 5) {
                         $sel=($pagelimit==$i)?'selected="selected"':'';
                          echo sprintf('<option value="%d" %s>'.__('show %s records').'</option>',$i,$sel,$i);
@@ -143,27 +178,78 @@ $info['id']=$staff->getId();
             </td>
         </tr>
         <tr>
-            <td width="180"><?php echo __('Auto Refresh Rate');?>:</td>
+            <td width="180"><?php echo __('Auto Refresh Rate');?>:
+              <div class="faded"><?php echo __('Tickets page refresh rate in minutes.'); ?></div>
+            </td>
             <td>
                 <select name="auto_refresh_rate">
                   <option value="0">&mdash; <?php echo __('disable');?> &mdash;</option>
                   <?php
                   $y=1;
                    for($i=1; $i <=30; $i+=$y) {
-                     $sel=($info['auto_refresh_rate']==$i)?'selected="selected"':'';
-                     echo sprintf('<option value="%1$d" %2$s>'
-                        .sprintf(
-                            _N('Every minute', 'Every %d minutes', $i), $i)
-                         .'</option>',$i,$sel);
+                     $sel=($staff->auto_refresh_rate==$i)?'selected="selected"':'';
+                     echo sprintf('<option value="%d" %s>%s</option>', $i, $sel,
+                        sprintf(_N('Every minute', 'Every %d minutes', $i), $i));
                      if($i>9)
                         $y=2;
                    } ?>
                 </select>
-                <em><?php echo __('(Tickets page refresh rate in minutes.)');?></em>
             </td>
         </tr>
+
         <tr>
-            <td width="180"><?php echo __('Default Signature');?>:</td>
+            <td><?php echo __('Default From Name');?>:
+              <div class="faded"><?php echo __('From name to use when replying to a thread');?></div>
+            </td>
+            <td>
+                <select name="default_from_name">
+                  <?php
+                   $options=array(
+                           'email' => __("Email Address Name"),
+                           'dept' => sprintf(__("Department Name (%s)"),
+                               __('if public' /* This is used in 'Department's Name (>if public<)' */)),
+                           'mine' => __('My Name'),
+                           '' => '— '.__('System Default').' —',
+                           );
+                  if ($cfg->hideStaffName())
+                    unset($options['mine']);
+
+                  foreach($options as $k=>$v) {
+                      echo sprintf('<option value="%s" %s>%s</option>',
+                                $k,($staff->default_from_name==$k)?'selected="selected"':'',$v);
+                  }
+                  ?>
+                </select>
+                <div class="error"><?php echo $errors['default_from_name']; ?></div>
+            </td>
+        </tr>
+        <tr>
+            <td><?php echo __('Thread View Order');?>:
+              <div class="faded"><?php echo __('The order of thread entries');?></div>
+            </td>
+            <td>
+                <select name="thread_view_order">
+                  <?php
+                   $options=array(
+                           'desc' => __('Descending'),
+                           'asc' => __('Ascending'),
+                           '' => '— '.__('System Default').' —',
+                           );
+                  foreach($options as $k=>$v) {
+                      echo sprintf('<option value="%s" %s>%s</option>',
+                                $k
+                                ,($staff->thread_view_order == $k) ? 'selected="selected"' : ''
+                                ,$v);
+                  }
+                  ?>
+                </select>
+                <div class="error"><?php echo $errors['thread_view_order']; ?></div>
+            </td>
+        </tr>
+        <tr>
+            <td><?php echo __('Default Signature');?>:
+              <div class="faded"><?php echo __('This can be selected when replying to a thread');?></div>
+            </td>
             <td>
                 <select name="default_signature_type">
                   <option value="none" selected="selected">&mdash; <?php echo __('None');?> &mdash;</option>
@@ -172,16 +258,17 @@ $info['id']=$staff->getId();
                        __('if set' /* This is used in 'Department Signature (>if set<)' */)));
                   foreach($options as $k=>$v) {
                       echo sprintf('<option value="%s" %s>%s</option>',
-                                $k,($info['default_signature_type']==$k)?'selected="selected"':'',$v);
+                                $k,($staff->default_signature_type==$k)?'selected="selected"':'',$v);
                   }
                   ?>
                 </select>
-                <em><?php echo __('(This can be selected when replying to a ticket)');?></em>
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['default_signature_type']; ?></span>
+                <div class="error"><?php echo $errors['default_signature_type']; ?></div>
             </td>
         </tr>
         <tr>
-            <td width="180"><?php echo __('Default Paper Size');?>:</td>
+            <td width="180"><?php echo __('Default Paper Size');?>:
+              <div class="faded"><?php echo __('Paper size used when printing tickets to PDF');?></div>
+            </td>
             <td>
                 <select name="default_paper_size">
                   <option value="none" selected="selected">&mdash; <?php echo __('None');?> &mdash;</option>
@@ -189,74 +276,121 @@ $info['id']=$staff->getId();
 
                   foreach(Export::$paper_sizes as $v) {
                       echo sprintf('<option value="%s" %s>%s</option>',
-                                $v,($info['default_paper_size']==$v)?'selected="selected"':'',__($v));
+                                $v,($staff->default_paper_size==$v)?'selected="selected"':'',__($v));
                   }
                   ?>
                 </select>
-                <em><?php echo __('Paper size used when printing tickets to PDF');?></em>
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['default_paper_size']; ?></span>
+                <div class="error"><?php echo $errors['default_paper_size']; ?></div>
             </td>
         </tr>
+      </tbody>
+      <tbody>
+        <tr class="header">
+          <th colspan="2">
+            <?php echo __('Localization'); ?>
+          </th>
+        </tr>
         <tr>
-            <td><?php echo __('Show Assigned Tickets');?>:</td>
+            <td><?php echo __('Time Zone');?>:</td>
             <td>
-                <input type="checkbox" name="show_assigned_tickets" <?php echo $info['show_assigned_tickets']?'checked="checked"':''; ?>>
-                <em><?php echo __('Show assigned tickets on open queue.');?></em>
-                &nbsp;<i class="help-tip icon-question-sign" href="#show_assigned_tickets"></i></em>
+                <?php
+                $TZ_NAME = 'timezone';
+                $TZ_TIMEZONE = $staff->timezone;
+                include STAFFINC_DIR.'templates/timezone.tmpl.php'; ?>
+                <div class="error"><?php echo $errors['timezone']; ?></div>
             </td>
         </tr>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Password');?></strong>: <?php echo __('To reset your password, provide your current password and a new password below.');?>&nbsp;<span class="error">&nbsp;<?php echo $errors['passwd']; ?></span></em>
-            </th>
-        </tr>
-        <?php if (!isset($_SESSION['_staff']['reset-token'])) { ?>
-        <tr>
-            <td width="180">
-                <?php echo __('Current Password');?>:
-            </td>
+        <tr><td><?php echo __('Time Format');?>:</td>
             <td>
-                <input type="password" size="18" name="cpasswd" value="<?php echo $info['cpasswd']; ?>">
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['cpasswd']; ?></span>
+                <select name="datetime_format">
+<?php
+    $datetime_format = $staff->datetime_format;
+    foreach (array(
+    'relative' => __('Relative Time'),
+    '' => '— '.__('System Default').' —',
+) as $v=>$name) { ?>
+                    <option value="<?php echo $v; ?>" <?php
+                    if ($v == $datetime_format)
+                        echo 'selected="selected"';
+                    ?>><?php echo $name; ?></option>
+<?php } ?>
+                </select>
             </td>
         </tr>
-        <?php } ?>
+<?php if ($cfg->getSecondaryLanguages()) { ?>
         <tr>
-            <td width="180">
-                <?php echo __('New Password');?>:
-            </td>
+            <td><?php echo __('Preferred Language'); ?>:</td>
             <td>
-                <input type="password" size="18" name="passwd1" value="<?php echo $info['passwd1']; ?>">
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['passwd1']; ?></span>
+        <?php
+        $langs = Internationalization::getConfiguredSystemLanguages(); ?>
+                <select name="lang">
+                    <option value="">&mdash; <?php echo __('Use Browser Preference'); ?> &mdash;</option>
+<?php foreach($langs as $l) {
+    $selected = ($staff->lang == $l['code']) ? 'selected="selected"' : ''; ?>
+                    <option value="<?php echo $l['code']; ?>" <?php echo $selected;
+                        ?>><?php echo Internationalization::getLanguageDescription($l['code']); ?></option>
+<?php } ?>
+                </select>
+                <span class="error">&nbsp;<?php echo $errors['lang']; ?></span>
             </td>
         </tr>
+<?php } ?>
+<?php if (extension_loaded('intl')) { ?>
         <tr>
-            <td width="180">
-                <?php echo __('Confirm New Password');?>:
-            </td>
+            <td><?php echo __('Preferred Locale');?>:</td>
             <td>
-                <input type="password" size="18" name="passwd2" value="<?php echo $info['passwd2']; ?>">
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['passwd2']; ?></span>
+                <select name="locale">
+                    <option value=""><?php echo __('Use Language Preference'); ?></option>
+<?php foreach (Internationalization::allLocales() as $code=>$name) { ?>
+                    <option value="<?php echo $code; ?>" <?php
+                        if ($code == $staff->locale)
+                            echo 'selected="selected"';
+                    ?>><?php echo $name; ?></option>
+<?php } ?>
+                </select>
             </td>
         </tr>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Signature');?></strong>: <?php echo __('Optional signature used on outgoing emails.');?>
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['signature']; ?></span>&nbsp;<i class="help-tip icon-question-sign" href="#signature"></i></em>
-            </th>
+<?php } ?>
+    </table>
+  </div>
+
+  <!-- ==================== SIGNATURES ======================== -->
+
+  <div id="signature" class="hidden">
+    <table class="table two-column" width="100%">
+      <tbody>
+        <tr class="header">
+          <th colspan="2">
+            <?php echo __('Signature'); ?>
+            <div><small><?php echo __(
+            "Optional signature used on outgoing emails.")
+            .' '.
+            __('Signature is made available as a choice, on ticket reply.'); ?>
+            </small></div>
+          </th>
         </tr>
         <tr>
-            <td colspan=2>
+            <td colspan="2">
                 <textarea class="richtext no-bar" name="signature" cols="21"
-                    rows="5" style="width: 60%;"><?php echo $info['signature']; ?></textarea>
-                <br><em><?php echo __('Signature is made available as a choice, on ticket reply.');?></em>
+                    rows="5" style="width: 60%;"><?php echo $staff->signature; ?></textarea>
             </td>
         </tr>
-    </tbody>
-</table>
-<p style="text-align:center;">
-    <input type="submit" name="submit" value="<?php echo __('Save Changes');?>">
-    <input type="reset"  name="reset"  value="<?php echo __('Reset Changes');?>">
-    <input type="button" name="cancel" value="<?php echo __('Cancel Changes');?>" onclick='window.location.href="index.php"'>
-</p>
+      </tbody>
+    </table>
+  </div>
+
+  <p style="text-align:center;">
+    <button class="button action-button" type="submit" name="submit" ><i class="icon-save"></i> <?php echo __('Save Changes'); ?></button>
+    <button class="button action-button" type="reset"  name="reset"><i class="icon-undo"></i>
+        <?php echo __('Reset');?></button>
+    <button class="red button action-button" type="button" name="cancel" onclick="window.history.go(-1);"><i class="icon-remove-circle"></i> <?php echo __('Cancel');?></button>
+  </p>
+    <div class="clear"></div>
 </form>
+<?php
+if ($staff->change_passwd) { ?>
+<script type="text/javascript">
+    $(function() { $('#change-pw-button').trigger('click'); });
+</script>
+<?php
+}
diff --git a/include/staff/pwreset.login.php b/include/staff/pwreset.login.php
index 54d57b62ca32c2f904c2ab75528d098cb59fcdea..9bac008f358188df9e72696d4118b644f7da5575 100644
--- a/include/staff/pwreset.login.php
+++ b/include/staff/pwreset.login.php
@@ -4,7 +4,11 @@ defined('OSTSCPINC') or die('Invalid path');
 $info = ($_POST)?Format::htmlchars($_POST):array();
 ?>
 
+<div id="brickwall"></div>
 <div id="loginBox">
+    <div id="blur">
+        <div id="background"></div>
+    </div>
     <h1 id="logo"><a href="index.php">
         <span class="valign-helper"></span>
         <img src="logo.php?login" alt="osTicket :: <?php echo __('Agent Password Reset');?>" />
@@ -22,8 +26,24 @@ $info = ($_POST)?Format::htmlchars($_POST):array();
         </fieldset>
         <input class="submit" type="submit" name="submit" value="Login"/>
     </form>
-</div>
 
-<div id="copyRights">Copyright &copy; <a href='http://www.osticket.com' target="_blank">osTicket.com</a></div>
+    <div id="company">
+        <div class="content">
+            <?php echo __('Copyright'); ?> &copy; <?php echo Format::htmlchars($ost->company) ?: date('Y'); ?>
+        </div>
+    </div>
+</div>
+<div id="poweredBy"><?php echo __('Powered by'); ?>
+    <a href="http://www.osticket.com" target="_blank">
+        <img alt="osTicket" src="images/osticket-grey.png" class="osticket-logo">
+    </a>
+</div>
+    <script>
+    document.addEventListener('DOMContentLoaded', function() {
+        if (undefined === window.getComputedStyle(document.documentElement).backgroundBlendMode) {
+            document.getElementById('loginBox').style.backgroundColor = 'white';
+        }
+    });
+    </script>
 </body>
 </html>
diff --git a/include/staff/pwreset.php b/include/staff/pwreset.php
index 93f8e9bb1ae7800eb9f17e98420e71a96ac52562..cfafa7f20435f139457f08a0a978597bf6efcf35 100644
--- a/include/staff/pwreset.php
+++ b/include/staff/pwreset.php
@@ -4,7 +4,11 @@ defined('OSTSCPINC') or die('Invalid path');
 $info = ($_POST && $errors)?Format::htmlchars($_POST):array();
 ?>
 
+<div id="brickwall"></div>
 <div id="loginBox">
+    <div id="blur">
+        <div id="background"></div>
+    </div>
     <h1 id="logo"><a href="index.php">
         <span class="valign-helper"></span>
         <img src="logo.php?login" alt="osTicket :: <?php echo __('Agent Password Reset');?>" />
@@ -21,8 +25,23 @@ $info = ($_POST && $errors)?Format::htmlchars($_POST):array();
         <input class="submit" type="submit" name="submit" value="<?php echo __('Send Email'); ?>"/>
     </form>
 
+    <div id="company">
+        <div class="content">
+            <?php echo __('Copyright'); ?> &copy; <?php echo Format::htmlchars($ost->company) ?: date('Y'); ?>
+        </div>
+    </div>
 </div>
-
-<div id="copyRights">Copyright &copy; <a href='http://www.osticket.com' target="_blank">osTicket.com</a></div>
+<div id="poweredBy"><?php echo __('Powered by'); ?>
+    <a href="http://www.osticket.com" target="_blank">
+        <img alt="osTicket" src="images/osticket-grey.png" class="osticket-logo">
+    </a>
+</div>
+    <script>
+    document.addEventListener('DOMContentLoaded', function() {
+        if (undefined === window.getComputedStyle(document.documentElement).backgroundBlendMode) {
+            document.getElementById('loginBox').style.backgroundColor = 'white';
+        }
+    });
+    </script>
 </body>
 </html>
diff --git a/include/staff/pwreset.sent.php b/include/staff/pwreset.sent.php
index b0a46d8d1ff7fa58e756b6b33833960b5992e8d2..bd6ce089638ec349d32bb393fefe71d9df64a7e1 100644
--- a/include/staff/pwreset.sent.php
+++ b/include/staff/pwreset.sent.php
@@ -4,7 +4,11 @@ defined('OSTSCPINC') or die('Invalid path');
 $info = ($_POST && $errors)?Format::htmlchars($_POST):array();
 ?>
 
+<div id="brickwall"></div>
 <div id="loginBox">
+    <div id="blur">
+        <div id="background"></div>
+    </div>
     <h1 id="logo"><a href="index.php">
         <span class="valign-helper"></span>
         <img src="logo.php?login" alt="osTicket :: <?php echo __('Agent Password Reset');?>" />
@@ -18,8 +22,24 @@ $info = ($_POST && $errors)?Format::htmlchars($_POST):array();
     <form action="index.php" method="get">
         <input class="submit" type="submit" name="submit" value="Login"/>
     </form>
-</div>
 
-<div id="copyRights">Copyright &copy; <a href='http://www.osticket.com' target="_blank">osTicket.com</a></div>
+    <div id="company">
+        <div class="content">
+            <?php echo __('Copyright'); ?> &copy; <?php echo Format::htmlchars($ost->company) ?: date('Y'); ?>
+        </div>
+    </div>
+</div>
+<div id="poweredBy"><?php echo __('Powered by'); ?>
+    <a href="http://www.osticket.com" target="_blank">
+        <img alt="osTicket" src="images/osticket-grey.png" class="osticket-logo">
+    </a>
+</div>
+    <script>
+    document.addEventListener('DOMContentLoaded', function() {
+        if (undefined === window.getComputedStyle(document.documentElement).backgroundBlendMode) {
+            document.getElementById('loginBox').style.backgroundColor = 'white';
+        }
+    });
+    </script>
 </body>
 </html>
diff --git a/include/staff/role.inc.php b/include/staff/role.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..9d82a8a318275582b0d1cf281c7ecd47242321a6
--- /dev/null
+++ b/include/staff/role.inc.php
@@ -0,0 +1,126 @@
+<?php
+
+$info=array();
+if ($role) {
+    $title = __('Update Role');
+    $action = 'update';
+    $submit_text = __('Save Changes');
+    $info = $role->getInfo();
+    $trans['name'] = $role->getTranslateTag('name');
+    $newcount=2;
+} else {
+    $title = __('Add New Role');
+    $action = 'add';
+    $submit_text = __('Add Role');
+    $newcount=4;
+}
+
+$info = Format::htmlchars(($errors && $_POST) ? array_merge($info, $_POST) : $info);
+
+?>
+<form action="" 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']); ?>">
+    <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
+    <h2><?php echo $title; ?>
+    <?php if (isset($info['name'])) { ?><small>
+    — <?php echo $info['name']; ?></small>
+        <?php } ?>
+    </h2>
+    <ul class="clean tabs">
+        <li class="active"><a href="#definition"><i class="icon-file"></i> <?php echo __('Definition'); ?></a></li>
+        <li><a href="#permissions"><i class="icon-lock"></i> <?php echo __('Permissions'); ?></a></li>
+    </ul>
+    <div id="definition" class="tab_content">
+        <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
+            <thead>
+                <tr>
+                    <th colspan="2">
+                        <em><?php echo __(
+                        'Roles are used to define agents\' permissions'
+                        ); ?>&nbsp;<i class="help-tip icon-question-sign"
+                        href="#roles"></i></em>
+                    </th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr>
+                    <td width="180" class="required"><?php echo __('Name'); ?>:</td>
+                    <td>
+                        <input size="50" type="text" name="name" value="<?php echo
+                        $info['name']; ?>" data-translate-tag="<?php echo $trans['name']; ?>"
+                        autofocus/>
+                        <span class="error">*&nbsp;<?php echo $errors['name']; ?></span>
+                    </td>
+                </tr>
+            </tbody>
+            <tbody>
+                <tr>
+                    <th colspan="7">
+                        <em><strong><?php echo __('Internal Notes'); ?></strong> </em>
+                    </th>
+                </tr>
+                <tr>
+                    <td colspan="7"><textarea name="notes" class="richtext no-bar"
+                        rows="6" cols="80"><?php
+                        echo $info['notes']; ?></textarea>
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+    </div>
+    <div id="permissions" class="hidden">
+        <?php
+            $setting = $role ? $role->getPermissionInfo() : array();
+            // Eliminate groups without any department-specific permissions
+            $buckets = array();
+            foreach (RolePermission::allPermissions() as $g => $perms) {
+                foreach ($perms as $k => $v) {
+                if ($v['primary'])
+                    continue;
+                    $buckets[$g][$k] = $v;
+            }
+        } ?>
+        <ul class="alt tabs">
+            <?php
+                $first = true;
+                foreach ($buckets as $g => $perms) { ?>
+                    <li <?php if ($first) { echo 'class="active"'; $first=false; } ?>>
+                        <a href="#<?php echo Format::slugify($g); ?>"><?php echo Format::htmlchars(__($g));?></a>
+                    </li>
+            <?php } ?>
+        </ul>
+        <?php
+        $first = true;
+        foreach ($buckets as $g => $perms) { ?>
+        <div class="tab_content <?php if (!$first) { echo 'hidden'; } else { $first = false; }
+            ?>" id="<?php echo Format::slugify($g); ?>">
+            <table class="table">
+                <?php foreach ($perms as $k => $v) { ?>
+                <tr>
+                    <td>
+                        <label>
+                            <?php
+                            echo sprintf('<input type="checkbox" name="perms[]" value="%s" %s />',
+                            $k, (isset($setting[$k]) && $setting[$k]) ?  'checked="checked"' : ''); ?>
+                            &nbsp;
+                            <?php echo Format::htmlchars(__($v['title'])); ?>
+                            —
+                            <em><?php echo Format::htmlchars(__($v['desc']));
+                            ?></em>
+                        </label>
+                    </td>
+                </tr>
+                <?php } ?>
+            </table>
+        </div>
+        <?php } ?>
+    </div>
+    <p class="centered">
+        <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
+        <input type="reset"  name="reset"  value="<?php echo __('Reset'); ?>">
+        <input type="button" name="cancel" value="<?php echo __('Cancel'); ?>"
+            onclick='window.location.href="?"'>
+    </p>
+</form>
diff --git a/include/staff/roles.inc.php b/include/staff/roles.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..7ce374cfe96bad198bbfe74061be44c0105d93c9
--- /dev/null
+++ b/include/staff/roles.inc.php
@@ -0,0 +1,133 @@
+<form action="roles.php" method="POST" name="roles">
+<div class="sticky bar">
+    <div class="content">
+        <div class="pull-left">
+            <h2><?php echo __('Roles'); ?></h2>
+        </div>
+        <div class="pull-right flush-right">
+            <a href="roles.php?a=add" class="green button action-button"><i class="icon-plus-sign"></i> <?php
+echo __('Add New Role'); ?></a>
+            <span class="action-button" data-dropdown="#action-dropdown-more">
+                <i class="icon-caret-down pull-right"></i>
+                <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+            </span>
+            <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                <ul id="actions">
+                    <li><a class="confirm" data-name="enable" href="roles.php?a=enable">
+                        <i class="icon-ok-sign icon-fixed-width"></i>
+                        <?php echo __('Enable'); ?></a></li>
+                    <li><a class="confirm" data-name="disable" href="roles.php?a=disable">
+                        <i class="icon-ban-circle icon-fixed-width"></i>
+                        <?php echo __('Disable'); ?></a></li>
+                    <li class="danger"><a class="confirm" data-name="delete" href="roles.php?a=delete">
+                        <i class="icon-trash icon-fixed-width"></i>
+                        <?php echo __('Delete'); ?></a></li>
+                </ul>
+            </div>
+        </div>
+        <div class="clear"></div>
+    </div>
+</div>
+<?php
+$page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
+$count = Role::objects()->count();
+$pageNav = new Pagenate($count, $page, PAGE_LIMIT);
+$pageNav->setURL('roles.php');
+$showing=$pageNav->showing().' '._N('role', 'roles', $count);
+
+csrf_token(); ?>
+<input type="hidden" name="do" value="mass_process" >
+<input type="hidden" id="action" name="a" value="" >
+<table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
+    <thead>
+        <tr>
+            <th width="4%">&nbsp;</th>
+            <th width="53%"><?php echo __('Name'); ?></th>
+            <th width="8%"><?php echo __('Status'); ?></th>
+            <th width="15%"><?php echo __('Created On') ?></th>
+            <th width="20%"><?php echo __('Last Updated'); ?></th>
+        </tr>
+    </thead>
+    <tbody>
+    <?php foreach (Role::objects()->order_by('name')
+                ->limit($pageNav->getLimit())
+                ->offset($pageNav->getStart()) as $role) {
+            $id = $role->getId();
+            $sel = false;
+            if ($ids && in_array($id, $ids))
+                $sel = true; ?>
+        <tr>
+            <td align="center">
+                <?php
+                if ($role->isDeleteable()) { ?>
+                <input width="7" type="checkbox" class="ckb" name="ids[]"
+                value="<?php echo $id; ?>"
+                    <?php echo $sel?'checked="checked"':''; ?>>
+                <?php
+                } else {
+                    echo '&nbsp;';
+                }
+                ?>
+            </td>
+            <td><a href="?id=<?php echo $id; ?>"><?php echo
+            $role->getLocal('name'); ?></a></td>
+            <td>&nbsp;<?php echo $role->isEnabled() ? __('Active') :
+            '<b>'.__('Disabled').'</b>'; ?></td>
+            <td><?php echo Format::date($role->getCreateDate()); ?></td>
+            <td><?php echo Format::datetime($role->getUpdateDate()); ?></td>
+        </tr>
+    <?php }
+    ?>
+    </tbody>
+    <tfoot>
+     <tr>
+        <td colspan="5">
+            <?php if($count){ ?>
+            <?php echo __('Select'); ?>:&nbsp;
+            <a id="selectAll" href="#ckb"><?php echo __('All'); ?></a>&nbsp;&nbsp;
+            <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 sprintf(__('No roles defined yet &mdash; %s add one %s!'),
+                    '<a href="roles.php?a=add">','</a>');
+            } ?>
+        </td>
+     </tr>
+    </tfoot>
+</table>
+<?php
+if ($count) //Show options..
+    echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
+?>
+</form>
+
+<div style="display:none;" class="dialog" id="confirm-action">
+    <h3><?php echo __('Please Confirm'); ?></h3>
+    <a class="close" href=""><i class="icon-remove-circle"></i></a>
+    <hr/>
+    <p class="confirm-action" style="display:none;" id="enable-confirm">
+        <?php echo sprintf(__('Are you sure want to <b>enable</b> %s?'),
+            _N('selected role', 'selected roles', 2));?>
+    </p>
+    <p class="confirm-action" style="display:none;" id="disable-confirm">
+        <?php echo sprintf(__('Are you sure want to <b>disable</b> %s?'),
+            _N('selected role', 'selected roles', 2));?>
+    </p>
+    <p class="confirm-action" style="display:none;" id="delete-confirm">
+        <font color="red"><strong><?php echo sprintf(
+        __('Are you sure you want to DELETE %s?'),
+        _N('selected role', 'selected roles', 2)); ?></strong></font>
+        <br><br><?php echo __('Deleted roles CANNOT be recovered.'); ?>
+    </p>
+    <div><?php echo __('Please confirm to continue.'); ?></div>
+    <hr style="margin-top:1em"/>
+    <p class="full-width">
+        <span class="buttons pull-left">
+            <input type="button" value="<?php echo __('No, Cancel'); ?>" class="close">
+        </span>
+        <span class="buttons pull-right">
+            <input type="button" value="<?php echo __('Yes, Do it!'); ?>" class="confirm">
+        </span>
+    </p>
+    <div class="clear"></div>
+</div>
diff --git a/include/staff/settings-access.inc.php b/include/staff/settings-access.inc.php
deleted file mode 100644
index 156507f2e031f28c6bda2feeba09a705538542c9..0000000000000000000000000000000000000000
--- a/include/staff/settings-access.inc.php
+++ /dev/null
@@ -1,223 +0,0 @@
-<?php
-if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config) die('Access Denied');
-
-?>
-<h2><?php echo __('Access Control Settings'); ?></h2>
-<form action="settings.php?t=access" method="post" id="save">
-<?php csrf_token(); ?>
-<input type="hidden" name="t" value="access" >
-<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2">
-    <thead>
-        <tr>
-            <th colspan="2">
-                <h4><?php echo __('Configure Access to this Help Desk'); ?></h4>
-            </th>
-        </tr>
-    </thead>
-    <tbody>
-        <tr>
-            <th colspan="2">
-                <em><b><?php echo __('Agent Authentication Settings'); ?></b></em>
-            </th>
-        </tr>
-        <tr><td><?php echo __('Password Expiration Policy'); ?>:</th>
-            <td>
-                <select name="passwd_reset_period">
-                   <option value="0"> &mdash; <?php echo __('No expiration'); ?> &mdash;</option>
-                  <?php
-                    for ($i = 1; $i <= 12; $i++) {
-                        echo sprintf('<option value="%d" %s>%s</option>',
-                                $i,(($config['passwd_reset_period']==$i)?'selected="selected"':''),
-                                sprintf(_N('Monthly', 'Every %d months', $i), $i));
-                    }
-                    ?>
-                </select>
-                <font class="error"><?php echo $errors['passwd_reset_period']; ?></font>
-                <i class="help-tip icon-question-sign" href="#password_expiration_policy"></i>
-            </td>
-        </tr>
-        <tr><td><?php echo __('Allow Password Resets'); ?>:</th>
-            <td>
-              <input type="checkbox" name="allow_pw_reset" <?php echo $config['allow_pw_reset']?'checked="checked"':''; ?>>
-              &nbsp;<i class="help-tip icon-question-sign" href="#allow_password_resets"></i>
-            </td>
-        </tr>
-        <tr><td><?php echo __('Reset Token Expiration'); ?>:</th>
-            <td>
-              <input type="text" name="pw_reset_window" size="6" value="<?php
-                    echo $config['pw_reset_window']; ?>">
-                    <em><?php echo __('minutes'); ?></em>
-                    <i class="help-tip icon-question-sign" href="#reset_token_expiration"></i>
-                &nbsp;<font class="error"><?php echo $errors['pw_reset_window']; ?></font>
-            </td>
-        </tr>
-        <tr><td><?php echo __('Agent Excessive Logins'); ?>:</td>
-            <td>
-                <select name="staff_max_logins">
-                  <?php
-                    for ($i = 1; $i <= 10; $i++) {
-                        echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['staff_max_logins']==$i)?'selected="selected"':''), $i);
-                    }
-                    ?>
-                </select> <?php echo __(
-                'failed login attempt(s) allowed before a lock-out is enforced'); ?>
-                <br/>
-                <select name="staff_login_timeout">
-                  <?php
-                    for ($i = 1; $i <= 10; $i++) {
-                        echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['staff_login_timeout']==$i)?'selected="selected"':''), $i);
-                    }
-                    ?>
-                </select> <?php echo __('minutes locked out'); ?>
-            </td>
-        </tr>
-        <tr><td><?php echo __('Agent Session Timeout'); ?>:</td>
-            <td>
-              <input type="text" name="staff_session_timeout" size=6 value="<?php echo $config['staff_session_timeout']; ?>">
-                <?php echo __('minutes'); ?> <em><?php echo __('(0 to disable)'); ?></em>. <i class="help-tip icon-question-sign" href="#staff_session_timeout"></i>
-            </td>
-        </tr>
-        <tr><td><?php echo __('Bind Agent Session to IP'); ?>:</td>
-            <td>
-              <input type="checkbox" name="staff_ip_binding" <?php echo $config['staff_ip_binding']?'checked="checked"':''; ?>>
-              <i class="help-tip icon-question-sign" href="#bind_staff_session_to_ip"></i>
-            </td>
-        </tr>
-        <tr>
-            <th colspan="2">
-                <em><b><?php echo __('End User Authentication Settings'); ?></b></em>
-            </th>
-        </tr>
-        <tr><td><?php echo __('Registration Required'); ?>:</td>
-            <td><input type="checkbox" name="clients_only" <?php
-                if ($config['clients_only'])
-                    echo 'checked="checked"'; ?>/> <?php echo __(
-                    'Require registration and login to create tickets'); ?>
-            <i class="help-tip icon-question-sign" href="#registration_method"></i>
-            </td>
-        <tr><td><?php echo __('Registration Method'); ?>:</td>
-            <td><select name="client_registration">
-<?php foreach (array(
-    'disabled' => __('Disabled — All users are guests'),
-    'public' => __('Public — Anyone can register'),
-    'closed' => __('Private — Only agents can register users'),)
-    as $key=>$val) { ?>
-        <option value="<?php echo $key; ?>" <?php
-        if ($config['client_registration'] == $key)
-            echo 'selected="selected"'; ?>><?php echo $val;
-        ?></option><?php
-    } ?>
-            </select>
-            <i class="help-tip icon-question-sign" href="#registration_method"></i>
-            </td>
-        </tr>
-        <tr><td><?php echo __('User Excessive Logins'); ?>:</td>
-            <td>
-                <select name="client_max_logins">
-                  <?php
-                    for ($i = 1; $i <= 10; $i++) {
-                        echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['client_max_logins']==$i)?'selected="selected"':''), $i);
-                    }
-
-                    ?>
-                </select> <?php echo __(
-                'failed login attempt(s) allowed before a lock-out is enforced'); ?>
-                <br/>
-                <select name="client_login_timeout">
-                  <?php
-                    for ($i = 1; $i <= 10; $i++) {
-                        echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['client_login_timeout']==$i)?'selected="selected"':''), $i);
-                    }
-                    ?>
-                </select> <?php echo __('minutes locked out'); ?>
-            </td>
-        </tr>
-        <tr><td><?php echo __('User Session Timeout'); ?>:</td>
-            <td>
-              <input type="text" name="client_session_timeout" size=6 value="<?php echo $config['client_session_timeout']; ?>">
-              <i class="help-tip icon-question-sign" href="#client_session_timeout"></i>
-            </td>
-        </tr>
-        <tr><td><?php echo __('Client Quick Access'); ?>:</td>
-            <td><input type="checkbox" name="client_verify_email" <?php
-                if ($config['client_verify_email'])
-                    echo 'checked="checked"'; ?>/> <?php echo __(
-                'Require email verification on "Check Ticket Status" page'); ?>
-            <i class="help-tip icon-question-sign" href="#client_verify_email"></i>
-            </td>
-        </tr>
-    </tbody>
-    <thead>
-        <tr>
-            <th colspan="2">
-                <h4><?php echo __('Authentication and Registration Templates'); ?></h4>
-            </th>
-        </tr>
-    </thead>
-    <tbody>
-<?php
-$res = db_query('select distinct(`type`), content_id, notes, name, updated from '
-    .PAGE_TABLE
-    .' where isactive=1 group by `type`');
-$contents = array();
-while (list($type, $id, $notes, $name, $u) = db_fetch_row($res))
-    $contents[$type] = array($id, $name, $notes, $u);
-
-$manage_content = function($title, $content) use ($contents) {
-    list($id, $name, $notes, $upd) = $contents[$content];
-    $notes = explode('. ', $notes);
-    $notes = $notes[0];
-    ?><tr><td colspan="2">
-    <a href="#ajax.php/content/<?php echo $id; ?>/manage"
-    onclick="javascript:
-        $.dialog($(this).attr('href').substr(1), 201);
-    return false;" class="pull-left"><i class="icon-file-text icon-2x"
-        style="color:#bbb;"></i> </a>
-    <span style="display:inline-block;width:90%;padding-left:10px;line-height:1.2em">
-    <a href="#ajax.php/content/<?php echo $id; ?>/manage"
-    onclick="javascript:
-        $.dialog($(this).attr('href').substr(1), 201);
-    return false;"><?php
-    echo Format::htmlchars($title); ?></a><br/>
-    <span class="faded"><?php
-        echo Format::display($notes); ?>
-    <em>(<?php echo sprintf(__('Last Updated %s'), Format::db_datetime($upd));
-        ?>)</em></span></span></td></tr><?php
-}; ?>
-        <tr>
-            <th colspan="2">
-                <em><b><?php echo __(
-                'Authentication and Registration Templates'); ?></b></em>
-            </th>
-        </tr>
-        <?php $manage_content(__('Agents'), 'pwreset-staff'); ?>
-        <?php $manage_content(__('Clients'), 'pwreset-client'); ?>
-        <?php $manage_content(__('Guest Ticket Access'), 'access-link'); ?>
-        <tr>
-            <th colspan="2">
-                <em><b><?php echo __('Sign In Pages'); ?></b></em>
-            </th>
-        </tr>
-        <?php $manage_content(__('Agent Login Banner'), 'banner-staff'); ?>
-        <?php $manage_content(__('Client Sign-In Page'), 'banner-client'); ?>
-        <tr>
-            <th colspan="2">
-                <em><b><?php echo __('User Account Registration'); ?></b></em>
-            </th>
-        </tr>
-        <?php $manage_content(__('Please Confirm Email Address Page'), 'registration-confirm'); ?>
-        <?php $manage_content(__('Confirmation Email'), 'registration-client'); ?>
-        <?php $manage_content(__('Account Confirmed Page'), 'registration-thanks'); ?>
-        <tr>
-            <th colspan="2">
-                <em><b><?php echo __('Agent Account Registration'); ?></b></em>
-            </th>
-        </tr>
-        <?php $manage_content(__('Agent Welcome Email'), 'registration-staff'); ?>
-</tbody>
-</table>
-<p style="text-align:center">
-    <input class="button" type="submit" name="submit" value="<?php echo __('Save Changes'); ?>">
-    <input class="button" type="reset" name="reset" value="<?php echo __('Reset Changes'); ?>">
-</p>
-</form>
diff --git a/include/staff/settings-agents.inc.php b/include/staff/settings-agents.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..2a06676f0bc6d82355ebaa84a78ea42067933085
--- /dev/null
+++ b/include/staff/settings-agents.inc.php
@@ -0,0 +1,209 @@
+<?php
+if (!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config) die('Access Denied');
+
+?>
+<h2><?php echo __('Agents Settings'); ?></h2>
+<form action="settings.php?t=agents" method="post" id="save">
+    <?php csrf_token(); ?>
+    <input type="hidden" name="t" value="agents" >
+    <ul class="tabs" id="agents-tabs">
+        <li class="active"><a href="#settings">
+            <i class="icon-asterisk"></i> <?php echo __('Settings'); ?></a></li>
+        <li><a href="#templates">
+            <i class="icon-file-text"></i> <?php echo __('Templates'); ?></a></li>
+    </ul>
+    <div id="agents-tabs_container">
+        <div id="settings" class="tab_content">
+            <table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2">
+                <tbody>
+                    <tr>
+                        <th colspan="2">
+                            <em><b><?php echo __('General Settings'); ?></b></em>
+                        </th>
+                    </tr>
+                    <tr>
+                        <td width="180"><?php echo __('Name Formatting'); ?>:</td>
+                        <td>
+                            <select name="agent_name_format">
+                                <?php foreach (PersonsName::allFormats() as $n=>$f) {
+                                list($desc, $func) = $f;
+                                $selected = ($config['agent_name_format'] == $n) ? 'selected="selected"' : ''; ?>
+                                <option value="<?php echo $n; ?>" <?php echo $selected;
+                                ?>><?php echo __($desc); ?></option>
+                                <?php } ?>
+                            </select>
+                            <i class="help-tip icon-question-sign" href="#agent_name_format"></i>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td><?php echo __('Agent Identity Masking'); ?>:</td>
+                        <td>
+                            <input type="checkbox" name="hide_staff_name" <?php echo $config['hide_staff_name']?'checked="checked"':''; ?>>
+                            <?php echo __("Hide agent's name on responses."); ?>
+                            <i class="help-tip icon-question-sign" href="#staff_identity_masking"></i>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td width="180"><?php echo __('Avatar Source'); ?>:</td>
+                        <td>
+                            <select name="agent_avatar">
+<?php                       require_once INCLUDE_DIR . 'class.avatar.php';
+                            foreach (AvatarSource::allSources() as $id=>$class) {
+                                $modes = $class::getModes();
+                                if ($modes) {
+                                    echo "<optgroup label=\"{$class::getName()}\">";
+                                    foreach ($modes as $mid=>$mname) {
+                                        $oid = "$id.$mid";
+                                        $selected = ($config['agent_avatar'] == $oid) ? 'selected="selected"' : '';
+                                        echo "<option {$selected} value=\"{$oid}\">{$class::getName()} / {$mname}</option>";
+                                    }
+                                    echo "</optgroup>";
+                                }
+                                else {
+                                    $selected = ($config['agent_avatar'] == $id) ? 'selected="selected"' : '';
+                                    echo "<option {$selected} value=\"{$id}\">{$class::getName()}</option>";
+                                }
+                            } ?>
+                            </select>
+                            <div class="error"><?php echo Format::htmlchars($errors['agent_avatar']); ?></div>
+                        </td>
+                    </tr>
+                    <tr>
+                        <th colspan="2">
+                            <em><b><?php echo __('Authentication Settings'); ?></b></em>
+                        </th>
+                    </tr>
+                    <tr>
+                        <td><?php echo __('Password Expiration Policy'); ?>:</td>
+                        <td>
+                            <select name="passwd_reset_period">
+                            <option value="0"> &mdash; <?php echo __('No expiration'); ?> &mdash;</option>
+                            <?php
+                                for ($i = 1; $i <= 12; $i++) {
+                                echo sprintf('<option value="%d" %s>%s</option>',
+                                    $i,(($config['passwd_reset_period']==$i)?'selected="selected"':''),
+                                    sprintf(_N('Monthly', 'Every %d months', $i), $i));
+                                }
+                            ?>
+                            </select>
+                            <font class="error"><?php echo $errors['passwd_reset_period']; ?></font>
+                            <i class="help-tip icon-question-sign" href="#password_expiration_policy"></i>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td><?php echo __('Allow Password Resets'); ?>:</td>
+                        <td>
+                            <input type="checkbox" name="allow_pw_reset" <?php echo $config['allow_pw_reset']?'checked="checked"':''; ?>>
+                            &nbsp;<i class="help-tip icon-question-sign" href="#allow_password_resets"></i>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td><?php echo __('Reset Token Expiration'); ?>:</td>
+                        <td>
+                            <input type="text" name="pw_reset_window" size="6" value="<?php
+                                echo $config['pw_reset_window']; ?>">
+                                <em><?php echo __('minutes'); ?></em>
+                                <i class="help-tip icon-question-sign" href="#reset_token_expiration"></i>
+                                &nbsp;<font class="error"><?php echo $errors['pw_reset_window']; ?></font>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td><?php echo __('Agent Excessive Logins'); ?>:</td>
+                        <td>
+                            <select name="staff_max_logins">
+                                <?php
+                                    for ($i = 1; $i <= 10; $i++) {
+                                        echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['staff_max_logins']==$i)?'selected="selected"':''), $i);
+                                    }
+                                ?>
+                            </select> <?php echo __(
+                                'failed login attempt(s) allowed before a lock-out is enforced'); ?>
+                            <br/>
+                            <select name="staff_login_timeout">
+                                <?php
+                                    for ($i = 1; $i <= 10; $i++) {
+                                        echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['staff_login_timeout']==$i)?'selected="selected"':''), $i);
+                                    }
+                                ?>
+                            </select> <?php echo __('minutes locked out'); ?>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td><?php echo __('Agent Session Timeout'); ?>:</td>
+                        <td>
+                            <input type="text" name="staff_session_timeout" size=6 value="<?php echo $config['staff_session_timeout']; ?>">
+                            <?php echo __('minutes'); ?> <em><?php echo __('(0 to disable)'); ?></em>. <i class="help-tip icon-question-sign" href="#staff_session_timeout"></i>
+                        </td>
+                    </tr>
+                    <tr>
+                        <td><?php echo __('Bind Agent Session to IP'); ?>:</td>
+                        <td>
+                            <input type="checkbox" name="staff_ip_binding" <?php echo $config['staff_ip_binding']?'checked="checked"':''; ?>>
+                            <i class="help-tip icon-question-sign" href="#bind_staff_session_to_ip"></i>
+                        </td>
+                    </tr>
+                </tbody>
+            </table>
+        </div>
+        <div id="templates" class="tab_content hidden">
+            <table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2">
+                <tbody>
+                    <?php
+                        $res = db_query('select distinct(`type`), id, notes, name, updated from '
+                        .PAGE_TABLE
+                        .' where isactive=1 group by `type`');
+                        $contents = array();
+                        while (list($type, $id, $notes, $name, $u) = db_fetch_row($res))
+                        $contents[$type] = array($id, $name, $notes, $u);
+
+                        $manage_content = function($title, $content) use ($contents) {
+                        list($id, $name, $notes, $upd) = $contents[$content];
+                        $notes = explode('. ', $notes);
+                        $notes = $notes[0];
+                    ?>
+                    <tr>
+                        <td colspan="2">
+                            <div style="padding:2px 5px">
+                                <a href="#ajax.php/content/<?php echo $id; ?>/manage"
+                                   onclick="javascript:
+                                    $.dialog($(this).attr('href').substr(1), 201);
+                                    return false;" class="pull-left">
+                                    <i class="icon-file-text icon-2x" style="color:#bbb;"></i>
+                                </a>
+                                <span style="display:inline-block;width:90%;width:calc(100% - 32px);padding-left:10px;line-height:1.2em">
+                                <a href="#ajax.php/content/<?php echo $id; ?>/manage"
+                                   onclick="javascript:
+                                    $.dialog($(this).attr('href').substr(1), 201, null, {size:'large'});
+                                    return false;"><?php
+                                    echo Format::htmlchars($title); ?>
+                                    </a>
+                                </span>
+                                <span class="faded"><?php
+                                    echo Format::display($notes); ?>
+                                    <br />
+                                    <em><?php echo sprintf(__('Last Updated %s'), Format::datetime($upd));
+                                    ?></em>
+                                </span>
+                            </div>
+                        </td>
+                    </tr>
+                        <?php
+                        }; ?>
+                    <tr>
+                        <th colspan="2">
+                            <em><b><?php echo __(
+                            'Authentication and Registration Templates &amp; Pages'); ?></b></em>
+                        </th>
+                    </tr>
+                    <?php $manage_content(__('Agent Welcome Email'), 'registration-staff'); ?>
+                    <?php $manage_content(__('Sign-in Login Banner'), 'banner-staff'); ?>
+                    <?php $manage_content(__('Password Reset Email'), 'pwreset-staff'); ?>
+                </tbody>
+            </table>
+        </div>
+    <p style="text-align:center">
+        <input class="button" type="submit" name="submit" value="<?php echo __('Save Changes'); ?>">
+        <input class="button" type="reset" name="reset" value="<?php echo __('Reset Changes'); ?>">
+    </p>
+    </div>
+</form>
diff --git a/include/staff/settings-alerts.inc.php b/include/staff/settings-alerts.inc.php
index 3a9326bc4127343b57831a14a81b8d50f1b5c5bb..4b69e18035bbfa10afeb6d2b91e854fe85fcc2e5 100644
--- a/include/staff/settings-alerts.inc.php
+++ b/include/staff/settings-alerts.inc.php
@@ -1,16 +1,4 @@
-<h2><?php echo __('Alerts and Notices'); ?>
-    <i class="help-tip icon-question-sign" href="#page_title"></i></h2>
-<form action="settings.php?t=alerts" method="post" id="save">
-<?php csrf_token(); ?>
-<input type="hidden" name="t" value="alerts" >
 <table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2">
-    <thead>
-        <tr>
-            <th>
-                <h4><?php echo __('Alerts and Notices sent to agents on ticket "events"'); ?></h4>
-            </th>
-        </tr>
-    </thead>
     <tbody>
         <tr><th><em><b><?php echo __('New Ticket Alert'); ?></b>:
             <i class="help-tip icon-question-sign" href="#ticket_alert"></i>
@@ -135,7 +123,7 @@
         <tr>
             <td>
               <input type="checkbox" name="assigned_alert_staff" <?php echo
-              $config['assigned_alert_staff']?'checked':''; ?>> <?php echo __('Assigned Agent / Team'); ?>
+              $config['assigned_alert_staff']?'checked':''; ?>> <?php echo __('Assigned Agent'); ?>
             </td>
         </tr>
         <tr>
@@ -233,8 +221,3 @@
         </tr>
     </tbody>
 </table>
-<p style="text-align:center;">
-    <input class="button" type="submit" name="submit" value="<?php echo __('Save Changes'); ?>">
-    <input class="button" type="reset" name="reset" value="<?php echo __('Reset Changes'); ?>">
-</p>
-</form>
diff --git a/include/staff/settings-autoresp.inc.php b/include/staff/settings-autoresp.inc.php
index 0a59f4b9d215589b6f1cd2ad2540854dc4cb4c7c..24729350f032bd976f86a5c0f3ea0734d055c714 100644
--- a/include/staff/settings-autoresp.inc.php
+++ b/include/staff/settings-autoresp.inc.php
@@ -1,12 +1,7 @@
-<h2><?php echo __('Autoresponder Settings'); ?></h2>
-<form action="settings.php?t=autoresp" method="post" id="save">
-<?php csrf_token(); ?>
-<input type="hidden" name="t" value="autoresp" >
 <table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2">
     <thead>
         <tr>
-            <th colspan="2">
-                <h4><?php echo __('Autoresponder Setting'); ?></h4>
+            <th colspan="2">
                 <em><?php echo __('Global setting - can be disabled at department or email level.'); ?></em>
             </th>
         </tr>
@@ -60,8 +55,3 @@ echo $config['overlimit_notice_active'] ? 'checked="checked"' : ''; ?>/>
         </tr>
     </tbody>
 </table>
-<p style="padding-left:200px;">
-    <input class="button" type="submit" name="submit" value="<?php echo __('Save Changes'); ?>">
-    <input class="button" type="reset" name="reset" value="<?php echo __('Reset Changes'); ?>">
-</p>
-</form>
diff --git a/include/staff/settings-emails.inc.php b/include/staff/settings-emails.inc.php
index a20dfd6617c76bd24b7b32d03b3de8514da07f8c..d08b0bec549c19db92eedc134a21ce146f0cd095 100644
--- a/include/staff/settings-emails.inc.php
+++ b/include/staff/settings-emails.inc.php
@@ -2,14 +2,13 @@
 if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config) die('Access Denied');
 ?>
 <h2><?php echo __('Email Settings and Options');?></h2>
-<form action="settings.php?t=emails" method="post" id="save">
+<form action="emailsettings.php" method="post" id="save">
 <?php csrf_token(); ?>
 <input type="hidden" name="t" value="emails" >
 <table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2">
     <thead>
         <tr>
             <th colspan="2">
-                <h4><?php echo __('Email Settings');?></h4>
                 <em><?php echo __('Note that some of the global settings can be overwridden at department/email level.');?></em>
             </th>
         </tr>
@@ -172,7 +171,7 @@ if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config)
         </tr>
     </tbody>
 </table>
-<p style="padding-left:250px;">
+<p style="text-align:center;">
     <input class="button" type="submit" name="submit" value="<?php echo __('Save Changes');?>">
     <input class="button" type="reset" name="reset" value="<?php echo __('Reset Changes');?>">
 </p>
diff --git a/include/staff/settings-kb.inc.php b/include/staff/settings-kb.inc.php
index 941b08e30fed4e3ca89c6023ce472ae4a452288f..2f7fa6762f54a60089e27ffc0c1f5d6d9d08b49d 100644
--- a/include/staff/settings-kb.inc.php
+++ b/include/staff/settings-kb.inc.php
@@ -9,7 +9,6 @@ if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config)
     <thead>
         <tr>
             <th colspan="2">
-                <h4><?php echo __('Knowledge Base Settings');?></h4>
                 <em><?php echo __("Disabling knowledge base disables clients' interface.");?></em>
             </th>
         </tr>
@@ -39,7 +38,7 @@ if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config)
         </tr>
     </tbody>
 </table>
-<p style="padding-left:210px;">
+<p style="text-align:center;">
     <input class="button" type="submit" name="submit" value="<?php echo __('Save Changes'); ?>">
     <input class="button" type="reset" name="reset" value="<?php echo __('Reset Changes'); ?>">
 </p>
diff --git a/include/staff/settings-pages.inc.php b/include/staff/settings-pages.inc.php
index 4d9910b4e2c9e04cd5d41fc40ddde1961f0e1abf..f3702000368d23ea8cc8ef02201641aaa8029326 100644
--- a/include/staff/settings-pages.inc.php
+++ b/include/staff/settings-pages.inc.php
@@ -6,13 +6,24 @@ $pages = Page::getPages();
 <form action="settings.php?t=pages" method="post" id="save"
     enctype="multipart/form-data">
 <?php csrf_token(); ?>
+
+
+
 <input type="hidden" name="t" value="pages" >
+
+<ul class="clean tabs">
+    <li class="active"><a href="#basic-information"><i class="icon-asterisk"></i>
+        <?php echo __('Basic Information'); ?></a></li>
+    <li><a href="#site-pages"><i class="icon-file"></i>
+        <?php echo __('Site Pages'); ?></a></li>
+    <li><a href="#logos"><i class="icon-picture"></i>
+        <?php echo __('Logos'); ?></a></li>
+    <li><a href="#backdrops"><i class="icon-picture"></i>
+        <?php echo __('Login Backdrop'); ?></a></li>
+</ul>
+
+<div class="tab_content" id="basic-information">
 <table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2">
-    <thead><tr>
-        <th colspan="2">
-            <h4><?php echo __('Basic Information'); ?></h4>
-        </th>
-    </tr></thead>
     <tbody>
     <?php
         $form = $ost->company->getForm();
@@ -20,10 +31,13 @@ $pages = Page::getPages();
         $form->render();
     ?>
     </tbody>
+</table>
+</div>
+<div class="hidden tab_content" id="site-pages">
+<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2">
     <thead>
         <tr>
             <th colspan="2">
-                <h4><?php echo __('Site Pages'); ?></h4>
                 <em><?php echo sprintf(__(
                 'To edit or add new pages go to %s Manage &gt; Site Pages %s'),
                 '<a href="pages.php">','</a>'); ?></em>
@@ -93,96 +107,184 @@ $pages = Page::getPages();
         </tr>
     </tbody>
 </table>
+</div>
+<div class="hidden tab_content" id="logos">
 <table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2">
     <thead>
         <tr>
             <th colspan="2">
-                <h4><?php echo __('Logos'); ?>
-                    <i class="help-tip icon-question-sign" href="#logos"></i>
-                    </h4>
-                <em><?php echo __('System Default Logo'); ?></em>
+                <em><?php echo __('System Default Logo'); ?><i class="help-tip icon-question-sign" href="#logos"></i></em>
             </th>
         </tr>
     </thead>
     <tbody>
         <tr>
-        <td colspan="2">
-<table style="width:100%">
-    <thead><tr>
-        <th>Client</th>
-        <th>Staff</th>
-        <th>Logo</th>
-    </tr></thead>
-    <tbody>
-        <tr>
-            <td>
-                <input type="radio" name="selected-logo" value="0"
-                    style="margin-left: 1em"
-                    <?php if (!$ost->getConfig()->getClientLogoId())
-                        echo 'checked="checked"'; ?>/>
-            </td><td>
-                <input type="radio" name="selected-logo-scp" value="0"
-                    style="margin-left: 1em"
-                    <?php if (!$ost->getConfig()->getStaffLogoId())
-                        echo 'checked="checked"'; ?>/>
-            </td><td>
-                <img src="<?php echo ROOT_PATH; ?>assets/default/images/logo.png"
-                    alt="Default Logo" valign="middle"
-                    style="box-shadow: 0 0 0.5em rgba(0,0,0,0.5);
-                        margin: 0.5em; height: 5em;
-                        vertical-align: middle"/>
-                <img src="<?php echo ROOT_PATH; ?>scp/images/ost-logo.png"
-                    alt="Default Logo" valign="middle"
-                    style="box-shadow: 0 0 0.5em rgba(0,0,0,0.5);
-                        margin: 0.5em; height: 5em;
-                        vertical-align: middle"/>
+            <td colspan="2">
+                <table style="width:100%">
+                    <thead>
+                        <tr>
+                            <th>Client</th>
+                            <th>Staff</th>
+                            <th>Logo</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr>
+                            <td>
+                                <input type="radio" name="selected-logo" value="0"
+                                       style="margin-left: 1em"
+                                       <?php if (!$ost->getConfig()->getClientLogoId())
+                                        echo 'checked="checked"'; ?>/>
+                            </td>
+                            <td>
+                                <input type="radio" name="selected-logo-scp" value="0"
+                                       style="margin-left: 1em"
+                                       <?php if (!$ost->getConfig()->getStaffLogoId())
+                                            echo 'checked="checked"'; ?>/>
+                            </td>
+                            <td>
+                                <img src="<?php echo ROOT_PATH; ?>assets/default/images/logo.png"
+                                     alt="Default Logo" valign="middle"
+                                     style="box-shadow: 0 0 0.5em rgba(0,0,0,0.5);
+                                            margin: 0.5em; height: 5em;
+                                            vertical-align: middle"/>
+                                <img src="<?php echo ROOT_PATH; ?>scp/images/ost-logo.png"
+                                     alt="Default Logo" valign="middle"
+                                     style="box-shadow: 0 0 0.5em rgba(0,0,0,0.5);
+                                            margin: 0.5em; height: 5em;
+                                            vertical-align: middle"/>
+                            </td>
+                        </tr>
+                        <tr>
+                            <th colspan="3">
+                                <em><?php echo __('Use a custom logo'); ?>&nbsp;<i class="help-tip icon-question-sign" href="#upload_a_new_logo"></i></em>
+                            </th>
+                        </tr>
+                        <?php
+                        $current = $ost->getConfig()->getClientLogoId();
+                        $currentScp = $ost->getConfig()->getStaffLogoId();
+                        foreach (AttachmentFile::allLogos() as $logo) { ?>
+                        <tr>
+                            <td>
+                                <input type="radio" name="selected-logo"
+                                       style="margin-left: 1em" value="<?php
+                            echo $logo->getId(); ?>" <?php
+                            if ($logo->getId() == $current)
+                                echo 'checked="checked"'; ?>/>
+                            </td>
+                            <td>
+                                <input type="radio" name="selected-logo-scp"
+                                       style="margin-left: 1em" value="<?php
+                            echo $logo->getId(); ?>" <?php
+                            if ($logo->getId() == $currentScp)
+                                echo 'checked="checked"'; ?>/>
+                            </td>
+                            <td>
+                                <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;
+                                            vertical-align: middle;"/>
+                                <?php if ($logo->getId() != $current && $logo->getId() != $currentScp) { ?>
+                                <label class="checkbox inline">
+                                    <input type="checkbox" name="delete-logo[]" value="<?php
+                                    echo $logo->getId(); ?>"/> <?php echo __('Delete'); ?>
+                                </label>
+                                <?php } ?>
+                            </td>
+                        </tr>
+                        <?php } ?>
+                    </tbody>
+                </table>
+                <b><?php echo __('Upload a new logo'); ?>:</b>
+                <input type="file" name="logo[]" size="30" value="" />
+                <font class="error"><br/><?php echo $errors['logo']; ?></font>
             </td>
         </tr>
-        <tr><th colspan="3">
-            <em><?php echo __('Use a custom logo'); ?>&nbsp;<i class="help-tip icon-question-sign" href="#upload_a_new_logo"></i></em>
-        </th></tr>
-    <?php
-    $current = $ost->getConfig()->getClientLogoId();
-    $currentScp = $ost->getConfig()->getStaffLogoId();
-    foreach (AttachmentFile::allLogos() as $logo) { ?>
-        <tr>
-            <td>
-                <input type="radio" name="selected-logo"
-                    style="margin-left: 1em" value="<?php
-                    echo $logo->getId(); ?>" <?php
-                    if ($logo->getId() == $current)
-                        echo 'checked="checked"'; ?>/>
-            </td><td>
-                <input type="radio" name="selected-logo-scp"
-                    style="margin-left: 1em" value="<?php
-                    echo $logo->getId(); ?>" <?php
-                    if ($logo->getId() == $currentScp)
-                        echo 'checked="checked"'; ?>/>
-            </td><td>
-                <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;
-                        vertical-align: middle;"/>
-                <?php if ($logo->getId() != $current && $logo->getId() != $currentScp) { ?>
-                <label>
-                <input type="checkbox" name="delete-logo[]" value="<?php
-                    echo $logo->getId(); ?>"/> <?php echo __('Delete'); ?>
-                </label>
-                <?php } ?>
-            </td>
-        </tr>
-<?php } ?>
     </tbody>
 </table>
-            <b><?php echo __('Upload a new logo'); ?>:</b>
-            <input type="file" name="logo[]" size="30" value="" />
-            <font class="error"><br/><?php echo $errors['logo']; ?></font>
-        </td>
+</div>
+
+<div class="hidden tab_content" id="backdrops">
+<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2">
+    <thead>
+        <tr>
+            <th colspan="2">
+                <em><?php echo __('System Default Backdrop'); ?><i
+                class="help-tip icon-question-sign" href="#backdrops"></i></em>
+            </th>
+        </tr>
+    </thead>
+    <tbody>
+        <tr>
+            <td colspan="2">
+                <table style="width:100%">
+                    <thead>
+                        <tr>
+                            <th>Staff</th>
+                            <th>Backdrop</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr>
+                            <td>
+                                <input type="radio" name="selected-backdrop" value="0"
+                                       style="margin-left: 1em"
+                                       <?php if (!$ost->getConfig()->getStaffLogoId())
+                                            echo 'checked="checked"'; ?>/>
+                            </td>
+                            <td>
+                                <img src="<?php echo ROOT_PATH; ?>scp/images/login-headquarters.jpg"
+                                     alt="Default Backdrop" valign="middle"
+                                     style="box-shadow: 0 0 0.5em rgba(0,0,0,0.5);
+                                            margin: 0.5em; height: 6em;
+                                            vertical-align: middle"/>
+                            </td>
+                        </tr>
+                        <tr>
+                            <th colspan="2">
+                                <em><?php echo __('Use a custom backdrop');
+                                ?>&nbsp;<i class="help-tip icon-question-sign" href="#upload_a_new_backdrop"></i></em>
+                            </th>
+                        </tr>
+                        <?php
+                        $current = $ost->getConfig()->getStaffLoginBackdropId();
+                        foreach (AttachmentFile::allBackdrops() as $logo) { ?>
+                        <tr>
+                            <td>
+                                <input type="radio" name="selected-backdrop"
+                                       style="margin-left: 1em" value="<?php
+                            echo $logo->getId(); ?>" <?php
+                            if ($logo->getId() == $current)
+                                echo 'checked="checked"'; ?>/>
+                            </td>
+                            <td>
+                                <img src="<?php echo $logo->getDownloadUrl(); ?>"
+                                     alt="Custom Backdrop" valign="middle"
+                                     style="box-shadow: 0 0 0.5em rgba(0,0,0,0.5);
+                                            margin: 0.5em; height: 6em;
+                                            vertical-align: middle;"/>
+                                <?php if ($logo->getId() != $current) { ?>
+                                <label class="checkbox inline">
+                                    <input type="checkbox" name="delete-backdrop[]" value="<?php
+                                    echo $logo->getId(); ?>"/> <?php echo __('Delete'); ?>
+                                </label>
+                                <?php } ?>
+                            </td>
+                        </tr>
+                        <?php } ?>
+                    </tbody>
+                </table>
+                <b><?php echo __('Upload a new backdrop'); ?>:</b>
+                <input type="file" name="backdrop[]" size="30" value="" />
+                <font class="error"><br/><?php echo $errors['backdrop']; ?></font>
+            </td>
         </tr>
     </tbody>
 </table>
-<p style="padding-left:250px;">
+</div>
+
+<p style="text-align:center;">
     <input class="button" type="submit" name="submit-button" value="<?php
     echo __('Save Changes'); ?>">
     <input class="button" type="reset" name="reset" value="<?php
@@ -197,7 +299,7 @@ $pages = Page::getPages();
     <p class="confirm-action" id="delete-confirm">
         <font color="red"><strong><?php echo sprintf(
         __('Are you sure you want to DELETE %s?'),
-        _N('selected logo', 'selected logos', 2)); ?></strong></font>
+        _N('selected image', 'selected images', 2)); ?></strong></font>
         <br/><br/><?php echo __('Deleted data CANNOT be recovered.'); ?>
     </p>
     <div><?php echo __('Please confirm to continue.'); ?></div>
diff --git a/include/staff/settings-system.inc.php b/include/staff/settings-system.inc.php
index 7b609ce98033de2f0239fdcf204730e902281881..6560e7c2074266aa4d69bb47c421cc9594759f42 100644
--- a/include/staff/settings-system.inc.php
+++ b/include/staff/settings-system.inc.php
@@ -3,7 +3,7 @@ if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config)
 
 $gmtime = Misc::gmtime();
 ?>
-<h2><?php echo __('System Settings and Preferences');?> - <span class="ltr">osTicket (<?php echo $cfg->getVersion(); ?>)</span></h2>
+<h2><?php echo __('System Settings and Preferences');?> <small>— <span class="ltr">osTicket (<?php echo $cfg->getVersion(); ?>)</span></small></h2>
 <form action="settings.php?t=system" method="post" id="save">
 <?php csrf_token(); ?>
 <input type="hidden" name="t" value="system" >
@@ -11,7 +11,6 @@ $gmtime = Misc::gmtime();
     <thead>
         <tr>
             <th colspan="2">
-                <h4><?php echo __('System Settings and Preferences'); ?></h4>
                 <em><b><?php echo __('General Settings'); ?></b></em>
             </th>
         </tr>
@@ -47,22 +46,29 @@ $gmtime = Misc::gmtime();
         <tr>
             <td width="220" class="required"><?php echo __('Default Department');?>:</td>
             <td>
-                <select name="default_dept_id">
+                <select name="default_dept_id" data-quick-add="department">
                     <option value="">&mdash; <?php echo __('Select Default Department');?> &mdash;</option>
                     <?php
-                    $sql='SELECT dept_id,dept_name FROM '.DEPT_TABLE.' WHERE ispublic=1';
-                    if(($res=db_query($sql)) && db_num_rows($res)){
-                        while (list($id, $name) = db_fetch_row($res)){
+                    if (($depts=Dept::getPublicDepartments())) {
+                        foreach ($depts as $id => $name) {
                             $selected = ($config['default_dept_id']==$id)?'selected="selected"':''; ?>
-                            <option value="<?php echo $id; ?>"<?php echo $selected; ?>><?php echo $name; ?> <?php echo __('Dept');?></option>
+                            <option value="<?php echo $id; ?>"<?php echo $selected; ?>><?php echo $name; ?></option>
                         <?php
                         }
                     } ?>
+                    <option value="0" data-quick-add>&mdash; <?php echo __('Add New');?> &mdash;</option>
                 </select>&nbsp;<font class="error">*&nbsp;<?php echo $errors['default_dept_id']; ?></font>
                 <i class="help-tip icon-question-sign" href="#default_department"></i>
             </td>
         </tr>
-
+        <tr>
+            <td><?php echo __('Collision Avoidance Duration'); ?>:</td>
+            <td>
+                <input type="text" name="autolock_minutes" size=4 value="<?php echo $config['autolock_minutes']; ?>">
+                <font class="error"><?php echo $errors['autolock_minutes']; ?></font>&nbsp;<?php echo __('minutes'); ?>
+                &nbsp;<i class="help-tip icon-question-sign" href="#collision_avoidance"></i>
+            </td>
+        </tr>
         <tr><td><?php echo __('Default Page Size');?>:</td>
             <td>
                 <select name="max_page_size">
@@ -108,17 +114,21 @@ $gmtime = Misc::gmtime();
             </td>
         </tr>
         <tr>
-            <td width="180"><?php echo __('Default Name Formatting'); ?>:</td>
+            <td><?php echo __('Show Avatars'); ?>:</td>
             <td>
-                <select name="name_format">
-<?php foreach (PersonsName::allFormats() as $n=>$f) {
-    list($desc, $func) = $f;
-    $selected = ($config['name_format'] == $n) ? 'selected="selected"' : ''; ?>
-                    <option value="<?php echo $n; ?>" <?php echo $selected;
-                        ?>><?php echo __($desc); ?></option>
-<?php } ?>
-                </select>
-                <i class="help-tip icon-question-sign" href="#default_name_formatting"></i>
+                <input type="checkbox" name="enable_avatars" <?php
+                echo $config['enable_avatars'] ? 'checked="checked"' : ''; ?>>
+                <?php echo __('Show Avatars on thread view.'); ?>
+                <i class="help-tip icon-question-sign" href="#enable_avatars"></i>
+            </td>
+        </tr>
+        <tr>
+            <td><?php echo __('Enable Rich Text'); ?>:</td>
+            <td>
+                <input type="checkbox" name="enable_richtext" <?php
+                echo $config['enable_richtext'] ? 'checked="checked"' : ''; ?>>
+                <?php echo __('Enable html in thread entries and email correspondence.'); ?>
+                <i class="help-tip icon-question-sign" href="#enable_richtext"></i>
             </td>
         </tr>
         <tr>
@@ -128,57 +138,250 @@ $gmtime = Misc::gmtime();
                 </em>
             </th>
         </tr>
-        <tr><td width="220" class="required"><?php echo __('Time Format');?>:</td>
+<?php if (extension_loaded('intl')) { ?>
+        <tr><td width="220" class="required"><?php echo __('Default Locale');?>:</td>
+            <td>
+                <select name="default_locale">
+                    <option value=""><?php echo __('Use Language Preference'); ?></option>
+                    <?php
+                    foreach (Internationalization::allLocales() as $code=>$name) { ?>
+                    <option value="<?php echo $code; ?>" <?php
+                        if ($code == $config['default_locale'])
+                            echo 'selected="selected"';
+                    ?>><?php echo $name; ?></option>
+
+                    <?php
+                    } ?>
+                </select>
+            </td>
+        </tr>
+<?php } ?>
+        <tr><td width="220" class="required"><?php echo __('Default Time Zone');?>:</td>
+            <td>
+                <?php
+                $TZ_TIMEZONE = $config['default_timezone'];
+                $TZ_NAME = 'default_timezone';
+                $TZ_ALLOW_DEFAULT = false;
+                include STAFFINC_DIR.'templates/timezone.tmpl.php'; ?>
+                <div class="error"><?php echo $errors['default_timezone']; ?></div>
+            </td>
+        </tr>
+        <tr><td width="220" class="required"><?php echo __('Date and Time Format');?>:</td>
+            <td>
+                <select name="date_formats" onchange="javascript:
+    $('#advanced-time').toggle($(this).find(':selected').val() == 'custom');
+">
+<?php foreach (array(
+    '' => __('Locale Defaults'),
+    '24' => __('Locale Defaults, 24-hour Time'),
+    'custom' => '— '.__("Advanced").' —',
+) as $v=>$name) { ?>
+                    <option value="<?php echo $v; ?>" <?php
+                    if ($v == $config['date_formats'])
+                        echo 'selected="selected"';
+                    ?>><?php echo $name; ?></option>
+<?php } ?>
+                </select>
+            </td>
+        </tr>
+
+    </tbody>
+    <tbody id="advanced-time" <?php if ($config['date_formats'] != 'custom')
+        echo 'style="display:none;"'; ?>>
+        <tr>
+            <td width="220" class="indented required"><?php echo __('Time Format');?>:</td>
             <td>
-                <input type="text" name="time_format" value="<?php echo $config['time_format']; ?>">
+                <input type="text" name="time_format" value="<?php echo $config['time_format']; ?>" class="date-format-preview">
                     &nbsp;<font class="error">*&nbsp;<?php echo $errors['time_format']; ?></font>
-                    <em><?php echo Format::date($config['time_format'], $gmtime, $config['tz_offset'], $config['enable_daylight_saving']); ?></em></td>
+                    <em><?php echo Format::time(null, false); ?></em>
+                <span class="faded date-format-preview" data-for="time_format">
+                    <?php echo Format::time('now'); ?>
+                </span>
+            </td>
         </tr>
-        <tr><td width="220" class="required"><?php echo __('Date Format');?>:</td>
-            <td><input type="text" name="date_format" value="<?php echo $config['date_format']; ?>">
+        <tr><td width="220" class="indented required"><?php echo __('Date Format');?>:</td>
+            <td><input type="text" name="date_format" value="<?php echo $config['date_format']; ?>" class="date-format-preview">
                         &nbsp;<font class="error">*&nbsp;<?php echo $errors['date_format']; ?></font>
-                        <em><?php echo Format::date($config['date_format'], $gmtime, $config['tz_offset'], $config['enable_daylight_saving']); ?></em>
+                        <em><?php echo Format::date(null, false); ?></em>
+                <span class="faded date-format-preview" data-for="date_format">
+                    <?php echo Format::date('now'); ?>
+                </span>
             </td>
         </tr>
-        <tr><td width="220" class="required"><?php echo __('Date and Time Format');?>:</td>
-            <td><input type="text" name="datetime_format" value="<?php echo $config['datetime_format']; ?>">
+        <tr><td width="220" class="indented required"><?php echo __('Date and Time Format');?>:</td>
+            <td><input type="text" name="datetime_format" value="<?php echo $config['datetime_format']; ?>" class="date-format-preview">
                         &nbsp;<font class="error">*&nbsp;<?php echo $errors['datetime_format']; ?></font>
-                        <em><?php echo Format::date($config['datetime_format'], $gmtime, $config['tz_offset'], $config['enable_daylight_saving']); ?></em>
+                        <em><?php echo Format::datetime(null, false); ?></em>
+                <span class="faded date-format-preview" data-for="datetime_format">
+                    <?php echo Format::datetime('now'); ?>
+                </span>
             </td>
         </tr>
-        <tr><td width="220" class="required"><?php echo __('Day, Date and Time Format');?>:</td>
-            <td><input type="text" name="daydatetime_format" value="<?php echo $config['daydatetime_format']; ?>">
+        <tr><td width="220" class="indented required"><?php echo __('Day, Date and Time Format');?>:</td>
+            <td><input type="text" name="daydatetime_format" value="<?php echo $config['daydatetime_format']; ?>" class="date-format-preview">
                         &nbsp;<font class="error">*&nbsp;<?php echo $errors['daydatetime_format']; ?></font>
-                        <em><?php echo Format::date($config['daydatetime_format'], $gmtime, $config['tz_offset'], $config['enable_daylight_saving']); ?></em>
+                        <em><?php echo Format::daydatetime(null, false); ?></em>
+                <span class="faded date-format-preview" data-for="daydatetime_format">
+                    <?php echo Format::daydatetime('now'); ?>
+                </span>
             </td>
         </tr>
-        <tr><td width="220" class="required"><?php echo __('Default Time Zone');?>:</td>
+    </tbody>
+    <tbody>
+        <tr>
+            <th colspan="2">
+                <em><b><?php echo __('System Languages'); ?></b>&nbsp;
+                <i class="help-tip icon-question-sign" href="#languages"></i>
+                </em>
+            </th>
+        </tr>
+        <tr><td><?php echo __('Primary Language'); ?>:</td>
             <td>
-                <select name="default_timezone_id">
-                    <option value="">&mdash; <?php echo __('Select Default Time Zone');?> &mdash;</option>
-                    <?php
-                    $sql='SELECT id, offset,timezone FROM '.TIMEZONE_TABLE.' ORDER BY id';
-                    if(($res=db_query($sql)) && db_num_rows($res)){
-                        while(list($id, $offset, $tz)=db_fetch_row($res)){
-                            $sel=($config['default_timezone_id']==$id)?'selected="selected"':'';
-                            echo sprintf('<option value="%d" %s>GMT %s - %s</option>', $id, $sel, $offset, $tz);
-                        }
+        <?php
+        $langs = Internationalization::availableLanguages(); ?>
+                <select name="system_language">
+                    <option value="">&mdash; <?php echo __('Select a Language'); ?> &mdash;</option>
+<?php foreach($langs as $l) {
+    $selected = ($config['system_language'] == $l['code']) ? 'selected="selected"' : ''; ?>
+                    <option value="<?php echo $l['code']; ?>" <?php echo $selected;
+                        ?>><?php echo Internationalization::getLanguageDescription($l['code']); ?></option>
+<?php } ?>
+                </select>
+                <span class="error">&nbsp;<?php echo $errors['system_language']; ?></span>
+                <i class="help-tip icon-question-sign" href="#primary_language"></i>
+            </td>
+        </tr>
+        <tr>
+            <td style="vertical-align:top;padding-top:4px;"><?php echo __('Secondary Languages'); ?>:</td>
+            <td><div id="secondary_langs" style="width: 300px"><?php
+            foreach ($cfg->getSecondaryLanguages() as $lang) {
+                $info = Internationalization::getLanguageInfo($lang); ?>
+            <div class="secondary_lang" style="cursor:move">
+            <i class="icon-sort"></i>&nbsp;
+            <span class="flag flag-<?php echo $info['flag']; ?>"></span>&nbsp;
+            <?php echo Internationalization::getLanguageDescription($lang); ?>
+            <input type="hidden" name="secondary_langs[]" value="<?php echo $lang; ?>"/>
+            <div class="pull-right">
+            <a href="#<?php echo $lang; ?>" onclick="javascript:
+                if (confirm('<?php echo __('You sure?'); ?>')) {
+                    $(this).closest('.secondary_lang')
+                        .find('input').remove();
+                    $(this).closest('.secondary_lang').slideUp();
+                }
+                return false;
+                "><i class="icon-trash"></i></a>
+            </div>
+            </div>
+<?php   } ?>
+            <script type="text/javascript">
+            </script>
+            </div>
+            <i class="icon-plus-sign"></i>&nbsp;
+            <select name="add_secondary_language">
+                <option value="">&mdash; <?php echo __('Add a Language'); ?> &mdash;</option>
+<?php foreach($langs as $l) {
+    $selected = ($config['add_secondary_language'] == $l['code']) ? 'selected="selected"' : '';
+    if (!$selected && $l['code'] == $cfg->getPrimaryLanguage())
+        continue;
+    if (!$selected && in_array($l['code'], $cfg->getSecondaryLanguages()))
+        continue; ?>
+                <option value="<?php echo $l['code']; ?>" <?php echo $selected;
+                    ?>><?php echo Internationalization::getLanguageDescription($l['code']); ?></option>
+<?php } ?>
+            </select>
+            <span class="error">&nbsp;<?php echo $errors['add_secondary_language']; ?></span>
+            <i class="help-tip icon-question-sign" href="#secondary_language"></i>
+        </td></tr>
+        <tr>
+            <th colspan="2">
+                <em><b><?php echo __('Attachments Storage and Settings');?></b>:<i
+                class="help-tip icon-question-sign" href="#attachments"></i></em>
+            </th>
+        </tr>
+        <tr>
+            <td width="180"><?php echo __('Store Attachments'); ?>:</td>
+            <td><select name="default_storage_bk"><?php
+                if (($bks = FileStorageBackend::allRegistered())) {
+                    foreach ($bks as $char=>$class) {
+                        $selected = $config['default_storage_bk'] == $char
+                            ? 'selected="selected"' : '';
+                        ?><option <?php echo $selected; ?> value="<?php echo $char; ?>"
+                        ><?php echo $class::$desc; ?></option><?php
                     }
-                    ?>
+                } else {
+                 echo sprintf('<option value="">%s</option>',
+                         __('Select Storage Backend'));
+                }?>
                 </select>
-                &nbsp;<font class="error">*&nbsp;<?php echo $errors['default_timezone_id']; ?></font>
+                &nbsp;<font class="error">*&nbsp;<?php echo
+                $errors['default_storage_bk']; ?></font>
+                <i class="help-tip icon-question-sign"
+                href="#default_storage_bk"></i>
             </td>
         </tr>
         <tr>
-            <td width="220"><?php echo __('Daylight Saving');?>:</td>
+            <td width="180"><?php echo __(
+                // Maximum size for agent-uploaded files (via SCP)
+                'Agent Maximum File Size');?>:</td>
             <td>
-                <input type="checkbox" name="enable_daylight_saving" <?php echo $config['enable_daylight_saving'] ? 'checked="checked"': ''; ?>><?php echo __('Observe daylight savings');?>
+                <select name="max_file_size">
+                    <option value="262144">&mdash; <?php echo __('Small'); ?> &mdash;</option>
+                    <?php $next = 512 << 10;
+                    $max = strtoupper(ini_get('upload_max_filesize'));
+                    $limit = (int) $max;
+                    if (!$limit) $limit = 2 << 20; # 2M default value
+                    elseif (strpos($max, 'K')) $limit <<= 10;
+                    elseif (strpos($max, 'M')) $limit <<= 20;
+                    elseif (strpos($max, 'G')) $limit <<= 30;
+                    while ($next <= $limit) {
+                        // Select the closest, larger value (in case the
+                        // current value is between two)
+                        $diff = $next - $config['max_file_size'];
+                        $selected = ($diff >= 0 && $diff < $next / 2)
+                            ? 'selected="selected"' : ''; ?>
+                        <option value="<?php echo $next; ?>" <?php echo $selected;
+                             ?>><?php echo Format::file_size($next);
+                             ?></option><?php
+                        $next *= 2;
+                    }
+                    // Add extra option if top-limit in php.ini doesn't fall
+                    // at a power of two
+                    if ($next < $limit * 2) {
+                        $selected = ($limit == $config['max_file_size'])
+                            ? 'selected="selected"' : ''; ?>
+                        <option value="<?php echo $limit; ?>" <?php echo $selected;
+                             ?>><?php echo Format::file_size($limit);
+                             ?></option><?php
+                    }
+                    ?>
+                </select>
+                <i class="help-tip icon-question-sign" href="#max_file_size"></i>
+                <div class="error"><?php echo $errors['max_file_size']; ?></div>
             </td>
         </tr>
     </tbody>
 </table>
-<p style="padding-left:250px;">
+<p style="text-align:center;">
     <input class="button" type="submit" name="submit" value="<?php echo __('Save Changes');?>">
     <input class="button" type="reset" name="reset" value="<?php echo __('Reset Changes');?>">
 </p>
 </form>
+<script type="text/javascript">
+$(function() {
+    $('#secondary_langs').sortable({
+        cursor: 'move'
+    });
+    var prev = [];
+    $('input.date-format-preview').keyup(function() {
+        var name = $(this).attr('name'),
+            div = $('span.date-format-preview[data-for='+name+']'),
+            current = $(this).val();
+        if (prev[name] && prev[name] == current)
+            return;
+        prev[name] = current;
+        div.text('...');
+        $.get('ajax.php/config/date-format', {format:$(this).val()})
+            .done(function(html) { div.html(html); });
+    });
+});
+</script>
diff --git a/include/staff/settings-tasks.inc.php b/include/staff/settings-tasks.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..dff1b8d79bfd05738712e2569660bc18e885db82
--- /dev/null
+++ b/include/staff/settings-tasks.inc.php
@@ -0,0 +1,308 @@
+<?php
+if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config) die('Access Denied');
+if(!($maxfileuploads=ini_get('max_file_uploads')))
+    $maxfileuploads=DEFAULT_MAX_FILE_UPLOADS;
+?>
+<h2><?php echo __('Tasks Settings and Options');?></h2>
+<form action="settings.php?t=tasks" method="post" id="save">
+<?php csrf_token(); ?>
+<input type="hidden" name="t" value="tasks" >
+
+<ul class="tabs" id="tasks-tabs">
+    <li class="active"><a href="#settings">
+        <i class="icon-asterisk"></i> <?php echo __('Settings'); ?></a></li>
+    <li><a href="#alerts">
+        <i class="icon-bell-alt"></i> <?php echo __('Alerts &amp; Notices'); ?></a></li>
+</ul>
+<div id="tasks-tabs_container">
+   <div id="settings" class="tab_content">
+    <table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2">
+        <thead>
+            <tr>
+                <th colspan="2">
+                    <em><?php echo __('Global default task settings and options.'); ?></em>
+                </th>
+            </tr>
+        </thead>
+        <tbody>
+            <tr>
+                <td>
+                    <?php echo __('Default Task Number Format'); ?>:
+                </td>
+                <td>
+                    <input type="text" name="task_number_format" value="<?php
+                    echo $config['task_number_format']; ?>"/>
+                    <span class="faded"><?php echo __('e.g.'); ?> <span id="format-example"><?php
+                        if ($config['task_sequence_id'])
+                            $seq = Sequence::lookup($config['task_sequence_id']);
+                        if (!isset($seq))
+                            $seq = new RandomSequence();
+                        echo $seq->current($config['task_number_format']);
+                        ?></span></span>
+                    <i class="help-tip icon-question-sign" href="#number_format"></i>
+                    <div class="error"><?php echo $errors['task_number_format']; ?></div>
+                </td>
+            </tr>
+            <tr><td width="220"><?php echo __('Default Task Number Sequence'); ?>:</td>
+    <?php $selected = 'selected="selected"'; ?>
+                <td>
+                    <select name="task_sequence_id">
+                    <option value="0" <?php if ($config['task_sequence_id'] == 0) echo $selected;
+                        ?>>&mdash; <?php echo __('Random'); ?> &mdash;</option>
+    <?php foreach (Sequence::objects() as $s) { ?>
+                    <option value="<?php echo $s->id; ?>" <?php
+                        if ($config['task_sequence_id'] == $s->id) echo $selected;
+                        ?>><?php echo $s->name; ?></option>
+    <?php } ?>
+                    </select>
+                    <button class="action-button pull-right" onclick="javascript:
+                    $.dialog('ajax.php/sequence/manage', 205);
+                    return false;
+                    "><i class="icon-gear"></i> <?php echo __('Manage'); ?></button>
+                    <i class="help-tip icon-question-sign" href="#sequence_id"></i>
+                </td>
+            </tr>
+            <tr>
+                <td width="180" class="required"><?php echo __('Default Priority');?>:</td>
+                <td>
+                    <select name="default_task_priority_id">
+                        <?php
+                        $priorities= db_query('SELECT priority_id,priority_desc FROM '.TICKET_PRIORITY_TABLE);
+                        while (list($id,$tag) = db_fetch_row($priorities)){ ?>
+                            <option value="<?php echo $id; ?>"<?php echo
+                                ($config['default_task_priority_id']==$id)?'selected':''; ?>><?php echo $tag; ?></option>
+                        <?php
+                        } ?>
+                    </select>
+                    &nbsp;<span class="error">*&nbsp;<?php echo
+                    $errors['default_task_priority_id']; ?></span> <i class="help-tip icon-question-sign" href="#default_priority"></i>
+                 </td>
+            </tr>
+            <tr>
+                <th colspan="2">
+                    <em><b><?php echo __('Attachments');?></b>:</em>
+                </th>
+            </tr>
+            <tr>
+                <td width="180"><?php echo __('Task Attachment Settings');?>:</td>
+                <td>
+    <?php
+                    $tform = TaskForm::objects()->one()->getForm();
+                    $f = $tform->getField('description');
+    ?>
+                    <a class="action-button field-config" style="overflow:inherit"
+                        href="#ajax.php/form/field-config/<?php
+                            echo $f->get('id'); ?>"
+                        onclick="javascript:
+                            $.dialog($(this).attr('href').substr(1), [201]);
+                            return false;
+                        "><i class="icon-edit"></i> <?php echo __('Config'); ?></a>
+                    <i class="help-tip icon-question-sign" href="#task_attachment_settings"></i>
+                </td>
+            </tr>
+        </tbody>
+    </table>
+   </div>
+   <div id="alerts" class="tab_content" style="display:none;">
+    <table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2">
+        <tbody>
+            <tr><th><em><b><?php echo __('New Task Alert'); ?></b>:
+                <i class="help-tip icon-question-sign" href="#task_alert"></i>
+                </em></th></tr>
+            <tr>
+                <td><em><b><?php echo __('Status'); ?>:</b></em> &nbsp;
+                    <input type="radio" name="task_alert_active"  value="1"
+                    <?php echo $config['task_alert_active'] ? 'checked="checked"' : ''; ?>
+                    /> <?php echo __('Enable'); ?>
+                    <input type="radio" name="task_alert_active"  value="0"
+                    <?php echo !$config['task_alert_active'] ? 'checked="checked"' : ''; ?> />
+                    <?php echo __('Disable'); ?>
+                    &nbsp;&nbsp;<font class="error">&nbsp;<?php echo $errors['task_alert_active']; ?></font></em>
+                 </td>
+            </tr>
+            <tr>
+                <td>
+                    <input type="checkbox" name="task_alert_admin" <?php
+                        echo $config['task_alert_admin'] ? 'checked="checked"' : ''; ?>>
+                    <?php echo __('Admin Email'); ?> <em>(<?php echo $cfg->getAdminEmail(); ?>)</em>
+                </td>
+            </tr>
+            <tr>
+                <td>
+                    <input type="checkbox" name="task_alert_dept_manager"
+                    <?php echo $config['task_alert_dept_manager'] ? 'checked="checked"' : ''; ?>>
+                    <?php echo __('Department Manager'); ?>
+                </td>
+            </tr>
+            <tr>
+                <td>
+                    <input type="checkbox" name="task_alert_dept_members"
+                    <?php echo $config['task_alert_dept_members'] ? 'checked="checked"' : ''; ?>>
+                    <?php echo __('Department Members'); ?>
+                </td>
+            </tr>
+            <tr><th><em><b><?php echo __('New Activity Alert'); ?></b>:
+                <i class="help-tip icon-question-sign" href="#activity_alert"></i>
+                </em></th></tr>
+            <tr>
+                <td><em><b><?php echo __('Status'); ?>:</b></em> &nbsp;
+                  <input type="radio" name="task_activity_alert_active" value="1"
+                  <?php echo $config['task_activity_alert_active'] ? 'checked="checked"' : ''; ?> />
+                    <?php echo __('Enable'); ?>
+                  &nbsp;&nbsp;
+                  <input type="radio" name="task_activity_alert_active"  value="0"
+                  <?php echo !$config['task_activity_alert_active'] ? 'checked="checked"' : ''; ?> />
+                    <?php echo __('Disable'); ?>
+                  &nbsp;&nbsp;&nbsp;<font class="error">&nbsp;<?php echo $errors['task_activity_alert_active']; ?></font>
+                </td>
+            </tr>
+            <tr>
+                <td>
+                  <input type="checkbox" name="task_activity_alert_laststaff" <?php
+                  echo $config['task_activity_alert_laststaff'] ? 'checked="checked"' : ''; ?>>
+                  <?php echo __('Last Respondent'); ?>
+                </td>
+            </tr>
+            <tr>
+                <td>
+                  <input type="checkbox" name="task_activity_alert_assigned"
+                  <?php echo $config['task_activity_alert_assigned'] ? 'checked="checked"' : ''; ?>>
+                  <?php echo __('Assigned Agent / Team'); ?>
+                </td>
+            </tr>
+            <tr>
+                <td>
+                  <input type="checkbox" name="task_activity_alert_dept_manager"
+                  <?php echo $config['task_activity_alert_dept_manager'] ? 'checked="checked"' : ''; ?>>
+                    <?php echo __('Department Manager'); ?>
+                </td>
+            </tr>
+            <tr><th><em><b><?php echo __('Task Assignment Alert'); ?></b>:
+                <i class="help-tip icon-question-sign" href="#assignment_alert"></i>
+                </em></th></tr>
+            <tr>
+                <td><em><b><?php echo __('Status'); ?>: </b></em> &nbsp;
+                  <input name="task_assignment_alert_active" value="1" type="radio"
+                    <?php echo $config['task_assignment_alert_active'] ? 'checked="checked"' : ''; ?>>
+                    <?php echo __('Enable'); ?>
+                    &nbsp;&nbsp;
+                  <input name="task_assignment_alert_active" value="0" type="radio"
+                    <?php echo !$config['task_assignment_alert_active'] ? 'checked="checked"' : ''; ?>>
+                    <?php echo __('Disable'); ?>
+                   &nbsp;&nbsp;&nbsp;<font class="error">&nbsp;<?php echo $errors['task_assignment_alert_active']; ?></font>
+                </td>
+            </tr>
+            <tr>
+                <td>
+                  <input type="checkbox" name="task_assignment_alert_staff" <?php echo
+                  $config['task_assignment_alert_staff'] ? 'checked="checked"' : ''; ?>>
+                  <?php echo __('Assigned Agent / Team'); ?>
+                </td>
+            </tr>
+            <tr>
+                <td>
+                  <input type="checkbox"name="task_assignment_alert_team_lead" <?php
+                  echo $config['task_assignment_alert_team_lead'] ? 'checked="checked"' : ''; ?>>
+                  <?php echo __('Team Lead'); ?>
+                </td>
+            </tr>
+            <tr>
+                <td>
+                  <input type="checkbox"name="task_assignment_alert_team_members"
+                  <?php echo $config['task_assignment_alert_team_members'] ? 'checked="checked"' : ''; ?>>
+                    <?php echo __('Team Members'); ?>
+                </td>
+            </tr>
+            <tr><th><em><b><?php echo __('Task Transfer Alert'); ?></b>:
+                <i class="help-tip icon-question-sign" href="#transfer_alert"></i>
+                </em></th></tr>
+            <tr>
+                <td><em><b><?php echo __('Status'); ?>:</b></em> &nbsp;
+                <input type="radio" name="task_transfer_alert_active"  value="1"
+                <?php echo $config['task_transfer_alert_active'] ? 'checked="checked"' : ''; ?> />
+                    <?php echo __('Enable'); ?>
+                <input type="radio" name="task_transfer_alert_active"  value="0"
+                <?php echo !$config['task_transfer_alert_active'] ? 'checked="checked"' : ''; ?> />
+                    <?php echo __('Disable'); ?>
+                  &nbsp;&nbsp;&nbsp;<font class="error">&nbsp;<?php
+                  echo $errors['task_transfer_alert_active']; ?></font>
+                </td>
+            </tr>
+            <tr>
+                <td>
+                  <input type="checkbox" name="task_transfer_alert_assigned"
+                  <?php echo $config['task_transfer_alert_assigned']?'checked="checked"':''; ?>>
+                    <?php echo __('Assigned Agent / Team'); ?>
+                </td>
+            </tr>
+            <tr>
+                <td>
+                  <input type="checkbox" name="task_transfer_alert_dept_manager"
+                  <?php echo $config['task_transfer_alert_dept_manager'] ? 'checked="checked"' : ''; ?>>
+                    <?php echo __('Department Manager'); ?>
+                </td>
+            </tr>
+            <tr>
+                <td>
+                  <input type="checkbox" name="task_transfer_alert_dept_members"
+                  <?php echo $config['task_transfer_alert_dept_members'] ? 'checked="checked"' : ''; ?>>
+                    <?php echo __('Department Members'); ?>
+                </td>
+            </tr>
+            <tr><th><em><b><?php echo __('Overdue Task Alert'); ?></b>:
+                <i class="help-tip icon-question-sign" href="#overdue_alert"></i>
+                </em></th></tr>
+            <tr>
+                <td><em><b><?php echo __('Status'); ?>:</b></em> &nbsp;
+                  <input type="radio" name="task_overdue_alert_active"  value="1"
+                    <?php echo $config['task_overdue_alert_active'] ? 'checked="checked"' : ''; ?> /> <?php echo __('Enable'); ?>
+                  <input type="radio" name="task_overdue_alert_active"  value="0"
+                    <?php echo !$config['task_overdue_alert_active'] ? 'checked="checked"' : ''; ?> /> <?php echo __('Disable'); ?>
+                  &nbsp;&nbsp;<font class="error">&nbsp;<?php echo $errors['task_overdue_alert_active']; ?></font>
+                </td>
+            </tr>
+            <tr>
+                <td>
+                  <input type="checkbox" name="task_overdue_alert_assigned" <?php
+                    echo $config['task_overdue_alert_assigned'] ? 'checked="checked"' : ''; ?>>
+                    <?php echo __('Assigned Agent / Team'); ?>
+                </td>
+            </tr>
+            <tr>
+                <td>
+                  <input type="checkbox" name="task_overdue_alert_dept_manager" <?php
+                    echo $config['task_overdue_alert_dept_manager'] ? 'checked="checked"' : ''; ?>>
+                    <?php echo __('Department Manager'); ?>
+                </td>
+            </tr>
+            <tr>
+                <td>
+                  <input type="checkbox" name="task_overdue_alert_dept_members" <?php
+                    echo $config['task_overdue_alert_dept_members'] ? 'checked="checked"' : ''; ?>>
+                    <?php echo __('Department Members'); ?>
+                </td>
+            </tr>
+        </tbody>
+    </table>
+   </div>
+</div>
+<p style="text-align:center;">
+    <input class="button" type="submit" name="submit" value="<?php echo __('Save Changes');?>">
+    <input class="button" type="reset" name="reset" value="<?php echo __('Reset Changes');?>">
+</p>
+</form>
+<script type="text/javascript">
+$(function() {
+    var request = null,
+      update_example = function() {
+      request && request.abort();
+      request = $.get('ajax.php/sequence/'
+        + $('[name=task_sequence_id] :selected').val(),
+        {'format': $('[name=task_number_format]').val()},
+        function(data) { $('#format-example').text(data); }
+      );
+    };
+    $('[name=task_sequence_id]').on('change', update_example);
+    $('[name=task_number_format]').on('keyup', update_example);
+});
+</script>
diff --git a/include/staff/settings-tickets.inc.php b/include/staff/settings-tickets.inc.php
index 945daa56eeaf9491669a3e7221c4d335542f6e96..59570b4423a44d7f95f435177b84044b2b7ef4f3 100644
--- a/include/staff/settings-tickets.inc.php
+++ b/include/staff/settings-tickets.inc.php
@@ -7,11 +7,20 @@ if(!($maxfileuploads=ini_get('max_file_uploads')))
 <form action="settings.php?t=tickets" method="post" id="save">
 <?php csrf_token(); ?>
 <input type="hidden" name="t" value="tickets" >
+
+<ul class="clean tabs">
+    <li class="active"><a href="#settings"><i class="icon-asterisk"></i>
+        <?php echo __('Settings'); ?></a></li>
+    <li><a href="#autoresp"><i class="icon-mail-reply-all"></i>
+        <?php echo __('Autoresponder'); ?></a></li>
+    <li><a href="#alerts"><i class="icon-bell-alt"></i>
+        <?php echo __('Alerts and Notices'); ?></a></li>
+</ul>
+<div class="tab_content" id="settings">
 <table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2">
     <thead>
         <tr>
             <th colspan="2">
-                <h4><?php echo __('Global Ticket Settings');?></h4>
                 <em><?php echo __('System-wide default ticket settings and options.'); ?></em>
             </th>
         </tr>
@@ -22,27 +31,28 @@ if(!($maxfileuploads=ini_get('max_file_uploads')))
                 <?php echo __('Default Ticket Number Format'); ?>:
             </td>
             <td>
-                <input type="text" name="number_format" value="<?php echo $config['number_format']; ?>"/>
+                <input type="text" name="ticket_number_format" value="<?php
+                echo $config['ticket_number_format']; ?>"/>
                 <span class="faded"><?php echo __('e.g.'); ?> <span id="format-example"><?php
-                    if ($config['sequence_id'])
-                        $seq = Sequence::lookup($config['sequence_id']);
+                    if ($config['ticket_sequence_id'])
+                        $seq = Sequence::lookup($config['ticket_sequence_id']);
                     if (!isset($seq))
                         $seq = new RandomSequence();
-                    echo $seq->current($config['number_format']);
+                    echo $seq->current($config['ticket_number_format']);
                     ?></span></span>
                 <i class="help-tip icon-question-sign" href="#number_format"></i>
-                <div class="error"><?php echo $errors['number_format']; ?></div>
+                <div class="error"><?php echo $errors['ticket_number_format']; ?></div>
             </td>
         </tr>
         <tr><td width="220"><?php echo __('Default Ticket Number Sequence'); ?>:</td>
 <?php $selected = 'selected="selected"'; ?>
             <td>
-                <select name="sequence_id">
-                <option value="0" <?php if ($config['sequence_id'] == 0) echo $selected;
+                <select name="ticket_sequence_id">
+                <option value="0" <?php if ($config['ticket_sequence_id'] == 0) echo $selected;
                     ?>>&mdash; <?php echo __('Random'); ?> &mdash;</option>
 <?php foreach (Sequence::objects() as $s) { ?>
                 <option value="<?php echo $s->id; ?>" <?php
-                    if ($config['sequence_id'] == $s->id) echo $selected;
+                    if ($config['ticket_sequence_id'] == $s->id) echo $selected;
                     ?>><?php echo $s->name; ?></option>
 <?php } ?>
                 </select>
@@ -136,17 +146,29 @@ if(!($maxfileuploads=ini_get('max_file_uploads')))
             </td>
         </tr>
         <tr>
-            <td><?php echo __('Maximum <b>Open</b> Tickets');?>:</td>
+            <td width="180"><?php echo __('Lock Semantics'); ?>:</td>
             <td>
-                <input type="text" name="max_open_tickets" size=4 value="<?php echo $config['max_open_tickets']; ?>">
-                <?php echo __('per end user'); ?> <i class="help-tip icon-question-sign" href="#maximum_open_tickets"></i>
+                <select name="ticket_lock" <?php if ($cfg->getLockTime() == 0) echo 'disabled="disabled"'; ?>>
+<?php foreach (array(
+    Lock::MODE_DISABLED => __('Disabled'),
+    Lock::MODE_ON_VIEW => __('Lock on view'),
+    Lock::MODE_ON_ACTIVITY => __('Lock on activity'),
+) as $v => $desc) { ?>
+                <option value="<?php echo $v; ?>" <?php
+                    if ($config['ticket_lock'] == $v) echo 'selected="selected"';
+                    ?>><?php echo $desc; ?></option>
+<?php } ?>
+                </select>
+                <div class="error"><?php echo $errors['ticket_lock']; ?></div>
             </td>
         </tr>
         <tr>
-            <td><?php echo __('Agent Collision Avoidance Duration'); ?>:</td>
+            <td><?php echo __('Maximum <b>Open</b> Tickets');?>:</td>
             <td>
-                <input type="text" name="autolock_minutes" size=4 value="<?php echo $config['autolock_minutes']; ?>">
-                <font class="error"><?php echo $errors['autolock_minutes']; ?></font>&nbsp;<?php echo __('minutes'); ?>&nbsp;<i class="help-tip icon-question-sign" href="#agent_collision_avoidance"></i>
+                <input type="text" name="max_open_tickets" size=4 value="<?php echo $config['max_open_tickets']; ?>">
+                <?php echo __('per end user'); ?>
+                <span class="error">*&nbsp;<?php echo $errors['max_open_tickets']; ?></span>
+                <i class="help-tip icon-question-sign" href="#maximum_open_tickets"></i>
             </td>
         </tr>
         <tr>
@@ -183,38 +205,13 @@ if(!($maxfileuploads=ini_get('max_file_uploads')))
                 <i class="help-tip icon-question-sign" href="#answered_tickets"></i>
             </td>
         </tr>
-        <tr>
-            <td><?php echo __('Agent Identity Masking'); ?>:</td>
-            <td>
-                <input type="checkbox" name="hide_staff_name" <?php echo $config['hide_staff_name']?'checked="checked"':''; ?>>
-                <?php echo __("Hide agent's name on responses."); ?>
-                <i class="help-tip icon-question-sign" href="#staff_identity_masking"></i>
-            </td>
-        </tr>
-        <tr>
-            <td><?php echo __('Enable HTML Ticket Thread'); ?>:</td>
-            <td>
-                <input type="checkbox" name="enable_html_thread" <?php
-                echo $config['enable_html_thread']?'checked="checked"':''; ?>>
-                <?php echo __('Enable rich text in ticket thread and autoresponse emails.'); ?>
-                <i class="help-tip icon-question-sign" href="#enable_html_ticket_thread"></i>
-            </td>
-        </tr>
-        <tr>
-            <td><?php echo __('Allow Client Updates'); ?>:</td>
-            <td>
-                <input type="checkbox" name="allow_client_updates" <?php
-                echo $config['allow_client_updates']?'checked="checked"':''; ?>>
-                <?php echo __('Allow clients to update ticket details via the web portal'); ?>
-            </td>
-        </tr>
         <tr>
             <th colspan="2">
                 <em><b><?php echo __('Attachments');?></b>:  <?php echo __('Size and maximum uploads setting mainly apply to web tickets.');?></em>
             </th>
         </tr>
         <tr>
-            <td width="180"><?php echo __('EndUser Attachment Settings');?>:</td>
+            <td width="180"><?php echo __('Ticket Attachment Settings');?>:</td>
             <td>
 <?php
                 $tform = TicketForm::objects()->one()->getForm();
@@ -230,63 +227,19 @@ if(!($maxfileuploads=ini_get('max_file_uploads')))
                 <i class="help-tip icon-question-sign" href="#ticket_attachment_settings"></i>
             </td>
         </tr>
-        <tr>
-            <td width="180"><?php echo __(
-                // Maximum size for agent-uploaded files (via SCP)
-                'Agent Maximum File Size');?>:</td>
-            <td>
-                <select name="max_file_size">
-                    <option value="262144">&mdash; <?php echo __('Small'); ?> &mdash;</option>
-                    <?php $next = 512 << 10;
-                    $max = strtoupper(ini_get('upload_max_filesize'));
-                    $limit = (int) $max;
-                    if (!$limit) $limit = 2 << 20; # 2M default value
-                    elseif (strpos($max, 'K')) $limit <<= 10;
-                    elseif (strpos($max, 'M')) $limit <<= 20;
-                    elseif (strpos($max, 'G')) $limit <<= 30;
-                    while ($next <= $limit) {
-                        // Select the closest, larger value (in case the
-                        // current value is between two)
-                        $diff = $next - $config['max_file_size'];
-                        $selected = ($diff >= 0 && $diff < $next / 2)
-                            ? 'selected="selected"' : ''; ?>
-                        <option value="<?php echo $next; ?>" <?php echo $selected;
-                             ?>><?php echo Format::file_size($next);
-                             ?></option><?php
-                        $next *= 2;
-                    }
-                    // Add extra option if top-limit in php.ini doesn't fall
-                    // at a power of two
-                    if ($next < $limit * 2) {
-                        $selected = ($limit == $config['max_file_size'])
-                            ? 'selected="selected"' : ''; ?>
-                        <option value="<?php echo $limit; ?>" <?php echo $selected;
-                             ?>><?php echo Format::file_size($limit);
-                             ?></option><?php
-                    }
-                    ?>
-                </select>
-                <i class="help-tip icon-question-sign" href="#max_file_size"></i>
-                <div class="error"><?php echo $errors['max_file_size']; ?></div>
-            </td>
-        </tr>
-        <?php if (($bks = FileStorageBackend::allRegistered())
-                && count($bks) > 1) { ?>
-        <tr>
-            <td width="180"><?php echo __('Store Attachments'); ?>:</td>
-            <td><select name="default_storage_bk"><?php
-                foreach ($bks as $char=>$class) {
-                    $selected = $config['default_storage_bk'] == $char
-                        ? 'selected="selected"' : '';
-                    ?><option <?php echo $selected; ?> value="<?php echo $char; ?>"
-                    ><?php echo $class::$desc; ?></option><?php
-                } ?>
-            </td>
-        </tr>
-        <?php } ?>
     </tbody>
 </table>
-<p style="padding-left:250px;">
+</div>
+<div class="hidden tab_content" id="autoresp"
+    data-tip-namespace="settings.autoresponder">
+    <?php include STAFFINC_DIR . 'settings-autoresp.inc.php'; ?>
+</div>
+<div class="hidden tab_content" id="alerts"
+    data-tip-namespace="settings.alerts">
+    <?php include STAFFINC_DIR . 'settings-alerts.inc.php'; ?>
+</div>
+
+<p style="text-align:center;">
     <input class="button" type="submit" name="submit" value="<?php echo __('Save Changes');?>">
     <input class="button" type="reset" name="reset" value="<?php echo __('Reset Changes');?>">
 </p>
@@ -297,12 +250,12 @@ $(function() {
       update_example = function() {
       request && request.abort();
       request = $.get('ajax.php/sequence/'
-        + $('[name=sequence_id] :selected').val(),
-        {'format': $('[name=number_format]').val()},
+        + $('[name=ticket_sequence_id] :selected').val(),
+        {'format': $('[name=ticket_number_format]').val()},
         function(data) { $('#format-example').text(data); }
       );
     };
-    $('[name=sequence_id]').on('change', update_example);
-    $('[name=number_format]').on('keyup', update_example);
+    $('[name=ticket_sequence_id]').on('change', update_example);
+    $('[name=ticket_number_format]').on('keyup', update_example);
 });
 </script>
diff --git a/include/staff/settings-users.inc.php b/include/staff/settings-users.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..4096ccbd921cc483729dc0534dad5be2787190a8
--- /dev/null
+++ b/include/staff/settings-users.inc.php
@@ -0,0 +1,192 @@
+<?php
+if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin() || !$config) die('Access Denied');
+
+?>
+<h2><?php echo __('Users Settings'); ?></h2>
+<form action="settings.php?t=users" method="post" id="save">
+<?php csrf_token(); ?>
+<input type="hidden" name="t" value="users" >
+<ul class="tabs" id="users-tabs">
+    <li class="active"><a href="#settings">
+        <i class="icon-asterisk"></i> <?php echo __('Settings'); ?></a></li>
+    <li><a href="#templates">
+        <i class="icon-file-text"></i> <?php echo __('Templates'); ?></a></li>
+</ul>
+<div id="users-tabs_container">
+   <div id="settings" class="tab_content">
+<table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2">
+    <tbody>
+
+        <tr>
+            <th colspan="2">
+                <em><b><?php echo __('General Settings'); ?></b></em>
+            </th>
+        </tr>
+        <tr>
+            <td width="180"><?php echo __('Name Formatting'); ?>:</td>
+            <td>
+                <select name="client_name_format">
+                <?php foreach (PersonsName::allFormats() as $n=>$f) {
+                    list($desc, $func) = $f;
+                    $selected = ($config['client_name_format'] == $n) ? 'selected="selected"' : ''; ?>
+                                    <option value="<?php echo $n; ?>" <?php echo $selected;
+                                        ?>><?php echo __($desc); ?></option>
+                <?php } ?>
+                </select>
+                <i class="help-tip icon-question-sign" href="#client_name_format"></i>
+            </td>
+        </tr>
+        <tr>
+            <td width="180"><?php echo __('Avatar Source'); ?>:</td>
+            <td>
+                <select name="client_avatar">
+<?php           require_once INCLUDE_DIR . 'class.avatar.php';
+                foreach (AvatarSource::allSources() as $id=>$class) {
+                    $modes = $class::getModes();
+                    if ($modes) {
+                        echo "<optgroup label=\"{$class::getName()}\">";
+                        foreach ($modes as $mid=>$mname) {
+                            $oid = "$id.$mid";
+                            $selected = ($config['client_avatar'] == $oid) ? 'selected="selected"' : '';
+                            echo "<option {$selected} value=\"{$oid}\">{$class::getName()} / {$mname}</option>";
+                        }
+                        echo "</optgroup>";
+                    }
+                    else {
+                        $selected = ($config['client_avatar'] == $id) ? 'selected="selected"' : '';
+                        echo "<option {$selected} value=\"{$id}\">{$class::getName()}</option>";
+                    }
+                } ?>
+                </select>
+                <div class="error"><?php echo Format::htmlchars($errors['client_avatar']); ?></div>
+            </td>
+        </tr>
+        <tr>
+            <th colspan="2">
+                <em><b><?php echo __('Authentication Settings'); ?></b></em>
+            </th>
+        </tr>
+        <tr><td><?php echo __('Registration Required'); ?>:</td>
+            <td><input type="checkbox" name="clients_only" <?php
+                if ($config['clients_only'])
+                    echo 'checked="checked"'; ?>/> <?php echo __(
+                    'Require registration and login to create tickets'); ?>
+            <i class="help-tip icon-question-sign" href="#registration_method"></i>
+            </td>
+        <tr><td><?php echo __('Registration Method'); ?>:</td>
+            <td><select name="client_registration">
+<?php foreach (array(
+    'disabled' => __('Disabled — All users are guests'),
+    'public' => __('Public — Anyone can register'),
+    'closed' => __('Private — Only agents can register users'),)
+    as $key=>$val) { ?>
+        <option value="<?php echo $key; ?>" <?php
+        if ($config['client_registration'] == $key)
+            echo 'selected="selected"'; ?>><?php echo $val;
+        ?></option><?php
+    } ?>
+            </select>
+            <i class="help-tip icon-question-sign" href="#registration_method"></i>
+            </td>
+        </tr>
+        <tr><td><?php echo __('User Excessive Logins'); ?>:</td>
+            <td>
+                <select name="client_max_logins">
+                  <?php
+                    for ($i = 1; $i <= 10; $i++) {
+                        echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['client_max_logins']==$i)?'selected="selected"':''), $i);
+                    }
+
+                    ?>
+                </select> <?php echo __(
+                'failed login attempt(s) allowed before a lock-out is enforced'); ?>
+                <br/>
+                <select name="client_login_timeout">
+                  <?php
+                    for ($i = 1; $i <= 10; $i++) {
+                        echo sprintf('<option value="%d" %s>%d</option>', $i,(($config['client_login_timeout']==$i)?'selected="selected"':''), $i);
+                    }
+                    ?>
+                </select> <?php echo __('minutes locked out'); ?>
+            </td>
+        </tr>
+        <tr><td><?php echo __('User Session Timeout'); ?>:</td>
+            <td>
+              <input type="text" name="client_session_timeout" size=6 value="<?php echo $config['client_session_timeout']; ?>">
+              <i class="help-tip icon-question-sign" href="#client_session_timeout"></i>
+            </td>
+        </tr>
+        <tr><td><?php echo __('Authentication Token'); ?>:</td>
+            <td><input type="checkbox" name="allow_auth_tokens" <?php
+                if ($config['allow_auth_tokens'])
+                    echo 'checked="checked"'; ?>/> <?php
+                    echo __('Enable use of authentication tokens to auto-login users'); ?>
+            <i class="help-tip icon-question-sign" href="#allow_auth_tokens"></i>
+            </td>
+        </tr>
+        <tr><td><?php echo __('Client Quick Access'); ?>:</td>
+            <td><input type="checkbox" name="client_verify_email" <?php
+                if ($config['client_verify_email'])
+                    echo 'checked="checked"'; ?>/> <?php echo __(
+                'Require email verification on "Check Ticket Status" page'); ?>
+            <i class="help-tip icon-question-sign" href="#client_verify_email"></i>
+            </td>
+        </tr>
+    </tbody>
+    </table>
+   </div>
+   <div id="templates" class="tab_content hidden">
+    <table class="form_table settings_table" width="940" border="0" cellspacing="0" cellpadding="2">
+    <tbody>
+<?php
+$res = db_query('select distinct(`type`), id, notes, name, updated from '
+    .PAGE_TABLE
+    .' where isactive=1 group by `type`');
+$contents = array();
+while (list($type, $id, $notes, $name, $u) = db_fetch_row($res))
+    $contents[$type] = array($id, $name, $notes, $u);
+
+$manage_content = function($title, $content) use ($contents) {
+    list($id, $name, $notes, $upd) = $contents[$content];
+    $notes = explode('. ', $notes);
+    $notes = $notes[0];
+    ?><tr><td colspan="2">
+    <div style="padding:2px 5px">
+    <a href="#ajax.php/content/<?php echo $id; ?>/manage"
+    onclick="javascript:
+        $.dialog($(this).attr('href').substr(1), 201);
+    return false;" class="pull-left"><i class="icon-file-text icon-2x"
+        style="color:#bbb;"></i> </a>
+    <span style="display:inline-block;width:90%;width:calc(100% - 32px);padding-left:10px;line-height:1.2em">
+    <a href="#ajax.php/content/<?php echo $id; ?>/manage"
+    onclick="javascript:
+        $.dialog($(this).attr('href').substr(1), 201, null, {size:'large'});
+    return false;"><?php
+    echo Format::htmlchars($title); ?></a><br/>
+        <span class="faded"><?php
+        echo Format::display($notes); ?>
+        <br><em><?php echo sprintf(__('Last Updated %s'), Format::datetime($upd));
+        ?></em></span>
+    </div></td></tr><?php
+}; ?>
+        <tr>
+            <th colspan="2">
+                <em><b><?php echo __(
+                'Authentication and Registration Templates &amp; Pages'); ?></b></em>
+            </th>
+        </tr>
+        <?php $manage_content(__('Guest Ticket Access'), 'access-link'); ?>
+        <?php $manage_content(__('Sign-In Page'), 'banner-client'); ?>
+        <?php $manage_content(__('Password Reset Email'), 'pwreset-client'); ?>
+        <?php $manage_content(__('Please Confirm Email Address Page'), 'registration-confirm'); ?>
+        <?php $manage_content(__('Account Confirmation Email'), 'registration-client'); ?>
+        <?php $manage_content(__('Account Confirmed Page'), 'registration-thanks'); ?>
+</tbody>
+</table>
+</div>
+<p style="text-align:center">
+    <input class="button" type="submit" name="submit" value="<?php echo __('Save Changes'); ?>">
+    <input class="button" type="reset" name="reset" value="<?php echo __('Reset Changes'); ?>">
+</p>
+</div>
+</form>
diff --git a/include/staff/slaplan.inc.php b/include/staff/slaplan.inc.php
index 494fecf180f27667cf3c2e8536dd18852cb45b78..a3c806d5a8d842b2aa11ca722a0e33119c964a80 100644
--- a/include/staff/slaplan.inc.php
+++ b/include/staff/slaplan.inc.php
@@ -7,6 +7,7 @@ if($sla && $_REQUEST['a']!='add'){
     $submit_text=__('Save Changes');
     $info=$sla->getInfo();
     $info['id']=$sla->getId();
+    $trans['name'] = $sla->getTranslateTag('name');
     $qs += array('id' => $sla->getId());
 }else {
     $title=__('Add New SLA Plan' /* SLA is abbreviation for Service Level Agreement */);
@@ -24,12 +25,15 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
  <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
- <h2><?php echo __('Service Level Agreement');?></h2>
+ <h2><?php echo $title; ?>
+    <?php if (isset($info['name'])) { ?><small>
+    — <?php echo $info['name']; ?></small>
+     <?php } ?>
+</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 __('Tickets are marked overdue on grace period violation.');?></em>
             </th>
         </tr>
@@ -40,7 +44,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
               <?php echo __('Name');?>:
             </td>
             <td>
-                <input type="text" size="30" name="name" value="<?php echo $info['name']; ?>">
+                <input type="text" size="30" name="name" value="<?php echo $info['name']; ?>"
+                    autofocus data-translate-tag="<?php echo $trans['name']; ?>"/>
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['name']; ?></span>&nbsp;<i class="help-tip icon-question-sign" href="#name"></i>
             </td>
         </tr>
@@ -86,8 +91,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
         </tr>
         <tr>
             <th colspan="2">
-                <em><strong><?php echo __('Admin Notes');?></strong>: <?php echo __('Internal notes.');?>
-                &nbsp;&nbsp;<i class="help-tip icon-question-sign" href="#admin_notes"></i></em>
+                <em><strong><?php echo __('Internal Notes');?></strong>: <?php echo __("be liberal, they're internal");?>
                 </em>
             </th>
         </tr>
diff --git a/include/staff/slaplans.inc.php b/include/staff/slaplans.inc.php
index b8f0b095f24c23062add377282625af766cd76c1..3e6c5c93207cba1af6df3648cd5971e0c6451fe8 100644
--- a/include/staff/slaplans.inc.php
+++ b/include/staff/slaplans.inc.php
@@ -2,105 +2,138 @@
 if(!defined('OSTADMININC') || !$thisstaff->isAdmin()) die('Access Denied');
 
 $qs = array();
-$sql='SELECT * FROM '.SLA_TABLE.' sla WHERE 1';
-$sortOptions=array('name'=>'sla.name','status'=>'sla.isactive','period'=>'sla.grace_period','date'=>'sla.created','updated'=>'sla.updated');
-$orderWays=array('DESC'=>'DESC','ASC'=>'ASC');
-$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'name';
-//Sorting options...
-if($sort && $sortOptions[$sort]) {
-    $order_column =$sortOptions[$sort];
+$sortOptions=array(
+        'name' => 'name',
+        'status' => 'isactive',
+        'period' => 'grace_period',
+        'created' => 'created',
+        'updated' => 'updated'
+        );
+
+$orderWays = array('DESC'=>'DESC', 'ASC'=>'ASC');
+$sort = ($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])]) ? strtolower($_REQUEST['sort']) : 'name';
+if ($sort && $sortOptions[$sort]) {
+    $order_column = $sortOptions[$sort];
 }
-$order_column=$order_column?$order_column:'sla.name';
 
-if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) {
-    $order=$orderWays[strtoupper($_REQUEST['order'])];
+$order_column = $order_column ? $order_column : 'name';
+
+if ($_REQUEST['order'] && isset($orderWays[strtoupper($_REQUEST['order'])])) {
+    $order = $orderWays[strtoupper($_REQUEST['order'])];
+} else {
+    $order = 'ASC';
 }
-$order=$order?$order:'ASC';
 
-if($order_column && strpos($order_column,',')){
+if ($order_column && strpos($order_column,',')) {
     $order_column=str_replace(','," $order,",$order_column);
 }
 $x=$sort.'_sort';
 $$x=' class="'.strtolower($order).'" ';
-$order_by="$order_column $order ";
-
-$total=db_count('SELECT count(*) FROM '.SLA_TABLE.' sla ');
-$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
-$pageNav=new Pagenate($total, $page, PAGE_LIMIT);
+$page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
+$count = SLA::objects()->count();
 $qstr = '&amp;'. Http::build_query($qs);
 $qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
 
+$pageNav = new Pagenate($count, $page, PAGE_LIMIT);
 $pageNav->setURL('slas.php', $qs);
-//Ok..lets roll...create the actual query
+$showing = $pageNav->showing().' '._N('SLA plan', 'SLA plans', $count);
 $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)))
-    $showing=$pageNav->showing().' '._N('SLA plan',
-        'SLA plans' /* SLA is abbreviation for Service Level Agreement */,
-        $total);
-else
-    $showing=__('No SLA plans found!' /* SLA is abbreviation for Service Level Agreement */);
-
 ?>
-
-<div class="pull-left" style="width:700px;padding-top:5px;">
- <h2><?php echo __('Service Level Agreements');?></h2>
-</div>
-<div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;">
- <b><a href="slas.php?a=add" class="Icon newsla"><?php echo __('Add New SLA Plan');?></a></b></div>
-<div class="clear"></div>
 <form action="slas.php" method="POST" name="slas">
+    <div class="sticky bar opaque">
+        <div class="content">
+            <div class="pull-left flush-left">
+                <h2><?php echo __('Service Level Agreements');?></h2>
+            </div>
+            <div class="pull-right flush-right">
+                <a href="slas.php?a=add" class="green button action-button"><i class="icon-plus-sign"></i> <?php echo __('Add New SLA Plan');?></a>
+                <span class="action-button" data-dropdown="#action-dropdown-more">
+                    <i class="icon-caret-down pull-right"></i>
+                    <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+                </span>
+                <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                    <ul id="actions">
+                        <li>
+                            <a class="confirm" data-name="enable" href="slas.php?a=enable">
+                                <i class="icon-ok-sign icon-fixed-width"></i>
+                                <?php echo __( 'Enable'); ?>
+                            </a>
+                        </li>
+                        <li>
+                            <a class="confirm" data-name="disable" href="slas.php?a=disable">
+                                <i class="icon-ban-circle icon-fixed-width"></i>
+                                <?php echo __( 'Disable'); ?>
+                            </a>
+                        </li>
+                        <li class="danger">
+                            <a class="confirm" data-name="delete" href="slas.php?a=delete">
+                                <i class="icon-trash icon-fixed-width"></i>
+                                <?php echo __( 'Delete'); ?>
+                            </a>
+                        </li>
+                    </ul>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="clear"></div>
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="mass_process" >
 <input type="hidden" id="action" name="a" value="" >
  <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption><?php echo $showing; ?></caption>
     <thead>
         <tr>
-            <th width="7">&nbsp;</th>
-            <th width="320"><a <?php echo $name_sort; ?> href="slas.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name');?></a></th>
-            <th width="100"><a <?php echo $status_sort; ?> href="slas.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Status');?></a></th>
-            <th width="130"><a <?php echo $period_sort; ?> href="slas.php?<?php echo $qstr; ?>&sort=period"><?php echo __('Grace Period (hrs)');?></a></th>
-            <th width="120" nowrap><a <?php echo $created_sort; ?>href="slas.php?<?php echo $qstr; ?>&sort=created"><?php echo __('Date Added');?></a></th>
-            <th width="150" nowrap><a <?php echo $updated_sort; ?>href="slas.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated');?></a></th>
+            <th width="4%">&nbsp;</th>
+            <th width="38%"><a <?php echo $name_sort; ?> href="slas.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name');?></a></th>
+            <th width="8%"><a <?php echo $status_sort; ?> href="slas.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Status');?></a></th>
+            <th><a <?php echo $period_sort; ?> href="slas.php?<?php echo $qstr; ?>&sort=period"><?php echo __('Grace Period (hrs)');?></a></th>
+            <th width="15%" nowrap><a <?php echo $created_sort; ?>href="slas.php?<?php echo $qstr; ?>&sort=created"><?php echo __('Date Added');?></a></th>
+            <th width="20%" nowrap><a <?php echo $updated_sort; ?>href="slas.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated');?></a></th>
         </tr>
     </thead>
     <tbody>
     <?php
         $total=0;
-        $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null;
-        if($res && db_num_rows($res)):
+        $ids = ($errors && is_array($_POST['ids'])) ? $_POST['ids'] : null;
+        if ($count) {
+            $slas = SLA::objects()
+                ->order_by(sprintf('%s%s',
+                            strcasecmp($order, 'DESC') ? '' : '-',
+                            $order_column))
+                ->limit($pageNav->getLimit())
+                ->offset($pageNav->getStart());
+
             $defaultId = $cfg->getDefaultSLAId();
-            while ($row = db_fetch_array($res)) {
+            foreach ($slas as $sla) {
                 $sel=false;
-                if($ids && in_array($row['id'],$ids))
+                $id = $sla->getId();
+                if($ids && in_array($id, $ids))
                     $sel=true;
 
                 $default = '';
-                if ($row['id'] == $defaultId)
+                if ($id == $defaultId)
                     $default = '<small><em>(Default)</em></small>';
                 ?>
-            <tr id="<?php echo $row['id']; ?>">
-                <td width=7px>
-                  <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['id']; ?>"
-                    <?php echo $sel?'checked="checked"':''; ?>>
+            <tr id="<?php echo $id; ?>">
+                <td align="center">
+                  <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $id; ?>"
+                    <?php echo $sel ? 'checked="checked"' :'' ; ?>>
                 </td>
-                <td>&nbsp;<a href="slas.php?id=<?php echo $row['id'];
-                    ?>"><?php echo Format::htmlchars($row['name']);
+                <td>&nbsp;<a href="slas.php?id=<?php echo $id;
+                    ?>"><?php echo Format::htmlchars($sla->getName());
                     ?></a>&nbsp;<?php echo $default; ?></td>
-                <td><?php echo $row['isactive']?__('Active'):'<b>'.__('Disabled').'</b>'; ?></td>
-                <td style="text-align:right;padding-right:35px;"><?php echo $row['grace_period']; ?>&nbsp;</td>
-                <td>&nbsp;<?php echo Format::db_date($row['created']); ?></td>
-                <td>&nbsp;<?php echo Format::db_datetime($row['updated']); ?></td>
+                <td><?php echo $sla->isActive() ? __('Active') : '<b>'.__('Disabled').'</b>'; ?></td>
+                <td style="text-align:right;padding-right:35px;"><?php echo $sla->getGracePeriod(); ?>&nbsp;</td>
+                <td>&nbsp;<?php echo Format::date($sla->getCreateDate()); ?></td>
+                <td>&nbsp;<?php echo Format::datetime($sla->getUpdateDate()); ?></td>
             </tr>
             <?php
-            } //end of while.
-        endif; ?>
+            } //end of foreach.
+        } ?>
     <tfoot>
      <tr>
         <td colspan="6">
-            <?php if($res && $num){ ?>
+            <?php if ($count) { ?>
             <?php echo __('Select');?>:&nbsp;
             <a id="selectAll" href="#ckb"><?php echo __('All');?></a>&nbsp;&nbsp;
             <a id="selectNone" href="#ckb"><?php echo __('None');?></a>&nbsp;&nbsp;
@@ -113,14 +146,10 @@ else
     </tfoot>
 </table>
 <?php
-if($res && $num): //Show options..
+if ($count): //Show options..
     echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
 ?>
-<p class="centered" id="actions">
-    <input class="button" type="submit" name="enable" value="<?php echo __('Enable');?>" >
-    <input class="button" type="submit" name="disable" value="<?php echo __('Disable');?>" >
-    <input class="button" type="submit" name="delete" value="<?php echo __('Delete');?>" >
-</p>
+
 <?php
 endif;
 ?>
diff --git a/include/staff/staff.inc.php b/include/staff/staff.inc.php
index 4bd04c2f7bc0cb50f131e4ab71706126d0248202..384e59d0756357b8f733f63fa31589b1b9d966fb 100644
--- a/include/staff/staff.inc.php
+++ b/include/staff/staff.inc.php
@@ -2,366 +2,536 @@
 if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
 
 $info = $qs = array();
-if($staff && $_REQUEST['a']!='add'){
+
+if ($_REQUEST['a']=='add'){
+    if (!$staff) {
+        $staff = Staff::create(array(
+            'isactive' => true,
+        ));
+        // Set some default permissions
+        $staff->updatePerms(array(
+            User::PERM_CREATE,
+            User::PERM_EDIT,
+            User::PERM_DELETE,
+            User::PERM_MANAGE,
+            User::PERM_DIRECTORY,
+            Organization::PERM_CREATE,
+            Organization::PERM_EDIT,
+            Organization::PERM_DELETE,
+            FAQ::PERM_MANAGE,
+        ));
+    }
+    $title=__('Add New Agent');
+    $action='create';
+    $submit_text=__('Create');
+}
+else {
     //Editing Department.
-    $title=__('Update Agent');
+    $title=__('Manage Agent');
     $action='update';
     $submit_text=__('Save Changes');
-    $passwd_text=__('To reset the password enter a new one below');
-    $info=$staff->getInfo();
-    $info['id']=$staff->getId();
-    $info['teams'] = $staff->getTeams();
-    $info['signature'] = Format::viewableImages($info['signature']);
+    $info['id'] = $staff->getId();
     $qs += array('id' => $staff->getId());
-}else {
-    $title=__('Add New Agent');
-    $action='create';
-    $submit_text=__('Add Agent');
-    $passwd_text=__('Temporary password required only for "Local" authentication');
-    //Some defaults for new staff.
-    $info['change_passwd']=1;
-    $info['welcome_email']=1;
-    $info['isactive']=1;
-    $info['isvisible']=1;
-    $info['isadmin']=0;
-    $info['timezone_id'] = $cfg->getDefaultTimezoneId();
-    $info['daylight_saving'] = $cfg->observeDaylightSaving();
-    $qs += array('a' => 'add');
 }
-$info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 ?>
+
 <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']); ?>">
- <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
- <h2><?php echo __('Agent Account');?></h2>
- <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
-    <thead>
-        <tr>
-            <th colspan="2">
-                <h4><?php echo $title; ?></h4>
-                <em><strong><?php echo __('User Information');?></strong></em>
-            </th>
-        </tr>
-    </thead>
-    <tbody>
-        <tr>
-            <td width="180" class="required">
-                <?php echo __('Username');?>:
-            </td>
-            <td>
-                <input type="text" size="30" class="staff-username typeahead"
-                     name="username" value="<?php echo $info['username']; ?>">
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['username']; ?></span>&nbsp;<i class="help-tip icon-question-sign" href="#username"></i>
-            </td>
-        </tr>
+  <?php csrf_token(); ?>
+  <input type="hidden" name="do" value="<?php echo $action; ?>">
+  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
+  <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
 
+  <h2><?php echo $title; ?>
+      <?php if (isset($staff->staff_id)) { ?><small>
+      — <?php echo $staff->getName(); ?></small>
+      <?php } ?>
+</h2>
+
+  <ul class="clean tabs">
+    <li class="active"><a href="#account"><i class="icon-user"></i> <?php echo __('Account'); ?></a></li>
+    <li><a href="#access"><?php echo __('Access'); ?></a></li>
+    <li><a href="#permissions"><?php echo __('Permisions'); ?></a></li>
+    <li><a href="#teams"><?php echo __('Teams'); ?></a></li>
+  </ul>
+
+  <div class="tab_content" id="account">
+    <table class="table two-column" width="940" border="0" cellspacing="0" cellpadding="2">
+      <tbody>
+        <tr><td colspan="2"><div>
+        <div class="avatar pull-left" style="width: 100px; margin: 10px;">
+            <?php echo $staff->getAvatar(); ?>
+        </div>
+        <table class="table two-column" border="0" cellspacing="2" cellpadding="2" style="width: 760px">
         <tr>
-            <td width="180" class="required">
-                <?php echo __('First Name');?>:
-            </td>
-            <td>
-                <input type="text" size="30" name="firstname" class="auto first"
-                     value="<?php echo $info['firstname']; ?>">
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['firstname']; ?></span>&nbsp;
-            </td>
+          <td class="required"><?php echo __('Name'); ?>:</td>
+          <td>
+            <input type="text" size="20" maxlength="64" style="width: 145px" name="firstname" class="auto first"
+              autofocus value="<?php echo Format::htmlchars($staff->firstname); ?>"
+              placeholder="<?php echo __("First Name"); ?>" />
+            <input type="text" size="20" maxlength="64" style="width: 145px" name="lastname" class="auto last"
+              value="<?php echo Format::htmlchars($staff->lastname); ?>"
+              placeholder="<?php echo __("Last Name"); ?>" />
+            <div class="error"><?php echo $errors['firstname']; ?></div>
+            <div class="error"><?php echo $errors['lastname']; ?></div>
+          </td>
         </tr>
         <tr>
-            <td width="180" class="required">
-                <?php echo __('Last Name');?>:
-            </td>
-            <td>
-                <input type="text" size="30" name="lastname" class="auto last"
-                    value="<?php echo $info['lastname']; ?>">
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['lastname']; ?></span>&nbsp;
-            </td>
+          <td class="required"><?php echo __('Email Address'); ?>:</td>
+          <td>
+            <input type="email" size="40" maxlength="64" style="width: 300px" name="email" class="auto email"
+              value="<?php echo Format::htmlchars($staff->email); ?>"
+              placeholder="<?php echo __('e.g. me@mycompany.com'); ?>" />
+            <div class="error"><?php echo $errors['email']; ?></div>
+          </td>
         </tr>
         <tr>
-            <td width="180" class="required">
-                <?php echo __('Email Address');?>:
-            </td>
-            <td>
-                <input type="text" size="30" name="email" class="auto email"
-                    value="<?php echo $info['email']; ?>">
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['email']; ?></span>&nbsp;<i class="help-tip icon-question-sign" href="#email_address"></i>
-            </td>
+          <td><?php echo __('Phone Number');?>:</td>
+          <td>
+            <input type="tel" size="18" name="phone" class="auto phone"
+              value="<?php echo Format::htmlchars($staff->phone); ?>" />
+            <?php echo __('Ext');?>
+            <input type="text" size="5" name="phone_ext"
+              value="<?php echo Format::htmlchars($staff->phone_ext); ?>">
+            <div class="error"><?php echo $errors['phone']; ?></div>
+            <div class="error"><?php echo $errors['phone_ext']; ?></div>
+          </td>
         </tr>
         <tr>
-            <td width="180">
-                <?php echo __('Phone Number');?>:
-            </td>
-            <td>
-                <input type="text" size="18" name="phone" class="auto phone"
-                    value="<?php echo $info['phone']; ?>">
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['phone']; ?></span>
-                <?php echo __('Ext');?> <input type="text" size="5" name="phone_ext" value="<?php echo $info['phone_ext']; ?>">
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['phone_ext']; ?></span>
-            </td>
+          <td><?php echo __('Mobile Number');?>:</td>
+          <td>
+            <input type="tel" size="18" name="mobile" class="auto phone"
+              value="<?php echo Format::htmlchars($staff->mobile); ?>" />
+            <div class="error"><?php echo $errors['mobile']; ?></div>
+          </td>
         </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Mobile Number');?>:
-            </td>
-            <td>
-                <input type="text" size="18" name="mobile" class="auto mobile"
-                    value="<?php echo $info['mobile']; ?>">
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['mobile']; ?></span>
-            </td>
+        </table></div></td></tr>
+      </tbody>
+      <!-- ================================================ -->
+      <tbody>
+        <tr class="header">
+          <th colspan="2">
+            <?php echo __('Authentication'); ?>
+          </th>
         </tr>
-<?php if (!$staff) { ?>
         <tr>
-            <td width="180"><?php echo __('Welcome Email'); ?></td>
-            <td><input type="checkbox" name="welcome_email" id="welcome-email" <?php
-                if ($info['welcome_email']) echo 'checked="checked"';
-                ?> onchange="javascript:
-                var sbk = $('#backend-selection');
-                if ($(this).is(':checked'))
-                    $('#password-fields').hide();
-                else if (sbk.val() == '' || sbk.val() == 'local')
-                    $('#password-fields').show();
-                " />
-                <?php echo __('Send sign in information'); ?>
-                &nbsp;<i class="help-tip icon-question-sign" href="#welcome_email"></i>
-            </td>
-        </tr>
+          <td class="required"><?php echo __('Username'); ?>:
+            <span class="error">*</span></td>
+          <td>
+            <input type="text" size="40" style="width:300px"
+              class="staff-username typeahead"
+              name="username" value="<?php echo Format::htmlchars($staff->username); ?>" />
+<?php if (!($bk = $staff->getAuthBackend()) || $bk->supportsPasswordChange()) { ?>
+            <button type="button" class="action-button" onclick="javascript:
+            $.dialog('ajax.php/staff/'+<?php echo $info['id'] ?: '0'; ?>+'/set-password', 201);">
+              <i class="icon-refresh"></i> <?php echo __('Set Password'); ?>
+            </button>
 <?php } ?>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Authentication'); ?></strong>: <?php echo $passwd_text; ?> &nbsp;<span class="error">&nbsp;<?php echo $errors['temppasswd']; ?></span>&nbsp;<i class="help-tip icon-question-sign" href="#account_password"></i></em>
-            </th>
+            <i class="offset help-tip icon-question-sign" href="#username"></i>
+            <div class="error"><?php echo $errors['username']; ?></div>
+          </td>
         </tr>
+<?php
+$bks = array();
+foreach (StaffAuthenticationBackend::allRegistered() as $ab) {
+  if (!$ab->supportsInteractiveAuthentication()) continue;
+  $bks[] = $ab;
+}
+if (count($bks) > 1) {
+?>
         <tr>
-            <td><?php echo __('Authentication Backend'); ?></td>
-            <td>
-            <select name="backend" id="backend-selection" onchange="javascript:
+          <td><?php echo __('Authentication Backend'); ?>:</td>
+          <td>
+            <select name="backend" id="backend-selection"
+              style="width:300px" onchange="javascript:
                 if (this.value != '' && this.value != 'local')
                     $('#password-fields').hide();
                 else if (!$('#welcome-email').is(':checked'))
                     $('#password-fields').show();
                 ">
-                <option value="">&mdash; <?php echo __('Use any available backend'); ?> &mdash;</option>
-            <?php foreach (StaffAuthenticationBackend::allRegistered() as $ab) {
-                if (!$ab->supportsInteractiveAuthentication()) continue; ?>
-                <option value="<?php echo $ab::$id; ?>" <?php
-                    if ($info['backend'] == $ab::$id)
-                        echo 'selected="selected"'; ?>><?php
-                    echo $ab->getName(); ?></option>
-            <?php } ?>
+              <option value="">&mdash; <?php echo __('Use any available backend'); ?> &mdash;</option>
+<?php foreach ($bks as $ab) { ?>
+              <option value="<?php echo $ab::$id; ?>" <?php
+                if ($staff->backend == $ab::$id)
+                  echo 'selected="selected"'; ?>><?php
+                echo $ab->getName(); ?></option>
+<?php } ?>
             </select>
-            </td>
+          </td>
         </tr>
-    </tbody>
-    <tbody id="password-fields" style="<?php
-        if ($info['welcome_email'] || ($info['backend'] && $info['backend'] != 'local'))
-            echo 'display:none;'; ?>">
-        <tr>
-            <td width="180">
-                <?php echo __('Password');?>:
-            </td>
-            <td>
-                <input type="password" size="18" name="passwd1" value="<?php echo $info['passwd1']; ?>">
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['passwd1']; ?></span>
-            </td>
+<?php
+} ?>
+      </tbody>
+      <!-- ================================================ -->
+      <tbody>
+        <tr class="header">
+          <th colspan="2">
+            <?php echo __('Status and Settings'); ?>
+          </th>
         </tr>
         <tr>
-            <td width="180">
-                <?php echo __('Confirm Password');?>:
-            </td>
-            <td>
-                <input type="password" size="18" name="passwd2" value="<?php echo $info['passwd2']; ?>">
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['passwd2']; ?></span>
-            </td>
+          <td colspan="2">
+            <div class="error"><?php echo $errors['isadmin']; ?></div>
+            <div class="error"><?php echo $errors['isactive']; ?></div>
+            <label class="checkbox">
+            <input type="checkbox" name="islocked" value="1"
+              <?php echo (!$staff->isactive) ? 'checked="checked"' : ''; ?> />
+              <?php echo __('Locked'); ?>
+            </label>
+            <label class="checkbox">
+            <input type="checkbox" name="isadmin" value="1"
+              <?php echo ($staff->isadmin) ? 'checked="checked"' : ''; ?> />
+              <?php echo __('Administrator'); ?>
+            </label>
+            <label class="checkbox">
+            <input type="checkbox" name="assigned_only"
+              <?php echo ($staff->assigned_only) ? 'checked="checked"' : ''; ?> />
+              <?php echo __('Limit ticket access to ONLY assigned tickets'); ?>
+            </label>
+            <label class="checkbox">
+            <input type="checkbox" name="onvacation"
+              <?php echo ($staff->onvacation) ? 'checked="checked"' : ''; ?> />
+              <?php echo __('Vacation Mode'); ?>
+            </label>
+            <br/>
         </tr>
+      </tbody>
+    </table>
 
-        <tr>
-            <td width="180">
-                <?php echo __('Forced Password Change');?>:
-            </td>
-            <td>
-                <input type="checkbox" name="change_passwd" value="0" <?php echo $info['change_passwd']?'checked="checked"':''; ?>>
-                <?php echo __('<strong>Force</strong> password change on next login.');?>
-                &nbsp;<i class="help-tip icon-question-sign" href="#forced_password_change"></i>
-            </td>
-        </tr>
-    </tbody>
-    <tbody>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __("Agent's Signature");?></strong>:
-                <?php echo __('Optional signature used on outgoing emails.');?>
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['signature']; ?></span></em>
-                &nbsp;<i class="help-tip icon-question-sign" href="#agents_signature"></i></em>
-            </th>
-        </tr>
-        <tr>
-            <td colspan=2>
-                <textarea class="richtext no-bar" name="signature" cols="21"
-                    rows="5" style="width: 60%;"><?php echo $info['signature']; ?></textarea>
-                <br><em><?php echo __('Signature is made available as a choice, on ticket reply.');?></em>
-            </td>
-        </tr>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Account Status & Settings');?></strong>: <?php echo __('Department and group assigned control access permissions.');?></em>
-            </th>
-        </tr>
-        <tr>
-            <td width="180" class="required">
-                <?php echo __('Account Type');?>:
-            </td>
-            <td>
-                <input type="radio" name="isadmin" value="1" <?php echo $info['isadmin']?'checked="checked"':''; ?>>
-                    <font color="red"><strong><?php echo __('Admin');?></strong></font>
-                <input type="radio" name="isadmin" value="0" <?php echo !$info['isadmin']?'checked="checked"':''; ?>><strong><?php echo __('Agent');?></strong>
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['isadmin']; ?></span>
-            </td>
-        </tr>
-        <tr>
-            <td width="180" class="required">
-                <?php echo __('Account Status');?>:
-            </td>
-            <td>
-                <input type="radio" name="isactive" value="1" <?php echo $info['isactive']?'checked="checked"':''; ?>><strong><?php echo __('Active');?></strong>
-                <input type="radio" name="isactive" value="0" <?php echo !$info['isactive']?'checked="checked"':''; ?>><strong><?php echo __('Locked');?></strong>
-                &nbsp;<span class="error">&nbsp;<?php echo $errors['isactive']; ?></span>&nbsp;<i class="help-tip icon-question-sign" href="#account_status"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180" class="required">
-                <?php echo __('Assigned Group');?>:
-            </td>
-            <td>
-                <select name="group_id" id="group_id">
-                    <option value="0">&mdash; <?php echo __('Select Group');?> &mdash;</option>
-                    <?php
-                    $sql='SELECT group_id, group_name, group_enabled as isactive FROM '.GROUP_TABLE.' ORDER BY group_name';
-                    if(($res=db_query($sql)) && db_num_rows($res)){
-                        while(list($id,$name,$isactive)=db_fetch_row($res)){
-                            $sel=($info['group_id']==$id)?'selected="selected"':'';
-                            echo sprintf('<option value="%d" %s>%s %s</option>',$id,$sel,$name,($isactive?'':__('(disabled)')));
-                        }
-                    }
-                    ?>
-                </select>
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['group_id']; ?></span>&nbsp;<i class="help-tip icon-question-sign" href="#assigned_group"></i>
-            </td>
-        </tr>
-        <tr>
-            <td width="180" class="required">
-                <?php echo __('Primary Department');?>:
-            </td>
-            <td>
-                <select name="dept_id" id="dept_id">
-                    <option value="0">&mdash; <?php echo __('Select Department');?> &mdash;</option>
-                    <?php
-                    $sql='SELECT dept_id, dept_name FROM '.DEPT_TABLE.' ORDER BY dept_name';
-                    if(($res=db_query($sql)) && db_num_rows($res)){
-                        while(list($id,$name)=db_fetch_row($res)){
-                            $sel=($info['dept_id']==$id)?'selected="selected"':'';
-                            echo sprintf('<option value="%d" %s>%s</option>',$id,$sel,$name);
-                        }
-                    }
-                    ?>
-                </select>
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['dept_id']; ?></span>&nbsp;<i class="help-tip icon-question-sign" href="#primary_department"></i>
-            </td>
+    <div style="padding:8px 3px; margin-top: 1.6em">
+        <strong class="big"><?php echo __('Internal Notes');?>: </strong>
+        <?php echo __("be liberal, they're internal.");?>
+    </div>
+
+    <textarea name="notes" class="richtext">
+      <?php echo Format::viewableImages($staff->notes); ?>
+    </textarea>
+  </div>
+
+  <!-- ============== DEPARTMENT ACCESS =================== -->
+
+  <div class="hidden tab_content" id="access">
+    <table class="table two-column" width="940" border="0" cellspacing="0" cellpadding="2">
+      <tbody>
+        <tr class="header">
+          <th colspan="3">
+            <?php echo __('Primary Department and Role'); ?>
+            <span class="error">*</span>
+            <div><small><?php echo __(
+            "Select the departments the agent is allowed to access and optionally select an effective role."
+          ); ?>
+            </small></div>
+          </th>
         </tr>
         <tr>
-            <td width="180" class="required">
-                <?php echo __("Agent's Time Zone");?>:
-            </td>
-            <td>
-                <select name="timezone_id" id="timezone_id">
-                    <option value="0">&mdash; <?php echo __('Select Time Zone');?> &mdash;</option>
-                    <?php
-                    $sql='SELECT id, offset,timezone FROM '.TIMEZONE_TABLE.' ORDER BY id';
-                    if(($res=db_query($sql)) && db_num_rows($res)){
-                        while(list($id,$offset, $tz)=db_fetch_row($res)){
-                            $sel=($info['timezone_id']==$id)?'selected="selected"':'';
-                            echo sprintf('<option value="%d" %s>GMT %s - %s</option>',$id,$sel,$offset,$tz);
-                        }
-                    }
-                    ?>
-                </select>
-                &nbsp;<span class="error">*&nbsp;<?php echo $errors['timezone_id']; ?></span>
-            </td>
+          <td style="vertical-align:top">
+            <select name="dept_id" id="dept_id" data-quick-add="department">
+              <option value="0">&mdash; <?php echo __('Select Department');?> &mdash;</option>
+              <?php
+              foreach (Dept::getDepartments() as $id=>$name) {
+                $sel=($staff->dept_id==$id)?'selected="selected"':'';
+                echo sprintf('<option value="%d" %s>%s</option>',$id,$sel,$name);
+              }
+              ?>
+              <option value="0" data-quick-add>&mdash; <?php echo __('Add New');?> &mdash;</option>
+            </select>
+            <i class="offset help-tip icon-question-sign" href="#primary_department"></i>
+            <div class="error"><?php echo $errors['dept_id']; ?></div>
+          </td>
+          <td style="vertical-align:top">
+            <select name="role_id" data-quick-add="role">
+              <option value="0">&mdash; <?php echo __('Select Role');?> &mdash;</option>
+              <?php
+              foreach (Role::getRoles() as $id=>$name) {
+                $sel=($staff->role_id==$id)?'selected="selected"':'';
+                echo sprintf('<option value="%d" %s>%s</option>',$id,$sel,$name);
+              }
+              ?>
+              <option value="0" data-quick-add>&mdash; <?php echo __('Add New');?> &mdash;</option>
+            </select>
+            <i class="offset help-tip icon-question-sign" href="#primary_role"></i>
+          </td>
+          <td>
+            <label class="inline checkbox">
+            <input type="checkbox" name="assign_use_pri_role" <?php
+                if ($staff->usePrimaryRoleOnAssignment())
+                    echo 'checked="checked"';
+                ?> />
+                <?php echo __('Fall back to primary role on assigned tickets'); ?>
+                <i class="icon-question-sign help-tip"
+                    href="#primary_role_on_assign"></i>
+            </label>
+
+            <div class="error"><?php echo $errors['role_id']; ?></div>
+          </td>
         </tr>
-        <tr>
-            <td width="180">
-               <?php echo __('Daylight Saving');?>:
-            </td>
-            <td>
-                <input type="checkbox" name="daylight_saving" value="1" <?php echo $info['daylight_saving']?'checked="checked"':''; ?>>
-                <?php echo __('Observe daylight saving');?>
-                <em>(<?php echo __('Current Time');?>: <strong><?php
-                    echo Format::date($cfg->getDateTimeFormat(),Misc::gmtime(),$info['tz_offset'],$info['daylight_saving']);
-                ?></strong>)
-                &nbsp;<i class="help-tip icon-question-sign" href="#daylight_saving"></i>
-                </em>
-            </td>
+      </tbody>
+      <tbody>
+        <tr id="extended_access_template" class="hidden">
+          <td>
+            <input type="hidden" data-name="dept_access[]" value="" />
+          </td>
+          <td>
+            <select data-name="dept_access_role" data-quick-add="role">
+              <option value="0">&mdash; <?php echo __('Select Role');?> &mdash;</option>
+              <?php
+              foreach (Role::getRoles() as $id=>$name) {
+                echo sprintf('<option value="%d" %s>%s</option>',$id,$sel,$name);
+              }
+              ?>
+              <option value="0" data-quick-add>&mdash; <?php echo __('Add New');?> &mdash;</option>
+            </select>
+          </td>
+          <td>
+            <label class="inline checkbox">
+              <input type="checkbox" data-name="dept_access_alerts" value="1" />
+              <?php echo __('Alerts'); ?>
+            </label>
+            <a href="#" class="pull-right drop-access" title="<?php echo __('Delete');
+              ?>"><i class="icon-trash"></i></a>
+          </td>
         </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Limited Access');?>:
-            </td>
-            <td>
-                <input type="checkbox" name="assigned_only" value="1" <?php echo $info['assigned_only']?'checked="checked"':''; ?>><?php echo __('Limit ticket access to ONLY assigned tickets.');?>
-                &nbsp;<i class="help-tip icon-question-sign" href="#limited_access"></i>
-            </td>
+      </tbody>
+      <tbody>
+        <tr class="header">
+          <th colspan="3">
+            <?php echo __('Extended Access'); ?>
+          </th>
         </tr>
-        <tr>
-            <td width="180">
-                <?php echo __('Directory Listing');?>:
-            </td>
-            <td>
-                <input type="checkbox" name="isvisible" value="1" <?php echo $info['isvisible']?'checked="checked"':''; ?>>&nbsp;<?php
-                echo __('Make visible in the Agent Directory'); ?>
-                &nbsp;<i class="help-tip icon-question-sign" href="#directory_listing"></i>
-            </td>
+<?php
+$depts = Dept::getDepartments();
+foreach ($staff->dept_access as $dept_access) {
+  unset($depts[$dept_access->dept_id]);
+}
+?>
+        <tr id="add_extended_access">
+          <td colspan="2">
+            <i class="icon-plus-sign"></i>
+            <select id="add_access" data-quick-add="department">
+              <option value="0">&mdash; <?php echo __('Select Department');?> &mdash;</option>
+              <?php
+              foreach ($depts as $id=>$name) {
+                echo sprintf('<option value="%d">%s</option>',$id,Format::htmlchars($name));
+              }
+              ?>
+              <option value="0" data-quick-add>&mdash; <?php echo __('Add New');?> &mdash;</option>
+            </select>
+            <button type="button" class="green button">
+              <?php echo __('Add'); ?>
+            </button>
+          </td>
         </tr>
+      </tbody>
+    </table>
+  </div>
+
+  <!-- ================= PERMISSIONS ====================== -->
+
+  <div id="permissions" class="hidden">
+<?php
+    $permissions = array();
+    foreach (RolePermission::allPermissions() as $g => $perms) {
+        foreach ($perms as $k=>$P) {
+            if (!$P['primary'])
+                continue;
+            if (!isset($permissions[$g]))
+                $permissions[$g] = array();
+            $permissions[$g][$k] = $P;
+        }
+    }
+?>
+    <ul class="alt tabs">
+<?php
+    $first = true;
+    foreach ($permissions as $g => $perms) { ?>
+      <li <?php if ($first) { echo 'class="active"'; $first=false; } ?>>
+        <a href="#<?php echo Format::slugify($g); ?>"><?php echo Format::htmlchars(__($g));?></a>
+      </li>
+<?php } ?>
+    </ul>
+<?php
+    $first = true;
+    foreach ($permissions as $g => $perms) { ?>
+    <div class="tab_content <?php if (!$first) { echo 'hidden'; } else { $first = false; }
+      ?>" id="<?php echo Format::slugify($g); ?>">
+      <table class="table">
+<?php foreach ($perms as $k => $v) { ?>
         <tr>
-            <td width="180">
-                <?php echo __('Vacation Mode');?>:
-            </td>
-            <td>
-                <input type="checkbox" name="onvacation" value="1" <?php echo $info['onvacation']?'checked="checked"':''; ?>>
-                    <?php echo __('Change Status to Vacation Mode'); ?>
-                    &nbsp;<i class="help-tip icon-question-sign" href="#vacation_mode"></i>
-            </td>
+          <td>
+            <label>
+            <?php
+            echo sprintf('<input type="checkbox" name="perms[]" value="%s" %s />',
+              $k, ($staff->hasPerm($k)) ? 'checked="checked"' : '');
+            ?>
+            &nbsp;
+            <?php echo Format::htmlchars(__($v['title'])); ?>
+            —
+            <em><?php echo Format::htmlchars(__($v['desc'])); ?></em>
+           </label>
+          </td>
         </tr>
-        <?php
-         //List team assignments.
-         $sql='SELECT team.team_id, team.name, isenabled FROM '.TEAM_TABLE.' team  ORDER BY team.name';
-         if(($res=db_query($sql)) && db_num_rows($res)){ ?>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Assigned Teams');?></strong>: <?php echo __("Agent will have access to tickets assigned to a team they belong to regardless of the ticket's department.");?> </em>
-            </th>
+<?php   } ?>
+      </table>
+    </div>
+<?php } ?>
+  </div>
+
+  <!-- ============== TEAM MEMBERSHIP =================== -->
+
+  <div class="hidden tab_content" id="teams">
+    <table class="table two-column" width="100%">
+      <tbody>
+        <tr class="header">
+          <th colspan="2">
+            <?php echo __('Assigned Teams'); ?>
+            <div><small><?php echo __(
+            "Agent will have access to tickets assigned to a team they belong to regardless of the ticket's department. Alerts can be enabled for each associated team."
+            ); ?>
+            </small></div>
+          </th>
         </tr>
-        <?php
-         while(list($id,$name,$isactive)=db_fetch_row($res)){
-             $checked=($info['teams'] && in_array($id,$info['teams']))?'checked="checked"':'';
-             echo sprintf('<tr><td colspan=2><input type="checkbox" name="teams[]" value="%d" %s>%s %s</td></tr>',
-                     $id,$checked,$name,($isactive?'':__('(disabled)')));
-         }
-        } ?>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Internal Notes'); ?></strong></em>
-            </th>
+<?php
+$teams = Team::getTeams();
+foreach ($staff->teams as $TM) {
+  unset($teams[$TM->team_id]);
+}
+?>
+        <tr id="join_team">
+          <td colspan="2">
+            <i class="icon-plus-sign"></i>
+            <select id="add_team" data-quick-add="team">
+              <option value="0">&mdash; <?php echo __('Select Team');?> &mdash;</option>
+              <?php
+              foreach ($teams as $id=>$name) {
+                echo sprintf('<option value="%d">%s</option>',$id,Format::htmlchars($name));
+              }
+              ?>
+              <option value="0" data-quick-add>&mdash; <?php echo __('Add New');?> &mdash;</option>
+            </select>
+            <button type="button" class="green button">
+              <?php echo __('Add'); ?>
+            </button>
+          </td>
         </tr>
-        <tr>
-            <td colspan=2>
-                <textarea class="richtext no-bar" name="notes" cols="28"
-                    rows="7" style="width: 80%;"><?php echo $info['notes']; ?></textarea>
-            </td>
+      </tbody>
+      <tbody>
+        <tr id="team_member_template" class="hidden">
+          <td>
+            <input type="hidden" data-name="teams[]" value="" />
+          </td>
+          <td>
+            <label>
+              <input type="checkbox" data-name="team_alerts" value="1" />
+              <?php echo __('Alerts'); ?>
+            </label>
+            <a href="#" class="pull-right drop-membership" title="<?php echo __('Delete');
+              ?>"><i class="icon-trash"></i></a>
+          </td>
         </tr>
-    </tbody>
-</table>
-<p style="padding-left:250px;">
-    <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
-    <input type="reset"  name="reset"  value="<?php echo __('Reset');?>">
-    <input type="button" name="cancel" value="<?php echo __('Cancel');?>" onclick='window.location.href="staff.php"'>
-</p>
+      </tbody>
+    </table>
+  </div>
+
+  <p style="text-align:center;">
+      <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
+      <input type="reset"  name="reset"  value="<?php echo __('Reset');?>">
+      <input type="button" name="cancel" value="<?php echo __('Cancel');?>" onclick="window.history.go(-1);">
+  </p>
 </form>
+
+<script type="text/javascript">
+var addAccess = function(daid, name, role, alerts, error) {
+  if (!daid) return;
+  var copy = $('#extended_access_template').clone();
+
+  copy.find('[data-name=dept_access\\[\\]]')
+    .attr('name', 'dept_access[]')
+    .val(daid);
+  copy.find('[data-name^=dept_access_role]')
+    .attr('name', 'dept_access_role['+daid+']')
+    .val(role || 0);
+  copy.find('[data-name^=dept_access_alerts]')
+    .attr('name', 'dept_access_alerts['+daid+']')
+    .prop('checked', alerts);
+  copy.find('td:first').append(document.createTextNode(name));
+  copy.attr('id', '').show().insertBefore($('#add_extended_access'));
+  copy.removeClass('hidden')
+  if (error)
+      $('<div class="error">').text(error).appendTo(copy.find('td:last'));
+  copy.find('a.drop-access').click(function() {
+    $('#add_access').append(
+      $('<option>')
+        .attr('value', copy.find('input[name^=dept_access][type=hidden]').val())
+        .text(copy.find('td:first').text())
+    );
+    copy.fadeOut(function() { $(this).remove(); });
+    return false;
+  });
+};
+
+$('#add_extended_access').find('button').on('click', function() {
+  var selected = $('#add_access').find(':selected'),
+      id = parseInt(selected.val());
+  if (!id)
+      return;
+  addAccess(id, selected.text(), 0, true);
+  selected.remove();
+  return false;
+});
+
+var joinTeam = function(teamid, name, alerts, error) {
+  if (!teamid) return;
+  var copy = $('#team_member_template').clone();
+
+  copy.find('[data-name=teams\\[\\]]')
+    .attr('name', 'teams[]')
+    .val(teamid);
+  copy.find('[data-name^=team_alerts]')
+    .attr('name', 'team_alerts['+teamid+']')
+    .prop('checked', alerts);
+  copy.find('td:first').append(document.createTextNode(name));
+  copy.attr('id', '').show().insertBefore($('#join_team'));
+  copy.removeClass('hidden');
+  if (error)
+      $('<div class="error">').text(error).appendTo(copy.find('td:last'));
+  copy.find('a.drop-membership').click(function() {
+    $('#add_team').append(
+      $('<option>')
+        .attr('value', copy.find('input[name^=teams][type=hidden]').val())
+        .text(copy.find('td:first').text())
+    );
+    copy.fadeOut(function() { $(this).remove(); });
+    return false;
+  });
+};
+
+$('#join_team').find('button').on('click', function() {
+  var selected = $('#add_team').find(':selected'),
+      id = parseInt(selected.val());
+  if (!id)
+      return;
+  joinTeam(id, selected.text(), true);
+  selected.remove();
+  return false;
+});
+
+
+<?php
+foreach ($staff->dept_access as $dept_access) {
+  echo sprintf('addAccess(%d, %s, %d, %d, %s);', $dept_access->dept_id,
+    JsonDataEncoder::encode($dept_access->dept->getName()),
+    $dept_access->role_id,
+    $dept_access->isAlertsEnabled(),
+    JsonDataEncoder::encode(@$errors['dept_access'][$dept_access->dept_id])
+  );
+}
+
+foreach ($staff->teams as $member) {
+  echo sprintf('joinTeam(%d, %s, %d, %s);', $member->team_id,
+    JsonDataEncoder::encode($member->team->getName()),
+    $member->isAlertsEnabled(),
+    JsonDataEncoder::encode(@$errors['teams'][$member->team_id])
+  );
+}
+
+?>
+</script>
diff --git a/include/staff/staffmembers.inc.php b/include/staff/staffmembers.inc.php
index 05bd86d93710b7dec492de70fa2aaf9c27e0f2a6..be3d276f69cee3c2d2f625a03080551d056eb50b 100644
--- a/include/staff/staffmembers.inc.php
+++ b/include/staff/staffmembers.inc.php
@@ -1,178 +1,217 @@
 <?php
-if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
+if (!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin())
+    die('Access Denied');
+
+$qstr='';
 $qs = array();
-$select='SELECT staff.*, grp.group_name, dept.dept_name as dept,count(m.team_id) as teams ';
-$from='FROM '.STAFF_TABLE.' staff '.
-      'LEFT JOIN '.GROUP_TABLE.' grp ON(staff.group_id=grp.group_id) '.
-      'LEFT JOIN '.DEPT_TABLE.' dept ON(staff.dept_id=dept.dept_id) '.
-      'LEFT JOIN '.TEAM_MEMBER_TABLE.' m ON(m.staff_id=staff.staff_id) ';
-$where='WHERE 1 ';
-
-if($_REQUEST['did'] && is_numeric($_REQUEST['did'])) {
-    $where.=' AND staff.dept_id='.db_input($_REQUEST['did']);
-    $qs += array('did' => $_REQUEST['did']);
-}
+$sortOptions = array(
+        'name' => array('firstname', 'lastname'),
+        'username' => 'username',
+        'status' => 'isactive',
+        'dept' => 'dept__name',
+        'created' => 'created',
+        'login' => 'lastlogin'
+        );
 
-if($_REQUEST['gid'] && is_numeric($_REQUEST['gid'])) {
-    $where.=' AND staff.group_id='.db_input($_REQUEST['gid']);
-    $qs += array('gid' => $_REQUEST['gid']);
-}
+$orderWays = array('DESC'=>'DESC', 'ASC'=>'ASC');
+$sort = ($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])]) ? strtolower($_REQUEST['sort']) : 'name';
 
-if($_REQUEST['tid'] && is_numeric($_REQUEST['tid'])) {
-    $where.=' AND m.team_id='.db_input($_REQUEST['tid']);
-    $qs += array('tid' => $_REQUEST['tid']);
+if ($sort && $sortOptions[$sort]) {
+    $order_column = $sortOptions[$sort];
 }
 
-$sortOptions=array('name'=>'staff.firstname,staff.lastname','username'=>'staff.username','status'=>'isactive',
-                   'group'=>'grp.group_name','dept'=>'dept.dept_name','created'=>'staff.created','login'=>'staff.lastlogin');
+$order_column = $order_column ? $order_column : array('firstname', 'lastname');
 
-switch ($cfg->getDefaultNameFormat()) {
+switch ($cfg->getAgentNameFormat()) {
 case 'last':
 case 'lastfirst':
 case 'legal':
-    $sortOptions['name'] = 'staff.lastname, staff.firstname';
+    $sortOptions['name'] = array('lastname', 'firstname');
     break;
 // Otherwise leave unchanged
 }
 
-$orderWays=array('DESC'=>'DESC','ASC'=>'ASC');
-$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'name';
-//Sorting options...
-if($sort && $sortOptions[$sort]) {
-    $order_column =$sortOptions[$sort];
+if ($_REQUEST['order'] && isset($orderWays[strtoupper($_REQUEST['order'])])) {
+    $order = $orderWays[strtoupper($_REQUEST['order'])];
+} else {
+    $order = 'ASC';
 }
-$order_column=$order_column?$order_column:'staff.firstname,staff.lastname';
 
-if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) {
-    $order=$orderWays[strtoupper($_REQUEST['order'])];
+$x=$sort.'_sort';
+$$x=' class="'.strtolower($order).'" ';
+
+//Filers
+$filters = array();
+if ($_REQUEST['did'] && is_numeric($_REQUEST['did'])) {
+    $filters += array('dept_id' => $_REQUEST['did']);
+    $qs += array('did' => $_REQUEST['did']);
 }
 
-$order=$order?$order:'ASC';
-if($order_column && strpos($order_column,',')){
-    $order_column=str_replace(','," $order,",$order_column);
+if ($_REQUEST['tid'] && is_numeric($_REQUEST['tid'])) {
+    $filters += array('teams__team_id' => $_REQUEST['tid']);
+    $qs += array('tid' => $_REQUEST['tid']);
 }
-$x=$sort.'_sort';
-$$x=' class="'.strtolower($order).'" ';
-$order_by="$order_column $order ";
 
-$total=db_count('SELECT count(DISTINCT staff.staff_id) '.$from.' '.$where);
-$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
-$pageNav=new Pagenate($total,$page,PAGE_LIMIT);
-$qstr = '&amp;'. Http::build_query($qs);
-$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
+//agents objects
+$agents = Staff::objects()
+    ->annotate(array(
+            'teams_count'=>SqlAggregate::COUNT('teams', true),
+    ))
+    ->select_related('dept', 'group');
+
+$order = strcasecmp($order, 'DESC') ? '' : '-';
+foreach ((array) $order_column as $C) {
+    $agents->order_by($order.$C);
+}
+
+if ($filters)
+    $agents->filter($filters);
 
+// paginate
+$page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
+$count = $agents->count();
+$pageNav = new Pagenate($count, $page, PAGE_LIMIT);
+$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
 $pageNav->setURL('staff.php', $qs);
-$qstr .= '&amp;order='.($order=='DESC' ? 'ASC' : 'DESC');
-$query="$select $from $where GROUP BY staff.staff_id ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
-//echo $query;
+$showing = $pageNav->showing().' '._N('agent', 'agents', $count);
+$qstr = '&amp;'. Http::build_query($qs);
+$qstr .= '&amp;order='.($order=='-' ? 'ASC' : 'DESC');
+
+// add limits.
+$agents->limit($pageNav->getLimit())->offset($pageNav->getStart());
 ?>
-<h2><?php echo __('Agents');?></h2>
-<div class="pull-left" style="width:700px;">
-    <form action="staff.php" method="GET" name="filter">
-     <input type="hidden" name="a" value="filter" >
-        <select name="did" id="did">
-             <option value="0">&mdash; <?php echo __('All Department');?> &mdash;</option>
-             <?php
-             $sql='SELECT dept.dept_id, dept.dept_name,count(staff.staff_id) as users  '.
-                  'FROM '.DEPT_TABLE.' dept '.
-                  'INNER JOIN '.STAFF_TABLE.' staff ON(staff.dept_id=dept.dept_id) '.
-                  'GROUP By dept.dept_id HAVING users>0 ORDER BY dept_name';
-             if(($res=db_query($sql)) && db_num_rows($res)){
-                 while(list($id,$name, $users)=db_fetch_row($res)){
-                     $sel=($_REQUEST['did'] && $_REQUEST['did']==$id)?'selected="selected"':'';
-                     echo sprintf('<option value="%d" %s>%s (%s)</option>',$id,$sel,$name,$users);
-                 }
-             }
-             ?>
-        </select>
-        <select name="gid" id="gid">
-            <option value="0">&mdash; <?php echo __('All Groups');?> &mdash;</option>
-             <?php
-             $sql='SELECT grp.group_id, group_name,count(staff.staff_id) as users '.
-                  'FROM '.GROUP_TABLE.' grp '.
-                  'INNER JOIN '.STAFF_TABLE.' staff ON(staff.group_id=grp.group_id) '.
-                  'GROUP BY grp.group_id ORDER BY group_name';
-             if(($res=db_query($sql)) && db_num_rows($res)){
-                 while(list($id,$name,$users)=db_fetch_row($res)){
-                     $sel=($_REQUEST['gid'] && $_REQUEST['gid']==$id)?'selected="selected"':'';
-                     echo sprintf('<option value="%d" %s>%s (%s)</option>',$id,$sel,$name,$users);
-                 }
-             }
-             ?>
-        </select>
-        <select name="tid" id="tid">
-            <option value="0">&mdash; <?php echo __('All Teams');?> &mdash;</option>
-             <?php
-             $sql='SELECT team.team_id, team.name, count(member.staff_id) as users FROM '.TEAM_TABLE.' team '.
-                  'INNER JOIN '.TEAM_MEMBER_TABLE.' member ON(member.team_id=team.team_id) '.
-                  'GROUP BY team.team_id ORDER BY team.name';
-             if(($res=db_query($sql)) && db_num_rows($res)){
-                 while(list($id,$name,$users)=db_fetch_row($res)){
-                     $sel=($_REQUEST['tid'] && $_REQUEST['tid']==$id)?'selected="selected"':'';
-                     echo sprintf('<option value="%d" %s>%s (%s)</option>',$id,$sel,$name,$users);
-                 }
-             }
-             ?>
-        </select>
-        &nbsp;&nbsp;
-        <input type="submit" name="submit" value="<?php echo __('Apply');?>"/>
-    </form>
- </div>
-<div class="pull-right flush-right" style="padding-right:5px;"><b><a href="staff.php?a=add" class="Icon newstaff"><?php echo __('Add New Agent');?></a></b></div>
+<div id="basic_search">
+    <div style="min-height:25px;">
+        <div class="pull-left">
+            <form action="staff.php" method="GET" name="filter">
+                <input type="hidden" name="a" value="filter">
+                <select name="did" id="did">
+                    <option value="0">&mdash;
+                        <?php echo __( 'All Department');?> &mdash;</option>
+                    <?php if (($depts=Dept::getDepartments())) { foreach ($depts as $id=> $name) { $sel=($_REQUEST['did'] && $_REQUEST['did']==$id)?'selected="selected"':''; echo sprintf('
+                    <option value="%d" %s>%s</option>',$id,$sel,$name); } } ?>
+                </select>
+                <select name="tid" id="tid">
+                    <option value="0">&mdash;
+                        <?php echo __( 'All Teams');?> &mdash;</option>
+                    <?php if (($teams=Team::getTeams())) { foreach ($teams as $id=> $name) { $sel=($_REQUEST['tid'] && $_REQUEST['tid']==$id)?'selected="selected"':''; echo sprintf('
+                    <option value="%d" %s>%s</option>',$id,$sel,$name); } } ?>
+                </select>
+                <input type="submit" name="submit" class="button muted" value="<?php echo __('Apply');?>" />
+            </form>
+        </div>
+    </div>
+</div>
+<div style="margin-bottom:20px; padding-top:5px;">
+    <div class="sticky bar opaque">
+        <div class="content">
+            <div class="pull-left flush-left">
+                <h2><?php echo __('Agents');?></h2>
+            </div>
+            <div class="pull-right">
+                <a class="green button action-button" href="staff.php?a=add">
+                    <i class="icon-plus-sign"></i>
+                    <?php echo __( 'Add New Agent'); ?>
+                </a>
+                <span class="action-button" data-dropdown="#action-dropdown-more">
+                <i class="icon-caret-down pull-right"></i>
+                <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+                </span>
+                <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                    <ul id="actions">
+                        <li>
+                            <a class="confirm" data-form-id="mass-actions" data-name="enable" href="staff.php?a=enable">
+                                <i class="icon-ok-sign icon-fixed-width"></i>
+                                <?php echo __( 'Enable'); ?>
+                            </a>
+                        </li>
+                        <li>
+                            <a class="confirm" data-form-id="mass-actions" data-name="disable" href="staff.php?a=disable">
+                                <i class="icon-ban-circle icon-fixed-width"></i>
+                                <?php echo __( 'Disable'); ?>
+                            </a>
+                        </li>
+                        <li>
+                            <a class="dialog-first" data-action="permissions" href="#staff/reset-permissions">
+                                <i class="icon-sitemap icon-fixed-width"></i>
+                                <?php echo __( 'Reset Permissions'); ?>
+                            </a>
+                        </li>
+                        <li>
+                            <a class="dialog-first" data-action="department" href="#staff/change-department">
+                                <i class="icon-truck icon-fixed-width"></i>
+                                <?php echo __( 'Change Department'); ?>
+                            </a>
+                        </li>
+                        <!-- TODO: Implement "Reset Access" mass action
+                    <li><a class="dialog-first" href="#staff/reset-access">
+                    <i class="icon-puzzle-piece icon-fixed-width"></i>
+                        <?php echo __('Reset Access'); ?></a></li>
+                    -->
+                        <li class="danger">
+                            <a class="confirm" data-form-id="mass-actions" data-name="delete" href="staff.php?a=delete">
+                                <i class="icon-trash icon-fixed-width"></i>
+                                <?php echo __( 'Delete'); ?>
+                            </a>
+                        </li>
+                    </ul>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
 <div class="clear"></div>
-<?php
-$res=db_query($query);
-if($res && ($num=db_num_rows($res)))
-    $showing=$pageNav->showing() . ' ' . _N('agent', 'agents', $num);
-else
-    $showing=__('No agents found!');
-?>
-<form action="staff.php" method="POST" name="staff" >
+
+<form id="mass-actions" action="staff.php" method="POST" name="staff" >
+
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="mass_process" >
  <input type="hidden" id="action" name="a" value="" >
  <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption><?php echo $showing; ?></caption>
     <thead>
         <tr>
-            <th width="7px">&nbsp;</th>
-            <th width="200"><a <?php echo $name_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name');?></a></th>
-            <th width="100"><a <?php echo $username_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=username"><?php echo __('Username');?></a></th>
-            <th width="100"><a  <?php echo $status_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Status');?></a></th>
-            <th width="120"><a  <?php echo $group_sort; ?>href="staff.php?<?php echo $qstr; ?>&sort=group"><?php echo __('Group');?></a></th>
-            <th width="150"><a  <?php echo $dept_sort; ?>href="staff.php?<?php echo $qstr; ?>&sort=dept"><?php echo __('Department');?></a></th>
-            <th width="100"><a <?php echo $created_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=created"><?php echo __('Created');?></a></th>
-            <th width="145"><a <?php echo $login_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=login"><?php echo __('Last Login');?></a></th>
+            <th width="4%">&nbsp;</th>
+            <th width="28%"><a <?php echo $name_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name');?></a></th>
+            <th width="16%"><a <?php echo $username_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=username"><?php echo __('Username');?></a></th>
+            <th width="8%"><a  <?php echo $status_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Status');?></a></th>
+            <th width="14%"><a  <?php echo $dept_sort; ?>href="staff.php?<?php echo $qstr; ?>&sort=dept"><?php echo __('Department');?></a></th>
+            <th width="14%"><a <?php echo $created_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=created"><?php echo __('Created');?></a></th>
+            <th width="16%"><a <?php echo $login_sort; ?> href="staff.php?<?php echo $qstr; ?>&sort=login"><?php echo __('Last Login');?></a></th>
         </tr>
     </thead>
     <tbody>
     <?php
-        if($res && db_num_rows($res)):
-            $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null;
-            while ($row = db_fetch_array($res)) {
+        if ($count):
+            $ids = ($errors && is_array($_POST['ids'])) ? $_POST['ids'] : null;
+            foreach ($agents as $agent) {
+                $id = $agent->getId();
                 $sel=false;
-                if($ids && in_array($row['staff_id'],$ids))
+                if ($ids && in_array($id, $ids))
                     $sel=true;
-                $name = new PersonsName(array('first' => $row['firstname'], 'last' => $row['lastname']));
                 ?>
-               <tr id="<?php echo $row['staff_id']; ?>">
-                <td width=7px>
-                  <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['staff_id']; ?>" <?php echo $sel?'checked="checked"':''; ?> >
-                <td><a href="staff.php?id=<?php echo $row['staff_id']; ?>"><?php echo Format::htmlchars($name); ?></a>&nbsp;</td>
-                <td><?php echo $row['username']; ?></td>
-                <td><?php echo $row['isactive']?__('Active'):'<b>'.__('Locked').'</b>'; ?>&nbsp;<?php echo $row['onvacation']?'<small>(<i>'.__('vacation').'</i>)</small>':''; ?></td>
-                <td><a href="groups.php?id=<?php echo $row['group_id']; ?>"><?php echo Format::htmlchars($row['group_name']); ?></a></td>
-                <td><a href="departments.php?id=<?php echo $row['dept_id']; ?>"><?php echo Format::htmlchars($row['dept']); ?></a></td>
-                <td><?php echo Format::db_date($row['created']); ?></td>
-                <td><?php echo Format::db_datetime($row['lastlogin']); ?>&nbsp;</td>
+               <tr id="<?php echo $id; ?>">
+                <td align="center">
+                  <input type="checkbox" class="ckb" name="ids[]"
+                  value="<?php echo $id; ?>" <?php echo $sel ? 'checked="checked"' : ''; ?> >
+                <td><a href="staff.php?id=<?php echo $id; ?>"><?php echo
+                Format::htmlchars((string) $agent->getName()); ?></a></td>
+                <td><?php echo $agent->getUserName(); ?></td>
+                <td><?php echo $agent->isActive() ? __('Active') :'<b>'.__('Locked').'</b>'; ?><?php
+                    echo $agent->onvacation ? ' <small>(<i>'.__('vacation').'</i>)</small>' : ''; ?></td>
+
+                <td><a href="departments.php?id=<?php echo
+                    $agent->getDeptId(); ?>"><?php
+                    echo Format::htmlchars((string) $agent->dept); ?></a></td>
+                <td><?php echo Format::date($agent->created); ?></td>
+                <td><?php echo Format::relativeTime(Misc::db2gmtime($agent->lastlogin)) ?: '<em class="faded">'.__('never').'</em>'; ?></td>
                </tr>
             <?php
-            } //end of while.
+            } //end of foreach
         endif; ?>
     <tfoot>
      <tr>
         <td colspan="8">
-            <?php if($res && $num){ ?>
+            <?php if ($count) { ?>
             <?php echo __('Select');?>:&nbsp;
             <a id="selectAll" href="#ckb"><?php echo __('All');?></a>&nbsp;&nbsp;
             <a id="selectNone" href="#ckb"><?php echo __('None');?></a>&nbsp;&nbsp;
@@ -185,18 +224,9 @@ else
     </tfoot>
 </table>
 <?php
-if($res && $num): //Show options..
+if ($count) { //Show options..
     echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
-?>
-<p class="centered" id="actions">
-    <input class="button" type="submit" name="enable" value="<?php echo __('Enable');?>" >
-    &nbsp;&nbsp;
-    <input class="button" type="submit" name="disable" value="<?php echo __('Lock');?>" >
-    &nbsp;&nbsp;
-    <input class="button" type="submit" name="delete" value="<?php echo __('Delete');?>">
-</p>
-<?php
-endif;
+}
 ?>
 </form>
 
@@ -231,3 +261,33 @@ endif;
     <div class="clear"></div>
 </div>
 
+<script type="text/javascript">
+$(document).on('click', 'a.dialog-first', function(e) {
+    e.preventDefault();
+    var action = $(this).data('action'),
+        $form = $('form#mass-actions');
+    if ($(':checkbox.ckb:checked', $form).length == 0) {
+        $.sysAlert(__('Oops'),
+            __('You need to select at least one item'));
+        return false;
+    }
+    ids = $form.find('.ckb');
+    $.dialog('ajax.php/' + $(this).attr('href').substr(1), 201, function (xhr, data) {
+        $form.find('#action').val(action);
+        data = JSON.parse(data);
+        if (data)
+            $.each(data, function(k, v) {
+              if (v.length) {
+                  $.each(v, function() {
+                      $form.append($('<input type="hidden">').attr('name', k+'[]').val(this));
+                  })
+              }
+              else {
+                  $form.append($('<input type="hidden">').attr('name', k).val(v));
+              }
+          });
+          $form.submit();
+    }, { data: ids.serialize()});
+    return false;
+});
+</script>
diff --git a/include/staff/syslogs.inc.php b/include/staff/syslogs.inc.php
index e364a92ea83c28172fd5cb0667db7adc20d40dfb..13d355928e79013c16098e01964812276d454eef 100644
--- a/include/staff/syslogs.inc.php
+++ b/include/staff/syslogs.inc.php
@@ -84,42 +84,58 @@ else
     $showing=__('No logs found!');
 ?>
 
-<h2><?php echo __('System Logs');?>
-    &nbsp;<i class="help-tip icon-question-sign" href="#system_logs"></i>
-</h2>
-<div id='filter' >
- <form action="logs.php" method="get">
-    <div style="padding-left:2px;">
-        <b><?php echo __('Date Span'); ?></b>&nbsp;<i class="help-tip icon-question-sign" href="#date_span"></i>
-        <?php echo __('Between'); ?>:
-        <input class="dp" id="sd" size=15 name="startDate" value="<?php echo Format::htmlchars($_REQUEST['startDate']); ?>" autocomplete=OFF>
-        &nbsp;&nbsp;
-        <input class="dp" id="ed" size=15 name="endDate" value="<?php echo Format::htmlchars($_REQUEST['endDate']); ?>" autocomplete=OFF>
-        &nbsp;<?php echo __('Log Level'); ?>:&nbsp;<i class="help-tip icon-question-sign" href="#type"></i>
-            <select name='type'>
-                <option value="" selected><?php echo __('All');?></option>
-                <option value="Error" <?php echo ($type=='Error')?'selected="selected"':''; ?>><?php echo __('Errors');?></option>
-                <option value="Warning" <?php echo ($type=='Warning')?'selected="selected"':''; ?>><?php echo __('Warnings');?></option>
-                <option value="Debug" <?php echo ($type=='Debug')?'selected="selected"':''; ?>><?php echo __('Debug');?></option>
-            </select>
-            &nbsp;&nbsp;
-            <input type="submit" Value="<?php echo __('Go!');?>" />
+<div id="basic_search">
+    <div style="height:25px">
+        <div id='filter' >
+            <form action="logs.php" method="get">
+                <div style="padding-left:2px;">
+                    <i class="help-tip icon-question-sign" href="#date_span"></i>
+                    <?php echo __('Between'); ?>:
+                    <input class="dp" id="sd" size=15 name="startDate" value="<?php echo Format::htmlchars($_REQUEST['startDate']); ?>" autocomplete=OFF>
+                    &nbsp;&nbsp;
+                    <input class="dp" id="ed" size=15 name="endDate" value="<?php echo Format::htmlchars($_REQUEST['endDate']); ?>" autocomplete=OFF>
+                    &nbsp;<?php echo __('Log Level'); ?>:&nbsp;<i class="help-tip icon-question-sign" href="#type"></i>
+                    <select name='type'>
+                        <option value="" selected><?php echo __('All');?></option>
+                        <option value="Error" <?php echo ($type=='Error')?'selected="selected"':''; ?>><?php echo __('Errors');?></option>
+                        <option value="Warning" <?php echo ($type=='Warning')?'selected="selected"':''; ?>><?php echo __('Warnings');?></option>                <option value="Debug" <?php echo ($type=='Debug')?'selected="selected"':''; ?>><?php echo __('Debug');?></option>
+                    </select>
+                    &nbsp;&nbsp;
+                    <input type="submit" Value="<?php echo __('Go!');?>" />
+                </div>
+            </form>
+        </div>
     </div>
- </form>
 </div>
+<div class="clear"></div>
 <form action="logs.php" method="POST" name="logs">
+    <div style="margin-bottom:20px; padding-top:5px;">
+        <div class="sticky bar opaque">
+            <div class="content">
+                <div class="pull-left flush-left">
+                    <h2><?php echo __('System Logs');?>
+            <i class="help-tip icon-question-sign" href="#system_logs"></i>
+            </h2>
+                </div>
+                <div id="actions" class="pull-right flush-right">
+                    <button class="red button" type="submit" name="delete"><i class="icon-trash"></i>
+                        <?php echo __( 'Delete Selected Entries');?>
+                    </button>
+                </div>
+            </div>
+        </div>
+    </div>
 <?php csrf_token(); ?>
  <input type="hidden" name="do" value="mass_process" >
  <input type="hidden" id="action" name="a" value="" >
  <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption><?php echo $showing; ?></caption>
     <thead>
         <tr>
-            <th width="7">&nbsp;</th>
-            <th width="320"><a <?php echo $title_sort; ?> href="logs.php?<?php echo $qstr; ?>&sort=title"><?php echo __('Log Title');?></a></th>
-            <th width="100"><a  <?php echo $type_sort; ?> href="logs.php?<?php echo $qstr; ?>&sort=type"><?php echo __('Log Type');?></a></th>
-            <th width="200" nowrap><a  <?php echo $date_sort; ?>href="logs.php?<?php echo $qstr; ?>&sort=date"><?php echo __('Log Date');?></a></th>
-            <th width="120"><a  <?php echo $ip_sort; ?> href="logs.php?<?php echo $qstr; ?>&sort=ip"><?php echo __('IP Address');?></a></th>
+            <th width="4%">&nbsp;</th>
+            <th width="40%"><a <?php echo $title_sort; ?> href="logs.php?<?php echo $qstr; ?>&sort=title"><?php echo __('Log Title');?></a></th>
+            <th width="11%"><a  <?php echo $type_sort; ?> href="logs.php?<?php echo $qstr; ?>&sort=type"><?php echo __('Log Type');?></a></th>
+            <th width="30%" nowrap><a  <?php echo $date_sort; ?>href="logs.php?<?php echo $qstr; ?>&sort=date"><?php echo __('Log Date');?></a></th>
+            <th width="15%"><a  <?php echo $ip_sort; ?> href="logs.php?<?php echo $qstr; ?>&sort=ip"><?php echo __('IP Address');?></a></th>
         </tr>
     </thead>
     <tbody>
@@ -133,12 +149,12 @@ else
                     $sel=true;
                 ?>
             <tr id="<?php echo $row['log_id']; ?>">
-                <td width=7px>
+                <td align="center" nowrap>
                   <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['log_id']; ?>"
                             <?php echo $sel?'checked="checked"':''; ?>> </td>
                 <td>&nbsp;<a class="tip" href="#log/<?php echo $row['log_id']; ?>"><?php echo Format::htmlchars($row['title']); ?></a></td>
                 <td><?php echo $row['log_type']; ?></td>
-                <td>&nbsp;<?php echo Format::db_daydatetime($row['created']); ?></td>
+                <td>&nbsp;<?php echo Format::daydatetime($row['created']); ?></td>
                 <td><?php echo $row['ip_address']; ?></td>
             </tr>
             <?php
@@ -164,9 +180,7 @@ else
 if($res && $num): //Show options..
     echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
 ?>
-<p class="centered" id="actions">
-    <input class="button" type="submit" name="delete" value="<?php echo __('Delete Selected Entries');?>">
-</p>
+
 <?php
 endif;
 ?>
diff --git a/include/staff/system.inc.php b/include/staff/system.inc.php
index 144b9a74ab6739ab4396e7ad603ef75eff3fd918..ee70faefb3dc4fd878342e65edf65bf28865d34f 100644
--- a/include/staff/system.inc.php
+++ b/include/staff/system.inc.php
@@ -33,15 +33,26 @@ $extensions = array(
             'name' => 'phar',
             'desc' => __('Highly recommended for plugins and language packs')
             ),
+        'intl' => array(
+            'name' => 'intl',
+            'desc' => __('Highly recommended for non western european language content')
+            ),
         'fileinfo' => array(
             'name' => 'fileinfo',
             'desc' => __('Used to detect file types for uploads')
             ),
+        'apcu' => array(
+            'name' => 'APCu',
+            'desc' => __('Improves overall performance')
+            ),
+        'Zend Opcache' => array(
+            'name' => 'Zend Opcache',
+            'desc' => __('Improves overall performance')
+            ),
         );
 
 ?>
 <h2><?php echo __('About this osTicket Installation'); ?></h2>
-<br/>
 <table class="list" width="100%";>
 <thead>
     <tr><th colspan="2"><?php echo __('Server Information'); ?></th></tr>
@@ -129,7 +140,7 @@ if (!$lv) { ?>
 </thead>
 <tbody>
     <tr><td><?php echo __('Schema'); ?></td>
-        <td><?php echo sprintf('<span class="ltr">%s (%s)</span>', DBNAME, DBHOST); ?> </td>
+        <td><?php echo sprintf('<span class="ltr">%s (%s)</span>', DBNAME, DBHOST); ?> </td></tr>
     </tr>
     <tr><td><?php echo __('Schema Signature'); ?></td>
         <td><?php echo $cfg->getSchemaSignature(); ?> </td>
@@ -145,7 +156,13 @@ if (!$lv) { ?>
         <td><?php
         $sql = 'SELECT SUM(LENGTH(filedata)) / 1048576 FROM '.FILE_CHUNK_TABLE;
         $space = db_result(db_query($sql));
-        echo sprintf('%.2f MiB', $space); ?></td>
+        echo sprintf('%.2f MiB', $space); ?></td></tr>
+    <tr><td><?php echo __('Timezone'); ?></td>
+        <td><?php echo $dbtz = db_timezone(); ?>
+          <?php if ($cfg->getDbTimezone() != $dbtz) { ?>
+            (<?php echo sprintf(__('Interpreted as %s'), $cfg->getDbTimezone()); ?>)
+          <?php } ?>
+        </td></tr>
 </tbody>
 </table>
 <br/>
diff --git a/include/staff/task-view.inc.php b/include/staff/task-view.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..a6b140481ad83effa08d99218803327a19c814e4
--- /dev/null
+++ b/include/staff/task-view.inc.php
@@ -0,0 +1,5 @@
+<div id="task_content">
+<?php
+require STAFFINC_DIR.'templates/task-view.tmpl.php';
+?>
+</div>
diff --git a/include/staff/tasks.inc.php b/include/staff/tasks.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..6d52d61fa2952b5413bd9eb6a62aebd71891e3ad
--- /dev/null
+++ b/include/staff/tasks.inc.php
@@ -0,0 +1,509 @@
+<?php
+$tasks = Task::objects();
+$date_header = $date_col = false;
+
+// Make sure the cdata materialized view is available
+TaskForm::ensureDynamicDataView();
+
+// Figure out REFRESH url — which might not be accurate after posting a
+// response
+list($path,) = explode('?', $_SERVER['REQUEST_URI'], 2);
+$args = array();
+parse_str($_SERVER['QUERY_STRING'], $args);
+
+// Remove commands from query
+unset($args['id']);
+unset($args['a']);
+
+$refresh_url = $path . '?' . http_build_query($args);
+
+$sort_options = array(
+    'updated' =>            __('Most Recently Updated'),
+    'created' =>            __('Most Recently Created'),
+    'due' =>                __('Due Soon'),
+    'number' =>             __('Task Number'),
+    'closed' =>             __('Most Recently Closed'),
+    'hot' =>                __('Longest Thread'),
+    'relevance' =>          __('Relevance'),
+);
+
+// Queues columns
+
+$queue_columns = array(
+        'number' => array(
+            'width' => '8%',
+            'heading' => __('Number'),
+            ),
+        'date' => array(
+            'width' => '20%',
+            'heading' => __('Date Created'),
+            'sort_col' => 'created',
+            ),
+        'title' => array(
+            'width' => '38%',
+            'heading' => __('Title'),
+            'sort_col' => 'cdata__title',
+            ),
+        'dept' => array(
+            'width' => '16%',
+            'heading' => __('Department'),
+            'sort_col'  => 'dept__name',
+            ),
+        'assignee' => array(
+            'width' => '16%',
+            'heading' => __('Agent'),
+            ),
+        );
+
+
+
+// Queue we're viewing
+$queue_key = sprintf('::Q:%s', ObjectModel::OBJECT_TYPE_TASK);
+$queue_name = $_SESSION[$queue_key] ?: '';
+
+switch ($queue_name) {
+case 'closed':
+    $status='closed';
+    $results_type=__('Completed Tasks');
+    $showassigned=true; //closed by.
+    $queue_sort_options = array('closed', 'updated', 'created', 'number', 'hot');
+
+    break;
+case 'overdue':
+    $status='open';
+    $results_type=__('Overdue Tasks');
+    $tasks->filter(array('isoverdue'=>1));
+    $queue_sort_options = array('updated', 'created', 'number', 'hot');
+    break;
+case 'assigned':
+    $status='open';
+    $staffId=$thisstaff->getId();
+    $results_type=__('My Tasks');
+    $tasks->filter(array('staff_id'=>$thisstaff->getId()));
+    $queue_sort_options = array('updated', 'created', 'hot', 'number');
+    break;
+default:
+case 'search':
+    $queue_sort_options = array('closed', 'updated', 'created', 'number', 'hot');
+    // Consider basic search
+    if ($_REQUEST['query']) {
+        $results_type=__('Search Results');
+        $tasks = $tasks->filter(Q::any(array(
+            'number__startswith' => $_REQUEST['query'],
+            'cdata__title__contains' => $_REQUEST['query'],
+        )));
+        unset($_SESSION[$queue_key]);
+        break;
+    } elseif (isset($_SESSION['advsearch:tasks'])) {
+        // XXX: De-duplicate and simplify this code
+        $form = $search->getFormFromSession('advsearch:tasks');
+        $form->loadState($_SESSION['advsearch:tasks']);
+        $tasks = $search->mangleQuerySet($tasks, $form);
+        $results_type=__('Advanced Search')
+            . '<a class="action-button" href="?clear_filter"><i class="icon-ban-circle"></i> <em>' . __('clear') . '</em></a>';
+        break;
+    }
+    // Fall-through and show open tickets
+case 'open':
+    $status='open';
+    $results_type=__('Open Tasks');
+    $queue_sort_options = array('created', 'updated', 'due', 'number', 'hot');
+    break;
+}
+
+// Apply filters
+$filters = array();
+if ($status) {
+    $SQ = new Q(array('flags__hasbit' => TaskModel::ISOPEN));
+    if (!strcasecmp($status, 'closed'))
+        $SQ->negate();
+
+    $filters[] = $SQ;
+}
+
+if ($filters)
+    $tasks->filter($filters);
+
+// Impose visibility constraints
+// ------------------------------------------------------------
+// -- Open and assigned to me
+$visibility = array(
+    new Q(array('flags__hasbit' => TaskModel::ISOPEN, 'staff_id' => $thisstaff->getId()))
+);
+// -- Routed to a department of mine
+if (!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts()))
+    $visibility[] = new Q(array('dept_id__in' => $depts));
+// -- Open and assigned to a team of mine
+if (($teams = $thisstaff->getTeams()) && count(array_filter($teams)))
+    $visibility[] = new Q(array(
+        'team_id__in' => array_filter($teams),
+        'flags__hasbit' => TaskModel::ISOPEN
+    ));
+$tasks->filter(Q::any($visibility));
+
+// Add in annotations
+$tasks->annotate(array(
+    'collab_count' => SqlAggregate::COUNT('thread__collaborators', true),
+    'attachment_count' => SqlAggregate::COUNT(SqlCase::N()
+       ->when(new SqlField('thread__entries__attachments__inline'), null)
+       ->otherwise(new SqlField('thread__entries__attachments')),
+        true
+    ),
+    'thread_count' => SqlAggregate::COUNT(SqlCase::N()
+        ->when(
+            new Q(array('thread__entries__flags__hasbit'=>ThreadEntry::FLAG_HIDDEN)),
+            null)
+        ->otherwise(new SqlField('thread__entries__id')),
+       true
+    ),
+));
+
+$tasks->values('id', 'number', 'created', 'staff_id', 'team_id',
+        'staff__firstname', 'staff__lastname', 'team__name',
+        'dept__name', 'cdata__title', 'flags');
+// Apply requested quick filter
+
+$queue_sort_key = sprintf(':Q%s:%s:sort', ObjectModel::OBJECT_TYPE_TASK, $queue_name);
+
+if (isset($_GET['sort'])) {
+    $_SESSION[$queue_sort_key] = array($_GET['sort'], $_GET['dir']);
+}
+elseif (!isset($_SESSION[$queue_sort_key])) {
+    $_SESSION[$queue_sort_key] = array($queue_sort_options[0], 0);
+}
+
+list($sort_cols, $sort_dir) = $_SESSION[$queue_sort_key];
+$orm_dir = $sort_dir ? QuerySet::ASC : QuerySet::DESC;
+$orm_dir_r = $sort_dir ? QuerySet::DESC : QuerySet::ASC;
+
+switch ($sort_cols) {
+case 'number':
+    $queue_columns['number']['sort_dir'] = $sort_dir;
+    $tasks->extra(array(
+        'order_by'=>array(
+            array(SqlExpression::times(new SqlField('number'), 1), $orm_dir)
+        )
+    ));
+    break;
+case 'due':
+    $queue_columns['date']['heading'] = __('Due Date');
+    $queue_columns['date']['sort'] = 'due';
+    $queue_columns['date']['sort_col'] = $date_col = 'duedate';
+    $tasks->values('duedate');
+    $tasks->order_by(SqlFunction::COALESCE(new SqlField('duedate'), 'zzz'), $orm_dir_r);
+    break;
+case 'closed':
+    $queue_columns['date']['heading'] = __('Date Closed');
+    $queue_columns['date']['sort'] = $sort_cols;
+    $queue_columns['date']['sort_col'] = $date_col = 'closed';
+    $queue_columns['date']['sort_dir'] = $sort_dir;
+    $tasks->values('closed');
+    $tasks->order_by($sort_dir ? 'closed' : '-closed');
+    break;
+case 'updated':
+    $queue_columns['date']['heading'] = __('Last Updated');
+    $queue_columns['date']['sort'] = $sort_cols;
+    $queue_columns['date']['sort_col'] = $date_col = 'updated';
+    $tasks->values('updated');
+    $tasks->order_by($sort_dir ? 'updated' : '-updated');
+    break;
+case 'hot':
+    $tasks->order_by('-thread_count');
+    $tasks->annotate(array(
+        'thread_count' => SqlAggregate::COUNT('thread__entries'),
+    ));
+    break;
+case 'assignee':
+    $tasks->order_by('staff__lastname', $orm_dir);
+    $tasks->order_by('staff__firstname', $orm_dir);
+    $tasks->order_by('team__name', $orm_dir);
+    $queue_columns['assignee']['sort_dir'] = $sort_dir;
+    break;
+default:
+    if ($sort_cols && isset($queue_columns[$sort_cols])) {
+        $queue_columns[$sort_cols]['sort_dir'] = $sort_dir;
+        if (isset($queue_columns[$sort_cols]['sort_col']))
+            $sort_cols = $queue_columns[$sort_cols]['sort_col'];
+        $tasks->order_by($sort_cols, $orm_dir);
+        break;
+    }
+case 'created':
+    $queue_columns['date']['heading'] = __('Date Created');
+    $queue_columns['date']['sort'] = 'created';
+    $queue_columns['date']['sort_col'] = $date_col = 'created';
+    $tasks->order_by($sort_dir ? 'created' : '-created');
+    break;
+}
+
+if (in_array($sort_cols, array('created', 'due', 'updated')))
+    $queue_columns['date']['sort_dir'] = $sort_dir;
+
+// Apply requested pagination
+$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
+$count = $tasks->count();
+$pageNav=new Pagenate($count, $page, PAGE_LIMIT);
+$pageNav->setURL('tasks.php', $args);
+$tasks = $pageNav->paginate($tasks);
+
+TaskForm::ensureDynamicDataView();
+
+// Save the query to the session for exporting
+$_SESSION[':Q:tasks'] = $tasks;
+
+// Mass actions
+$actions = array();
+
+if ($thisstaff->hasPerm(Task::PERM_ASSIGN, false)) {
+    $actions += array(
+            'assign' => array(
+                'icon' => 'icon-user',
+                'action' => __('Assign Tasks')
+            ));
+}
+
+if ($thisstaff->hasPerm(Task::PERM_TRANSFER, false)) {
+    $actions += array(
+            'transfer' => array(
+                'icon' => 'icon-share',
+                'action' => __('Transfer Tasks')
+            ));
+}
+
+if ($thisstaff->hasPerm(Task::PERM_DELETE, false)) {
+    $actions += array(
+            'delete' => array(
+                'icon' => 'icon-trash',
+                'action' => __('Delete Tasks')
+            ));
+}
+
+
+?>
+<!-- SEARCH FORM START -->
+<div id='basic_search'>
+  <div class="pull-right" style="height:25px">
+    <span class="valign-helper"></span>
+    <?php
+        require STAFFINC_DIR.'templates/queue-sort.tmpl.php';
+    ?>
+   </div>
+    <form action="tasks.php" method="get" onsubmit="javascript:
+        $.pjax({
+        url:$(this).attr('action') + '?' + $(this).serialize(),
+        container:'#pjax-container',
+        timeout: 2000
+        });
+        return false;">
+        <input type="hidden" name="a" value="search">
+        <input type="hidden" name="search-type" value=""/>
+        <div class="attached input">
+            <input type="text" class="basic-search" data-url="ajax.php/tasks/lookup" name="query"
+                   autofocus size="30" value="<?php echo Format::htmlchars($_REQUEST['query'], true); ?>"
+                   autocomplete="off" autocorrect="off" autocapitalize="off">
+            <button type="submit" class="attached button"><i class="icon-search"></i>
+            </button>
+        </div>
+    </form>
+
+</div>
+<!-- SEARCH FORM END -->
+<div class="clear"></div>
+<div style="margin-bottom:20px; padding-top:5px;">
+<div class="sticky bar opaque">
+    <div class="content">
+        <div class="pull-left flush-left">
+            <h2><a href="<?php echo $refresh_url; ?>"
+                title="<?php echo __('Refresh'); ?>"><i class="icon-refresh"></i> <?php echo
+                $results_type.$showing; ?></a></h2>
+        </div>
+        <div class="pull-right flush-right">
+           <?php
+           if ($count)
+                echo Task::getAgentActions($thisstaff, array('status' => $status));
+            ?>
+        </div>
+    </div>
+</div>
+<div class="clear"></div>
+<form action="tasks.php" method="POST" name='tasks' id="tasks">
+<?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'], true); ?>" >
+ <table class="list" border="0" cellspacing="1" cellpadding="2" width="940">
+    <thead>
+        <tr>
+            <?php if ($thisstaff->canManageTickets()) { ?>
+	        <th width="4%">&nbsp;</th>
+            <?php } ?>
+
+            <?php
+            // Query string
+            unset($args['sort'], $args['dir'], $args['_pjax']);
+            $qstr = Http::build_query($args);
+            // Show headers
+            foreach ($queue_columns as $k => $column) {
+                echo sprintf( '<th width="%s"><a href="?sort=%s&dir=%s&%s"
+                        class="%s">%s</a></th>',
+                        $column['width'],
+                        $column['sort'] ?: $k,
+                        $column['sort_dir'] ? 0 : 1,
+                        $qstr,
+                        isset($column['sort_dir'])
+                        ? ($column['sort_dir'] ? 'asc': 'desc') : '',
+                        $column['heading']);
+            }
+            ?>
+        </tr>
+     </thead>
+     <tbody>
+        <?php
+        // Setup Subject field for display
+        $total=0;
+        $title_field = TaskForm::getInstance()->getField('title');
+        $ids=($errors && $_POST['tids'] && is_array($_POST['tids']))?$_POST['tids']:null;
+        foreach ($tasks as $T) {
+            $T['isopen'] = ($T['flags'] & TaskModel::ISOPEN != 0); //XXX:
+            $total += 1;
+            $tag=$T['staff_id']?'assigned':'openticket';
+            $flag=null;
+            if($T['lock__staff_id'] && $T['lock__staff_id'] != $thisstaff->getId())
+                $flag='locked';
+            elseif($T['isoverdue'])
+                $flag='overdue';
+
+            $assignee = '';
+            $dept = Dept::getLocalById($T['dept_id'], 'name', $T['dept__name']);
+            $assinee ='';
+            if ($T['staff_id']) {
+                $staff =  new AgentsName($T['staff__firstname'].' '.$T['staff__lastname']);
+                $assignee = sprintf('<span class="Icon staffAssigned">%s</span>',
+                        Format::truncate((string) $staff, 40));
+            } elseif($T['team_id']) {
+                $assignee = sprintf('<span class="Icon teamAssigned">%s</span>',
+                    Format::truncate(Team::getLocalById($T['team_id'], 'name', $T['team__name']),40));
+            }
+
+            $threadcount=$T['thread_count'];
+            $number = $T['number'];
+            if ($T['isopen'])
+                $number = sprintf('<b>%s</b>', $number);
+
+            $title = Format::truncate($title_field->display($title_field->to_php($T['cdata__title'])), 40);
+            ?>
+            <tr id="<?php echo $T['id']; ?>">
+                <?php
+                if ($thisstaff->canManageTickets()) {
+                    $sel = false;
+                    if ($ids && in_array($T['id'], $ids))
+                        $sel = true;
+                    ?>
+                <td align="center" class="nohover">
+                    <input class="ckb" type="checkbox" name="tids[]"
+                        value="<?php echo $T['id']; ?>" <?php echo $sel?'checked="checked"':''; ?>>
+                </td>
+                <?php } ?>
+                <td nowrap>
+                  <a class="preview"
+                    href="tasks.php?id=<?php echo $T['id']; ?>"
+                    data-preview="#tasks/<?php echo $T['id']; ?>/preview"
+                    ><?php echo $number; ?></a></td>
+                <td align="center" nowrap><?php echo
+                Format::datetime($T[$date_col ?: 'created']); ?></td>
+                <td><a <?php if ($flag) { ?> class="Icon <?php echo $flag; ?>Ticket" title="<?php echo ucfirst($flag); ?> Ticket" <?php } ?>
+                    href="tasks.php?id=<?php echo $T['id']; ?>"><?php
+                    echo $title; ?></a>
+                     <?php
+                        if ($threadcount>1)
+                            echo "<small>($threadcount)</small>&nbsp;".'<i
+                                class="icon-fixed-width icon-comments-alt"></i>&nbsp;';
+                        if ($T['collab_count'])
+                            echo '<i class="icon-fixed-width icon-group faded"></i>&nbsp;';
+                        if ($T['attachment_count'])
+                            echo '<i class="icon-fixed-width icon-paperclip"></i>&nbsp;';
+                    ?>
+                </td>
+                <td nowrap>&nbsp;<?php echo Format::truncate($dept, 40); ?></td>
+                <td nowrap>&nbsp;<?php echo $assignee; ?></td>
+            </tr>
+            <?php
+            } //end of foreach
+        if (!$total)
+            $ferror=__('There are no tasks matching your criteria.');
+        ?>
+    </tbody>
+    <tfoot>
+     <tr>
+        <td colspan="6">
+            <?php if($total && $thisstaff->canManageTickets()){ ?>
+            <?php echo __('Select');?>:&nbsp;
+            <a id="selectAll" href="#ckb"><?php echo __('All');?></a>&nbsp;&nbsp;
+            <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 '<i>';
+                echo $ferror?Format::htmlchars($ferror):__('Query returned 0 results.');
+                echo '</i>';
+            } ?>
+        </td>
+     </tr>
+    </tfoot>
+    </table>
+    <?php
+    if ($total>0) { //if we actually had any tasks returned.
+        echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;';
+        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>
+
+<div style="display:none;" class="dialog" id="confirm-action">
+    <h3><?php echo __('Please Confirm');?></h3>
+    <a class="close" href=""><i class="icon-remove-circle"></i></a>
+    <hr/>
+    <p class="confirm-action" style="display:none;" id="mark_overdue-confirm">
+        <?php echo __('Are you sure want to flag the selected tasks as <font color="red"><b>overdue</b></font>?');?>
+    </p>
+    <div><?php echo __('Please confirm to continue.');?></div>
+    <hr style="margin-top:1em"/>
+    <p class="full-width">
+        <span class="buttons pull-left">
+            <input type="button" value="<?php echo __('No, Cancel');?>" class="close">
+        </span>
+        <span class="buttons pull-right">
+            <input type="button" value="<?php echo __('Yes, Do it!');?>" class="confirm">
+        </span>
+     </p>
+    <div class="clear"></div>
+</div>
+<script type="text/javascript">
+$(function() {
+
+    $(document).off('.new-task');
+    $(document).on('click.new-task', 'a.new-task', function(e) {
+        e.preventDefault();
+        var url = 'ajax.php/'
+        +$(this).attr('href').substr(1)
+        +'?_uid='+new Date().getTime();
+        var $options = $(this).data('dialogConfig');
+        $.dialog(url, [201], function (xhr) {
+            var tid = parseInt(xhr.responseText);
+            if (tid) {
+                 window.location.href = 'tasks.php?id='+tid;
+            } else {
+                $.pjax.reload('#pjax-container');
+            }
+        }, $options);
+
+        return false;
+    });
+
+    $('[data-toggle=tooltip]').tooltip();
+});
+</script>
diff --git a/include/staff/team.inc.php b/include/staff/team.inc.php
index db953b5a060c0954fab412ab2b5b9668e54e4310..3552d2cc7f925ba106ae85c3e73a7315bd6464f0 100644
--- a/include/staff/team.inc.php
+++ b/include/staff/team.inc.php
@@ -1,37 +1,52 @@
 <?php
 if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
-$info = $qs = array();
-if($team && $_REQUEST['a']!='add'){
+$info = $members = $qs = array();
+if ($team && $_REQUEST['a']!='add') {
     //Editing Team
     $title=__('Update Team');
     $action='update';
     $submit_text=__('Save Changes');
-    $info=$team->getInfo();
-    $info['id']=$team->getId();
+    $trans['name'] = $team->getTranslateTag('name');
+    $members = $team->getMembers();
     $qs += array('id' => $team->getId());
-}else {
+} else {
     $title=__('Add New Team');
     $action='create';
     $submit_text=__('Create Team');
-    $info['isenabled']=1;
-    $info['noalerts']=0;
+    if (!$team) {
+        $team = Team::create(array(
+            'flags' => Team::FLAG_ENABLED,
+        ));
+    }
     $qs += array('a' => $_REQUEST['a']);
 }
-$info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
+
+$info = $team->getInfo();
 ?>
 <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']); ?>">
- <input type="hidden" name="id" value="<?php echo $info['id']; ?>">
- <h2><?php echo __('Team');?></h2>
+ <input type="hidden" name="id" value="<?php echo $team->getId(); ?>">
+ <h2><?php echo $title; ?>
+    <?php if (isset($team->name)) { ?><small>
+    — <?php echo $team->getName(); ?></small>
+    <?php } ?>
     <i class="help-tip icon-question-sign" href="#teams"></i>
-    </h2>
+</h2>
+<br>
+<ul class="clean tabs">
+    <li class="active"><a href="#team">
+        <i class="icon-file"></i> <?php echo __('Team'); ?></a></li>
+    <li><a href="#members">
+        <i class="icon-group"></i> <?php echo __('Members'); ?></a></li>
+</ul>
+
+<div id="team" class="tab_content">
  <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
     <thead>
         <tr>
             <th colspan="2">
-                <h4><?php echo $title; ?></h4>
                 <em><strong><?php echo __('Team Information'); ?></strong>:</em>
             </th>
         </tr>
@@ -42,7 +57,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <?php echo __('Name');?>:
             </td>
             <td>
-                <input type="text" size="30" name="name" value="<?php echo $info['name']; ?>">
+                <input type="text" size="30" name="name" value="<?php echo Format::htmlchars($team->name); ?>"
+                    autofocus data-translate-tag="<?php echo $trans['name']; ?>"/>
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['name']; ?></span>
             </td>
         </tr>
@@ -52,9 +68,9 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
             </td>
             <td>
                 <span>
-                <input type="radio" name="isenabled" value="1" <?php echo $info['isenabled']?'checked="checked"':''; ?>><strong><?php echo __('Active');?></strong>
+                <input type="radio" name="isenabled" value="1" <?php echo $team->isEnabled()?'checked="checked"':''; ?>><strong><?php echo __('Active');?></strong>
                 &nbsp;
-                <input type="radio" name="isenabled" value="0" <?php echo !$info['isenabled']?'checked="checked"':''; ?>><?php echo __('Disabled');?>
+                <input type="radio" name="isenabled" value="0" <?php echo !$team->isEnabled()?'checked="checked"':''; ?>><?php echo __('Disabled');?>
                 &nbsp;<span class="error">*&nbsp;</span>
                 <i class="help-tip icon-question-sign" href="#status"></i>
                 </span>
@@ -66,17 +82,15 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
             </td>
             <td>
                 <span>
-                <select name="lead_id">
+                <select id="team-lead-select" name="lead_id" data-quick-add="staff">
                     <option value="0">&mdash; <?php echo __('None');?> &mdash;</option>
-                    <option value="" disabled="disabled"><?php echo __('Select Team Lead (Optional)');?></option>
-                    <?php
-                    if($team && ($members=$team->getMembers())){
+<?php               if ($members) {
                         foreach($members as $k=>$staff){
-                            $selected=($info['lead_id'] && $staff->getId()==$info['lead_id'])?'selected="selected"':'';
+                            $selected=($team->lead_id && $staff->getId()==$team->lead_id)?'selected="selected"':'';
                             echo sprintf('<option value="%d" %s>%s</option>',$staff->getId(),$selected,$staff->getName());
                         }
-                    }
-                    ?>
+                    } ?>
+                    <option value="0" data-quick-add>&mdash; <?php echo __('Add New');?> &mdash;</option>
                 </select>
                 &nbsp;<span class="error"><?php echo $errors['lead_id']; ?></span>
                 <i class="help-tip icon-question-sign" href="#lead"></i>
@@ -88,30 +102,11 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <?php echo __('Assignment Alert');?>:
             </td>
             <td>
-                <input type="checkbox" name="noalerts" value="1" <?php echo $info['noalerts']?'checked="checked"':''; ?> >
+                <input type="checkbox" name="noalerts" value="1" <?php echo !$team->alertsEnabled()?'checked="checked"':''; ?> >
                 <?php echo __('<strong>Disable</strong> for this Team'); ?>
                 <i class="help-tip icon-question-sign" href="#assignment_alert"></i>
             </td>
         </tr>
-        <?php
-        if($team && ($members=$team->getMembers())){ ?>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('Team Members'); ?></strong>:
-                <i class="help-tip icon-question-sign" href="#members"></i>
-</em>
-            </th>
-        </tr>
-        <?php
-            foreach($members as $k=>$staff){
-                echo sprintf('<tr><td colspan=2><span style="width:350px;padding-left:5px; display:block;" class="pull-left">
-                            <b><a href="staff.php?id=%d">%s</a></span></b>
-                            &nbsp;<input type="checkbox" name="remove[]" value="%d"><i>'.__('Remove').'</i></td></tr>',
-                          $staff->getId(),$staff->getName(),$staff->getId());
-
-
-            }
-        } ?>
         <tr>
             <th colspan="2">
                 <em><strong><?php echo __('Admin Notes');?></strong>: <?php echo __('Internal notes viewable by all admins.');?>&nbsp;</em>
@@ -120,14 +115,131 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
         <tr>
             <td colspan=2>
                 <textarea class="richtext no-bar" name="notes" cols="21"
-                    rows="8" style="width: 80%;"><?php echo $info['notes']; ?></textarea>
+                    rows="8" style="width: 80%;"><?php echo Format::htmlchars($team->notes); ?></textarea>
             </td>
         </tr>
     </tbody>
 </table>
+</div>
+
+<?php
+$agents = Staff::getStaffMembers();
+foreach ($members as $m)
+    unset($agents[$m->staff_id]);
+?>
+
+<div id="members" class="tab_content" style="display:none">
+   <table class="two-column table" width="100%">
+    <tbody>
+        <tr class="header">
+            <td colspan="2">
+                <?php echo __('Team Members'); ?>
+                <div><small>
+                <?php echo __('Agents who are members of this team'); ?>
+                <i class="help-tip icon-question-sign" href="#members"></i>
+                </small></div>
+            </td>
+        </tr>
+      <tr id="add_member">
+        <td colspan="2">
+          <i class="icon-plus-sign"></i>
+          <select id="add_access" data-quick-add="staff">
+            <option value="0">&mdash; <?php echo __('Select Agent');?> &mdash;</option>
+            <?php
+            foreach ($agents as $id=>$name) {
+              echo sprintf('<option value="%d">%s</option>',$id,Format::htmlchars($name));
+            }
+            ?>
+            <option value="0" data-quick-add>&mdash; <?php echo __('Add New');?> &mdash;</option>
+          </select>
+          <button type="button" class="action-button">
+            <?php echo __('Add'); ?>
+          </button>
+        </td>
+      </tr>
+    </tbody>
+    <tbody>
+      <tr id="member_template" class="hidden">
+        <td>
+          <input type="hidden" data-name="members[]" value="" />
+        </td>
+        <td>
+          <label>
+            <input type="checkbox" data-name="member_alerts" value="1" />
+            <?php echo __('Alerts'); ?>
+          </label>
+          <a href="#" class="pull-right drop-membership" title="<?php echo __('Delete');
+            ?>"><i class="icon-trash"></i></a>
+        </td>
+      </tr>
+    </tbody>
+   </table>
+</div>
+
 <p style="text-align:center">
     <input type="submit" name="submit" value="<?php echo $submit_text; ?>">
     <input type="reset"  name="reset"  value="<?php echo __('Reset');?>">
-    <input type="button" name="cancel" value="<?php echo __('Cancel');?>" onclick='window.location.href="teams.php"'>
+    <input type="button" name="cancel" value="<?php echo __('Cancel');?>" onclick='window.location.href="?"'>
 </p>
 </form>
+
+<script type="text/javascript">
+var addMember = function(staffid, name, alerts, error) {
+  if (!staffid) return;
+  var copy = $('#member_template').clone();
+
+  copy.find('[data-name=members\\[\\]]')
+    .attr('name', 'members[]')
+    .val(staffid);
+  copy.find('[data-name^=member_alerts]')
+    .attr('name', 'member_alerts['+staffid+']')
+    .prop('checked', alerts);
+  copy.find('td:first').append(document.createTextNode(name));
+  copy.attr('id', '').show().insertBefore($('#add_member'));
+  copy.removeClass('hidden')
+  if (error)
+      $('<div class="error">').text(error).appendTo(copy.find('td:last'));
+};
+
+$('#add_member').find('button').on('click', function() {
+  var selected = $('#add_access').find(':selected'),
+      id = parseInt(selected.val());
+  if (!id)
+    return;
+  addMember(id, selected.text(), true);
+  if ($('#team-lead-select option[value='+id+']').length === 0) {
+    $('#team-lead-select').find('option[data-quick-add]')
+    .before(
+      $('<option>').val(selected.val()).text(selected.text())
+    );
+  }
+  selected.remove();
+  return false;
+});
+
+$(document).on('click', 'a.drop-membership', function() {
+  var tr = $(this).closest('tr'),
+      id = tr.find('input[name^=members][type=hidden]').val();
+  $('#add_access').append(
+    $('<option>')
+    .attr('value', id)
+    .text(tr.find('td:first').text())
+  );
+  $('#team-lead-select option[value='+id+']').remove();
+  tr.fadeOut(function() { $(this).remove(); });
+  return false;
+});
+
+<?php
+if ($team) {
+    foreach ($team->members->sort(function($a) { return $a->staff->getName(); }) as $member) {
+        echo sprintf('addMember(%d, %s, %d, %s);',
+            $member->staff_id,
+            JsonDataEncoder::encode((string) $member->staff->getName()),
+            $member->isAlertsEnabled(),
+            JsonDataEncoder::encode($errors['members'][$member->staff_id])
+        );
+    }
+}
+?>
+</script>
diff --git a/include/staff/teams.inc.php b/include/staff/teams.inc.php
index b8ea0f24d0406a72b90ccd384769d2e9bb261aaf..ec1bfbfdbe7299b4c9fe4ca0fbf0d976d90072e7 100644
--- a/include/staff/teams.inc.php
+++ b/include/staff/teams.inc.php
@@ -2,102 +2,141 @@
 if(!defined('OSTADMININC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access Denied');
 
 $qs = array();
-$sql='SELECT team.*,count(m.staff_id) as members,CONCAT_WS(" ",lead.firstname,lead.lastname) as team_lead '.
-     ' FROM '.TEAM_TABLE.' team '.
-     ' LEFT JOIN '.TEAM_MEMBER_TABLE.' m ON(m.team_id=team.team_id) '.
-     ' LEFT JOIN '.STAFF_TABLE.' lead ON(lead.staff_id=team.lead_id) ';
-$sql.=' WHERE 1';
-$sortOptions=array('name'=>'team.name','status'=>'team.isenabled','members'=>'members','lead'=>'team_lead','created'=>'team.created');
-$orderWays=array('DESC'=>'DESC','ASC'=>'ASC');
-$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'name';
+$sortOptions=array(
+        'name' => 'name',
+        'status' => 'isenabled',
+        'members' => 'members_count',
+        'lead' => 'lead__lastname',
+        'created' => 'created',
+        'updated' => 'updated',
+        );
+
+$orderWays = array('DESC'=>'DESC', 'ASC'=>'ASC');
+$sort = ($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])]) ? strtolower($_REQUEST['sort']) : 'name';
+
 //Sorting options...
-if($sort && $sortOptions[$sort]) {
-    $order_column =$sortOptions[$sort];
+if ($sort && $sortOptions[$sort]) {
+    $order_column = $sortOptions[$sort];
 }
-$order_column=$order_column?$order_column:'team.name';
 
-if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])]) {
-    $order=$orderWays[strtoupper($_REQUEST['order'])];
+$order_column = $order_column ? $order_column : 'name';
+
+if ($_REQUEST['order'] && isset($orderWays[strtoupper($_REQUEST['order'])])) {
+    $order = $orderWays[strtoupper($_REQUEST['order'])];
+} else {
+    $order = 'ASC';
 }
-$order=$order?$order:'ASC';
 
-if($order_column && strpos($order_column,',')){
+if ($order_column && strpos($order_column,',')) {
     $order_column=str_replace(','," $order,",$order_column);
 }
 $x=$sort.'_sort';
 $$x=' class="'.strtolower($order).'" ';
-$order_by="$order_column $order ";
-
-$qs += array('order' => ($order=='DESC' ? 'ASC' : 'DESC'));
+$page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
+$count = Team::objects()->count();
+$pageNav = new Pagenate($count, $page, PAGE_LIMIT);
 $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 .= '&amp;order='.urlencode($order=='DESC' ? 'ASC' : 'DESC');
 
-$query="$sql GROUP BY team.team_id ORDER BY $order_by";
-$res=db_query($query);
-if($res && ($num=db_num_rows($res)))
-    $showing=sprintf(__('Showing 1-%1$d of %2$d teams'), $num, $num);
-else
-    $showing=__('No teams found!');
 
 ?>
-<div class="pull-left" style="width:700px;padding-top:5px;">
- <h2><?php echo __('Teams');?>
-    <i class="help-tip icon-question-sign" href="#teams"></i>
-    </h2>
- </div>
-<div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;">
-    <b><a href="teams.php?a=add" class="Icon newteam"><?php echo __('Add New Team');?></a></b></div>
-<div class="clear"></div>
 <form action="teams.php" method="POST" name="teams">
+<div class="sticky bar">
+    <div class="content">
+        <div class="pull-left">
+            <h2><?php echo __('Teams');?>
+            <i class="help-tip icon-question-sign notsticky" href="#teams"></i>
+            </h2>
+        </div>
+        <div class="pull-right flush-right">
+            <a href="teams.php?a=add" class="green button action-button"><i class="icon-plus-sign"></i> <?php echo __('Add New Team');?></a>
+            <span class="action-button" data-dropdown="#action-dropdown-more">
+                <i class="icon-caret-down pull-right"></i>
+                <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+            </span>
+            <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                <ul id="actions">
+                    <li><a class="confirm" data-name="enable" href="teams.php?a=enable">
+                        <i class="icon-ok-sign icon-fixed-width"></i>
+                        <?php echo __('Enable'); ?></a></li>
+                    <li><a class="confirm" data-name="disable" href="teams.php?a=disable">
+                        <i class="icon-ban-circle icon-fixed-width"></i>
+                        <?php echo __('Disable'); ?></a></li>
+                    <li class="danger"><a class="confirm" data-name="delete" href="teams.php?a=delete">
+                        <i class="icon-trash icon-fixed-width"></i>
+                        <?php echo __('Delete'); ?></a></li>
+                </ul>
+            </div>
+        </div>
+        <div class="clear"></div>
+    </div>
+</div>
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="mass_process" >
  <input type="hidden" id="action" name="a" value="" >
  <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption><?php echo $showing; ?></caption>
     <thead>
         <tr>
-            <th width="7px">&nbsp;</th>
-            <th width="250"><a <?php echo $name_sort; ?> href="teams.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Team Name');?></a></th>
-            <th width="80"><a  <?php echo $status_sort; ?> href="teams.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Status');?></a></th>
-            <th width="80"><a  <?php echo $members_sort; ?>href="teams.php?<?php echo $qstr; ?>&sort=members"><?php echo __('Members');?></a></th>
-            <th width="200"><a  <?php echo $lead_sort; ?> href="teams.php?<?php echo $qstr; ?>&sort=lead"><?php echo __('Team Lead');?></a></th>
-            <th width="100"><a  <?php echo $created_sort; ?> href="teams.php?<?php echo $qstr; ?>&sort=created"><?php echo __('Created');?></a></th>
-            <th width="130"><a  <?php echo $updated_sort; ?> href="teams.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated');?></a></th>
+            <th width="4%">&nbsp;</th>
+            <th width="25%"><a <?php echo $name_sort; ?> href="teams.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Team Name');?></a></th>
+            <th width="8%"><a  <?php echo $status_sort; ?> href="teams.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Status');?></a></th>
+            <th width="8%"><a  <?php echo $members_sort; ?>href="teams.php?<?php echo $qstr; ?>&sort=members"><?php echo __('Members');?></a></th>
+            <th width="20%"><a  <?php echo $lead_sort; ?> href="teams.php?<?php echo $qstr; ?>&sort=lead"><?php echo __('Team Lead');?></a></th>
+            <th width="15%"><a  <?php echo $created_sort; ?> href="teams.php?<?php echo $qstr; ?>&sort=created"><?php echo __('Created');?></a></th>
+            <th width="20%"><a  <?php echo $updated_sort; ?> href="teams.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated');?></a></th>
         </tr>
     </thead>
     <tbody>
     <?php
-        $total=0;
-        $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null;
-        if($res && db_num_rows($res)):
-            while ($row = db_fetch_array($res)) {
+        $ids= ($errors && is_array($_POST['ids'])) ? $_POST['ids'] : null;
+        if ($count) {
+            $teams = Team::objects()
+                ->annotate(array(
+                        'members_count'=>SqlAggregate::COUNT('members__staff', true),
+                ))
+                ->order_by(sprintf('%s%s',
+                            strcasecmp($order, 'DESC') ? '' : '-',
+                            $order_column))
+                ->limit($pageNav->getLimit())
+                ->offset($pageNav->getStart());
+
+            foreach ($teams as $team) {
+                $id = $team->getId();
                 $sel=false;
-                if($ids && in_array($row['team_id'],$ids))
+                if ($ids && in_array($id, $ids))
                     $sel=true;
                 ?>
-            <tr id="<?php echo $row['team_id']; ?>">
-                <td width=7px>
-                  <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['team_id']; ?>"
-                            <?php echo $sel?'checked="checked"':''; ?>> </td>
-                <td><a href="teams.php?id=<?php echo $row['team_id']; ?>"><?php echo $row['name']; ?></a> &nbsp;</td>
-                <td>&nbsp;<?php echo $row['isenabled']?__('Active'):'<b>'.__('Disabled').'</b>'; ?></td>
+            <tr id="<?php echo $id; ?>">
+                <td align="center">
+                  <input type="checkbox" class="ckb" name="ids[]"
+                  value="<?php echo $id; ?>"
+                            <?php echo $sel ? 'checked="checked"' : ''; ?>> </td>
+                <td><a href="teams.php?id=<?php echo $id; ?>"><?php echo
+                $team->getName(); ?></a> &nbsp;</td>
+                <td>&nbsp;<?php echo $team->isActive() ? __('Active') : '<b>'.__('Disabled').'</b>'; ?></td>
                 <td style="text-align:right;padding-right:25px">&nbsp;&nbsp;
-                    <?php if($row['members']>0) { ?>
-                        <a href="staff.php?tid=<?php echo $row['team_id']; ?>"><?php echo $row['members']; ?></a>
-                    <?php }else{ ?> 0
+                    <?php if ($team->members_count > 0) { ?>
+                        <a href="staff.php?tid=<?php echo $id; ?>"><?php
+                            echo $team->members_count; ?></a>
+                    <?php } else { ?> 0
                     <?php } ?>
                     &nbsp;
                 </td>
-                <td><a href="staff.php?id=<?php echo $row['lead_id']; ?>"><?php echo $row['team_lead']; ?>&nbsp;</a></td>
-                <td><?php echo Format::db_date($row['created']); ?>&nbsp;</td>
-                <td><?php echo Format::db_datetime($row['updated']); ?>&nbsp;</td>
+                <td><a href="staff.php?id=<?php
+                    echo $team->getLeadId(); ?>"><?php echo $team->lead ?: ''; ?>&nbsp;</a></td>
+                <td><?php echo Format::date($team->created); ?>&nbsp;</td>
+                <td><?php echo Format::datetime($team->updated); ?>&nbsp;</td>
             </tr>
             <?php
-            } //end of while.
-        endif; ?>
+            } //end of foreach
+        }?>
     <tfoot>
      <tr>
         <td colspan="7">
-            <?php if($res && $num){ ?>
+            <?php if ($count){ ?>
             <?php echo __('Select');?>:&nbsp;
             <a id="selectAll" href="#ckb"><?php echo __('All');?></a>&nbsp;&nbsp;
             <a id="selectNone" href="#ckb"><?php echo __('None');?></a>&nbsp;&nbsp;
@@ -110,13 +149,10 @@ else
     </tfoot>
 </table>
 <?php
-if($res && $num): //Show options..
+if ($count): //Show options..
+     echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
 ?>
-<p class="centered" id="actions">
-    <input class="button" type="submit" name="enable" value="<?php echo __('Enable');?>" >
-    <input class="button" type="submit" name="disable" value="<?php echo __('Disable');?>" >
-    <input class="button" type="submit" name="delete" value="<?php echo __('Delete');?>" >
-</p>
+
 <?php
 endif;
 ?>
diff --git a/include/staff/template.inc.php b/include/staff/template.inc.php
index 814ce16c90e76be4fddf97c1e2b806058811e623..7dd34df896a9dfacdaab3c3bf76370e1bd72dd9e 100644
--- a/include/staff/template.inc.php
+++ b/include/staff/template.inc.php
@@ -14,7 +14,7 @@ if($template && $_REQUEST['a']!='add'){
     $action='add';
     $submit_text=__('Add Template');
     $info['isactive']=isset($info['isactive'])?$info['isactive']:0;
-    $info['lang_id'] = $cfg->getSystemLanguage();
+    $info['lang_id'] = $cfg->getPrimaryLanguage();
     $qs += array('a' => $_REQUEST['a']);
 }
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
@@ -24,12 +24,15 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
  <input type="hidden" name="do" value="<?php echo $action; ?>">
  <input type="hidden" name="a" value="<?php echo Format::htmlchars($_REQUEST['a']); ?>">
  <input type="hidden" name="tpl_id" value="<?php echo $info['tpl_id']; ?>">
- <h2><?php echo __('Email Template');?></h2>
+ <h2><?php echo $title; ?>
+    <?php if (isset($info['name'])) { ?><small>
+    — <?php echo $info['name']; ?></small>
+     <?php } ?>
+</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 __('Template information');?></em>
             </th>
         </tr>
@@ -91,7 +94,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
             if (isset($impl[$cn])) {
                 echo sprintf('<tr><td colspan="2">&nbsp;<strong><a href="templates.php?id=%d&a=manage">%s</a></strong>, <span class="faded">%s</span><br/>&nbsp;%s</td></tr>',
                 $impl[$cn]->getId(), Format::htmlchars(__($info['name'])),
-                sprintf(__('Updated %s'), Format::db_datetime($impl[$cn]->getLastUpdated())),
+                sprintf(__('Updated %s'), Format::datetime($impl[$cn]->getLastUpdated())),
                 Format::htmlchars(__($info['desc'])));
             } else {
                 echo sprintf('<tr><td colspan=2>&nbsp;<strong><a
diff --git a/include/staff/templates.inc.php b/include/staff/templates.inc.php
index aece8b0648ce172b6f7b2dde4277c3632229408c..0ed4dea249352587ccc084b28e835ee58e0ed102 100644
--- a/include/staff/templates.inc.php
+++ b/include/staff/templates.inc.php
@@ -42,27 +42,56 @@ else
     $showing=__('No templates found!');
 
 ?>
-
-<div class="pull-left" style="width:700px;padding-top:5px;">
- <h2><?php echo __('Email Template Sets'); ?></h2>
-</div>
-<div class="pull-right flush-right" style="padding-top:5px;padding-right:5px;">
- <b><a href="templates.php?a=add" class="Icon newEmailTemplate"><?php echo __('Add New Template Set'); ?></a></b></div>
-<div class="clear"></div>
 <form action="templates.php" method="POST" name="tpls">
+    <div class="sticky bar opaque">
+        <div class="content">
+            <div class="pull-left flush-left">
+                <h2><?php echo __('Email Template Sets'); ?></h2>
+            </div>
+            <div class="pull-right flush-right">
+                <a href="templates.php?a=add" class="green button action-button"><i class="icon-plus-sign"></i> <?php echo __('Add New Template Set'); ?></a>
+                <span class="action-button" data-dropdown="#action-dropdown-more">
+                    <i class="icon-caret-down pull-right"></i>
+                    <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+                </span>
+                <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                    <ul id="actions">
+                        <li>
+                            <a class="confirm" data-name="enable" href="templates.php?a=enable">
+                                <i class="icon-ok-sign icon-fixed-width"></i>
+                                <?php echo __( 'Enable'); ?>
+                            </a>
+                        </li>
+                        <li>
+                            <a class="confirm" data-name="disable" href="templates.php?a=disable">
+                                <i class="icon-ban-circle icon-fixed-width"></i>
+                                <?php echo __( 'Disable'); ?>
+                            </a>
+                        </li>
+                        <li class="danger">
+                            <a class="confirm" data-name="delete" href="templates.php?a=delete">
+                                <i class="icon-trash icon-fixed-width"></i>
+                                <?php echo __( 'Delete'); ?>
+                            </a>
+                        </li>
+                    </ul>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="clear"></div>
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="mass_process" >
 <input type="hidden" id="action" name="a" value="" >
  <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption><?php echo $showing; ?></caption>
     <thead>
         <tr>
-            <th width="7">&nbsp;</th>
-            <th width="350"><a <?php echo $name_sort; ?> href="templates.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name'); ?></a></th>
-            <th width="100"><a  <?php echo $status_sort; ?> href="templates.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Status'); ?></a></th>
-            <th width="80"><a <?php echo $inuse_sort; ?> href="templates.php?<?php echo $qstr; ?>&sort=inuse"><?php echo __('In-Use'); ?></a></th>
-            <th width="120" nowrap><a  <?php echo $created_sort; ?>href="templates.php?<?php echo $qstr; ?>&sort=created"><?php echo __('Date Added'); ?></a></th>
-            <th width="150" nowrap><a  <?php echo $updated_sort; ?>href="templates.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated'); ?></a></th>
+            <th width="4%">&nbsp;</th>
+            <th width="46%"><a <?php echo $name_sort; ?> href="templates.php?<?php echo $qstr; ?>&sort=name"><?php echo __('Name'); ?></a></th>
+            <th width="10%"><a  <?php echo $status_sort; ?> href="templates.php?<?php echo $qstr; ?>&sort=status"><?php echo __('Status'); ?></a></th>
+            <th width="10%"><a <?php echo $inuse_sort; ?> href="templates.php?<?php echo $qstr; ?>&sort=inuse"><?php echo __('In-Use'); ?></a></th>
+            <th width="10%" nowrap><a  <?php echo $created_sort; ?>href="templates.php?<?php echo $qstr; ?>&sort=created"><?php echo __('Date Added'); ?></a></th>
+            <th width="20%" nowrap><a  <?php echo $updated_sort; ?>href="templates.php?<?php echo $qstr; ?>&sort=updated"><?php echo __('Last Updated'); ?></a></th>
         </tr>
     </thead>
     <tbody>
@@ -80,7 +109,7 @@ else
                 $default=($defaultTplId==$row['tpl_id'])?'<small class="fadded">('.__('System Default').')</small>':'';
                 ?>
             <tr id="<?php echo $row['tpl_id']; ?>">
-                <td width=7px>
+                <td align="center">
                   <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['tpl_id']; ?>"
                             <?php echo $sel?'checked="checked"':''; ?> <?php echo $default?'disabled="disabled"':''; ?> >
                 </td>
@@ -88,8 +117,8 @@ else
                 &nbsp;<?php echo $default; ?></td>
                 <td>&nbsp;<?php echo $row['isactive']?__('Active'):'<b>'.__('Disabled').'</b>'; ?></td>
                 <td>&nbsp;&nbsp;<?php echo ($inuse)?'<b>'.__('Yes').'</b>':__('No'); ?></td>
-                <td>&nbsp;<?php echo Format::db_date($row['created']); ?></td>
-                <td>&nbsp;<?php echo Format::db_datetime($row['updated']); ?></td>
+                <td>&nbsp;<?php echo Format::date($row['created']); ?></td>
+                <td>&nbsp;<?php echo Format::datetime($row['updated']); ?></td>
             </tr>
             <?php
             } //end of while.
@@ -113,11 +142,7 @@ else
 if($res && $num): //Show options..
     echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
 ?>
-<p class="centered" id="actions">
-    <input class="button" type="submit" name="enable" value="<?php echo __('Enable');?>" >
-    <input class="button" type="submit" name="disable" value="<?php echo __('Disable');?>" >
-    <input class="button" type="submit" name="delete" value="<?php echo __('Delete');?>" >
-</p>
+
 <?php
 endif;
 ?>
diff --git a/include/staff/templates/advanced-search-field.tmpl.php b/include/staff/templates/advanced-search-field.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..12086ceb9b63533f3f1bdb36aa8b3ad01c8eddd8
--- /dev/null
+++ b/include/staff/templates/advanced-search-field.tmpl.php
@@ -0,0 +1,22 @@
+<input type="hidden" name="fields[]" value="<?php echo $name; ?>"/>
+<?php foreach ($fields as $F) { ?>
+<fieldset id="field<?php echo $F->getWidget()->id;
+    ?>" <?php 
+        $class = array();
+        @list($name, $sub) = explode('+', $F->get('name'), 2);
+        if (!$F->isVisible()) $class[] = "hidden";
+        if ($sub === 'method')
+            $class[] = "adv-search-method";
+        elseif ($sub === 'search')
+            $class[] = "adv-search-field";
+        elseif ($F->get('__searchval__'))
+            $class[] = "adv-search-val";
+        if ($class)
+            echo 'class="'.implode(' ', $class).'"';
+        ?>>
+    <?php echo $F->render(); ?>
+    <?php foreach ($F->errors() as $E) {
+        ?><div class="error"><?php echo $E; ?></div><?php
+    } ?>
+</fieldset>
+<?php } ?>
diff --git a/include/staff/templates/advanced-search.tmpl.php b/include/staff/templates/advanced-search.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..cc14fa18df075e28b0369cb40474791cbd5a5cde
--- /dev/null
+++ b/include/staff/templates/advanced-search.tmpl.php
@@ -0,0 +1,228 @@
+<div id="advanced-search">
+<h3 class="drag-handle"><?php echo __('Advanced Ticket Search');?></h3>
+<a class="close" href=""><i class="icon-remove-circle"></i></a>
+<hr/>
+<form action="#tickets/search" method="post" name="search">
+<div class="row">
+<div class="span6">
+    <input type="hidden" name="a" value="search">
+<?php
+foreach ($form->errors(true) ?: array() as $message) {
+    ?><div class="error-banner"><?php echo $message;?></div><?php
+}
+
+$info = $search->getSearchFields($form);
+foreach (array_keys($info) as $F) {
+    ?><input type="hidden" name="fields[]" value="<?php echo $F; ?>"/><?php
+}
+$errors = !!$form->errors();
+$inbody = false;
+$first_field = true;
+foreach ($form->getFields() as $name=>$field) {
+    @list($name, $sub) = explode('+', $field->get('name'), 2);
+    if ($sub === 'search') {
+        if (!$first_field) {
+            echo '</div></div>';
+        }
+        echo '<div class="adv-search-field-container">';
+        $inbody = false;
+        $first_field = false;
+    }
+    elseif (!$first_field && !$inbody) {
+        echo sprintf('<div class="adv-search-field-body %s">',
+            !$errors && isset($info[$name]) && $info[$name]['active'] ? 'hidden' : '');
+        $inbody = true;
+    }
+?>
+    <fieldset id="field<?php echo $field->getWidget()->id; ?>" <?php
+        $class = array();
+        if (!$field->isVisible())
+            $class[] = "hidden";
+        if ($sub === 'method')
+            $class[] = "adv-search-method";
+        elseif ($sub === 'search')
+            $class[] = "adv-search-field";
+        elseif ($field->get('__searchval__'))
+            $class[] = "adv-search-val";
+        if ($class)
+            echo 'class="'.implode(' ', $class).'"';
+        ?>>
+        <?php echo $field->render(); ?>
+        <?php if (!$errors && $sub === 'search' && isset($info[$name]) && $info[$name]['active']) { ?>
+            <span style="padding-left: 5px">
+            <a href="#"  data-name="<?php echo Format::htmlchars($name); ?>" onclick="javascript:
+    var $this = $(this),
+        name = $this.data('name'),
+        expanded = $this.data('expanded') || false;
+    $this.closest('.adv-search-field-container').find('.adv-search-field-body').slideDown('fast');
+    $this.find('span.faded').hide();
+    $this.find('i').removeClass('icon-caret-right').addClass('icon-caret-down');
+    return false;
+"><i class="icon-caret-right"></i>
+            <span class="faded"><?php echo $search->describeField($info[$name]); ?></span>
+            </a>
+            </span>
+        <?php } ?>
+        <?php foreach ($field->errors() as $E) {
+            ?><div class="error"><?php echo $E; ?></div><?php
+        } ?>
+    </fieldset>
+    <?php if ($name[0] == ':' && substr($name, -7) == '+search') {
+        list($N,) = explode('+', $name, 2);
+?>
+    <input type="hidden" name="fields[]" value="<?php echo $N; ?>"/>
+    <?php }
+}
+if (!$first_field)
+    echo '</div></div>';
+?>
+<div id="extra-fields"></div>
+<hr/>
+<select id="search-add-new-field" name="new-field" style="max-width: 300px;">
+    <option value="">— <?php echo __('Add Other Field'); ?> —</option>
+<?php
+foreach ($matches as $name => $fields) { ?>
+    <optgroup label="<?php echo $name; ?>">
+<?php
+    foreach ($fields as $id => $desc) { ?>
+        <option value="<?php echo $id; ?>" <?php
+            if (isset($state[$id])) echo 'disabled="disabled"';
+        ?>><?php echo ($desc instanceof FormField ? $desc->getLocal('label') : $desc); ?></option>
+<?php } ?>
+    </optgroup>
+<?php } ?>
+</select>
+
+</div>
+<div class="span6" style="border-left:1px solid #888;position:relative;padding-bottom:26px;">
+<div style="margin-bottom: 0.5em;"><b style="font-size: 110%;"><?php echo __('Saved Searches'); ?></b></div>
+<hr>
+<div id="saved-searches" class="accordian" style="max-height:200px;overflow-y:auto;">
+<?php foreach (SavedSearch::forStaff($thisstaff) as $S) { ?>
+    <dt class="saved-search">
+        <a href="#" class="load-search"><?php echo $S->title; ?>
+        <i class="icon-chevron-down pull-right"></i>
+        </a>
+    </dt>
+    <dd>
+        <span>
+            <button type="button" onclick="javascript:$(this).closest('form').attr({
+'method': 'get', 'action': '#tickets/search/<?php echo $S->id; ?>'}).trigger('submit');"><i class="icon-chevron-left"></i> <?php echo __('Load'); ?></button>
+            <button type="button" onclick="javascript:
+var that = this;
+$.ajax({
+    url: 'ajax.php/tickets/search/<?php echo $S->id; ?>',
+    type: 'POST',
+    data: {'form': $(this).closest('.dialog').find('form[name=search]').serializeArray()},
+    dataType: 'json',
+    success: function(json) {
+      if (!json.id)
+        return;
+      $(that).closest('dd').effect('highlight');
+    }
+});
+return false;
+"><i class="icon-save"></i> <?php echo __('Update'); ?></button>
+        </span>
+        <span class="pull-right">
+            <button type="button" title="<?php echo __('Delete'); ?>" onclick="javascript:
+    if (!confirm(__('You sure?'))) return false;
+    var that = this;
+    $.ajax({
+        'url': 'ajax.php/tickets/search/<?php echo $S->id; ?>',
+        'type': 'delete',
+        'dataType': 'json',
+        'success': function(json) {
+            if (json.success) {
+                $(that).closest('dd').prev('dt').slideUp().next('dd').slideUp();
+            }
+        }
+    });
+    return false;
+"><i class="icon-trash"></i></button>
+        </span>
+    </dd>
+<?php } ?>
+</div>
+<div style="position:absolute;bottom:0">
+<hr>
+    <form method="post">
+    <div class="attached input">
+    <input name="title" type="text" size="27" placeholder="<?php
+        echo __('Enter a title for the search'); ?>"/>
+        <a class="attached button" href="#tickets/search/create" onclick="javascript:
+$.ajax({
+    url: 'ajax.php/' + $(this).attr('href').substr(1),
+    type: 'POST',
+    data: {'name': $(this).closest('form').find('[name=title]').val(),
+           'form': $(this).closest('.dialog').find('form[name=search]').serializeArray()},
+    dataType: 'json',
+    success: function(json) {
+      if (!json.id)
+        return;
+      $('<dt>')
+        .append($('<a>').text(' ' + json.title)
+          .prepend($('<i>').addClass('icon-chevron-left'))
+        ).appendTo($('#saved-searches'));
+    }
+});
+return false;
+"><i class="icon-save"></i></a>
+    </div>
+</div>
+</div>
+</div>
+
+<hr/>
+<div>
+    <div id="search-hint" class="pull-left">
+    </div>
+    <div class="buttons pull-right">
+        <button class="button" type="submit" id="do_search"><i class="icon-search"></i>
+            <?php echo __('Search'); ?></button>
+    </div>
+</div>
+
+</form>
+
+<style type="text/css">
+#advanced-search .span6 .select2 {
+  max-width: 300px !important;
+}
+</style>
+
+<script type="text/javascript">
+$(function() {
+  $('#advanced-search [data-dropdown]').dropdown();
+
+  var I = setInterval(function() {
+    var A = $('#saved-searches.accordian');
+    if (!A.length) return;
+    clearInterval(I);
+
+    var allPanels = $('dd', A).hide();
+    $('dt > a', A).click(function() {
+      $('dt', A).removeClass('active');
+      allPanels.slideUp();
+      $(this).parent().addClass('active').next().slideDown();
+      return false;
+    });
+  }, 200);
+
+  $('#search-add-new-field').on('change', function() {
+    var that=this;
+    $.ajax({
+      url: 'ajax.php/tickets/search/field/'+$(this).val(),
+      type: 'get',
+      dataType: 'json',
+      success: function(json) {
+        if (!json.success)
+          return false;
+        ff_uid = json.ff_uid;
+        $(that).find(':selected').prop('disabled', true);
+        $('#extra-fields').append($(json.html));
+      }
+    });
+  });
+});
+</script>
diff --git a/include/staff/templates/assign.tmpl.php b/include/staff/templates/assign.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..639460ac42c045840f1ec279de9ec4467357f5cb
--- /dev/null
+++ b/include/staff/templates/assign.tmpl.php
@@ -0,0 +1,67 @@
+<?php
+global $cfg;
+
+$form = $form ?: AssignmentForm::instantiate($info);
+
+if (!$info[':title'])
+    $info[':title'] = __('Assign');
+?>
+<h3 class="drag-handle"><?php echo $info[':title']; ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<div class="clear"></div>
+<hr/>
+<?php
+if ($info['error']) {
+    echo sprintf('<p id="msg_error">%s</p>', $info['error']);
+} elseif ($info['warn']) {
+    echo sprintf('<p id="msg_warning">%s</p>', $info['warn']);
+} elseif ($info['msg']) {
+    echo sprintf('<p id="msg_notice">%s</p>', $info['msg']);
+} elseif ($info['notice']) {
+   echo sprintf('<p id="msg_info"><i class="icon-info-sign"></i> %s</p>',
+           $info['notice']);
+}
+
+
+$action = $info[':action'] ?: ('#');
+?>
+<div style="display:block; margin:5px;">
+<form class="mass-action" method="post"
+    name="assign"
+    id="<?php echo $form->getId(); ?>"
+    action="<?php echo $action; ?>">
+    <table width="100%">
+        <?php
+        if ($info[':extra']) {
+            ?>
+        <tbody>
+            <tr><td colspan="2"><strong><?php echo $info[':extra'];
+            ?></strong></td> </tr>
+        </tbody>
+        <?php
+        }
+       ?>
+        <tbody>
+            <tr><td colspan=2>
+             <?php
+             $options = array('template' => 'simple', 'form_id' => 'assign');
+             $form->render($options);
+             ?>
+            </td> </tr>
+        </tbody>
+    </table>
+    <hr>
+    <p class="full-width">
+        <span class="buttons pull-left">
+            <input type="reset" value="<?php echo __('Reset'); ?>">
+            <input type="button" name="cancel" class="close"
+            value="<?php echo __('Cancel'); ?>">
+        </span>
+        <span class="buttons pull-right">
+            <input type="submit" value="<?php
+            echo $verb ?: __('Assign'); ?>">
+        </span>
+     </p>
+</form>
+</div>
+<div class="clear"></div>
diff --git a/include/staff/templates/collaborators-preview.tmpl.php b/include/staff/templates/collaborators-preview.tmpl.php
index 964c225e7db174f8e0a614889104378f14d7e312..89b734ac420d7b9e95187e22bebcf23f22c98f03 100644
--- a/include/staff/templates/collaborators-preview.tmpl.php
+++ b/include/staff/templates/collaborators-preview.tmpl.php
@@ -2,26 +2,28 @@
 <table border="0" cellspacing="" cellpadding="1">
 <colgroup><col style="min-width: 250px;"></col></colgroup>
 <?php
-if (($users=$ticket->getCollaborators())) {?>
+if (($users=$thread->getCollaborators())) {?>
 <?php
     foreach($users as $user) {
-        echo sprintf('<tr><td %s><i class="icon-%s"></i> %s <em>&lt;%s&gt;</em></td></tr>',
+        echo sprintf('<tr><td %s>%s%s <em class="faded">&lt;%s&gt;</em></td></tr>',
                 ($user->isActive()? '' : 'class="faded"'),
-                ($user->isActive()? 'comments' :  'comment-alt'),
+                (($U = $user->getUser()) && ($A = $U->getAvatar()))
+                    ? $A->getImageTag(20) : sprintf('<i class="icon-%s"></i>',
+                        ($user->isActive()? 'comments' :  'comment-alt')),
                 Format::htmlchars($user->getName()),
                 $user->getEmail());
     }
 }  else {
-    echo "<strong>".__("Ticket doesn't have any collaborators.")."</strong>";
+    echo "<strong>".__("Thread doesn't have any collaborators.")."</strong>";
 }?>
 </table>
 <?php
 $options = array();
 
 $options[] = sprintf(
-        '<a class="collaborators" id="managecollab" href="#tickets/%d/collaborators">%s</a>',
-        $ticket->getId(),
-        $ticket->getNumCollaborators()
+        '<a class="collaborators" id="managecollab" href="#thread/%d/collaborators">%s</a>',
+        $thread->getId(),
+        $thread->getNumCollaborators()
         ? __('Manage Collaborators') : __('Add Collaborator')
         );
 
diff --git a/include/staff/templates/collaborators.tmpl.php b/include/staff/templates/collaborators.tmpl.php
index d8d339c882dae25b1f5592f43a40c95420e76f66..4393ba1f2fd06acb40f6d7b1f5713809cd90f773 100644
--- a/include/staff/templates/collaborators.tmpl.php
+++ b/include/staff/templates/collaborators.tmpl.php
@@ -1,4 +1,4 @@
-<h3><?php echo __('Ticket Collaborators'); ?></h3>
+<h3 class="drag-handle"><?php echo __('Collaborators'); ?></h3>
 <b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
 <?php
 if($info && $info['msg']) {
@@ -6,17 +6,19 @@ if($info && $info['msg']) {
 } ?>
 <hr/>
 <?php
-if(($users=$ticket->getCollaborators())) {?>
+if(($users=$thread->getCollaborators())) {?>
 <div id="manage_collaborators">
-<form method="post" class="collaborators" action="#tickets/<?php echo $ticket->getId(); ?>/collaborators">
+<form method="post" class="collaborators" action="#thread/<?php echo $thread->getId(); ?>/collaborators">
     <table border="0" cellspacing="1" cellpadding="1" width="100%">
     <?php
     foreach($users as $user) {
         $checked = $user->isActive() ? 'checked="checked"' : '';
         echo sprintf('<tr>
                         <td>
+                            <label class="inline checkbox">
                             <input type="checkbox" name="cid[]" id="c%d" value="%d" %s>
-                            <a class="collaborator" href="#collaborators/%d/view">%s</a>
+                            </label>
+                            <a class="collaborator" href="#thread/%d/collaborators/%d/view">%s%s</a>
                             <span class="faded"><em>%s</em></span></td>
                         <td width="10">
                             <input type="hidden" name="del[]" id="d%d" value="">
@@ -26,7 +28,10 @@ if(($users=$ticket->getCollaborators())) {?>
                     $user->getId(),
                     $user->getId(),
                     $checked,
+                    $thread->getId(),
                     $user->getId(),
+                    (($U = $user->getUser()) && ($A = $U->getAvatar()))
+                        ? $U->getAvatar()->getImageTag(24) : '',
                     Format::htmlchars($user->getName()),
                     $user->getEmail(),
                     $user->getId(),
@@ -36,7 +41,7 @@ if(($users=$ticket->getCollaborators())) {?>
     </table>
     <hr style="margin-top:1em"/>
     <div><a class="collaborator"
-        href="#tickets/<?php echo $ticket->getId(); ?>/add-collaborator"
+        href="#thread/<?php echo $thread->getId(); ?>/add-collaborator"
         ><i class="icon-plus-sign"></i> <?php echo __('Add New Collaborator'); ?></a></div>
     <div id="savewarning" style="display:none; padding-top:2px;"><p
     id="msg_warning"><?php echo __('You have made changes that you need to save.'); ?></p></div>
@@ -57,15 +62,22 @@ if(($users=$ticket->getCollaborators())) {?>
     echo __("Bro, not sure how you got here!");
 }
 
-if ($_POST && $ticket && $ticket->getNumCollaborators()) {
+if ($_POST && $thread && $thread->getNumCollaborators()) {
+
+    $collaborators = sprintf('Participants (%d)',
+            $thread->getNumCollaborators());
+
     $recipients = sprintf(__('Recipients (%d of %d)'),
-          $ticket->getNumActiveCollaborators(),
-          $ticket->getNumCollaborators());
+          $thread->getNumActiveCollaborators(),
+          $thread->getNumCollaborators());
     ?>
     <script type="text/javascript">
         $(function() {
             $('#emailcollab').show();
-            $('#recipients').html('<?php echo $recipients; ?>');
+            $('#t<?php echo $thread->getId(); ?>-recipients')
+            .html('<?php echo $recipients; ?>');
+            $('#t<?php echo $thread->getId(); ?>-collaborators')
+            .html('<?php echo $collaborators; ?>');
             });
     </script>
 <?php
diff --git a/include/staff/templates/content-manage.tmpl.php b/include/staff/templates/content-manage.tmpl.php
index 7170aeb291e9f8b626099ac32f0d6c0438fc7c3e..bf9410b2fa895652712fb0170a3581ab0ce48aa6 100644
--- a/include/staff/templates/content-manage.tmpl.php
+++ b/include/staff/templates/content-manage.tmpl.php
@@ -1,24 +1,74 @@
-<h3><?php echo __('Manage Content'); ?> &mdash; <?php echo Format::htmlchars($content->getName()); ?></h3>
+<h3 class="drag-handle"><?php echo __('Manage Content'); ?> &mdash; <?php echo Format::htmlchars($content->getName()); ?></h3>
 <a class="close" href=""><i class="icon-remove-circle"></i></a>
 <hr/>
+
 <?php if ($errors['err']) { ?>
 <div class="error-banner">
     <?php echo $errors['err']; ?>
 </div>
 <?php } ?>
-<form method="post" action="#content/<?php echo $info['id']; ?>">
+<form method="post" action="#content/<?php echo $content->getId(); ?>"
+        style="clear:none">
+
+<?php
+if (count($langs) > 1) { ?>
+    <ul class="tabs alt clean" id="content-trans">
+    <li class="empty"><i class="icon-globe" title="<?php echo __('This content is translatable'); ?>"></i></li>
+<?php foreach ($langs as $tag=>$nfo) { ?>
+    <li class="<?php if ($tag == $cfg->getPrimaryLanguage()) echo "active";
+        ?>"><a href="#translation-<?php echo $tag; ?>" title="<?php
+        echo Internationalization::getLanguageDescription($tag);
+    ?>"><span class="flag flag-<?php echo strtolower($nfo['flag']); ?>"></span>
+    </a></li>
+<?php } ?>
+    </ul>
+<?php
+} ?>
+
+<div id="content-trans_container">
+    <div id="translation-<?php echo $cfg->getPrimaryLanguage(); ?>"
+        class="tab_content" lang="<?php echo $cfg->getPrimaryLanguage(); ?>">
     <div class="error"><?php echo $errors['name']; ?></div>
     <input type="text" style="width: 100%; font-size: 14pt" name="name" value="<?php
-        echo Format::htmlchars($info['name']); ?>" />
+    echo Format::htmlchars($info['title']); ?>" spellcheck="true"
+        lang="<?php echo $cfg->getPrimaryLanguage(); ?>" />
     <div style="margin-top: 5px">
     <div class="error"><?php echo $errors['body']; ?></div>
-    <textarea class="richtext no-bar" name="body"><?php
-    echo Format::viewableImages($info['body']);
+    <textarea class="richtext no-bar" name="body"
+        data-root-context="<?php echo $content->getType();
+        ?>"><?php echo Format::htmlchars(Format::viewableImages($info['body']));
+        ?></textarea>
+    </div>
+    </div>
+
+<?php foreach ($langs as $tag=>$nfo) {
+        if ($tag == $cfg->getPrimaryLanguage())
+            continue;
+        $trans = $info['trans'][$tag]; ?>
+    <div id="translation-<?php echo $tag; ?>" class="tab_content hidden"
+        dir="<?php echo $nfo['direction']; ?>" lang="<?php echo $tag; ?>">
+    <input type="text" style="width: 100%; font-size: 14pt"
+        name="trans[<?php echo $tag; ?>][title]" value="<?php
+        echo Format::htmlchars($trans['title']); ?>"
+        placeholder="<?php echo __('Title'); ?>"  spellcheck="true"
+        lang="<?php echo $tag; ?>" />
+    <div style="margin-top: 5px">
+    <textarea class="richtext no-bar" data-direction=<?php echo $nfo['direction']; ?>
+        data-root-context="<?php echo $content->getType(); ?>"
+        placeholder="<?php echo __('Message content'); ?>"
+        name="trans[<?php echo $tag; ?>][body]"><?php
+    echo Format::htmlchars(Format::viewableImages($trans['body']));
 ?></textarea>
     </div>
-    <div id="msg_info" style="margin-top:7px"><?php
+    </div>
+<?php } ?>
+
+    <div class="info-banner" style="margin-top:7px"><?php
 echo $content->getNotes(); ?></div>
-    <hr/>
+
+</div>
+
+    <hr class="clear"/>
     <p class="full-width">
         <span class="buttons pull-left">
             <input type="reset" value="<?php echo __('Reset'); ?>">
@@ -29,6 +79,6 @@ echo $content->getNotes(); ?></div>
             <input type="submit" value="<?php echo __('Save Changes'); ?>">
         </span>
      </p>
-</form>
 </div>
+</form>
 <div class="clear"></div>
diff --git a/include/staff/templates/delete.tmpl.php b/include/staff/templates/delete.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..08d547799a9307d62ed55a41aed5d1f792de0c18
--- /dev/null
+++ b/include/staff/templates/delete.tmpl.php
@@ -0,0 +1,72 @@
+<?php
+global $cfg;
+
+if (!$info[':title'])
+    $info[':title'] = __('Delete');
+?>
+<h3 class="drag-handle"><?php echo $info[':title']; ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<div class="clear"></div>
+<hr/>
+<?php
+if ($info['error']) {
+    echo sprintf('<p id="msg_error">%s</p>', $info['error']);
+} elseif ($info['warn']) {
+    echo sprintf('<p id="msg_warning">%s</p>', $info['warn']);
+} elseif ($info['msg']) {
+    echo sprintf('<p id="msg_notice">%s</p>', $info['msg']);
+} elseif ($info['notice']) {
+   echo sprintf('<p id="msg_info"><i class="icon-info-sign"></i> %s</p>',
+           $info['notice']);
+}
+
+
+$action = $info[':action'] ?: ('#');
+?>
+<div style="display:block; margin:5px;">
+<form class="mass-action" method="post"
+    name="delete"
+    id="delete"
+    action="<?php echo $action; ?>">
+    <table width="100%">
+        <?php
+        if ($info[':extra']) {
+            ?>
+        <tbody>
+            <tr><td colspan="2"><strong><?php echo $info[':extra'];
+            ?></strong></td> </tr>
+        </tbody>
+        <?php
+        }
+       ?>
+        <tbody>
+            <tr>
+                <td colspan="2">
+                    <?php
+                    $placeholder = $info[':placeholder'] ?: __('Optional reason for the deletion');
+                    ?>
+                    <textarea name="comments" id="comments"
+                        cols="50" rows="3" wrap="soft" style="width:100%"
+                        class="<?php if ($cfg->isRichTextEnabled()) echo 'richtext';
+                        ?> no-bar small"
+                        placeholder="<?php echo $placeholder; ?>"><?php
+                        echo $info['comments']; ?></textarea>
+                </td>
+            </tr>
+        </tbody>
+    </table>
+    <hr>
+    <p class="full-width">
+        <span class="buttons pull-left">
+            <input type="reset" value="<?php echo __('Reset'); ?>">
+            <input type="button" name="cancel" class="close"
+            value="<?php echo __('Cancel'); ?>">
+        </span>
+        <span class="buttons pull-right">
+            <input type="submit" class="red button" value="<?php
+            echo $verb ?: __('Delete'); ?>">
+        </span>
+     </p>
+</form>
+</div>
+<div class="clear"></div>
diff --git a/include/staff/templates/dynamic-field-config.tmpl.php b/include/staff/templates/dynamic-field-config.tmpl.php
index 51701b6ee1f66ce93c66fba5845a14abfd65d78b..0e8d7c83553c45f942be744b8d483a61d7f51b1f 100644
--- a/include/staff/templates/dynamic-field-config.tmpl.php
+++ b/include/staff/templates/dynamic-field-config.tmpl.php
@@ -1,8 +1,123 @@
-    <h3><?php echo __('Field Configuration'); ?> &mdash; <?php echo $field->get('label') ?></h3>
+    <h3 class="drag-handle"><?php echo __('Field Configuration'); ?> &mdash; <?php echo $field->get('label') ?></h3>
     <a class="close" href=""><i class="icon-remove-circle"></i></a>
     <hr/>
     <form method="post" action="#form/field-config/<?php
             echo $field->get('id'); ?>">
+<ul class="tabs" id="fieldtabs">
+    <li class="active"><a href="#config"><i class="icon-cogs"></i> <?php echo __('Field Setup'); ?></a></li>
+    <li><a href="#visibility"><i class="icon-beaker"></i> <?php echo __('Settings'); ?></a></li>
+</ul>
+
+<div class="hidden tab_content" id="visibility">
+    <div>
+    <div class="span4">
+        <div style="margin-bottom:5px"><strong><?php echo __('Enabled'); ?></strong>
+        <i class="help-tip icon-question-sign"
+            data-title="<?php echo __('Enabled'); ?>"
+            data-content="<?php echo __('This field can be disabled which will remove it from the form for new entries, but will preserve the data on all current entries.'); ?>"></i>
+        </div>
+    </div>
+    <div class="span6">
+    <input type="checkbox" name="flags[]" value="<?php
+            echo DynamicFormField::FLAG_ENABLED; ?>" <?php
+            if ($field->hasFlag(DynamicFormField::FLAG_ENABLED)) echo 'checked="checked"';
+            if ($field->hasFlag(DynamicFormField::FLAG_MASK_DISABLE)) echo ' disabled="disabled"';
+        ?>> <?php echo __('Enabled'); ?><br/>
+    </div>
+    <hr class="faded"/>
+
+    <div class="span4">
+        <div style="margin-bottom:5px"><strong><?php echo __('Visible'); ?></strong>
+        <i class="help-tip icon-question-sign"
+            data-title="<?php echo __('Visible'); ?>"
+            data-content="<?php echo __('Making fields <em>visible</em> allows agents and endusers to view and create information in this field.'); ?>"></i>
+        </div>
+    </div>
+    <div class="span3">
+        <input type="checkbox" name="flags[]" value="<?php
+            echo DynamicFormField::FLAG_CLIENT_VIEW; ?>" <?php
+            if ($field->hasFlag(DynamicFormField::FLAG_CLIENT_VIEW)) echo 'checked="checked"';
+            if ($field->isPrivacyForced()) echo ' disabled="disabled"';
+        ?>> <?php echo __('For EndUsers'); ?><br/>
+    </div>
+    <div class="span3">
+        <input type="checkbox" name="flags[]" value="<?php
+            echo DynamicFormField::FLAG_AGENT_VIEW; ?>" <?php
+            if ($field->hasFlag(DynamicFormField::FLAG_AGENT_VIEW)) echo 'checked="checked"';
+            if ($field->isPrivacyForced()) echo ' disabled="disabled"';
+        ?>> <?php echo __('For Agents'); ?><br/>
+    </div>
+
+<?php if ($field->getImpl()->hasData()) { ?>
+    <hr class="faded"/>
+
+    <div class="span4">
+        <div style="margin-bottom:5px"><strong><?php echo __('Required'); ?></strong>
+        <i class="help-tip icon-question-sign"
+            data-title="<?php echo __('Required'); ?>"
+            data-content="<?php echo __('New entries cannot be created unless all <em>required</em> fields have valid data.'); ?>"></i>
+        </div>
+    </div>
+    <div class="span3">
+        <input type="checkbox" name="flags[]" value="<?php
+            echo DynamicFormField::FLAG_CLIENT_REQUIRED; ?>" <?php
+            if ($field->hasFlag(DynamicFormField::FLAG_CLIENT_REQUIRED)) echo 'checked="checked"';
+            if ($field->isRequirementForced()) echo ' disabled="disabled"';
+        ?>> <?php echo __('For EndUsers'); ?><br/>
+    </div>
+    <div class="span3">
+        <input type="checkbox" name="flags[]" value="<?php
+            echo DynamicFormField::FLAG_AGENT_REQUIRED; ?>" <?php
+            if ($field->hasFlag(DynamicFormField::FLAG_AGENT_REQUIRED)) echo 'checked="checked"';
+            if ($field->isRequirementForced()) echo ' disabled="disabled"';
+        ?>> <?php echo __('For Agents'); ?><br/>
+    </div>
+    <hr class="faded"/>
+
+    <div class="span4">
+        <div style="margin-bottom:5px"><strong>Editable</strong>
+        <i class="help-tip icon-question-sign"
+            data-content="<?php echo __('Fields marked editable allow agents and endusers to update the content of this field after the form entry has been created.'); ?>"
+            data-title="<?php echo __('Editable'); ?>"></i>
+        </div>
+    </div>
+
+    <div class="span3">
+        <input type="checkbox" name="flags[]" value="<?php
+            echo DynamicFormField::FLAG_CLIENT_EDIT; ?>" <?php
+            if ($field->hasFlag(DynamicFormField::FLAG_CLIENT_EDIT)) echo 'checked="checked"';
+        ?>> <?php echo __('For EndUsers'); ?><br/>
+    </div>
+    <div class="span3">
+        <input type="checkbox" name="flags[]" value="<?php
+            echo DynamicFormField::FLAG_AGENT_EDIT; ?>" <?php
+            if ($field->hasFlag(DynamicFormField::FLAG_AGENT_EDIT)) echo 'checked="checked"';
+        ?>> <?php echo __('For Agents'); ?><br/>
+    </div>
+
+<?php if (in_array($field->get('form')->get('type'), array('G', 'T', 'A'))) { ?>
+    <hr class="faded"/>
+
+    <div class="span4">
+        <div style="margin-bottom:5px"><strong><?php __('Data Integrity');
+    ?></strong>
+        <i class="help-tip icon-question-sign"
+            data-title="<?php echo __('Required to close a thread'); ?>"
+            data-content="<?php echo __('Optionally, this field can prevent closing a thread until it has valid data.'); ?>"></i>
+        </div>
+    </div>
+    <div class="span6">
+        <input type="checkbox" name="flags[]" value="<?php
+            echo DynamicFormField::FLAG_CLOSE_REQUIRED; ?>" <?php
+            if ($field->hasFlag(DynamicFormField::FLAG_CLOSE_REQUIRED)) echo 'checked="checked"';
+        ?>> <?php echo __('Require entry to close a thread'); ?><br/>
+    </div>
+<?php } ?>
+<?php } ?>
+    </div>
+</div>
+
+<div class="tab_content" id="config">
         <?php
         echo csrf_token();
         $form = $field->getConfigurationForm();
@@ -12,7 +127,7 @@
                 ?>" <?php if (!$f->isVisible()) echo 'style="display:none;"'; ?>>
             <div class="field-label <?php if ($f->get('required')) echo 'required'; ?>">
             <label for="<?php echo $f->getWidget()->name; ?>">
-                <?php echo Format::htmlchars($f->get('label')); ?>:
+                <?php echo Format::htmlchars($f->getLocal('label')); ?>:
       <?php if ($f->get('required')) { ?>
                 <span class="error">*</span>
       <?php } ?>
@@ -20,7 +135,7 @@
             <?php
             if ($f->get('hint')) { ?>
                 <br/><em style="color:gray;display:inline-block"><?php
-                    echo Format::htmlchars($f->get('hint')); ?></em>
+                    echo Format::viewableImages($f->get('hint')); ?></em>
             <?php
             } ?>
             </div><div>
@@ -44,11 +159,14 @@
             <em style="color:gray;display:inline-block">
                 <?php echo __('Help text shown with the field'); ?></em>
         </div>
-        <div>
-        <textarea style="width:100%" name="hint" rows="2" cols="40"><?php
+        <div style="width:100%">
+        <textarea style="width:90%; width:calc(100% - 20px)" name="hint" rows="2" cols="40"
+            class="richtext small no-bar"
+            data-translate-tag="<?php echo $field->getTranslateTag('hint'); ?>"><?php
             echo Format::htmlchars($field->get('hint')); ?></textarea>
         </div>
         </div>
+</div>
         <hr>
         <p class="full-width">
             <span class="buttons pull-left">
@@ -61,3 +179,41 @@
          </p>
     </form>
     <div class="clear"></div>
+
+<script type="text/javascript">
+   // Make translatable fields translatable
+   $('input[data-translate-tag]').translatable();
+</script>
+
+<style type="text/css">
+.span3 {
+    width: 22.25%;
+    margin: 0 1%;
+    display: inline-block;
+    vertical-align: top;
+}
+.span4 {
+    width: 30.25%;
+    margin: 0 1%;
+    display: inline-block;
+    vertical-align: top;
+}
+.span6 {
+    width: 47.25%;
+    margin: 0 1%;
+    display: inline-block;
+    vertical-align: top;
+}
+.span12 {
+    width: 97%;
+    margin: 0 1%;
+    display: inline-block;
+    vertical-align: top;
+}
+.dialog input[type=text], .dialog select {
+    margin: 2px;
+}
+hr.faded {
+    opacity: 0.3;
+}
+</style>
diff --git a/include/staff/templates/dynamic-form-fields-view.tmpl.php b/include/staff/templates/dynamic-form-fields-view.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..9bca7741d4492bef7e12415acd54e88c8508bc25
--- /dev/null
+++ b/include/staff/templates/dynamic-form-fields-view.tmpl.php
@@ -0,0 +1,40 @@
+<tbody data-form-id="<?php echo $form->get('id'); ?>">
+    <tr>
+        <td class="handle" colspan="7">
+            <input type="hidden" name="forms[]" value="<?php echo $form->get('id'); ?>" />
+            <div class="pull-right">
+            <i class="icon-large icon-move icon-muted"></i>
+            <a href="#" title="<?php echo __('Delete'); ?>" onclick="javascript:
+            if (confirm(__('You sure?'))) {
+                var tbody = $(this).closest('tbody');
+                $(this).closest('form')
+                    .find('[name=form_id] [value=' + tbody.data('formId') + ']')
+                    .prop('disabled', false);
+                tbody.fadeOut(function(){this.remove()});
+            }
+            return false;"><i class="icon-large icon-trash"></i></a>
+            </div>
+            <div><strong><?php echo Format::htmlchars($form->getLocal('title')); ?></strong></div>
+            <div><?php echo Format::display($form->getLocal('instructions')); ?></div>
+        </td>
+    </tr>
+    <tr class="header">
+        <td><?php echo __('Enable'); ?></td>
+        <td><?php echo __('Label'); ?></td>
+        <td><?php echo __('Type'); ?></td>
+        <td><?php echo __('Visibility'); ?></td>
+        <td><?php echo __('Variable'); ?></td>
+    </tr>
+<?php
+    foreach ($form->getFields() as $f) { ?>
+    <tr>
+        <td><input type="checkbox" name="fields[]" value="<?php
+            echo $f->get('id'); ?>" <?php
+            if ($f->isEnabled()) echo 'checked="checked"'; ?>/></td>
+        <td><?php echo $f->get('label'); ?></td>
+        <td><?php $t=FormField::getFieldType($f->get('type')); echo __($t[0]); ?></td>
+        <td><?php echo $f->getVisibilityDescription(); ?></td>
+        <td><?php echo $f->get('name'); ?></td>
+    </tr>
+    <?php } ?>
+</tbody>
diff --git a/include/staff/templates/dynamic-form-simple.tmpl.php b/include/staff/templates/dynamic-form-simple.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..ca7a91b4982f8646f35e322229face5e9e46b4b1
--- /dev/null
+++ b/include/staff/templates/dynamic-form-simple.tmpl.php
@@ -0,0 +1,45 @@
+<div class="form-simple">
+    <?php
+    echo $form->getMedia();
+    foreach ($form->getFields() as $name=>$f) { ?>
+        <div class="flush-left custom-field" id="field<?php echo $f->getWidget()->id;
+            ?>" <?php if (!$f->isVisible()) echo 'style="display:none;"'; ?>>
+        <div>
+  <?php if ($f->get('label')) { ?>
+        <div class="field-label <?php if ($f->get('required')) echo 'required'; ?>">
+        <label for="<?php echo $f->getWidget()->name; ?>">
+            <?php echo Format::htmlchars($f->get('label')); ?>:
+  <?php if ($f->get('required')) { ?>
+            <span class="error">*</span>
+  <?php } ?>
+        </label>
+        </div>
+  <?php } ?>
+        <?php
+        if ($f->get('hint')) { ?>
+            <em style="color:gray;display:block"><?php
+                echo Format::viewableImages($f->get('hint')); ?></em>
+        <?php
+        } ?>
+        </div><div>
+        <?php
+        $f->render($options);
+        ?>
+        </div>
+        <?php
+        if ($f->errors()) { ?>
+            <div id="field<?php echo $f->getWidget()->id; ?>_error">
+            <?php
+            foreach ($f->errors() as $e) { ?>
+                <div class="error"><?php echo $e; ?></div>
+            <?php
+            } ?>
+            </div>
+        <?php
+        } ?>
+        </div>
+    <?php
+    }
+    $form->emitJavascript($options);
+    ?>
+</div>
diff --git a/include/staff/templates/dynamic-form.tmpl.php b/include/staff/templates/dynamic-form.tmpl.php
index 3987d9ee0ab9582db3fbcd57d669621477da6846..ef18c8bd01c5a988f063d80caea823da0f857ca1 100644
--- a/include/staff/templates/dynamic-form.tmpl.php
+++ b/include/staff/templates/dynamic-form.tmpl.php
@@ -20,25 +20,29 @@ if (isset($options['entry']) && $options['mode'] == 'edit') { ?>
 <?php } ?>
 <?php if ($form->getTitle()) { ?>
     <tr><th colspan="2">
-        <em><strong><?php echo Format::htmlchars($form->getTitle()); ?></strong>:
-        <?php echo Format::htmlchars($form->getInstructions()); ?>
+        <em>
 <?php if ($options['mode'] == 'edit') { ?>
         <div class="pull-right">
     <?php if ($options['entry']
-                && $options['entry']->getForm()->get('type') == 'G') { ?>
+                && $options['entry']->getDynamicForm()->get('type') == 'G') { ?>
             <a href="#" title="Delete Entry" onclick="javascript:
                 $(this).closest('tbody').remove();
                 return false;"><i class="icon-trash"></i></a>&nbsp;
     <?php } ?>
             <i class="icon-sort" title="Drag to Sort"></i>
         </div>
-<?php } ?></em>
+<?php } ?>
+        <strong><?php echo Format::htmlchars($form->getTitle()); ?></strong>:
+        <div><?php echo Format::display($form->getInstructions()); ?></div>
+        </em>
     </th></tr>
     <?php
     }
     foreach ($form->getFields() as $field) {
         try {
-            if (!$field->isVisibleToStaff())
+            if (!$field->isEnabled())
+                continue;
+            if ($options['mode'] == 'edit' && !$field->isEditableToStaff())
                 continue;
         }
         catch (Exception $e) {
@@ -50,18 +54,18 @@ if (isset($options['entry']) && $options['mode'] == 'edit') { ?>
                 <?php
             }
             else { ?>
-                <td class="multi-line <?php if ($field->get('required')) echo 'required';
+                <td class="multi-line <?php if ($field->isRequiredForStaff() || $field->isRequiredForClose()) echo 'required';
                 ?>" style="min-width:120px;" <?php if ($options['width'])
                     echo "width=\"{$options['width']}\""; ?>>
-                <?php echo Format::htmlchars($field->get('label')); ?>:</td>
+                <?php echo Format::htmlchars($field->getLocal('label')); ?>:</td>
                 <td><div style="position:relative"><?php
             }
-            $field->render(); ?>
-            <?php if ($field->get('required')) { ?>
-                <font class="error">*</font>
+            $field->render($options); ?>
+            <?php if (!$field->isBlockLevel() && $field->isRequiredForStaff()) { ?>
+                <span class="error">*</span>
             <?php
             }
-            if (($a = $field->getAnswer()) && $a->isDeleted()) {
+            if ($field->isStorable() && ($a = $field->getAnswer()) && $a->isDeleted()) {
                 ?><a class="action-button float-right danger overlay" title="Delete this data"
                     href="#delete-answer"
                     onclick="javascript:if (confirm('<?php echo __('You sure?'); ?>'))
@@ -77,14 +81,19 @@ if (isset($options['entry']) && $options['mode'] == 'edit') { ?>
                 ?>" data-entry-id="<?php echo $field->getAnswer()->get('entry_id');
                 ?>"> <i class="icon-trash"></i> </a></div><?php
             }
+            if ($a && !$a->getValue() && $field->isRequiredForClose()) {
+?><i class="icon-warning-sign help-tip warning"
+    data-title="<?php echo __('Required to close ticket'); ?>"
+    data-content="<?php echo __('Data is required in this field in order to close the related ticket'); ?>"
+/></i><?php
+            }
             if ($field->get('hint') && !$field->isBlockLevel()) { ?>
                 <br /><em style="color:gray;display:inline-block"><?php
-                    echo Format::htmlchars($field->get('hint')); ?></em>
+                    echo Format::viewableImages($field->getLocal('hint')); ?></em>
             <?php
             }
             foreach ($field->errors() as $e) { ?>
-                <br />
-                <font class="error"><?php echo Format::htmlchars($e); ?></font>
+                <div class="error"><?php echo Format::htmlchars($e); ?></div>
             <?php } ?>
             </div></td>
         </tr>
diff --git a/include/staff/templates/faq-print.tmpl.php b/include/staff/templates/faq-print.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..6f07f898dbd0c00510b2af5e63975c1c9c095e59
--- /dev/null
+++ b/include/staff/templates/faq-print.tmpl.php
@@ -0,0 +1,12 @@
+<div class="faq-title flush-left"><?php echo $faq->getLocalQuestion() ?>
+</div>
+
+<div class="faded"><?php echo __('Last updated');?>
+    <?php echo Format::daydatetime($faq->getUpdateDate()); ?>
+</div>
+
+<br/>
+
+<div class="thread-body bleed">
+<?php echo $faq->getLocalAnswer(); ?>
+</div>
diff --git a/include/staff/templates/form-manage.tmpl.php b/include/staff/templates/form-manage.tmpl.php
index 378fad751e0b7d52b019b17c7ffd9b2bd5030a4f..9794bdbdda6a3e0687f8803e49eee563b85c9a06 100644
--- a/include/staff/templates/form-manage.tmpl.php
+++ b/include/staff/templates/form-manage.tmpl.php
@@ -1,4 +1,4 @@
-<h3><i class="icon-paste"></i> <?php echo __('Manage Forms'); ?></i></h3>
+<h3 class="drag-handle"><i class="icon-paste"></i> <?php echo __('Manage Forms'); ?></i></h3>
 <b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
 <hr/><?php echo __(
 'Sort the forms on this ticket by click and dragging on them. Use the box below the forms list to add new forms to the ticket.'
@@ -12,9 +12,9 @@ $current_list = array();
 foreach ($forms as $e) { ?>
 <div class="sortable row-item" data-id="<?php echo $e->get('id'); ?>">
     <input type="hidden" name="forms[]" value="<?php echo $e->get('form_id'); ?>" />
-    <i class="icon-reorder"></i> <?php echo $e->getForm()->getTitle();
+    <i class="icon-reorder"></i> <?php echo $e->getTitle();
     $current_list[] = $e->get('form_id');
-    if ($e->getForm()->get('type') == 'G') { ?>
+    if ($e->getDynamicForm()->get('type') == 'G') { ?>
     <div class="button-group">
     <div class="delete"><a href="#"><i class="icon-trash"></i></a></div>
     </div>
@@ -23,31 +23,52 @@ foreach ($forms as $e) { ?>
 <?php } ?>
 </div>
 <hr/>
+<div>
 <i class="icon-plus"></i>&nbsp;
 <select name="new-form" onchange="javascript:
-    var $sel = $(this).find('option:selected');
-    $('#ticket-entries').append($('<div></div>').addClass('sortable row-item')
-        .text(' '+$sel.text())
-        .data('id', $sel.val())
-        .prepend($('<i>').addClass('icon-reorder'))
-        .append($('<input/>').attr({name:'forms[]', type:'hidden'}).val($sel.val()))
-        .append($('<div></div>').addClass('button-group')
-          .append($('<div></div>').addClass('delete')
-            .append($('<a href=\'#\'>').append($('<i>').addClass('icon-trash')))
-        ))
-    );
-    $sel.prop('disabled',true);">
+    $(this).parent().find('button').trigger('click');">
 <option selected="selected" disabled="disabled"><?php
     echo __('Add a form'); ?></option>
-<?php foreach (DynamicForm::objects()->filter(array(
-    'type'=>'G')) as $f
-) {
+<?php foreach (DynamicForm::objects()
+    ->filter(array('type'=>'G'))
+    ->exclude(array('flags__hasbit' => DynamicForm::FLAG_DELETED))
+    as $f) {
     if (in_array($f->get('id'), $current_list))
         continue;
     ?><option value="<?php echo $f->get('id'); ?>"><?php
     echo $f->getTitle(); ?></option><?php
 } ?>
 </select>
+<button type="button" class="inline green button" onclick="javascript:
+    var select = $(this).parent().find('select'),
+        $sel = select.find('option:selected'),
+        id = $sel.val();
+    if (!id || !parseInt(id))
+        return;
+    if ($sel.prop('disabled'))
+        return;
+    $('#ticket-entries').append($('<div></div>').addClass('sortable row-item')
+        .text(' '+$sel.text())
+        .data('id', id)
+        .prepend($('<i>').addClass('icon-reorder'))
+        .append($('<input/>').attr({name:'forms[]', type:'hidden'}).val(id))
+        .append($('<div></div>').addClass('button-group')
+          .append($('<div></div>').addClass('delete')
+            .append($('<a href=\'#\'>')
+              .append($('<i>').addClass('icon-trash'))
+              .click(function() {
+                $sel.prop('disabled',false);
+                $(this).closest('div.row-item').remove();
+                $('#delete-warning').show();
+                return false;
+              })
+            )
+        ))
+    );
+    $sel.prop('disabled',true);"><i class="icon-plus-sign"></i>
+<?php echo __('Add'); ?></button>
+</div>
+
 <div id="delete-warning" style="display:none">
 <hr>
     <div id="msg_warning"><?php echo __(
@@ -70,13 +91,5 @@ foreach ($forms as $e) { ?>
 <script type="text/javascript">
 $(function() {
     $('#ticket-entries').sortable({containment:'parent',tolerance:'pointer'});
-    $('#ticket-entries .delete a').live('click', function() {
-        var $div = $(this).closest('.sortable.row-item');
-        $('select[name=new-form]').find('option[data-id='+$div.data('id')+']')
-            .prop('disabled',false);
-        $div.remove();
-        $('#delete-warning').show();
-        return false;
-    })
 });
 </script>
diff --git a/include/staff/templates/inline-form.tmpl.php b/include/staff/templates/inline-form.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..02b3aa6bfe5de7fee0e3eeb94a19106926b01a2b
--- /dev/null
+++ b/include/staff/templates/inline-form.tmpl.php
@@ -0,0 +1,26 @@
+<div><?php
+foreach ($form->getFields() as $field) { ?>
+    <span style="display:inline-block;padding-right:5px;vertical-align:middle">
+<?php   if (!$field->isBlockLevel()) { ?>
+        <span class="<?php if ($field->get('required')) echo 'required'; ?>">
+            <?php echo Format::htmlchars($field->get('label')); ?></span>
+<?php   } ?>
+        <div><?php
+        $field->render(); ?>
+        <?php if ($field->get('required')) { ?>
+            <span class="error">*</span>
+        <?php
+        }
+        if ($field->get('hint') && !$field->isBlockLevel()) { ?>
+            <br/><em style="color:gray;display:inline-block"><?php
+                echo Format::viewableImages($field->get('hint')); ?></em>
+        <?php
+        }
+        foreach ($field->errors() as $e) { ?>
+            <br />
+            <span class="error"><?php echo Format::htmlchars($e); ?></span>
+        <?php } ?>
+        </div>
+    </span><?php
+} ?>
+</div>
diff --git a/include/staff/templates/list-import.tmpl.php b/include/staff/templates/list-import.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..bf367c76ce58d10d762aea0631f0fa03d034c06f
--- /dev/null
+++ b/include/staff/templates/list-import.tmpl.php
@@ -0,0 +1,85 @@
+<h3 class="drag-handle"><?php echo $info['title']; ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<hr/>
+<?php
+if ($info['error']) {
+    echo sprintf('<p id="msg_error">%s</p>', $info['error']);
+} elseif ($info['warn']) {
+    echo sprintf('<p id="msg_warning">%s</p>', $info['warn']);
+} elseif ($info['msg']) {
+    echo sprintf('<p id="msg_notice">%s</p>', $info['msg']);
+} ?>
+<ul class="tabs" id="user-import-tabs">
+    <li class="active"><a href="#copy-paste"
+        ><i class="icon-edit"></i>&nbsp;<?php echo __('Copy Paste'); ?></a></li>
+    <li><a href="#upload"
+        ><i class="icon-fixed-width icon-cloud-upload"></i>&nbsp;<?php echo __('Upload'); ?></a></li>
+</ul>
+
+<form action="<?php echo $info['action']; ?>" method="post" enctype="multipart/form-data"
+    onsubmit="javascript:
+    if ($(this).find('[name=import]').val()) {
+        $(this).attr('action', '<?php echo $info['upload_url']; ?>');
+        $(document).unbind('submit.dialog');
+    }">
+<?php echo csrf_token();
+if ($org_id) { ?>
+    <input type="hidden" name="id" value="<?php echo $org_id; ?>"/>
+<?php } ?>
+<div id="user-import-tabs_container">
+<div class="tab_content" id="copy-paste" style="margin:5px;">
+<h2 style="margin-bottom:10px"><?php echo __('Value and Abbreviation'); ?></h2>
+<p><?php echo __(
+'Enter one name and abbreviation per line.'); ?><br/><em><?php echo __(
+'To import items with properties, use the Upload tab.'); ?></em>
+</p>
+<textarea name="pasted" style="display:block;width:100%;height:8em;padding:5px"
+    placeholder="<?php echo __('e.g. My Location, MY'); ?>">
+<?php echo $info['pasted']; ?>
+</textarea>
+</div>
+
+<div class="hidden tab_content" id="upload" style="margin:5px;">
+<h2 style="margin-bottom:10px"><?php echo __('Import a CSV File'); ?></h2>
+<p>
+<em><?php echo __(
+'Use the columns shown in the table below. To add more properties, use the Properties tab.  Only properties with `variable` defined can be imported.'); ?>
+</p>
+<table class="list"><tr>
+<?php
+    $fields = array('Value', 'Abbreviation');
+    $data = array(
+        array('Value' => __('My Location'), 'Abbreviation' => 'MY')
+    );
+    foreach ($list->getConfigurationForm()->getFields() as $f)
+        if ($f->get('name'))
+            $fields[] = $f->get('label');
+    foreach ($fields as $f) { ?>
+        <th><?php echo mb_convert_case($f, MB_CASE_TITLE); ?></th>
+<?php } ?>
+</tr>
+<?php
+    foreach ($data as $d) {
+        foreach ($fields as $f) {
+            ?><td><?php
+            if (isset($d[$f])) echo $d[$f];
+            ?></td><?php
+        }
+    } ?>
+</tr></table>
+<br/>
+<input type="file" name="import"/>
+</div>
+
+    <hr>
+    <p class="full-width">
+        <span class="buttons pull-left">
+            <input type="reset" value="<?php echo __('Reset'); ?>">
+            <input type="button" name="cancel" class="close"  value="<?php
+            echo __('Cancel'); ?>">
+        </span>
+        <span class="buttons pull-right">
+            <input type="submit" value="<?php echo __('Import Items'); ?>">
+        </span>
+     </p>
+</form>
diff --git a/include/staff/templates/list-item-properties.tmpl.php b/include/staff/templates/list-item-properties.tmpl.php
index dcfc34b92d5bb7f51497cd227a13a2f609a79bc6..a307baa7eb3d6781ac802d13e9b26d3f24d33865 100644
--- a/include/staff/templates/list-item-properties.tmpl.php
+++ b/include/staff/templates/list-item-properties.tmpl.php
@@ -1,63 +1,60 @@
-    <h3><?php echo __('Item Properties'); ?> &mdash; <?php echo $item->getValue() ?></h3>
-    <a class="close" href=""><i class="icon-remove-circle"></i></a>
-    <hr/>
-    <form method="post" action="#list/<?php
-            echo $list->getId(); ?>/item/<?php
-            echo $item->getId(); ?>/properties">
-        <?php
-        echo csrf_token();
-        $config = $item->getConfiguration();
-        $internal = $item->isInternal();
-        $form = $item->getConfigurationForm();
-        echo $form->getMedia();
-        foreach ($form->getFields() as $f) {
-            ?>
-            <div class="custom-field" id="field<?php
-                echo $f->getWidget()->id; ?>"
-                <?php
-                if (!$f->isVisible()) echo 'style="display:none;"'; ?>>
-            <div class="field-label">
-            <label for="<?php echo $f->getWidget()->name; ?>"
-                style="vertical-align:top;padding-top:0.2em">
-                <?php echo Format::htmlchars($f->get('label')); ?>:</label>
-                <?php
-                if (!$internal && $f->isEditable() && $f->get('hint')) { ?>
-                    <br /><em style="color:gray;display:inline-block"><?php
-                        echo Format::htmlchars($f->get('hint')); ?></em>
-                <?php
-                } ?>
-            </div><div>
-            <?php
-            if ($internal && !$f->isEditable())
-                $f->render('view');
-            else {
-                $f->render();
-                if ($f->get('required')) { ?>
-                    <font class="error">*</font>
-                <?php
-                }
-            }
-            ?>
-            </div>
-            <?php
-            foreach ($f->errors() as $e) { ?>
-                <div class="error"><?php echo $e; ?></div>
-            <?php } ?>
-            </div>
-            <?php
-        }
-        ?>
-        </table>
-        <hr>
-        <p class="full-width">
-            <span class="buttons pull-left">
-                <input type="reset" value="<?php echo __('Reset'); ?>">
-                <input type="button" value="<?php echo __('Cancel'); ?>" class="close">
-            </span>
-            <span class="buttons pull-right">
-                <input type="submit" value="<?php echo __('Save'); ?>">
-            </span>
-         </p>
-    </form>
-    <div class="clear"></div>
+<?php
+    $properties_form = $item ? $item->getConfigurationForm($_POST ?: null)
+        : $list->getConfigurationForm($_POST ?: null);
+    $hasProperties = count($properties_form->getFields()) > 0;
+?>
+<h3 class="drag-handle"><?php echo $list->getName(); ?> &mdash; <?php
+    echo $item ? $item->getValue() : __('Add New List Item'); ?></h3>
+<a class="close" href=""><i class="icon-remove-circle"></i></a>
+<hr/>
 
+<?php if ($hasProperties) { ?>
+<ul class="tabs" id="item_tabs">
+    <li class="active">
+        <a href="#value"><i class="icon-reorder"></i>
+        <?php echo __('Value'); ?></a>
+    </li>
+    <li><a href="#item-properties"><i class="icon-asterisk"></i>
+        <?php echo __('Item Properties'); ?></a>
+    </li>
+</ul>
+<?php } ?>
+
+<form method="post" id="item_tabs_container" action="<?php echo $action; ?>">
+    <?php
+    echo csrf_token();
+    $internal = $item ? $item->isInternal() : false;
+?>
+
+<div class="tab_content" id="value">
+<?php
+    $form = $item_form;
+    include 'dynamic-form-simple.tmpl.php';
+?>
+</div>
+
+<div class="tab_content hidden" id="item-properties">
+<?php
+    if ($hasProperties) {
+        $form = $properties_form;
+        include 'dynamic-form-simple.tmpl.php';
+    }
+?>
+</div>
+
+<hr>
+<p class="full-width">
+    <span class="buttons pull-left">
+        <input type="reset" value="<?php echo __('Reset'); ?>">
+        <input type="button" value="<?php echo __('Cancel'); ?>" class="close">
+    </span>
+    <span class="buttons pull-right">
+        <input type="submit" value="<?php echo __('Save'); ?>">
+    </span>
+ </p>
+</form>
+
+<script type="text/javascript">
+   // Make translatable fields translatable
+   $('input[data-translate-tag], textarea[data-translate-tag]').translatable();
+</script>
diff --git a/include/staff/templates/list-item-row.tmpl.php b/include/staff/templates/list-item-row.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..574f28e97d8cc2ca9b4e1c00c96e1b293f1aee41
--- /dev/null
+++ b/include/staff/templates/list-item-row.tmpl.php
@@ -0,0 +1,41 @@
+<?php
+    $id = $item->getId(); ?>
+    <tr id="list-item-<?php echo $id; ?>" class="<?php if (!$item->isEnabled()) echo 'disabled'; ?>">
+        <td nowrap><?php echo $icon; ?>
+            <input type="hidden" name="sort-<?php echo $id; ?>"
+            value="<?php echo $item->getSortOrder(); ?>"/>
+            <input type="checkbox" value="<?php echo $id; ?>" class="mass nowarn"/>
+        </td>
+        <td>
+            <a class="field-config"
+               style="overflow:inherit"
+               href="#list/<?php
+                echo $list->getId(); ?>/item/<?php
+                echo $id ?>/update"
+               id="item-<?php echo $id; ?>"
+            ><?php
+                echo sprintf('<i class="icon-edit" %s></i> ',
+                        ($prop_fields && !$item->getConfiguration())
+                        ? 'style="color:red; font-weight:bold;"' : '');
+            ?>
+            <?php echo Format::htmlchars($item->getValue()); ?>
+            <?php
+            if ($list->hasAbbrev() && ($A = $item->getAbbrev())) { ?>
+                ( <?php echo Format::htmlchars($A); ?> )
+            <?php
+            } ?>
+<?php           if ($errors["value-$id"])
+                echo sprintf('<div class="error">%s</div>',
+                        $errors["value-$id"]);
+            ?>
+            </a>
+        </td>
+<?php $props = $item->getConfiguration();
+if ($prop_fields) {
+    foreach ($prop_fields as $F) { ?>
+        <td style="max-width: 20%"><span class="truncate"><?php
+        echo $F->display($props[$F->get('id')]);
+        ?></span></td>
+<?php }
+} ?>
+    </tr>
diff --git a/include/staff/templates/list-items.tmpl.php b/include/staff/templates/list-items.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..d843a68d2b79b9e6c9b038b296a447a889a3b9b2
--- /dev/null
+++ b/include/staff/templates/list-items.tmpl.php
@@ -0,0 +1,139 @@
+<?php
+    if ($list) {
+        $page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
+        $count = $list->getNumItems();
+        $pageNav = new Pagenate($count, $page, PAGE_LIMIT);
+        if ($list->getSortMode() == 'SortCol')
+            $pageNav->setSlack(1);
+        $pageNav->setURL('lists.php?id='.$list->getId().'&a=items');
+    }
+    ?>
+    <div style="margin: 5px 0">
+    <?php if ($list) { ?>
+    <div class="pull-left">
+        <input type="text" placeholder="<?php echo __('Search items'); ?>"
+            data-url="ajax.php/list/<?php echo $list->getId(); ?>/items/search"
+            size="25" id="items-search" value="<?php
+            echo Format::htmlchars($_POST['search']); ?>"/>
+    </div>
+    <div class="pull-right">
+<?php
+if ($list->allowAdd()) { ?>
+        <a class="green button action-button field-config"
+            href="#list/<?php
+            echo $list->getId(); ?>/item/add">
+            <i class="icon-plus-sign"></i>
+            <?php echo __('Add New Item'); ?>
+        </a>
+<?php
+    if (method_exists($list, 'importCsv')) { ?>
+        <a class="action-button field-config"
+            href="#list/<?php
+            echo $list->getId(); ?>/import">
+            <i class="icon-upload"></i>
+            <?php echo __('Import Items'); ?>
+        </a>
+<?php
+    }
+} ?>
+        <span class="action-button pull-right" data-dropdown="#action-dropdown-more">
+            <i class="icon-caret-down pull-right"></i>
+            <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+        </span>
+        <div id="action-dropdown-more" class="action-dropdown anchor-right">
+            <ul>
+                <li><a class="items-action" href="#list/<?php echo $list->getId(); ?>/disable">
+                    <i class="icon-ban-circle icon-fixed-width"></i>
+                    <?php echo __('Disable'); ?></a></li>
+                <li><a class="items-action" href="#list/<?php echo $list->getId(); ?>/enable">
+                    <i class="icon-ok-sign icon-fixed-width"></i>
+                    <?php echo __('Enable'); ?></a></li>
+                <li class="danger"><a class="items-action" href="#list/<?php echo $list->getId(); ?>/delete">
+                    <i class="icon-trash icon-fixed-width"></i>
+                    <?php echo __('Delete'); ?></a></li>
+            </ul>
+        </div>
+    </div>
+    <?php } ?>
+
+    <div class="clear"></div>
+    </div>
+
+
+<?php
+$prop_fields = ($list) ? $list->getSummaryFields() : array();
+?>
+
+    <table class="form_table fixed" width="940" border="0" cellspacing="0" cellpadding="2">
+    <thead>
+        <tr>
+            <th width="28" nowrap></th>
+            <th><?php echo __('Value'); ?></th>
+<?php
+if ($prop_fields) {
+    foreach ($prop_fields as $F) { ?>
+            <th><?php echo $F->getLocal('label'); ?></th>
+<?php
+    }
+} ?>
+        </tr>
+    </thead>
+
+    <tbody id="list-items" <?php if (!isset($_POST['search']) && $list && $list->get('sort_mode') == 'SortCol') { ?>
+            class="sortable-rows" data-sort="sort-"<?php } ?>>
+        <?php
+        if ($list) {
+            $icon = ($list->get('sort_mode') == 'SortCol')
+                ? '<i class="icon-sort"></i>&nbsp;' : '';
+            $items = $list->getAllItems();
+            $items = $pageNav->paginate($items);
+            // Emit a marker for the first sort offset ?>
+            <input type="hidden" id="sort-offset" value="<?php echo
+                max($items[0]->sort, $pageNav->getStart()); ?>"/>
+<?php
+            foreach ($items as $item) {
+                include STAFFINC_DIR . 'templates/list-item-row.tmpl.php';
+            }
+        } ?>
+    </tbody>
+    </table>
+<?php if ($pageNav && $pageNav->getNumPages()) { ?>
+    <div><?php echo __('Page').':'.$pageNav->getPageLinks('items', $pjax_container); ?></div>
+<?php } ?>
+</div>
+<script type="text/javascript">
+$(function() {
+  var last_req;
+  $('input#items-search').typeahead({
+    source: function (typeahead, query) {
+      if (last_req)
+        last_req.abort();
+      var $el = this.$element;
+      var url = $el.data('url')+'?q='+query;
+      last_req = $.ajax({
+        url: url,
+        dataType: 'json',
+        success: function (data) {
+          typeahead.process(data);
+        }
+      });
+    },
+    onselect: function (obj) {
+      var $el = this.$element,
+          url = 'ajax.php/list/{0}/item/{1}/update'
+            .replace('{0}', obj.list_id)
+            .replace('{1}', obj.id);
+      $.dialog(url, [201], function (xhr, resp) {
+        var json = $.parseJSON(resp);
+        if (json && json.success) {
+          if (json.id && json.row) {
+            $('#list-item-' + json.id).replaceWith(json.row);
+          }
+        }
+      });
+      this.$element.val('');
+    },
+    property: "display"
+  });
+});
+</script>
diff --git a/include/staff/templates/navigation.tmpl.php b/include/staff/templates/navigation.tmpl.php
index 8f0444999c7d0c169acbc599dfbfa69285a2462d..5c86f47153b767c1c9bfd3d47c13b5f4e501236e 100644
--- a/include/staff/templates/navigation.tmpl.php
+++ b/include/staff/templates/navigation.tmpl.php
@@ -1,6 +1,8 @@
 <?php
 if(($tabs=$nav->getTabs()) && is_array($tabs)){
     foreach($tabs as $name =>$tab) {
+        if ($tab['href'][0] != '/')
+            $tab['href'] = ROOT_PATH . 'scp/' . $tab['href'];
         echo sprintf('<li class="%s %s"><a href="%s">%s</a>',
             $tab['active'] ? 'active':'inactive',
             @$tab['class'] ?: '',
@@ -10,6 +12,8 @@ if(($tabs=$nav->getTabs()) && is_array($tabs)){
             foreach($subnav as $k => $item) {
                 if (!($id=$item['id']))
                     $id="nav$k";
+                if ($item['href'][0] != '/')
+                    $item['href'] = ROOT_PATH . 'scp/' . $item['href'];
 
                 echo sprintf(
                     '<li><a class="%s" href="%s" title="%s" id="%s">%s</a></li>',
diff --git a/include/staff/templates/org-delete.tmpl.php b/include/staff/templates/org-delete.tmpl.php
index 16e06c83b556e6bb2a034ba46d38c5c4834e3ec7..fe32251e36743842eef8539282569f1669e70677 100644
--- a/include/staff/templates/org-delete.tmpl.php
+++ b/include/staff/templates/org-delete.tmpl.php
@@ -6,7 +6,7 @@ if (!$info['title'])
 $info['warn'] = __('Deleted organization CANNOT be recovered');
 
 ?>
-<h3><?php echo $info['title']; ?></h3>
+<h3 class="drag-handle"><?php echo $info['title']; ?></h3>
 <b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
 <hr/>
 <?php
@@ -26,7 +26,7 @@ if ($info['error']) {
 <?php foreach ($org->getDynamicData() as $entry) {
 ?>
     <tr><td colspan="2" style="border-bottom: 1px dotted black"><strong><?php
-         echo $entry->getForm()->get('title'); ?></strong></td></tr>
+         echo $entry->getTitle(); ?></strong></td></tr>
 <?php foreach ($entry->getAnswers() as $a) { ?>
     <tr style="vertical-align:top"><td style="width:30%;border-bottom: 1px dotted #ccc"><?php echo Format::htmlchars($a->getField()->get('label'));
          ?>:</td>
diff --git a/include/staff/templates/org-lookup.tmpl.php b/include/staff/templates/org-lookup.tmpl.php
index 10a4ce28fbf75916ba718e52e45bd0c9f38ae25a..07d6c7687f8d559d44c4fb0eb761b04c8d4ed58f 100644
--- a/include/staff/templates/org-lookup.tmpl.php
+++ b/include/staff/templates/org-lookup.tmpl.php
@@ -9,7 +9,7 @@ if ($info['search'] === false)
 
 ?>
 <div id="the-lookup-form">
-<h3><?php echo $info['title']; ?></h3>
+<h3 class="drag-handle"><?php echo $info['title']; ?></h3>
 <b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
 <hr/>
 <div><p id="msg_info"><i class="icon-info-sign"></i>&nbsp; <?php echo $msg_info; ?></p></div>
@@ -17,7 +17,8 @@ if ($info['search'] === false)
 if ($info['search'] !== false) { ?>
 <div style="margin-bottom:10px;">
     <input type="text" class="search-input" style="width:100%;"
-    placeholder="Search by name" id="org-search" autocorrect="off" autocomplete="off"/>
+    placeholder="Search by name" id="org-search"
+    autofocus autocorrect="off" autocomplete="off"/>
 </div>
 <?php
 }
@@ -41,7 +42,7 @@ if ($info['error']) {
     <table style="margin-top: 1em;">
 <?php foreach ($org->getDynamicData() as $entry) { ?>
     <tr><td colspan="2" style="border-bottom: 1px dotted black"><strong><?php
-         echo $entry->getForm()->get('title'); ?></strong></td></tr>
+         echo $entry->getForm()->getTitle(); ?></strong></td></tr>
 <?php foreach ($entry->getAnswers() as $a) { ?>
     <tr style="vertical-align:top"><td style="width:30%;border-bottom: 1px dotted #ccc"><?php echo Format::htmlchars($a->getField()->get('label'));
          ?>:</td>
diff --git a/include/staff/templates/org-profile.tmpl.php b/include/staff/templates/org-profile.tmpl.php
index 2b83f76b602a1fdbf32149957eb6118ed865efa1..520c73ebfd2c6356fff050714aea0cb99222b163 100644
--- a/include/staff/templates/org-profile.tmpl.php
+++ b/include/staff/templates/org-profile.tmpl.php
@@ -4,9 +4,7 @@ $info=($_POST && $errors)?Format::input($_POST):@Format::htmlchars($org->getInfo
 if (!$info['title'])
     $info['title'] = Format::htmlchars($org->getName());
 ?>
-<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery.multiselect.min.js"></script>
-<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/jquery.multiselect.css"/>
-<h3><?php echo $info['title']; ?></h3>
+<h3 class="drag-handle"><?php echo $info['title']; ?></h3>
 <b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
 <hr/>
 <?php
@@ -15,16 +13,16 @@ if ($info['error']) {
 } elseif ($info['msg']) {
     echo sprintf('<p id="msg_notice">%s</p>', $info['msg']);
 } ?>
-<ul class="tabs">
-    <li><a href="#tab-profile" class="active"
+<ul class="tabs" id="orgprofile">
+    <li class="active"><a href="#profile"
         ><i class="icon-edit"></i>&nbsp;<?php echo __('Fields'); ?></a></li>
     <li><a href="#contact-settings"
         ><i class="icon-fixed-width icon-cogs faded"></i>&nbsp;<?php
         echo __('Settings'); ?></a></li>
 </ul>
-<form method="post" class="org" action="<?php echo $action; ?>">
-
-<div class="tab_content" id="tab-profile" style="margin:5px;">
+<form method="post" class="org" action="<?php echo $action; ?>" data-tip-namespace="org">
+<div id="orgprofile_container">
+<div class="tab_content" id="profile" style="margin:5px;">
 <?php
 $action = $info['action'] ? $info['action'] : ('#orgs/'.$org->getId());
 if ($ticket && $ticket->getOwnerId() == $user->getId())
@@ -40,7 +38,7 @@ if ($ticket && $ticket->getOwnerId() == $user->getId())
     </table>
 </div>
 
-<div class="tab_content" id="contact-settings" style="display:none;margin:5px;">
+<div class="hidden tab_content" id="contact-settings" style="margin:5px;">
     <table style="width:100%">
         <tbody>
             <tr>
@@ -89,7 +87,9 @@ if ($ticket && $ticket->getOwnerId() == $user->getId())
                     <?php echo __('Primary Contacts'); ?>:
                 </td>
                 <td>
-                    <select name="contacts[]" id="primary_contacts" multiple="multiple">
+                    <select name="contacts[]" id="primary_contacts" multiple="multiple"
+                        data-placeholder="<?php echo __('Select Contacts'); ?>">
+                        <option value=""></option>
 <?php               foreach ($org->allMembers() as $u) { ?>
                         <option value="<?php echo $u->id; ?>" <?php
                             if ($u->isPrimaryContact())
@@ -98,6 +98,22 @@ if ($ticket && $ticket->getOwnerId() == $user->getId())
                     </select>
                     <br/><span class="error"><?php echo $errors['contacts']; ?></span>
                 </td>
+            </tr>
+            <tr>
+                <td width="180">
+                    <?php echo __('Ticket Sharing'); ?>:
+                </td>
+                <td>
+                    <select name="sharing">
+                        <option value=""><?php echo __('Disable'); ?></option>
+                        <option value="sharing-primary" <?php echo $info['sharing-primary'] ? 'selected="selected"' : '';
+                            ?>><?php echo __('Primary contacts see all tickets'); ?></option>
+                        <option value="sharing-all" <?php echo $info['sharing-all'] ? 'selected="selected"' : '';
+                            ?>><?php echo __('All members see all tickets'); ?></option>
+                    </select>
+                    <i class="help-tip icon-question-sign" href="#org_sharing"></i>
+                </td>
+            </tr>
             <tr>
                 <th colspan="2">
                     <?php echo __('Automated Collaboration'); ?>:
@@ -123,7 +139,8 @@ if ($ticket && $ticket->getOwnerId() == $user->getId())
             </tr>
             <tr>
                 <th colspan="2">
-                    <?php echo __('Main Domain'); ?>
+                    <?php echo __('Email Domain'); ?>
+                    <i class="help-tip icon-question-sign" href="#email_domain"></i>
                 </th>
             </tr>
             <tr>
@@ -139,7 +156,7 @@ if ($ticket && $ticket->getOwnerId() == $user->getId())
         </tbody>
     </table>
 </div>
-
+</div>
 <div class="clear"></div>
 
 <hr>
@@ -170,6 +187,6 @@ $(function() {
         $('div#org-profile').fadeIn();
         return false;
     });
-    $("#primary_contacts").multiselect({'noneSelectedText':'<?php echo __('Select Contacts'); ?>'});
+    $("#primary_contacts").select2({width: '300px'});
 });
 </script>
diff --git a/include/staff/templates/org.tmpl.php b/include/staff/templates/org.tmpl.php
index 06f82255c7736dc9db02d58f65ed5e51073f2b18..4b8bc97fa31cd6a8585bc944efb0d770c92f7532 100644
--- a/include/staff/templates/org.tmpl.php
+++ b/include/staff/templates/org.tmpl.php
@@ -2,7 +2,7 @@
 if (!$info['title'])
     $info['title'] = Format::htmlchars($org->getName());
 ?>
-<h3><?php echo $info['title']; ?></h3>
+<h3 class="drag-handle"><?php echo $info['title']; ?></h3>
 <b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
 <hr/>
 <?php
@@ -28,7 +28,7 @@ if ($info['error']) {
 <?php foreach ($org->getDynamicData() as $entry) {
 ?>
     <tr><td colspan="2" style="border-bottom: 1px dotted black"><strong><?php
-         echo $entry->getForm()->get('title'); ?></strong></td></tr>
+         echo $entry->getTitle(); ?></strong></td></tr>
 <?php foreach ($entry->getAnswers() as $a) { ?>
     <tr style="vertical-align:top"><td style="width:30%;border-bottom: 1px dotted #ccc"><?php echo Format::htmlchars($a->getField()->get('label'));
          ?>:</td>
@@ -40,7 +40,7 @@ if ($info['error']) {
     </table>
     <div class="clear"></div>
     <hr>
-    <div class="faded">Last updated <b><?php echo Format::db_datetime($org->getUpdateDate()); ?> </b></div>
+    <div class="faded">Last updated <b><?php echo Format::datetime($org->getUpdateDate()); ?> </b></div>
 </div>
 <div id="org-form" style="display:<?php echo $forms ? 'block' : 'none'; ?>;">
 <div><p id="msg_info"><i class="icon-info-sign"></i>&nbsp; <?php echo __(
diff --git a/include/staff/templates/queue-sort.tmpl.php b/include/staff/templates/queue-sort.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..42b56437057c208450bfbc3c29ffc89e2732ced6
--- /dev/null
+++ b/include/staff/templates/queue-sort.tmpl.php
@@ -0,0 +1,33 @@
+
+<span class="action-button muted" data-dropdown="#sort-dropdown" data-toggle="tooltip" title="<?php echo $sort_options[$sort_cols]; ?>">
+  <i class="icon-caret-down pull-right"></i>
+  <span><i class="icon-sort-by-attributes-alt <?php if ($sort_dir) echo 'icon-flip-vertical'; ?>"></i> <?php echo __('Sort');?></span>
+</span>
+<div id="sort-dropdown" class="action-dropdown anchor-right"
+onclick="javascript:
+var query = addSearchParam({'sort': $(event.target).data('mode'), 'dir': $(event.target).data('dir')});
+$.pjax({
+    url: '?' + query,
+    timeout: 2000,
+    container: '#pjax-container'});">
+  <ul class="bleed-left">
+    <?php foreach ($queue_sort_options as $mode) {
+    $desc = $sort_options[$mode];
+    $icon = '';
+    $dir = '0';
+    $selected = $sort_cols == $mode; ?>
+    <li <?php
+    if ($selected) {
+    echo 'class="active"';
+    $dir = ($sort_dir == '1') ? '0' : '1'; // Flip the direction
+    $icon = ($sort_dir == '1') ? 'icon-hand-up' : 'icon-hand-down';
+    }
+    ?>>
+        <a href="#" data-mode="<?php echo $mode; ?>" data-dir="<?php echo $dir; ?>">
+          <i class="icon-fixed-width <?php echo $icon; ?>"
+          ></i> <?php echo Format::htmlchars($desc); ?></a>
+      </li>
+    <?php } ?>
+ </ul>
+</div>
+
diff --git a/include/staff/templates/quick-add-role.tmpl.php b/include/staff/templates/quick-add-role.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..929107b716468dcc302faa6e7705c8215f149d2f
--- /dev/null
+++ b/include/staff/templates/quick-add-role.tmpl.php
@@ -0,0 +1,26 @@
+<?php
+include 'quick-add.tmpl.php';
+$clone = $form->getField('clone')->getWidget()->name;
+$permissions = $form->getField('perms')->getWidget()->name;
+$name = $form->getField('name')->getWidget()->name;
+?>
+<script type="text/javascript">
+  $('#_<?php echo $clone; ?>').change(function() {
+    var $this = $(this),
+        id = $this.val(),
+        form = $this.closest('form'),
+        name = $('[name="<?php echo $name; ?>"]:first', form);
+    $.ajax({
+      url: 'ajax.php/admin/role/'+id+'/perms',
+      dataType: 'json',
+      success: function(json) {
+        $('[name="<?php echo $permissions; ?>[]"]', form).prop('checked', false);
+        $.each(json, function(k, v) {
+          form.find('[value="'+k+'"]', form).prop('checked', !!v);
+        });
+        if (!name.val())
+          name.val(__('Copy of {0}').replace('{0}', $this.find(':selected').text()));
+      }
+    });
+  });
+</script>
diff --git a/include/staff/templates/quick-add.tmpl.php b/include/staff/templates/quick-add.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..7f04098f316b34b96d305cad3649bd90fbde5e5e
--- /dev/null
+++ b/include/staff/templates/quick-add.tmpl.php
@@ -0,0 +1,25 @@
+<h3 class="drag-handle"><?php echo $title ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<div class="clear"></div>
+<hr/>
+<?php if (isset($errors['err'])) { ?>
+    <div id="msg_error" class="error-banner"><?php echo Format::htmlchars($errors['err']); ?></div>
+<?php } ?>
+<form method="post" action="#<?php echo $path; ?>">
+  <div class="quick-add">
+    <?php echo $form->asTable(); ?>
+  </div>
+  <hr>
+  <p class="full-width">
+    <span class="buttons pull-left">
+      <input type="reset" value="<?php echo __('Reset'); ?>" />
+      <input type="button" name="cancel" class="close"
+        value="<?php echo __('Cancel'); ?>" />
+    </span>
+    <span class="buttons pull-right">
+      <input type="submit" value="<?php
+        echo $verb ?: __('Create'); ?>" />
+    </span>
+  </p>
+  <div class="clear"></div>
+</form>
diff --git a/include/staff/templates/reset-agent-permissions.tmpl.php b/include/staff/templates/reset-agent-permissions.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..b69dbd72309ab78def3c70f1c7a308874c02d4a4
--- /dev/null
+++ b/include/staff/templates/reset-agent-permissions.tmpl.php
@@ -0,0 +1,22 @@
+<?php
+include 'quick-add.tmpl.php';
+$clone = $form->getField('clone')->getWidget()->name;
+$permissions = $form->getField('perms')->getWidget()->name;
+?>
+<script type="text/javascript">
+  $('#_<?php echo $clone; ?>').change(function() {
+    var $this = $(this),
+        id = $this.val(),
+        form = $this.closest('form');
+    $.ajax({
+      url: 'ajax.php/staff/'+id+'/perms',
+      dataType: 'json',
+      success: function(json) {
+        $('[name="<?php echo $permissions; ?>[]"]', form).prop('checked', false);
+        $.each(json, function(k, v) {
+          form.find('[value="'+k+'"]', form).prop('checked', !!v);
+        });
+      }
+    });
+  });
+</script>
diff --git a/include/staff/templates/sequence-manage.tmpl.php b/include/staff/templates/sequence-manage.tmpl.php
index 6827cd6267d5d5f0c8cbae54a0af44cf162c91be..d8ec18dcf272d7ff80a74b674452076ba7168e86 100644
--- a/include/staff/templates/sequence-manage.tmpl.php
+++ b/include/staff/templates/sequence-manage.tmpl.php
@@ -1,4 +1,4 @@
-<h3><i class="icon-wrench"></i> <?php echo __('Manage Sequences'); ?></i></h3>
+<h3 class="drag-handle"><i class="icon-wrench"></i> <?php echo __('Manage Sequences'); ?></i></h3>
 <b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
 <hr/><?php echo __(
 'Sequences are used to generate sequential numbers. Various sequences can be
@@ -20,7 +20,7 @@ foreach ($sequences as $e) {
         <i class="icon-sort-by-order"></i>
         <div style="display:inline-block" class="name"> <?php echo $e->getName(); ?> </div>
         <div class="manage-buttons pull-right">
-            <span class="faded">next</span>
+            <span class="faded"><?php echo __('next'); ?></span>
             <span class="current"><?php echo $e->current(); ?></span>
         </div>
         <div class="button-group">
@@ -136,7 +136,7 @@ $(function() {
   $(document).on('click.seq', '#sequences .save a', save);
   $(document).on('click.seq', '#sequences .delete a', remove);
   $('.close, input:submit').click(function() {
-      $(document).die('click.seq');
+      $(document).off('click.seq');
   });
 });
 </script>
diff --git a/include/staff/templates/set-password.tmpl.php b/include/staff/templates/set-password.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..bc3d66ff50fc8e2d870b11e255c2c38a3a2ad287
--- /dev/null
+++ b/include/staff/templates/set-password.tmpl.php
@@ -0,0 +1,22 @@
+<h3 class="drag-handle"><?php echo $title ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<div class="clear"></div>
+<hr/>
+<form method="post" action="#<?php echo $path; ?>">
+  <div class="inset">
+    <?php $form->render(); ?>
+  </div>
+  <hr>
+  <p class="full-width">
+    <span class="buttons pull-left">
+      <input type="reset" value="<?php echo __('Reset'); ?>" />
+      <input type="button" name="cancel" class="close"
+        value="<?php echo __('Cancel'); ?>" />
+    </span>
+    <span class="buttons pull-right">
+      <input type="submit" value="<?php
+        echo $verb ?: __('Update'); ?>" />
+    </span>
+  </p>
+  <div class="clear"></div>
+</form>
diff --git a/include/staff/templates/simple-form.tmpl.php b/include/staff/templates/simple-form.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..6661f5c97477ee268de902553bce95604de1a101
--- /dev/null
+++ b/include/staff/templates/simple-form.tmpl.php
@@ -0,0 +1,48 @@
+<?php if ($form->getTitle()) { ?>
+    <h1><strong><?php echo Format::htmlchars($form->getTitle()); ?></strong>:
+        <div><small><?php echo Format::htmlchars($form->getInstructions()); ?></small></div>
+    </h1>
+    <?php
+    }
+    foreach ($form->getFields() as $field) { ?>
+        <div class="form-field"><?php
+        if (!$field->isBlockLevel()) { ?>
+            <div class="<?php if ($field->isRequired()) echo 'required';
+                ?>" style="display:inline-block;width:27%;">
+                <?php echo Format::htmlchars($field->getLocal('label')); ?>:
+            <?php if ($field->isRequired()) { ?>
+                <span class="error">*</span>
+            <?php
+            }
+            if ($field->get('hint')) { ?>
+                <div class="faded hint"><?php
+                echo Format::viewableImages($field->getLocal('hint'));
+                ?></div>
+<?php       } ?>
+            </div>
+            <div style="display:inline-block;max-width:73%"><?php
+        }
+        $field->render($options);
+        foreach ($field->errors() as $e) { ?>
+            <div class="error"><?php echo Format::htmlchars($e); ?></div>
+        <?php }
+        if (!$field->isBlockLevel()) { ?>
+            </div>
+        <?php } ?>
+        </div>
+<?php } ?>
+<style type="text/css">
+.form-field div {
+  vertical-align: top;
+}
+.form-field div + div {
+  padding-left: 10px;
+}
+.form-field .hint {
+  font-size: 95%;
+}
+.form-field {
+  margin-top: 5px;
+  padding: 5px 0;
+}
+</style>
diff --git a/include/staff/templates/status-options.tmpl.php b/include/staff/templates/status-options.tmpl.php
index edfdf19564e0367a1ebaa97e3adc1e08d075cf04..3b493f37927b9cbf608f0a698fa06a8ca5c6e469 100644
--- a/include/staff/templates/status-options.tmpl.php
+++ b/include/staff/templates/status-options.tmpl.php
@@ -12,32 +12,38 @@ $actions= array(
             'action' => 'reopen'
             ),
         );
+
+$states = array('open');
+if ($thisstaff->getRole($ticket ? $ticket->getDeptId() : null)->hasPerm(TicketModel::PERM_CLOSE)
+        && (!$ticket || !$ticket->getMissingRequiredFields()))
+    $states = array_merge($states, array('closed'));
+
+$statusId = $ticket ? $ticket->getStatusId() : 0;
+$nextStatuses = array();
+foreach (TicketStatusList::getStatuses(
+            array('states' => $states)) as $status) {
+    if (!isset($actions[$status->getState()])
+            || $statusId == $status->getId())
+        continue;
+    $nextStatuses[] = $status;
+}
+
+if (!$nextStatuses)
+    return;
 ?>
 
 <span
-    class="action-button pull-right"
-    data-dropdown="#action-dropdown-statuses">
+    class="action-button"
+    data-dropdown="#action-dropdown-statuses" data-placement="bottom" data-toggle="tooltip" title="<?php echo __('Change Status'); ?>">
     <i class="icon-caret-down pull-right"></i>
     <a class="tickets-action"
         href="#statuses"><i
-        class="icon-flag"></i> <?php
-        echo __('Change Status'); ?></a>
+        class="icon-flag"></i></a>
 </span>
 <div id="action-dropdown-statuses"
     class="action-dropdown anchor-right">
     <ul>
-    <?php
-    $states = array('open');
-    if ($thisstaff->canCloseTickets())
-        $states = array_merge($states, array('closed'));
-
-    $statusId = $ticket ? $ticket->getStatusId() : 0;
-    foreach (TicketStatusList::getStatuses(
-                array('states' => $states))->all() as $status) {
-        if (!isset($actions[$status->getState()])
-                || $statusId == $status->getId())
-            continue;
-        ?>
+<?php foreach ($nextStatuses as $status) { ?>
         <li>
             <a class="no-pjax <?php
                 echo $ticket? 'ticket-action' : 'tickets-action'; ?>"
@@ -48,7 +54,7 @@ $actions= array(
                             $status->getId()); ?>"
                 <?php
                 if (isset($actions[$status->getState()]['href']))
-                    echo sprintf('data-href="%s"',
+                    echo sprintf('data-redirect="%s"',
                             $actions[$status->getState()]['href']);
 
                 ?>
diff --git a/include/staff/templates/sub-navigation.tmpl.php b/include/staff/templates/sub-navigation.tmpl.php
index 0cb5e6ed4737ebdfb67e08f80c716b665ddf0d6e..6e40d33c8274b9fd29bf85973c3dc968d17118c7 100644
--- a/include/staff/templates/sub-navigation.tmpl.php
+++ b/include/staff/templates/sub-navigation.tmpl.php
@@ -17,7 +17,13 @@ if(($subnav=$nav->getSubMenu()) && is_array($subnav)){
         if (!($id=$item['id']))
             $id="subnav$k";
 
-        echo sprintf('<li><a class="%s" href="%s" title="%s" id="%s">%s</a></li>',
-                $class, $item['href'], $item['title'], $id, $item['desc']);
+        //Extra attributes
+        $attr = '';
+        if ($item['attr'])
+            foreach ($item['attr'] as $name => $value)
+                $attr.=  sprintf("%s='%s' ", $name, $value);
+
+        echo sprintf('<li><a class="%s" href="%s" title="%s" id="%s" %s>%s</a></li>',
+                $class, $item['href'], $item['title'], $id, $attr, $item['desc']);
     }
 }
diff --git a/include/staff/templates/task-edit.tmpl.php b/include/staff/templates/task-edit.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..f86e8a70a4c751d92b23b5bf592849bb66b7c08a
--- /dev/null
+++ b/include/staff/templates/task-edit.tmpl.php
@@ -0,0 +1,66 @@
+<?php
+global $cfg;
+
+if (!$info['title'])
+    $info['title'] = sprintf(__('%s Tasks #%s'),
+            __('Edit'), $task->getNumber()
+            );
+
+$action = $info['action'] ?: ('#tasks/'.$task->getId().'/edit');
+
+$namespace = sprintf('task.%d.edit', $task->getId());
+
+?>
+<div id="task-form">
+<h3 class="drag-handle"><?php echo $info['title']; ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<hr/>
+<?php
+
+if ($info['error']) {
+    echo sprintf('<p id="msg_error">%s</p>', $info['error']);
+} elseif ($info['warning']) {
+    echo sprintf('<p id="msg_warning">%s</p>', $info['warning']);
+} elseif ($info['msg']) {
+    echo sprintf('<p id="msg_notice">%s</p>', $info['msg']);
+} ?>
+<div id="edit-task-form" style="display:block;">
+<form method="post" class="task" action="<?php echo $action; ?>">
+    <div>
+    <?php
+    if ($forms) {
+        foreach ($forms as $form)
+            echo $form->getForm(false, array('mode' => 'edit'))->asTable(
+                    __('Task Information'),
+                    array(
+                        'draft-namespace' => $namespace,
+                        )
+                    );
+    }
+    ?>
+    </div>
+    <div><strong><?php echo __('Internal Note');?></strong>:
+     <font class="error">&nbsp;<?php echo $errors['note'];?></font></div>
+    <div>
+        <textarea class="richtext no-bar" name="note" cols="21" rows="6"
+            style="width:80%;"
+            placeholder="<?php echo __('Reason for editing the task (optional)'); ?>"
+            >
+            <?php echo $info['note'];
+            ?></textarea>
+    </div>
+    <hr>
+    <p class="full-width">
+        <span class="buttons pull-left">
+            <input type="reset" value="<?php echo __('Reset'); ?>">
+            <input type="button" name="cancel" class="close"
+                value="<?php echo __('Cancel'); ?>">
+        </span>
+        <span class="buttons pull-right">
+            <input type="submit" value="<?php echo __('Update'); ?>">
+        </span>
+     </p>
+</form>
+</div>
+<div class="clear"></div>
+</div>
diff --git a/include/staff/templates/task-preview.tmpl.php b/include/staff/templates/task-preview.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..e1f32892c94ce791f7d90979920a2e885e1a6286
--- /dev/null
+++ b/include/staff/templates/task-preview.tmpl.php
@@ -0,0 +1,126 @@
+<?php
+$error=$msg=$warn=null;
+
+if (!$task->checkStaffPerm($thisstaff))
+     $warn.= __('You do not have access to this task');
+elseif ($task->isOverdue())
+    $warn.='&nbsp;<span class="Icon overdueTicket">'.__('Marked overdue!').'</span>';
+
+echo sprintf(
+        '<div style="width:600px; padding: 2px 2px 0 5px;" id="t%s">
+         <h2>'.__('Task #%s').': %s</h2>',
+         $task->getNumber(),
+         $task->getNumber(),
+         Format::htmlchars($task->getTitle()));
+
+if($error)
+    echo sprintf('<div id="msg_error">%s</div>',$error);
+elseif($msg)
+    echo sprintf('<div id="msg_notice">%s</div>',$msg);
+elseif($warn)
+    echo sprintf('<div id="msg_warning">%s</div>',$warn);
+
+echo '<ul class="tabs" id="task-preview">';
+
+echo '
+        <li class="active"><a href="#summary"
+            ><i class="icon-list-alt"></i>&nbsp;'.__('Task Summary').'</a></li>';
+if ($task->getThread()->getNumCollaborators()) {
+    echo sprintf('
+        <li><a id="collab_tab" href="#collab"
+            ><i class="icon-fixed-width icon-group
+            faded"></i>&nbsp;'.__('Collaborators (%d)').'</a></li>',
+            $task->getThread()->getNumCollaborators());
+}
+echo '</ul>';
+echo '<div id="task-preview_container">';
+echo '<div class="tab_content" id="summary">';
+echo '<table border="0" cellspacing="" cellpadding="1" width="100%" class="ticket_info">';
+$status=sprintf('<span>%s</span>',ucfirst($task->getStatus()));
+echo sprintf('
+        <tr>
+            <th width="100">'.__('Status').':</th>
+            <td>%s</td>
+        </tr>
+        <tr>
+            <th>'.__('Created').':</th>
+            <td>%s</td>
+        </tr>',$status,
+        Format::datetime($task->getCreateDate()));
+
+if ($task->isClosed()) {
+
+    echo sprintf('
+            <tr>
+                <th>'.__('Completed').':</th>
+                <td>%s</td>
+            </tr>',
+            Format::datetime($task->getCloseDate()));
+
+} elseif ($task->isOpen() && $task->duedate) {
+    echo sprintf('
+            <tr>
+                <th>'.__('Due Date').':</th>
+                <td>%s</td>
+            </tr>',
+            Format::datetime($task->duedate));
+}
+echo '</table>';
+
+
+echo '<hr>
+    <table border="0" cellspacing="" cellpadding="1" width="100%" class="ticket_info">';
+if ($task->isOpen()) {
+    echo sprintf('
+            <tr>
+                <th width="100">'.__('Assigned To').':</th>
+                <td>%s</td>
+            </tr>', $task->getAssigned() ?: ' <span class="faded">&mdash; '.__('Unassigned').' &mdash;</span>');
+}
+echo sprintf(
+    '
+        <tr>
+            <th width="100">'.__('Department').':</th>
+            <td>%s</td>
+        </tr>',
+    Format::htmlchars($task->dept->getName())
+    );
+
+echo '
+    </table>';
+echo '</div>';
+?>
+<?php
+//TODO: add link to view if the user has permission
+?>
+<div class="hidden tab_content" id="collab">
+    <table border="0" cellspacing="" cellpadding="1">
+        <colgroup><col style="min-width: 250px;"></col></colgroup>
+        <?php
+        if (($collabs=$task->getThread()->getCollaborators())) {?>
+        <?php
+            foreach($collabs as $collab) {
+                echo sprintf('<tr><td %s><i class="icon-%s"></i>
+                        <a href="users.php?id=%d" class="no-pjax">%s</a> <em>&lt;%s&gt;</em></td></tr>',
+                        ($collab->isActive()? '' : 'class="faded"'),
+                        ($collab->isActive()? 'comments' :  'comment-alt'),
+                        $collab->getUserId(),
+                        $collab->getName(),
+                        $collab->getEmail());
+            }
+        }  else {
+            echo __("Task doesn't have any collaborators.");
+        }?>
+    </table>
+    <br>
+    <?php
+    echo sprintf('<span><a class="collaborators"
+                            href="#thread/%d/collaborators">%s</a></span>',
+                            $task->getThreadId(),
+                            $task->getThread()->getNumCollaborators()
+                                ? __('Manage Collaborators') : __('Add Collaborator')
+                                );
+    ?>
+</div>
+</div>
+</div>
diff --git a/include/staff/templates/task-print.tmpl.php b/include/staff/templates/task-print.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..e07bef43711faa1bb56861a97b0124daf614cd08
--- /dev/null
+++ b/include/staff/templates/task-print.tmpl.php
@@ -0,0 +1,245 @@
+<html>
+
+<head>
+    <style type="text/css">
+@page {
+    header: html_def;
+    footer: html_def;
+    margin: 15mm;
+    margin-top: 30mm;
+    margin-bottom: 22mm;
+}
+.logo {
+  max-width: 220px;
+  max-height: 71px;
+  width: auto;
+  height: auto;
+  margin: 0;
+}
+#task_thread .message,
+#task_thread .response,
+#task_thread .note {
+    margin-top:10px;
+    border:1px solid #aaa;
+    border-bottom:2px solid #aaa;
+}
+#task_thread .header {
+    text-align:left;
+    border-bottom:1px solid #aaa;
+    padding:3px;
+    width: 100%;
+    table-layout: fixed;
+}
+#task_thread .message .header {
+    background:#C3D9FF;
+}
+#task_thread .response .header {
+    background:#FFE0B3;
+}
+#task_thread .note .header {
+    background:#FFE;
+}
+#task_thread .info {
+    padding:5px;
+    background: snow;
+    border-top: 0.3mm solid #ccc;
+}
+
+table.meta-data {
+    width: 100%;
+}
+table.custom-data {
+    margin-top: 10px;
+}
+table.custom-data th {
+    width: 25%;
+}
+table.custom-data th,
+table.meta-data th {
+    text-align: right;
+    background-color: #ddd;
+    padding: 3px 8px;
+}
+table.meta-data td {
+    padding: 3px 8px;
+}
+.faded {
+    color:#666;
+}
+.pull-left {
+    float: left;
+}
+.pull-right {
+    float: right;
+}
+.flush-right {
+    text-align: right;
+}
+.flush-left {
+    text-align: left;
+}
+.ltr {
+    direction: ltr;
+    unicode-bidi: embed;
+}
+.headline {
+    border-bottom: 2px solid black;
+    font-weight: bold;
+}
+div.hr {
+    border-top: 0.2mm solid #bbb;
+    margin: 0.5mm 0;
+    font-size: 0.0001em;
+}
+.thread-entry, .thread-body {
+    page-break-inside: avoid;
+}
+<?php include ROOT_DIR . 'css/thread.css'; ?>
+    </style>
+</head>
+<body>
+
+<htmlpageheader name="def" style="display:none">
+<?php if ($logo = $cfg->getClientLogo()) { ?>
+    <img src="cid:<?php echo $logo->getKey(); ?>" class="logo"/>
+<?php } else { ?>
+    <img src="<?php echo INCLUDE_DIR . 'fpdf/print-logo.png'; ?>" class="logo"/>
+<?php } ?>
+    <div class="hr">&nbsp;</div>
+    <table><tr>
+        <td class="flush-left"><?php echo (string) $ost->company; ?></td>
+        <td class="flush-right"><?php echo Format::daydatetime(Misc::gmtime()); ?></td>
+    </tr></table>
+</htmlpageheader>
+
+<htmlpagefooter name="def" style="display:none">
+    <div class="hr">&nbsp;</div>
+    <table width="100%"><tr><td class="flush-left">
+        Task #<?php echo $task->getNumber(); ?> printed by
+        <?php echo $thisstaff->getUserName(); ?> on
+        <?php echo Format::daydatetime(Misc::gmtime()); ?>
+    </td>
+    <td class="flush-right">
+        Page {PAGENO}
+    </td>
+    </tr></table>
+</htmlpagefooter>
+
+<!-- Task metadata -->
+<h1>Task #<?php echo $task->getNumber(); ?></h1>
+<table class="meta-data" cellpadding="0" cellspacing="0">
+<tbody>
+<tr>
+    <th><?php echo __('Status'); ?></th>
+    <td><?php echo $task->getStatus(); ?></td>
+    <th><?php echo __('Department'); ?></th>
+    <td><?php echo $task->getDept(); ?></td>
+</tr>
+<tr>
+    <th><?php echo __('Create Date'); ?></th>
+    <td><?php echo Format::datetime($task->getCreateDate()); ?></td>
+    <?php
+    if ($task->isOpen()) { ?>
+    <th><?php echo __('Assigned To'); ?></th>
+    <td><?php echo $task->getAssigned(); ?></td>
+    <?php
+    } else { ?>
+    <th><?php echo __('Closed By');?>:</th>
+    <td>
+        <?php
+        if (($staff = $task->getStaff()))
+            echo Format::htmlchars($staff->getName());
+        else
+            echo '<span class="faded">&mdash; '.__('Unknown').' &mdash;</span>';
+    ?>
+    </td>
+    <?php
+    } ?>
+</tr>
+<tr>
+    <?php
+    if ($task->isOpen()) {?>
+    <th><?php echo __('Due Date'); ?></th>
+    <td><?php echo Format::datetime($task->getDueDate()); ?></td>
+    <?php
+    } else { ?>
+    <th><?php echo __('Close Date'); ?></th>
+    <td><?php echo Format::datetime($task->getCloseDate()); ?></td>
+    <?php
+    } ?>
+    <th><?php echo __('Collaborators'); ?></th>
+    <td><?php echo $task->getParticipants(); ?></td>
+</tr>
+</tbody>
+</table>
+<!-- Custom Data -->
+<?php
+foreach (DynamicFormEntry::forObject($task->getId(),
+            ObjectModel::OBJECT_TYPE_TASK) as $form) {
+    // Skip core fields shown earlier on the view
+    $answers = $form->getAnswers()->exclude(Q::any(array(
+        'field__flags__hasbit' => DynamicFormField::FLAG_EXT_STORED,
+        'field__name__in' => array('title')
+    )));
+    if (count($answers) == 0)
+        continue;
+    ?>
+        <table class="custom-data" cellspacing="0" cellpadding="4" width="100%" border="0">
+        <tr><td colspan="2" class="headline flush-left"><?php echo $form->getTitle(); ?></th></tr>
+        <?php foreach($answers as $a) {
+            if (!($v = $a->display())) continue; ?>
+            <tr>
+                <th><?php
+    echo $a->getField()->get('label');
+                ?>:</th>
+                <td><?php
+    echo $v;
+                ?></td>
+            </tr>
+            <?php } ?>
+        </table>
+    <?php
+    $idx++;
+} ?>
+
+<!-- Task Thread -->
+<h2><?php echo $task->getTitle(); ?></h2>
+<div id="task_thread">
+<?php
+$types = array('M', 'R', 'N');
+if ($entries = $task->getThreadEntries($types)) {
+    $entryTypes=array('M'=>'message','R'=>'response', 'N'=>'note');
+    foreach ($entries as $entry) { ?>
+        <div class="thread-entry <?php echo $entryTypes[$entry->type]; ?>">
+            <table class="header" style="width:100%"><tr><td>
+                    <span><?php
+                        echo Format::datetime($entry->created);?></span>
+                    <span style="padding:0 1em" class="faded title"><?php
+                        echo Format::truncate($entry->title, 100); ?></span>
+                </td>
+                <td class="flush-right faded title" style="white-space:no-wrap">
+                    <?php
+                        echo Format::htmlchars($entry->getName()); ?></span>
+                </td>
+            </tr></table>
+            <div class="thread-body">
+                <div><?php echo $entry->getBody()->display('pdf'); ?></div>
+            <?php
+            if ($entry->has_attachments
+                    && ($files = $entry->attachments)) { ?>
+                <div class="info">
+<?php           foreach ($files as $A) { ?>
+                    <div>
+                        <span><?php echo Format::htmlchars($A->file->name); ?></span>
+                        <span class="faded">(<?php echo Format::file_size($A->file->size); ?>)</span>
+                    </div>
+<?php           } ?>
+                </div>
+<?php       } ?>
+            </div>
+        </div>
+<?php }
+} ?>
+</div>
+</body>
+</html>
diff --git a/include/staff/templates/task-status.tmpl.php b/include/staff/templates/task-status.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..a6cf6594bb6a4abac698f379a987485c9b22b7e0
--- /dev/null
+++ b/include/staff/templates/task-status.tmpl.php
@@ -0,0 +1,72 @@
+<?php
+global $cfg;
+
+if (!$info[':title'])
+    $info[':title'] = __('Change Tasks Status');
+
+?>
+<h3 class="drag-handle"><?php echo $info[':title']; ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<div class="clear"></div>
+<hr/>
+<?php
+if ($info['error']) {
+    echo sprintf('<p id="msg_error">%s</p>', $info['error']);
+} elseif ($info['warn']) {
+    echo sprintf('<p id="msg_warning">%s</p>', $info['warn']);
+} elseif ($info['msg']) {
+    echo sprintf('<p id="msg_notice">%s</p>', $info['msg']);
+} elseif ($info['notice']) {
+   echo sprintf('<p id="msg_info"><i class="icon-info-sign"></i> %s</p>',
+           $info['notice']);
+}
+
+$action = $info[':action'] ?: ('#tasks/mass/'. $action);
+?>
+<div style="display:block; margin:5px;">
+    <form method="post" name="status" id="status"
+        action="<?php echo $action; ?>"
+        class="mass-action">
+        <input type="hidden" name="status" value="<?php echo $info['status']; ?>" >
+        <table width="100%">
+            <?php
+            if ($info[':extra']) {
+                ?>
+            <tbody>
+                <tr><td colspan="2"><strong><?php echo $info[':extra'];
+                ?></strong></td> </tr>
+            </tbody>
+            <?php
+            }
+            ?>
+            <tbody>
+                <tr>
+                    <td colspan="2">
+                        <?php
+                        $placeholder = $info[':placeholder'] ?: __('Optional reason for status change (internal note)');
+                        ?>
+                        <textarea name="comments" id="comments"
+                            cols="50" rows="3" wrap="soft" style="width:100%"
+                            class="<?php if ($cfg->isRichTextEnabled()) echo 'richtext';
+                            ?> no-bar"
+                            placeholder="<?php echo $placeholder; ?>"><?php
+                            echo $info['comments']; ?></textarea>
+                    </td>
+                </tr>
+            </tbody>
+        </table>
+        <hr>
+        <p class="full-width">
+            <span class="buttons pull-left">
+                <input type="reset" value="<?php echo __('Reset'); ?>">
+                <input type="button" name="cancel" class="close"
+                value="<?php echo __('Cancel'); ?>">
+            </span>
+            <span class="buttons pull-right">
+                <input type="submit" value="<?php
+                echo $verb ?: __('Submit'); ?>">
+            </span>
+         </p>
+    </form>
+</div>
+<div class="clear"></div>
diff --git a/include/staff/templates/task-view.tmpl.php b/include/staff/templates/task-view.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..f53a410ef722f9928630a6bdce9948a6c644989b
--- /dev/null
+++ b/include/staff/templates/task-view.tmpl.php
@@ -0,0 +1,677 @@
+<?php
+if (!defined('OSTSCPINC')
+    || !$thisstaff || !$task
+    || !($role = $thisstaff->getRole($task->getDeptId())))
+    die('Invalid path');
+
+global $cfg;
+
+$id = $task->getId();
+$dept = $task->getDept();
+$thread = $task->getThread();
+
+$iscloseable = $task->isCloseable();
+$canClose = ($role->hasPerm(TaskModel::PERM_CLOSE) && $iscloseable === true);
+$actions = array();
+
+if ($task->isOpen() && $role->hasPerm(Task::PERM_ASSIGN)) {
+
+    if ($task->getStaffId() != $thisstaff->getId()
+            && (!$dept->assignMembersOnly()
+                || $dept->isMember($thisstaff))) {
+        $actions += array(
+                'claim' => array(
+                    'href' => sprintf('#tasks/%d/claim', $task->getId()),
+                    'icon' => 'icon-user',
+                    'label' => __('Claim'),
+                    'redirect' => 'tasks.php'
+                ));
+    }
+
+    $actions += array(
+            'assign/agents' => array(
+                'href' => sprintf('#tasks/%d/assign/agents', $task->getId()),
+                'icon' => 'icon-user',
+                'label' => __('Assign to Agent'),
+                'redirect' => 'tasks.php'
+            ));
+
+    $actions += array(
+            'assign/teams' => array(
+                'href' => sprintf('#tasks/%d/assign/teams', $task->getId()),
+                'icon' => 'icon-user',
+                'label' => __('Assign to Team'),
+                'redirect' => 'tasks.php'
+            ));
+}
+
+if ($role->hasPerm(Task::PERM_TRANSFER)) {
+    $actions += array(
+            'transfer' => array(
+                'href' => sprintf('#tasks/%d/transfer', $task->getId()),
+                'icon' => 'icon-share',
+                'label' => __('Transfer'),
+                'redirect' => 'tasks.php'
+            ));
+}
+
+$actions += array(
+        'print' => array(
+            'href' => sprintf('tasks.php?id=%d&a=print', $task->getId()),
+            'class' => 'no-pjax',
+            'icon' => 'icon-print',
+            'label' => __('Print')
+        ));
+
+if ($role->hasPerm(Task::PERM_EDIT)) {
+    $actions += array(
+            'edit' => array(
+                'href' => sprintf('#tasks/%d/edit', $task->getId()),
+                'icon' => 'icon-edit',
+                'dialog' => '{"size":"large"}',
+                'label' => __('Edit')
+            ));
+}
+
+if ($role->hasPerm(Task::PERM_DELETE)) {
+    $actions += array(
+            'delete' => array(
+                'href' => sprintf('#tasks/%d/delete', $task->getId()),
+                'icon' => 'icon-trash',
+                'class' => 'red button',
+                'label' => __('Delete'),
+                'redirect' => 'tasks.php'
+            ));
+}
+
+$info=($_POST && $errors)?Format::input($_POST):array();
+
+if ($task->isOverdue())
+    $warn.='&nbsp;&nbsp;<span class="Icon overdueTicket">'.__('Marked overdue!').'</span>';
+
+?>
+<div>
+    <div class="sticky bar">
+       <div class="content">
+        <div class="pull-left flush-left">
+            <?php
+            if ($ticket) { ?>
+                <strong>
+                <a id="all-ticket-tasks" href="#">
+                <?php
+                    echo sprintf(__('All Tasks (%s)'),
+                            $ticket->getNumTasks());
+                 ?></a>
+                &nbsp;/&nbsp;
+                <a id="reload-task" class="preview"
+                    <?php
+                    echo ' class="preview" ';
+                    echo sprintf('data-preview="#tasks/%d/preview" ', $task->getId());
+                    echo sprintf('href="#tickets/%s/tasks/%d/view" ',
+                            $ticket->getId(), $task->getId()
+                            );
+                    ?>><?php echo sprintf(__('Task #%s'), $task->getNumber()); ?></a>
+                </strong>
+            <?php
+            } else { ?>
+               <h2>
+                <a  id="reload-task"
+                    href="tasks.php?id=<?php echo $task->getId(); ?>"><i
+                    class="icon-refresh"></i>&nbsp;<?php
+                    echo sprintf(__('Task #%s'), $task->getNumber()); ?></a>
+                </h2>
+            <?php
+            } ?>
+        </div>
+        <div class="flush-right">
+            <?php
+            if ($ticket) { ?>
+            <a  id="task-view"
+                target="_blank"
+                class="action-button"
+                href="tasks.php?id=<?php
+                 echo $task->getId(); ?>"><i class="icon-share"></i> <?php
+                            echo __('View Task'); ?></a>
+            <span
+                class="action-button"
+                data-dropdown="#action-dropdown-task-options">
+                <i class="icon-caret-down pull-right"></i>
+                <a class="task-action"
+                    href="#task-options"><i
+                    class="icon-reorder"></i> <?php
+                    echo __('Actions'); ?></a>
+            </span>
+            <div id="action-dropdown-task-options"
+                class="action-dropdown anchor-right">
+                <ul>
+
+                    <?php
+                    if ($task->isOpen()) { ?>
+                    <li>
+                        <a class="no-pjax task-action"
+                            href="#tasks/<?php echo $task->getId(); ?>/reopen"><i
+                            class="icon-fixed-width icon-undo"></i> <?php
+                            echo __('Reopen');?> </a>
+                    </li>
+                    <?php
+                    } else {
+                    ?>
+                    <li>
+                        <a class="no-pjax task-action"
+                            href="#tasks/<?php echo $task->getId(); ?>/close"><i
+                            class="icon-fixed-width icon-ok-circle"></i> <?php
+                            echo __('Close');?> </a>
+                    </li>
+                    <?php
+                    } ?>
+                    <?php
+                    foreach ($actions as $a => $action) { ?>
+                    <li <?php if ($action['class']) echo sprintf("class='%s'", $action['class']); ?> >
+                        <a class="no-pjax task-action" <?php
+                            if ($action['dialog'])
+                                echo sprintf("data-dialog-config='%s'", $action['dialog']);
+                            if ($action['redirect'])
+                                echo sprintf("data-redirect='%s'", $action['redirect']);
+                            ?>
+                            href="<?php echo $action['href']; ?>"
+                            <?php
+                            if (isset($action['href']) &&
+                                    $action['href'][0] != '#') {
+                                echo 'target="blank"';
+                            } ?>
+                            ><i class="<?php
+                            echo $action['icon'] ?: 'icon-tag'; ?>"></i> <?php
+                            echo  $action['label']; ?></a>
+                    </li>
+                <?php
+                } ?>
+                </ul>
+            </div>
+            <?php
+           } else { ?>
+                <span
+                    class="action-button"
+                    data-dropdown="#action-dropdown-tasks-status">
+                    <i class="icon-caret-down pull-right"></i>
+                    <a class="tasks-status-action"
+                        href="#statuses"
+                        data-placement="bottom"
+                        data-toggle="tooltip"
+                        title="<?php echo __('Change Status'); ?>"><i
+                        class="icon-flag"></i></a>
+                </span>
+                <div id="action-dropdown-tasks-status"
+                    class="action-dropdown anchor-right">
+                    <ul>
+                        <?php
+                        if ($task->isClosed()) { ?>
+                        <li>
+                            <a class="no-pjax task-action"
+                                href="#tasks/<?php echo $task->getId(); ?>/reopen"><i
+                                class="icon-fixed-width icon-undo"></i> <?php
+                                echo __('Reopen');?> </a>
+                        </li>
+                        <?php
+                        } else {
+                        ?>
+                        <li>
+                            <a class="no-pjax task-action"
+                                href="#tasks/<?php echo $task->getId(); ?>/close"><i
+                                class="icon-fixed-width icon-ok-circle"></i> <?php
+                                echo __('Close');?> </a>
+                        </li>
+                        <?php
+                        } ?>
+                    </ul>
+                </div>
+                <?php
+                // Assign
+                unset($actions['claim'], $actions['assign/agents'], $actions['assign/teams']);
+                if ($task->isOpen() && $role->hasPerm(Task::PERM_ASSIGN)) {?>
+                <span class="action-button"
+                    data-dropdown="#action-dropdown-assign"
+                    data-placement="bottom"
+                    data-toggle="tooltip"
+                    title=" <?php echo $task->isAssigned() ? __('Reassign') : __('Assign'); ?>"
+                    >
+                    <i class="icon-caret-down pull-right"></i>
+                    <a class="task-action" id="task-assign"
+                        data-redirect="tasks.php"
+                        href="#tasks/<?php echo $task->getId(); ?>/assign"><i class="icon-user"></i></a>
+                </span>
+                <div id="action-dropdown-assign" class="action-dropdown anchor-right">
+                  <ul>
+                    <?php
+                    // Agent can claim team assigned ticket
+                    if ($task->getStaffId() != $thisstaff->getId()
+                            && (!$dept->assignMembersOnly()
+                                || $dept->isMember($thisstaff))
+                            ) { ?>
+                     <li><a class="no-pjax task-action"
+                        data-redirect="tasks.php"
+                        href="#tasks/<?php echo $task->getId(); ?>/claim"><i
+                        class="icon-chevron-sign-down"></i> <?php echo __('Claim'); ?></a>
+                    <?php
+                    } ?>
+                     <li><a class="no-pjax task-action"
+                        data-redirect="tasks.php"
+                        href="#tasks/<?php echo $task->getId(); ?>/assign/agents"><i
+                        class="icon-user"></i> <?php echo __('Agent'); ?></a>
+                     <li><a class="no-pjax task-action"
+                        data-redirect="tasks.php"
+                        href="#tasks/<?php echo $task->getId(); ?>/assign/teams"><i
+                        class="icon-group"></i> <?php echo __('Team'); ?></a>
+                  </ul>
+                </div>
+                <?php
+                } ?>
+                <?php
+                foreach ($actions as $action) {?>
+                <span class="action-button <?php echo $action['class'] ?: ''; ?>">
+                    <a class="task-action"
+                        <?php
+                        if ($action['dialog'])
+                            echo sprintf("data-dialog-config='%s'", $action['dialog']);
+                        if ($action['redirect'])
+                            echo sprintf("data-redirect='%s'", $action['redirect']);
+                        ?>
+                        href="<?php echo $action['href']; ?>"
+                        data-placement="bottom"
+                        data-toggle="tooltip"
+                        title="<?php echo $action['label']; ?>">
+                        <i class="<?php
+                        echo $action['icon'] ?: 'icon-tag'; ?>"></i>
+                    </a>
+                </span>
+           <?php
+                }
+           } ?>
+        </div>
+    </div>
+   </div>
+</div>
+
+<div class="clear tixTitle has_bottom_border">
+    <h3>
+    <?php
+        $title = TaskForm::getInstance()->getField('title');
+        echo $title->display($task->getTitle());
+    ?>
+    </h3>
+</div>
+<?php
+if (!$ticket) { ?>
+    <table class="ticket_info" cellspacing="0" cellpadding="0" width="940" border="0">
+        <tr>
+            <td width="50%">
+                <table border="0" cellspacing="" cellpadding="4" width="100%">
+                    <tr>
+                        <th width="100"><?php echo __('Status');?>:</th>
+                        <td><?php echo $task->getStatus(); ?></td>
+                    </tr>
+
+                    <tr>
+                        <th><?php echo __('Created');?>:</th>
+                        <td><?php echo Format::datetime($task->getCreateDate()); ?></td>
+                    </tr>
+                    <?php
+                    if($task->isOpen()){ ?>
+                    <tr>
+                        <th><?php echo __('Due Date');?>:</th>
+                        <td><?php echo $task->duedate ?
+                        Format::datetime($task->duedate) : '<span
+                        class="faded">&mdash; '.__('None').' &mdash;</span>'; ?></td>
+                    </tr>
+                    <?php
+                    }else { ?>
+                    <tr>
+                        <th><?php echo __('Completed');?>:</th>
+                        <td><?php echo Format::datetime($task->getCloseDate()); ?></td>
+                    </tr>
+                    <?php
+                    }
+                    ?>
+                </table>
+            </td>
+            <td width="50%" style="vertical-align:top">
+                <table cellspacing="0" cellpadding="4" width="100%" border="0">
+
+                    <tr>
+                        <th><?php echo __('Department');?>:</th>
+                        <td><?php echo Format::htmlchars($task->dept->getName()); ?></td>
+                    </tr>
+                    <?php
+                    if ($task->isOpen()) { ?>
+                    <tr>
+                        <th width="100"><?php echo __('Assigned To');?>:</th>
+                        <td>
+                            <?php
+                            if ($assigned=$task->getAssigned())
+                                echo Format::htmlchars($assigned);
+                            else
+                                echo '<span class="faded">&mdash; '.__('Unassigned').' &mdash;</span>';
+                            ?>
+                        </td>
+                    </tr>
+                    <?php
+                    } else { ?>
+                    <tr>
+                        <th width="100"><?php echo __('Closed By');?>:</th>
+                        <td>
+                            <?php
+                            if (($staff = $task->getStaff()))
+                                echo Format::htmlchars($staff->getName());
+                            else
+                                echo '<span class="faded">&mdash; '.__('Unknown').' &mdash;</span>';
+                            ?>
+                        </td>
+                    </tr>
+                    <?php
+                    } ?>
+                    <tr>
+                        <th><?php echo __('Collaborators');?>:</th>
+                        <td>
+                            <?php
+                            $collaborators = __('Add Participants');
+                            if ($task->getThread()->getNumCollaborators())
+                                $collaborators = sprintf(__('Participants (%d)'),
+                                        $task->getThread()->getNumCollaborators());
+
+                            echo sprintf('<span><a class="collaborators preview"
+                                    href="#thread/%d/collaborators"><span
+                                    id="t%d-collaborators">%s</span></a></span>',
+                                    $task->getThreadId(),
+                                    $task->getThreadId(),
+                                    $collaborators);
+                           ?>
+                        </td>
+                    </tr>
+                </table>
+            </td>
+        </tr>
+    </table>
+    <br>
+    <br>
+    <table class="ticket_info" cellspacing="0" cellpadding="0" width="940" border="0">
+    <?php
+    $idx = 0;
+    foreach (DynamicFormEntry::forObject($task->getId(),
+                ObjectModel::OBJECT_TYPE_TASK) as $form) {
+        $answers = $form->getAnswers()->exclude(Q::any(array(
+            'field__flags__hasbit' => DynamicFormField::FLAG_EXT_STORED,
+            'field__name__in' => array('title')
+        )));
+        if (!$answers || count($answers) == 0)
+            continue;
+
+        ?>
+            <tr>
+            <td colspan="2">
+                <table cellspacing="0" cellpadding="4" width="100%" border="0">
+                <?php foreach($answers as $a) {
+                    if (!($v = $a->display())) continue; ?>
+                    <tr>
+                        <th width="100"><?php
+                            echo $a->getField()->get('label');
+                        ?>:</th>
+                        <td><?php
+                            echo $v;
+                        ?></td>
+                    </tr>
+                    <?php
+                } ?>
+                </table>
+            </td>
+            </tr>
+        <?php
+        $idx++;
+    } ?>
+    </table>
+<?php
+} ?>
+<div class="clear"></div>
+<div id="task_thread_container">
+    <div id="task_thread_content" class="tab_content">
+     <?php
+     $task->getThread()->render(array('M', 'R', 'N'),
+             array(
+                 'mode' => Thread::MODE_STAFF,
+                 'container' => 'taskThread',
+                 'sort' => $thisstaff->thread_view_order
+                 )
+             );
+     ?>
+   </div>
+</div>
+<div class="clear"></div>
+<?php if($errors['err']) { ?>
+    <div id="msg_error"><?php echo $errors['err']; ?></div>
+<?php }elseif($msg) { ?>
+    <div id="msg_notice"><?php echo $msg; ?></div>
+<?php }elseif($warn) { ?>
+    <div id="msg_warning"><?php echo $warn; ?></div>
+<?php }
+
+if ($ticket)
+    $action = sprintf('#tickets/%d/tasks/%d',
+            $ticket->getId(), $task->getId());
+else
+    $action = 'tasks.php?id='.$task->getId();
+?>
+<div id="task_response_options" class="<?php echo $ticket ? 'ticket_task_actions' : ''; ?> sticky bar stop actions">
+    <ul class="tabs">
+        <?php
+        if ($role->hasPerm(TaskModel::PERM_REPLY)) { ?>
+        <li class="active"><a href="#task_reply"><?php echo __('Post Update');?></a></li>
+        <li><a href="#task_note"><?php echo __('Post Internal Note');?></a></li>
+        <?php
+        }?>
+    </ul>
+    <?php
+    if ($role->hasPerm(TaskModel::PERM_REPLY)) { ?>
+    <form id="task_reply" class="tab_content spellcheck"
+        action="<?php echo $action; ?>"
+        name="task_reply" method="post" enctype="multipart/form-data">
+        <?php csrf_token(); ?>
+        <input type="hidden" name="id" value="<?php echo $task->getId(); ?>">
+        <input type="hidden" name="a" value="postreply">
+        <input type="hidden" name="lockCode" value="<?php echo ($mylock) ? $mylock->getCode() : ''; ?>">
+        <span class="error"></span>
+        <table style="width:100%" border="0" cellspacing="0" cellpadding="3">
+            <tbody id="collab_sec" style="display:table-row-group">
+             <tr>
+                <td>
+                    <input type='checkbox' value='1' name="emailcollab" id="emailcollab"
+                        <?php echo ((!$info['emailcollab'] && !$errors) || isset($info['emailcollab']))?'checked="checked"':''; ?>
+                        style="display:<?php echo $thread->getNumCollaborators() ? 'inline-block': 'none'; ?>;"
+                        >
+                    <?php
+                    $recipients = __('Add Participants');
+                    if ($thread->getNumCollaborators())
+                        $recipients = sprintf(__('Recipients (%d of %d)'),
+                                $thread->getNumActiveCollaborators(),
+                                $thread->getNumCollaborators());
+
+                    echo sprintf('<span><a class="collaborators preview"
+                            href="#thread/%d/collaborators"><span id="t%d-recipients">%s</span></a></span>',
+                            $thread->getId(),
+                            $thread->getId(),
+                            $recipients);
+                   ?>
+                </td>
+             </tr>
+            </tbody>
+            <tbody id="update_sec">
+            <tr>
+                <td>
+                    <div class="error"><?php echo $errors['response']; ?></div>
+                    <input type="hidden" name="draft_id" value=""/>
+                    <textarea name="response" id="task-response" cols="50"
+                        data-signature-field="signature" data-dept-id="<?php echo $dept->getId(); ?>"
+                        data-signature="<?php
+                            echo Format::htmlchars(Format::viewableImages($signature)); ?>"
+                        placeholder="<?php echo __( 'Start writing your update here.'); ?>"
+                        rows="9" wrap="soft"
+                        class="<?php if ($cfg->isRichTextEnabled()) echo 'richtext';
+                            ?> draft draft-delete" <?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('task.response', $task->getId(), $info['task.response']);
+    echo $attrs; ?>><?php echo $draft ?: $info['task.response'];
+                    ?></textarea>
+                <div id="task_response_form_attachments" class="attachments">
+                <?php
+                    if ($reply_attachments_form)
+                        print $reply_attachments_form->getField('attachments')->render();
+                ?>
+                </div>
+               </td>
+            </tr>
+            <tr>
+                <td>
+                    <div><?php echo __('Status');?>
+                        <span class="faded"> - </span>
+                        <select  name="task:status">
+                            <option value="open" <?php
+                                echo $task->isOpen() ?
+                                'selected="selected"': ''; ?>> <?php
+                                echo _('Open'); ?></option>
+                            <?php
+                            if ($task->isClosed() || $canClose) {
+                                ?>
+                            <option value="closed" <?php
+                                echo $task->isClosed() ?
+                                'selected="selected"': ''; ?>> <?php
+                                echo _('Closed'); ?></option>
+                            <?php
+                            } ?>
+                        </select>
+                        &nbsp;<span class='error'><?php echo
+                        $errors['task:status']; ?></span>
+                    </div>
+                </td>
+            </tr>
+        </table>
+       <p  style="text-align:center;">
+           <input class="save pending" type="submit" value="<?php echo __('Post Update');?>">
+           <input type="reset" value="<?php echo __('Reset');?>">
+       </p>
+    </form>
+    <?php
+    } ?>
+    <form id="task_note"
+        action="<?php echo $action; ?>"
+        class="tab_content spellcheck <?php
+            echo $role->hasPerm(TaskModel::PERM_REPLY) ? 'hidden' : ''; ?>"
+        name="task_note"
+        method="post" enctype="multipart/form-data">
+        <?php csrf_token(); ?>
+        <input type="hidden" name="id" value="<?php echo $task->getId(); ?>">
+        <input type="hidden" name="a" value="postnote">
+        <table width="100%" border="0" cellspacing="0" cellpadding="3">
+            <tr>
+                <td>
+                    <div><span class='error'><?php echo $errors['note']; ?></span></div>
+                    <textarea name="note" id="task-note" cols="80"
+                        placeholder="<?php echo __('Internal Note details'); ?>"
+                        rows="9" wrap="soft" data-draft-namespace="task.note"
+                        data-draft-object-id="<?php echo $task->getId(); ?>"
+                        class="richtext ifhtml draft draft-delete"><?php
+                        echo $info['note'];
+                        ?></textarea>
+                    <div class="attachments">
+                    <?php
+                        if ($note_attachments_form)
+                            print $note_attachments_form->getField('attachments')->render();
+                    ?>
+                    </div>
+                </td>
+            </tr>
+            <tr>
+                <td>
+                    <div><?php echo __('Status');?>
+                        <span class="faded"> - </span>
+                        <select  name="task:status">
+                            <option value="open" <?php
+                                echo $task->isOpen() ?
+                                'selected="selected"': ''; ?>> <?php
+                                echo _('Open'); ?></option>
+                            <?php
+                            if ($task->isClosed() || $canClose) {
+                                ?>
+                            <option value="closed" <?php
+                                echo $task->isClosed() ?
+                                'selected="selected"': ''; ?>> <?php
+                                echo _('Closed'); ?></option>
+                            <?php
+                            } ?>
+                        </select>
+                        &nbsp;<span class='error'><?php echo
+                        $errors['task:status']; ?></span>
+                    </div>
+                </td>
+            </tr>
+        </table>
+       <p  style="text-align:center;">
+           <input class="save pending" type="submit" value="<?php echo __('Post Note');?>">
+           <input type="reset" value="<?php echo __('Reset');?>">
+       </p>
+    </form>
+ </div>
+<?php
+echo $reply_attachments_form->getMedia();
+?>
+
+<script type="text/javascript">
+$(function() {
+    $(document).off('.tasks-content');
+    $(document).on('click.tasks-content', '#all-ticket-tasks', function(e) {
+        e.preventDefault();
+        $('div#task_content').hide().empty();
+        $('div#tasks_content').show();
+        return false;
+     });
+
+    $(document).off('.task-action');
+    $(document).on('click.task-action', 'a.task-action', function(e) {
+        e.preventDefault();
+        var url = 'ajax.php/'
+        +$(this).attr('href').substr(1)
+        +'?_uid='+new Date().getTime();
+        var $options = $(this).data('dialogConfig');
+        var $redirect = $(this).data('redirect');
+        $.dialog(url, [201], function (xhr) {
+            if (!!$redirect)
+                window.location.href = $redirect;
+            else
+                $.pjax.reload('#pjax-container');
+        }, $options);
+
+        return false;
+    });
+
+    $(document).off('.tf');
+    $(document).on('submit.tf', '.ticket_task_actions form', function(e) {
+        e.preventDefault();
+        var $form = $(this);
+        var $container = $('div#task_content');
+        $.ajax({
+            type:  $form.attr('method'),
+            url: 'ajax.php/'+$form.attr('action').substr(1),
+            data: $form.serialize(),
+            cache: false,
+            success: function(resp, status, xhr) {
+                $container.html(resp);
+                $('#msg_notice, #msg_error',$container)
+                .delay(5000)
+                .slideUp();
+            }
+        })
+        .done(function() { })
+        .fail(function() { });
+     });
+    <?php
+    if ($ticket) { ?>
+    $('#ticket-tasks-count').html(<?php echo $ticket->getNumTasks(); ?>);
+   <?php
+    } ?>
+});
+</script>
diff --git a/include/staff/templates/task.tmpl.php b/include/staff/templates/task.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..7bcafc77d68a267e0a20f62fdf8480f5edec29b0
--- /dev/null
+++ b/include/staff/templates/task.tmpl.php
@@ -0,0 +1,49 @@
+<?php
+
+if (!$info['title'])
+    $info['title'] = __('New Task');
+
+$namespace = 'task.add';
+if ($ticket)
+    $namespace = sprintf('ticket.%d.task', $ticket->getId());
+
+?>
+<div id="task-form">
+<h3 class="drag-handle"><?php echo $info['title']; ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<hr/>
+<?php
+
+if ($info['error']) {
+    echo sprintf('<p id="msg_error">%s</p>', $info['error']);
+} elseif ($info['warning']) {
+    echo sprintf('<p id="msg_warning">%s</p>', $info['warning']);
+} elseif ($info['msg']) {
+    echo sprintf('<p id="msg_notice">%s</p>', $info['msg']);
+} ?>
+<div id="new-task-form" style="display:block;">
+<form method="post" class="org" action="<?php echo $info['action'] ?: '#tasks/add'; ?>">
+    <?php
+        $form = $form ?: TaskForm::getInstance();
+        echo $form->getForm()->asTable(' ',
+                array('draft-namespace' => $namespace)
+                );
+
+        $iform = $iform ?: TaskForm::getInternalForm();
+        echo $iform->asTable(__("Task Visibility & Assignment"));
+?>
+    <hr>
+    <p class="full-width">
+        <span class="buttons pull-left">
+            <input type="reset" value="<?php echo __('Reset'); ?>">
+            <input type="button" name="cancel" class="close"
+                value="<?php echo __('Cancel'); ?>">
+        </span>
+        <span class="buttons pull-right">
+            <input type="submit" value="<?php echo __('Create Task'); ?>">
+        </span>
+     </p>
+</form>
+</div>
+<div class="clear"></div>
+</div>
diff --git a/include/staff/templates/tasks-actions.tmpl.php b/include/staff/templates/tasks-actions.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..dc59eae4fc5827a7231c39dbdbb4a7fc2f9c0d2e
--- /dev/null
+++ b/include/staff/templates/tasks-actions.tmpl.php
@@ -0,0 +1,221 @@
+<?php
+// Tasks' mass actions based on logged in agent
+
+$actions = array();
+
+if ($agent->hasPerm(Task::PERM_CLOSE, false)) {
+
+    if (isset($options['status'])) {
+        $status = $options['status'];
+    ?>
+        <span
+            class="action-button"
+            data-dropdown="#action-dropdown-tasks-status">
+            <i class="icon-caret-down pull-right"></i>
+            <a class="tasks-status-action"
+                href="#statuses"
+                data-placement="bottom"
+                data-toggle="tooltip"
+                title="<?php echo __('Change Status'); ?>"><i
+                class="icon-flag"></i></a>
+        </span>
+        <div id="action-dropdown-tasks-status"
+            class="action-dropdown anchor-right">
+            <ul>
+                <?php
+                if (!$status || !strcasecmp($status, 'closed')) { ?>
+                <li>
+                    <a class="no-pjax tasks-action"
+                        href="#tasks/mass/reopen"><i
+                        class="icon-fixed-width icon-undo"></i> <?php
+                        echo __('Reopen');?> </a>
+                </li>
+                <?php
+                }
+                if (!$status || !strcasecmp($status, 'open')) {
+                ?>
+                <li>
+                    <a class="no-pjax tasks-action"
+                        href="#tasks/mass/close"><i
+                        class="icon-fixed-width icon-ok-circle"></i> <?php
+                        echo __('Close');?> </a>
+                </li>
+                <?php
+                } ?>
+            </ul>
+        </div>
+<?php
+    } else {
+
+        $actions += array(
+                'reopen' => array(
+                    'icon' => 'icon-undo',
+                    'action' => __('Reopen')
+                ));
+
+        $actions += array(
+                'close' => array(
+                    'icon' => 'icon-ok-circle',
+                    'action' => __('Close')
+                ));
+    }
+}
+
+if ($agent->hasPerm(Task::PERM_ASSIGN, false)) {
+    $actions += array(
+            'claim' => array(
+                'icon' => 'icon-user',
+                'action' => __('Claim')
+            ));
+     $actions += array(
+            'assign/agents' => array(
+                'icon' => 'icon-user',
+                'action' => __('Assign to Agent')
+            ));
+    $actions += array(
+            'assign/teams' => array(
+                'icon' => 'icon-group',
+                'action' => __('Assign to Team')
+            ));
+}
+
+if ($agent->hasPerm(Task::PERM_TRANSFER, false)) {
+    $actions += array(
+            'transfer' => array(
+                'icon' => 'icon-share',
+                'action' => __('Transfer')
+            ));
+}
+
+if ($agent->hasPerm(Task::PERM_DELETE, false)) {
+    $actions += array(
+            'delete' => array(
+                'class' => 'danger',
+                'icon' => 'icon-trash',
+                'action' => __('Delete')
+            ));
+}
+if ($actions && !isset($options['status'])) {
+    $more = $options['morelabel'] ?: __('More');
+    ?>
+    <span
+        class="action-button"
+        data-dropdown="#action-dropdown-moreoptions">
+        <i class="icon-caret-down pull-right"></i>
+        <a class="tasks-action"
+            href="#moreoptions"><i
+            class="icon-reorder"></i> <?php
+            echo $more; ?></a>
+    </span>
+    <div id="action-dropdown-moreoptions"
+        class="action-dropdown anchor-right">
+        <ul>
+    <?php foreach ($actions as $a => $action) { ?>
+            <li <?php
+                if ($action['class'])
+                    echo sprintf("class='%s'", $action['class']); ?> >
+                <a class="no-pjax tasks-action"
+                    <?php
+                    if ($action['dialog'])
+                        echo sprintf("data-dialog-config='%s'", $action['dialog']);
+                    if ($action['redirect'])
+                        echo sprintf("data-redirect='%s'", $action['redirect']);
+                    ?>
+                    href="<?php
+                    echo sprintf('#tasks/mass/%s', $a); ?>"
+                    ><i class="icon-fixed-width <?php
+                    echo $action['icon'] ?: 'icon-tag'; ?>"></i> <?php
+                    echo $action['action']; ?></a>
+            </li>
+        <?php
+        } ?>
+        </ul>
+    </div>
+ <?php
+ } else {
+    // Mass Claim/Assignment
+    if ($agent->hasPerm(Task::PERM_ASSIGN, false)) {?>
+    <span
+        class="action-button" data-placement="bottom"
+        data-dropdown="#action-dropdown-assign" data-toggle="tooltip" title=" <?php
+        echo __('Assign'); ?>">
+        <i class="icon-caret-down pull-right"></i>
+        <a class="tasks-action" id="tasks-assign"
+            href="#tasks/mass/assign"><i class="icon-user"></i></a>
+    </span>
+    <div id="action-dropdown-assign" class="action-dropdown anchor-right">
+      <ul>
+         <li><a class="no-pjax tasks-action"
+            href="#tasks/mass/claim"><i
+            class="icon-chevron-sign-down"></i> <?php echo __('Claim'); ?></a>
+         <li><a class="no-pjax tasks-action"
+            href="#tasks/mass/assign/agents"><i
+            class="icon-user"></i> <?php echo __('Agent'); ?></a>
+         <li><a class="no-pjax tasks-action"
+            href="#tasks/mass/assign/teams"><i
+            class="icon-group"></i> <?php echo __('Team'); ?></a>
+      </ul>
+    </div>
+    <?php
+    }
+
+    // Mass Transfer
+    if ($agent->hasPerm(Task::PERM_TRANSFER, false)) {?>
+    <span class="action-button">
+     <a class="tasks-action" id="tasks-transfer" data-placement="bottom"
+        data-toggle="tooltip" title="<?php echo __('Transfer'); ?>"
+        href="#tasks/mass/transfer"><i class="icon-share"></i></a>
+    </span>
+    <?php
+    }
+
+
+    // Mass Delete
+    if ($agent->hasPerm(Task::PERM_DELETE, false)) {?>
+    <span class="red button action-button">
+     <a class="tasks-action" id="tasks-delete" data-placement="bottom"
+        data-toggle="tooltip" title="<?php echo __('Delete'); ?>"
+        href="#tasks/mass/delete"><i class="icon-trash"></i></a>
+    </span>
+<?php
+    }
+} ?>
+
+
+<script type="text/javascript">
+$(function() {
+    $(document).off('.tasks-actions');
+    $(document).on('click.tasks-actions', 'a.tasks-action', function(e) {
+        e.preventDefault();
+        var $form = $('form#tasks');
+        var count = checkbox_checker($form, 1);
+        if (count) {
+            var tids = $('.ckb:checked', $form).map(function() {
+                    return this.value;
+                    }).get();
+            var url = 'ajax.php/'
+            +$(this).attr('href').substr(1)
+            +'?count='+count
+            +'&tids='+tids.join(',')
+            +'&_uid='+new Date().getTime();
+            var $redirect = $(this).data('redirect');
+            $.dialog(url, [201], function (xhr) {
+                if (!!$redirect)
+                    $.pjax({url: $redirect, container:'#pjax-container'});
+                else
+                  <?php
+                  if (isset($options['callback_url']))
+                    echo sprintf("$.pjax({url: '%s', container: '%s', push: false});",
+                           $options['callback_url'],
+                           @$options['container'] ?: '#pjax-container'
+                           );
+                  else
+                    echo sprintf("$.pjax.reload('%s');",
+                            @$options['container'] ?: '#pjax-container');
+                 ?>
+             });
+        }
+        return false;
+    });
+});
+</script>
diff --git a/include/staff/templates/thread-email-headers.tmpl.php b/include/staff/templates/thread-email-headers.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..631d43c2e225c09d0531e57fa589990dd134bb62
--- /dev/null
+++ b/include/staff/templates/thread-email-headers.tmpl.php
@@ -0,0 +1,15 @@
+<h3 class="drag-handle"><?php echo __('Raw Email Headers'); ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<hr/>
+
+<pre style="max-height: 300px; overflow-y: scroll">
+<?php echo Format::htmlchars($headers); ?>
+</pre>
+
+<hr>
+<p class="full-width">
+    <span class="buttons pull-right">
+        <input type="button" name="cancel" class="close"
+            value="<?php echo __('Close'); ?>">
+    </span>
+</p>
diff --git a/include/staff/templates/thread-entries.tmpl.php b/include/staff/templates/thread-entries.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..cabd7560b722df841904cfda1398f87bd93eb0ab
--- /dev/null
+++ b/include/staff/templates/thread-entries.tmpl.php
@@ -0,0 +1,95 @@
+<?php
+
+$sort = 'id';
+if ($options['sort'] && !strcasecmp($options['sort'], 'DESC'))
+    $sort = '-id';
+
+$cmp = function ($a, $b) use ($sort) {
+    return ($sort == 'id')
+        ? ($a < $b) : $a > $b;
+};
+
+$events = $events->order_by($sort);
+$events = $events->getIterator();
+$events->rewind();
+$event = $events->current();
+$htmlId = $options['html-id'] ?: ('thread-'.$this->getId());
+
+$thread_attachments = array();
+foreach (Attachment::objects()->filter(array(
+    'thread_entry__thread__id' => $this->getId(),
+))->select_related('thread_entry', 'file') as $att) {
+    $thread_attachments[$att->object_id][] = $att;
+}
+?>
+<div id="<?php echo $htmlId; ?>">
+    <div id="thread-items" data-thread-id="<?php echo $this->getId(); ?>">
+    <?php
+    if ($entries->exists(true)) {
+        // Go through all the entries and bucket them by time frame
+        $buckets = array();
+        $rel = 0;
+        foreach ($entries as $i=>$E) {
+            // First item _always_ shows up
+            if ($i != 0)
+                // Set relative time resolution to 12 hours
+                $rel = Format::relativeTime(Misc::db2gmtime($E->created, false, 43200));
+            $buckets[$rel][] = $E;
+        }
+
+        // Go back through the entries and render them on the page
+        foreach ($buckets as $rel=>$entries) {
+            // TODO: Consider adding a date boundary to indicate significant
+            //       changes in dates between thread items.
+            foreach ($entries as $entry) {
+                // Emit all events prior to this entry
+                while ($event && $cmp($event->timestamp, $entry->created)) {
+                    $event->render(ThreadEvent::MODE_STAFF);
+                    $events->next();
+                    $event = $events->current();
+                }
+                ?><div id="thread-entry-<?php echo $entry->getId(); ?>"><?php
+                include STAFFINC_DIR . 'templates/thread-entry.tmpl.php';
+                ?></div><?php
+            }
+        }
+    }
+
+    // Emit all other events
+    while ($event) {
+        $event->render(ThreadEvent::MODE_STAFF);
+        $events->next();
+        $event = $events->current();
+    }
+    // This should never happen
+    if (count($entries) + count($events) == 0) {
+        echo '<p><em>'.__('No entries have been posted to this thread.').'</em></p>';
+    }
+    ?>
+    </div>
+</div>
+<script type="text/javascript">
+    $(function() {
+        var container = '<?php echo $htmlId; ?>';
+
+        // Set inline image urls.
+        <?php
+        $urls = array();
+        foreach ($thread_attachments as $eid=>$atts) {
+            foreach ($atts as $A) {
+                if (!$A->inline)
+                    continue;
+                $urls[strtolower($A->file->getKey())] = array(
+                    'download_url' => $A->file->getDownloadUrl(),
+                    'filename' => $A->getFilename(),
+                );
+            }
+        }
+        ?>
+        $('#'+container).data('imageUrls', <?php echo JsonDataEncoder::encode($urls); ?>);
+        // Trigger thread processing.
+        if ($.thread)
+            $.thread.onLoad(container,
+                    {autoScroll: <?php echo $sort == 'id' ? 'true' : 'false'; ?>});
+    });
+</script>
diff --git a/include/staff/templates/thread-entry-edit.tmpl.php b/include/staff/templates/thread-entry-edit.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..3f2d0fe56235af4ab6e539da566dc6ba578ce1ee
--- /dev/null
+++ b/include/staff/templates/thread-entry-edit.tmpl.php
@@ -0,0 +1,83 @@
+<h3 class="drag-handle"><?php echo __('Edit Thread Entry'); ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<hr/>
+
+<form method="post" action="<?php echo $this->getAjaxUrl(true); ?>">
+
+<input type="text" style="width:100%;font-size:14px" placeholder="<?php
+    echo __('Title'); ?>" name="title" value="<?php
+    echo Format::htmlchars($this->entry->title); ?>"/>
+<hr style="height:0"/>
+<textarea style="display: block; width: 100%; height: auto; min-height: 150px;"
+<?php if ($poster && $this->entry->type == 'R') {
+    $signature_type = $poster->getDefaultSignatureType();
+    $signature = '';
+    if (($T = $this->entry->getThread()->getObject()))
+        $dept = $T->getDept();
+
+    switch ($poster->getDefaultSignatureType()) {
+    case 'dept':
+        if ($dept && $dept->canAppendSignature())
+           $signature = $dept->getSignature();
+       break;
+    case 'mine':
+        $signature = $poster->getSignature();
+        $signature_type = 'theirs';
+        break;
+    } ?>
+    data-dept-id="<?php echo $dept ? $dept->getId() : 0; ?>"
+    data-poster-id="<?php echo $this->entry->staff_id; ?>"
+    data-signature-field="signature"
+    data-signature="<?php echo Format::htmlchars(Format::viewableImages($signature)); ?>"
+<?php } ?>
+    name="body"
+    class="large <?php
+        if ($cfg->isRichTextEnabled() && $this->entry->format == 'html')
+            echo 'richtext';
+    ?>"><?php echo htmlspecialchars(Format::viewableImages($this->entry->body));
+?></textarea>
+
+<?php if ($this->entry->type == 'R') { ?>
+<div style="margin:10px 0;"><strong><?php echo __('Signature'); ?>:</strong>
+    <label><input type="radio" name="signature" value="none" checked="checked"> <?php echo __('None');?></label>
+    <?php
+    if ($poster
+        && $poster->getId() != $thisstaff->getId()
+        && $poster->getSignature()
+    ) { ?>
+    <label><input type="radio" name="signature" value="theirs"
+        <?php echo ($info['signature']=='theirs')?'checked="checked"':''; ?>> <?php echo __('Their Signature');?></label>
+    <?php
+    }
+    if ($thisstaff->getSignature()) {?>
+    <label><input type="radio" name="signature" value="mine"
+        <?php echo ($info['signature']=='mine')?'checked="checked"':''; ?>> <?php echo __('My Signature');?></label>
+    <?php
+    } ?>
+    <?php
+    if ($dept && $dept->canAppendSignature()) { ?>
+    <label><input type="radio" name="signature" value="dept"
+        <?php echo ($info['signature']=='dept')?'checked="checked"':''; ?>>
+        <?php echo sprintf(__('Department Signature (%s)'), Format::htmlchars($dept->getName())); ?></label>
+    <?php
+    } ?>
+</div>
+<?php } # end of type == 'R' ?>
+
+<hr>
+<div class="full-width">
+    <span class="buttons pull-left">
+        <input type="button" name="cancel" class="close"
+            value="<?php echo __('Cancel'); ?>">
+    </span>
+    <span class="buttons pull-right">
+        <button type="submit" name="commit" value="save" class="button"
+            ><?php echo __('Save'); ?></button>
+<?php if ($this->entry->type == 'R') { ?>
+        <button type="submit" name="commit" value="resend" class="button"
+            ><?php echo __('Save and Resend'); ?></button>
+<?php } ?>
+    </span>
+</div>
+
+</form>
diff --git a/include/staff/templates/thread-entry-resend.tmpl.php b/include/staff/templates/thread-entry-resend.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..f5bc8eb9aa222b8a3569d357abf66bf9fcceb7f1
--- /dev/null
+++ b/include/staff/templates/thread-entry-resend.tmpl.php
@@ -0,0 +1,50 @@
+<h3 class="drag-handle"><?php echo __('Resend Entry'); ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<hr/>
+
+<form method="post" action="<?php echo $this->getAjaxUrl(true); ?>">
+
+<div class="thread-body" style="background-color: transparent; max-height: 150px; width: 100%; overflow: scroll;">
+    <?php echo $this->entry->getBody()->toHtml(); ?>
+</div>
+
+<?php if ($this->entry->type == 'R') { ?>
+<div style="margin:10px 0;"><strong><?php echo __('Signature'); ?>:</strong>
+    <label><input type="radio" name="signature" value="none" checked="checked"> <?php echo __('None');?></label>
+    <?php
+    if ($poster
+        && $poster->getId() != $thisstaff->getId()
+        && $poster->getSignature()
+    ) { ?>
+    <label><input type="radio" name="signature" value="theirs"
+        <?php echo ($info['signature']=='theirs')?'checked="checked"':''; ?>> <?php echo __('Their Signature');?></label>
+    <?php
+    }
+    if ($thisstaff->getSignature()) {?>
+    <label><input type="radio" name="signature" value="mine"
+        <?php echo ($info['signature']=='mine')?'checked="checked"':''; ?>> <?php echo __('My Signature');?></label>
+    <?php
+    } ?>
+    <?php
+    if ($dept && $dept->canAppendSignature()) { ?>
+    <label><input type="radio" name="signature" value="dept"
+        <?php echo ($info['signature']=='dept')?'checked="checked"':''; ?>>
+        <?php echo sprintf(__('Department Signature (%s)'), Format::htmlchars($dept->getName())); ?></label>
+    <?php
+    } ?>
+</div>
+<?php } # end of type == 'R' ?>
+
+<hr>
+<p class="full-width">
+    <span class="buttons pull-left">
+        <input type="button" name="cancel" class="close"
+            value="<?php echo __('Cancel'); ?>">
+    </span>
+    <span class="buttons pull-right">
+        <input type="submit" name="save"
+            value="<?php echo __('Resend'); ?>">
+    </span>
+</p>
+
+</form>
diff --git a/include/staff/templates/thread-entry-view.tmpl.php b/include/staff/templates/thread-entry-view.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..06f73f4188f8930845e08af8aaae938508a496c4
--- /dev/null
+++ b/include/staff/templates/thread-entry-view.tmpl.php
@@ -0,0 +1,75 @@
+<h3 class="drag-handle"><?php echo __('Original Thread Entry'); ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<hr/>
+
+<div id="history" class="accordian">
+
+<?php
+$E = $entry;
+$i = 0;
+$omniscient = $thisstaff->hasPerm(ThreadEntry::PERM_EDIT);
+do {
+    $i++;
+    if (!$omniscient
+        // The current version is always visible
+        && $i > 1
+        // If you originally posted it, you can see all the edits
+        && $E->staff_id != $thisstaff->getId()
+        // You can see your own edits
+        && ($E->editor != $thisstaff->getId() || $E->editor_type != 'S')
+    ) {
+        // Skip edits made by other agents
+        continue;
+    } ?>
+<dt>
+    <a href="#"><i class="icon-copy"></i>
+    <strong><?php if ($E->title)
+        echo Format::htmlchars($E->title).' — '; ?></strong>
+    <em><?php if (strpos($E->updated, '0000-') === false)
+        echo sprintf(__('Edited on %s by %s'), Format::datetime($E->updated),
+            ($editor = $E->getEditor()) ? $editor->getName() : '');
+    else
+        echo __('Original'); ?></em>
+    </a>
+</dt>
+<dd class="hidden" style="background-color:transparent">
+    <div class="thread-body" style="background-color:transparent">
+        <?php echo $E->getBody()->toHtml(); ?>
+    </div>
+</dd>
+<?php
+}
+while (($E = $E->getParent()) && $E->type == $entry->type);
+?>
+
+</div>
+
+<hr>
+<p class="full-width">
+    <span class="buttons pull-right">
+        <input type="button" name="cancel" class="close"
+            value="<?php echo __('Close'); ?>">
+    </span>
+</p>
+
+</form>
+
+<script type="text/javascript">
+$(function() {
+  var I = setInterval(function() {
+    var A = $('#history.accordian');
+    if (!A.length) return;
+    clearInterval(I);
+
+    var allPanels = $('dd', A).hide().removeClass('hidden');
+    $('dt > a', A).click(function() {
+      if (!$(this).parent().is('.active')) {
+        $('dt', A).removeClass('active');
+        allPanels.slideUp();
+        $(this).parent().addClass('active').next().slideDown();
+      }
+      return false;
+    });
+    allPanels.last().show().prev().addClass('active');
+  }, 100);
+});
diff --git a/include/staff/templates/thread-entry.tmpl.php b/include/staff/templates/thread-entry.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..a904670f2b6043c4ef980121a93a2ff3cc15a337
--- /dev/null
+++ b/include/staff/templates/thread-entry.tmpl.php
@@ -0,0 +1,116 @@
+<?php
+global $thisstaff, $cfg;
+$timeFormat = null;
+if ($thisstaff && !strcasecmp($thisstaff->datetime_format, 'relative')) {
+    $timeFormat = function($datetime) {
+        return Format::relativeTime(Misc::db2gmtime($datetime));
+    };
+}
+
+$entryTypes = array('M'=>'message', 'R'=>'response', 'N'=>'note');
+$user = $entry->getUser() ?: $entry->getStaff();
+$name = $user ? $user->getName() : $entry->poster;
+$avatar = '';
+if ($user && $cfg->isAvatarsEnabled())
+    $avatar = $user->getAvatar();
+?>
+<div class="thread-entry <?php
+    echo $entry->isSystem() ? 'system' : $entryTypes[$entry->type]; ?> <?php if ($avatar) echo 'avatar'; ?>">
+<?php if ($avatar) { ?>
+    <span class="<?php echo ($entry->type == 'M') ? 'pull-right' : 'pull-left'; ?> avatar">
+<?php echo $avatar; ?>
+    </span>
+<?php } ?>
+    <div class="header">
+        <div class="pull-right">
+<?php   if ($entry->hasActions()) {
+            $actions = $entry->getActions(); ?>
+        <span class="muted-button pull-right" data-dropdown="#entry-action-more-<?php echo $entry->getId(); ?>">
+            <i class="icon-caret-down"></i>
+        </span>
+        <div id="entry-action-more-<?php echo $entry->getId(); ?>" class="action-dropdown anchor-right">
+            <ul class="title">
+<?php       foreach ($actions as $group => $list) {
+                foreach ($list as $id => $action) { ?>
+                <li>
+                    <a class="no-pjax" href="#" onclick="javascript:
+                    <?php echo str_replace('"', '\\"', $action->getJsStub()); ?>; return false;">
+                    <i class="<?php echo $action->getIcon(); ?>"></i> <?php
+                    echo $action->getName();
+                ?></a></li>
+<?php           }
+            } ?>
+            </ul>
+        </div>
+<?php   } ?>
+        <span class="textra light">
+<?php   if ($entry->flags & ThreadEntry::FLAG_EDITED) { ?>
+            <span class="label label-bare" title="<?php
+            echo sprintf(__('Edited on %s by %s'), Format::datetime($entry->updated),
+                ($editor = $entry->getEditor()) ? $editor->getName() : '');
+                ?>"><?php echo __('Edited'); ?></span>
+<?php   }
+        if ($entry->flags & ThreadEntry::FLAG_RESENT) { ?>
+            <span class="label label-bare"><?php echo __('Resent'); ?></span>
+<?php   }
+        if ($entry->flags & ThreadEntry::FLAG_COLLABORATOR) { ?>
+            <span class="label label-bare"><?php echo __('Collaborator'); ?></span>
+<?php   } ?>
+        </span>
+        </div>
+<?php
+        echo sprintf(__('<b>%s</b> posted %s'), $name,
+            sprintf('<a name="entry-%d" href="#entry-%1$s"><time %s
+                datetime="%s" data-toggle="tooltip" title="%s">%s</time></a>',
+                $entry->id,
+                $timeFormat ? 'class="relative"' : '',
+                date(DateTime::W3C, Misc::db2gmtime($entry->created)),
+                Format::daydatetime($entry->created),
+                $timeFormat ? $timeFormat($entry->created) : Format::datetime($entry->created)
+            )
+        ); ?>
+        <span style="max-width:400px" class="faded title truncate"><?php
+            echo $entry->title; ?></span>
+        </span>
+    </div>
+    <div class="thread-body no-pjax">
+        <div><?php echo $entry->getBody()->toHtml(); ?></div>
+        <div class="clear"></div>
+<?php
+    // The strangeness here is because .has_attachments is an annotation from
+    // Thread::getEntries(); however, this template may be used in other
+    // places such as from thread entry editing
+    $atts = isset($thread_attachments) ? $thread_attachments[$entry->id] : $entry->attachments;
+    if (isset($atts) && $atts) {
+?>
+    <div class="attachments"><?php
+        foreach ($atts as $A) {
+            if ($A->inline)
+                continue;
+            $size = '';
+            if ($A->file->size)
+                $size = sprintf('<small class="filesize faded">%s</small>', Format::file_size($A->file->size));
+?>
+        <span class="attachment-info">
+        <i class="icon-paperclip icon-flip-horizontal"></i>
+        <a class="no-pjax truncate filename" href="<?php echo $A->file->getDownloadUrl();
+            ?>" download="<?php echo Format::htmlchars($A->getFilename()); ?>"
+            target="_blank"><?php echo Format::htmlchars($A->getFilename());
+        ?></a><?php echo $size;?>
+        </span>
+<?php   }
+    echo '</div>';
+    }
+?>
+    </div>
+<?php
+    if (!isset($thread_attachments) && ($urls = $entry->getAttachmentUrls())) { ?>
+        <script type="text/javascript">
+            $('#thread-entry-<?php echo $entry->getId(); ?>')
+                .data('urls', <?php
+                    echo JsonDataEncoder::encode($urls); ?>)
+                .data('id', <?php echo $entry->getId(); ?>);
+        </script>
+<?php
+    } ?>
+</div>
diff --git a/include/staff/templates/thread-event.tmpl.php b/include/staff/templates/thread-event.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..f98a1e3200776ca8727d586ee1dc47ead485540a
--- /dev/null
+++ b/include/staff/templates/thread-event.tmpl.php
@@ -0,0 +1,8 @@
+<div class="thread-event <?php if ($event->uid) echo 'action'; ?>">
+        <span class="type-icon">
+          <i class="faded icon-<?php echo $event->getIcon(); ?>"></i>
+        </span>
+        <span class="faded description">
+            <?php echo $event->getDescription(ThreadEvent::MODE_STAFF); ?>
+        </span>
+</div>
diff --git a/include/staff/templates/ticket-preview.tmpl.php b/include/staff/templates/ticket-preview.tmpl.php
index 92d120563de7baa0d55412223407b5d9d773e55d..3533c8af7d8fef1dff05e00b25580e39bcf83d53 100644
--- a/include/staff/templates/ticket-preview.tmpl.php
+++ b/include/staff/templates/ticket-preview.tmpl.php
@@ -6,7 +6,9 @@
 
 $staff=$ticket->getStaff();
 $lock=$ticket->getLock();
+$role=$thisstaff->getRole($ticket->getDeptId());
 $error=$msg=$warn=null;
+$thread = $ticket->getThread();
 
 if($lock && $lock->getStaffId()==$thisstaff->getId())
     $warn.='&nbsp;<span class="Icon lockedTicket">'
@@ -16,7 +18,7 @@ elseif($ticket->isOverdue())
 
 echo sprintf(
         '<div style="width:600px; padding: 2px 2px 0 5px;" id="t%s">
-         <h2>'.__('Ticket #%s').': %s</h2><br>',
+         <h2>'.__('Ticket #%s').': %s</h2>',
          $ticket->getNumber(),
          $ticket->getNumber(),
          Format::htmlchars($ticket->getSubject()));
@@ -28,20 +30,20 @@ elseif($msg)
 elseif($warn)
     echo sprintf('<div id="msg_warning">%s</div>',$warn);
 
-echo '<ul class="tabs">';
+echo '<ul class="tabs" id="ticket-preview">';
 
 echo '
-        <li><a id="preview_tab" href="#preview" class="active"
+        <li class="active"><a id="preview_tab" href="#preview"
             ><i class="icon-list-alt"></i>&nbsp;'.__('Ticket Summary').'</a></li>';
-if ($ticket->getNumCollaborators()) {
+if ($thread && $thread->getNumCollaborators()) {
 echo sprintf('
         <li><a id="collab_tab" href="#collab"
             ><i class="icon-fixed-width icon-group
             faded"></i>&nbsp;'.__('Collaborators (%d)').'</a></li>',
-            $ticket->getNumCollaborators());
+            $thread->getNumCollaborators());
 }
 echo '</ul>';
-
+echo '<div id="ticket-preview_container">';
 echo '<div class="tab_content" id="preview">';
 echo '<table border="0" cellspacing="" cellpadding="1" width="100%" class="ticket_info">';
 
@@ -62,14 +64,14 @@ echo sprintf('
             <th>'.__('Created').':</th>
             <td>%s</td>
         </tr>',$ticket_state,
-        Format::db_datetime($ticket->getCreateDate()));
+        Format::datetime($ticket->getCreateDate()));
 if($ticket->isClosed()) {
     echo sprintf('
             <tr>
                 <th>'.__('Closed').':</th>
                 <td>%s   <span class="faded">by %s</span></td>
             </tr>',
-            Format::db_datetime($ticket->getCloseDate()),
+            Format::datetime($ticket->getCloseDate()),
             ($staff?$staff->getName():'staff')
             );
 } elseif($ticket->getEstDueDate()) {
@@ -78,7 +80,7 @@ if($ticket->isClosed()) {
                 <th>'.__('Due Date').':</th>
                 <td>%s</td>
             </tr>',
-            Format::db_datetime($ticket->getEstDueDate()));
+            Format::datetime($ticket->getEstDueDate()));
 }
 echo '</table>';
 
@@ -116,17 +118,19 @@ echo '
     </table>';
 echo '</div>'; // ticket preview content.
 ?>
-<div class="tab_content" id="collab" style="display:none;">
+<div class="hidden tab_content" id="collab">
     <table border="0" cellspacing="" cellpadding="1">
         <colgroup><col style="min-width: 250px;"></col></colgroup>
         <?php
-        if (($collabs=$ticket->getCollaborators())) {?>
+        if ($thread && ($collabs=$thread->getCollaborators())) {?>
         <?php
             foreach($collabs as $collab) {
-                echo sprintf('<tr><td %s><i class="icon-%s"></i>
+                echo sprintf('<tr><td %s>%s
                         <a href="users.php?id=%d" class="no-pjax">%s</a> <em>&lt;%s&gt;</em></td></tr>',
                         ($collab->isActive()? '' : 'class="faded"'),
-                        ($collab->isActive()? 'comments' :  'comment-alt'),
+                        (($U = $collab->getUser()) && ($A = $U->getAvatar()))
+                            ? $A->getImageTag(20) : sprintf('<i class="icon-%s"></i>',
+                                $collab->isActive() ? 'comments' :  'comment-alt'),
                         $collab->getUserId(),
                         $collab->getName(),
                         $collab->getEmail());
@@ -140,11 +144,12 @@ echo '</div>'; // ticket preview content.
     echo sprintf('<span><a class="collaborators"
                             href="#tickets/%d/collaborators">%s</a></span>',
                             $ticket->getId(),
-                            $ticket->getNumCollaborators()
+                            $thread && $thread->getNumCollaborators()
                                 ? __('Manage Collaborators') : __('Add Collaborator')
                                 );
     ?>
 </div>
+</div>
 <?php
 $options = array();
 $options[]=array('action'=>sprintf(__('Thread (%d)'),$ticket->getThreadCount()),'url'=>"tickets.php?id=$tid");
@@ -154,16 +159,16 @@ if($ticket->getNumNotes())
 if($ticket->isOpen())
     $options[]=array('action'=>__('Reply'),'url'=>"tickets.php?id=$tid#reply");
 
-if($thisstaff->canAssignTickets())
+if ($role->hasPerm(TicketModel::PERM_ASSIGN))
     $options[]=array('action'=>($ticket->isAssigned()?__('Reassign'):__('Assign')),'url'=>"tickets.php?id=$tid#assign");
 
-if($thisstaff->canTransferTickets())
-    $options[]=array('action'=>'Transfer','url'=>"tickets.php?id=$tid#transfer");
+if ($role->hasPerm(TicketModel::PERM_TRANSFER))
+    $options[]=array('action'=>__('Transfer'),'url'=>"tickets.php?id=$tid#transfer");
 
-$options[]=array('action'=>'Post Note','url'=>"tickets.php?id=$tid#note");
+$options[]=array('action'=>__('Post Note'),'url'=>"tickets.php?id=$tid#note");
 
-if($thisstaff->canEditTickets())
-    $options[]=array('action'=>'Edit Ticket','url'=>"tickets.php?id=$tid&a=edit");
+if ($role->hasPerm(TicketModel::PERM_EDIT))
+    $options[]=array('action'=>__('Edit Ticket'),'url'=>"tickets.php?id=$tid&a=edit");
 
 if($options) {
     echo '<ul class="tip_menu">';
diff --git a/include/staff/templates/ticket-print.tmpl.php b/include/staff/templates/ticket-print.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..97074ed2682031b73672ecd817a5ba0085f30394
--- /dev/null
+++ b/include/staff/templates/ticket-print.tmpl.php
@@ -0,0 +1,254 @@
+<html>
+
+<head>
+    <style type="text/css">
+@page {
+    header: html_def;
+    footer: html_def;
+    margin: 15mm;
+    margin-top: 30mm;
+    margin-bottom: 22mm;
+}
+.logo {
+  max-width: 220px;
+  max-height: 71px;
+  width: auto;
+  height: auto;
+  margin: 0;
+}
+#ticket_thread .message,
+#ticket_thread .response,
+#ticket_thread .note {
+    margin-top:10px;
+    border:1px solid #aaa;
+    border-bottom:2px solid #aaa;
+}
+#ticket_thread .header {
+    text-align:left;
+    border-bottom:1px solid #aaa;
+    padding:3px;
+    width: 100%;
+    table-layout: fixed;
+}
+#ticket_thread .message .header {
+    background:#C3D9FF;
+}
+#ticket_thread .response .header {
+    background:#FFE0B3;
+}
+#ticket_thread .note .header {
+    background:#FFE;
+}
+#ticket_thread .info {
+    padding:5px;
+    background: snow;
+    border-top: 0.3mm solid #ccc;
+}
+
+table.meta-data {
+    width: 100%;
+}
+table.custom-data {
+    margin-top: 10px;
+}
+table.custom-data th {
+    width: 25%;
+}
+table.custom-data th,
+table.meta-data th {
+    text-align: right;
+    background-color: #ddd;
+    padding: 3px 8px;
+}
+table.meta-data td {
+    padding: 3px 8px;
+}
+.faded {
+    color:#666;
+}
+.pull-left {
+    float: left;
+}
+.pull-right {
+    float: right;
+}
+.flush-right {
+    text-align: right;
+}
+.flush-left {
+    text-align: left;
+}
+.ltr {
+    direction: ltr;
+    unicode-bidi: embed;
+}
+.headline {
+    border-bottom: 2px solid black;
+    font-weight: bold;
+}
+div.hr {
+    border-top: 0.2mm solid #bbb;
+    margin: 0.5mm 0;
+    font-size: 0.0001em;
+}
+.thread-entry, .thread-body {
+    page-break-inside: avoid;
+}
+<?php include ROOT_DIR . 'css/thread.css'; ?>
+    </style>
+</head>
+<body>
+
+<htmlpageheader name="def" style="display:none">
+<?php if ($logo = $cfg->getClientLogo()) { ?>
+    <img src="cid:<?php echo $logo->getKey(); ?>" class="logo"/>
+<?php } else { ?>
+    <img src="<?php echo INCLUDE_DIR . 'fpdf/print-logo.png'; ?>" class="logo"/>
+<?php } ?>
+    <div class="hr">&nbsp;</div>
+    <table><tr>
+        <td class="flush-left"><?php echo (string) $ost->company; ?></td>
+        <td class="flush-right"><?php echo Format::daydatetime(Misc::gmtime()); ?></td>
+    </tr></table>
+</htmlpageheader>
+
+<htmlpagefooter name="def" style="display:none">
+    <div class="hr">&nbsp;</div>
+    <table width="100%"><tr><td class="flush-left">
+        Ticket #<?php echo $ticket->getNumber(); ?> printed by
+        <?php echo $thisstaff->getUserName(); ?> on
+        <?php echo Format::daydatetime(Misc::gmtime()); ?>
+    </td>
+    <td class="flush-right">
+        Page {PAGENO}
+    </td>
+    </tr></table>
+</htmlpagefooter>
+
+<!-- Ticket metadata -->
+<h1>Ticket #<?php echo $ticket->getNumber(); ?></h1>
+<table class="meta-data" cellpadding="0" cellspacing="0">
+<tbody>
+<tr>
+    <th><?php echo __('Status'); ?></th>
+    <td><?php echo $ticket->getStatus(); ?></td>
+    <th><?php echo __('Name'); ?></th>
+    <td><?php echo $ticket->getOwner()->getName(); ?></td>
+</tr>
+<tr>
+    <th><?php echo __('Priority'); ?></th>
+    <td><?php echo $ticket->getPriority(); ?></td>
+    <th><?php echo __('Email'); ?></th>
+    <td><?php echo $ticket->getEmail(); ?></td>
+</tr>
+<tr>
+    <th><?php echo __('Department'); ?></th>
+    <td><?php echo $ticket->getDept(); ?></td>
+    <th><?php echo __('Phone'); ?></th>
+    <td><?php echo $ticket->getPhoneNumber(); ?></td>
+</tr>
+<tr>
+    <th><?php echo __('Create Date'); ?></th>
+    <td><?php echo Format::datetime($ticket->getCreateDate()); ?></td>
+    <th><?php echo __('Source'); ?></th>
+    <td><?php echo $ticket->getSource(); ?></td>
+</tr>
+</tbody>
+<tbody>
+    <tr><td colspan="4" class="spacer">&nbsp;</td></tr>
+</tbody>
+<tbody>
+<tr>
+    <th><?php echo __('Assigned To'); ?></th>
+    <td><?php echo $ticket->getAssigned(); ?></td>
+    <th><?php echo __('Help Topic'); ?></th>
+    <td><?php echo $ticket->getHelpTopic(); ?></td>
+</tr>
+<tr>
+    <th><?php echo __('SLA Plan'); ?></th>
+    <td><?php if ($sla = $ticket->getSLA()) echo $sla->getName(); ?></td>
+    <th><?php echo __('Last Response'); ?></th>
+    <td><?php echo Format::datetime($ticket->getLastResponseDate()); ?></td>
+</tr>
+<tr>
+    <th><?php echo __('Due Date'); ?></th>
+    <td><?php echo Format::datetime($ticket->getEstDueDate()); ?></td>
+    <th><?php echo __('Last Message'); ?></th>
+    <td><?php echo Format::datetime($ticket->getLastMessageDate()); ?></td>
+</tr>
+</tbody>
+</table>
+
+<!-- Custom Data -->
+<?php
+foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) {
+    // Skip core fields shown earlier in the ticket view
+    $answers = $form->getAnswers()->exclude(Q::any(array(
+        'field__flags__hasbit' => DynamicFormField::FLAG_EXT_STORED,
+        'field__name__in' => array('subject', 'priority')
+    )));
+    if (count($answers) == 0)
+        continue;
+    ?>
+        <table class="custom-data" cellspacing="0" cellpadding="4" width="100%" border="0">
+        <tr><td colspan="2" class="headline flush-left"><?php echo $form->getTitle(); ?></th></tr>
+        <?php foreach($answers as $a) {
+            if (!($v = $a->display())) continue; ?>
+            <tr>
+                <th><?php
+    echo $a->getField()->get('label');
+                ?>:</th>
+                <td><?php
+    echo $v;
+                ?></td>
+            </tr>
+            <?php } ?>
+        </table>
+    <?php
+    $idx++;
+} ?>
+
+<!-- Ticket Thread -->
+<h2><?php echo $ticket->getSubject(); ?></h2>
+<div id="ticket_thread">
+<?php
+$types = array('M', 'R');
+if ($this->includenotes)
+    $types[] = 'N';
+
+if ($thread = $ticket->getThreadEntries($types)) {
+    $threadTypes=array('M'=>'message','R'=>'response', 'N'=>'note');
+    foreach ($thread as $entry) { ?>
+        <div class="thread-entry <?php echo $threadTypes[$entry->type]; ?>">
+            <table class="header" style="width:100%"><tr><td>
+                    <span><?php
+                        echo Format::datetime($entry->created);?></span>
+                    <span style="padding:0 1em" class="faded title"><?php
+                        echo Format::truncate($entry->title, 100); ?></span>
+                </td>
+                <td class="flush-right faded title" style="white-space:no-wrap">
+                    <?php
+                        echo Format::htmlchars($entry->getName()); ?></span>
+                </td>
+            </tr></table>
+            <div class="thread-body">
+                <div><?php echo $entry->getBody()->display('pdf'); ?></div>
+            <?php
+            if ($entry->has_attachments
+                    && ($files = $entry->attachments)) { ?>
+                <div class="info">
+<?php           foreach ($files as $A) { ?>
+                    <div>
+                        <span><?php echo Format::htmlchars($A->file->name); ?></span>
+                        <span class="faded">(<?php echo Format::file_size($A->file->size); ?>)</span>
+                    </div>
+<?php           } ?>
+                </div>
+<?php       } ?>
+            </div>
+        </div>
+<?php }
+} ?>
+</div>
+</body>
+</html>
diff --git a/include/staff/templates/ticket-status.tmpl.php b/include/staff/templates/ticket-status.tmpl.php
index 5e0d84ae3c82be28c5880b5444ffc65ea513a279..9731af3b9bf6a75bb7cebac35b92bb0d61b96ec4 100644
--- a/include/staff/templates/ticket-status.tmpl.php
+++ b/include/staff/templates/ticket-status.tmpl.php
@@ -2,10 +2,10 @@
 global $cfg;
 
 if (!$info['title'])
-    $info['title'] = 'Change Tickets Status';
+    $info['title'] = __('Change Tickets Status');
 
 ?>
-<h3><?php echo $info['title']; ?></h3>
+<h3 class="drag-handle"><?php echo $info['title']; ?></h3>
 <b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
 <div class="clear"></div>
 <hr/>
@@ -84,7 +84,8 @@ $action = $info['action'] ?: ('#tickets/status/'. $state);
                         ?>
                         <textarea name="comments" id="comments"
                             cols="50" rows="3" wrap="soft" style="width:100%"
-                            class="richtext ifhtml no-bar"
+                            class="<?php if ($cfg->isRichTextEnabled()) echo 'richtext';
+                            ?> no-bar small"
                             placeholder="<?php echo $placeholder; ?>"><?php
                             echo $info['comments']; ?></textarea>
                     </td>
diff --git a/include/staff/templates/tickets-actions.tmpl.php b/include/staff/templates/tickets-actions.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..0535a5af2832a7da0354b6fa53dc88373ff6f6ca
--- /dev/null
+++ b/include/staff/templates/tickets-actions.tmpl.php
@@ -0,0 +1,82 @@
+<?php
+// Tickets mass actions based on logged in agent
+
+// Status change
+if ($agent->canManageTickets())
+    echo TicketStatus::status_options();
+
+
+// Mass Claim/Assignment
+if ($agent->hasPerm(Ticket::PERM_ASSIGN, false)) {?>
+<span
+    class="action-button" data-placement="bottom"
+    data-dropdown="#action-dropdown-assign" data-toggle="tooltip" title=" <?php
+    echo __('Assign'); ?>">
+    <i class="icon-caret-down pull-right"></i>
+    <a class="tickets-action" id="tickets-assign"
+        href="#tickets/mass/assign"><i class="icon-user"></i></a>
+</span>
+<div id="action-dropdown-assign" class="action-dropdown anchor-right">
+  <ul>
+     <li><a class="no-pjax tickets-action"
+        href="#tickets/mass/claim"><i
+        class="icon-chevron-sign-down"></i> <?php echo __('Claim'); ?></a>
+     <li><a class="no-pjax tickets-action"
+        href="#tickets/mass/assign/agents"><i
+        class="icon-user"></i> <?php echo __('Agent'); ?></a>
+     <li><a class="no-pjax tickets-action"
+        href="#tickets/mass/assign/teams"><i
+        class="icon-group"></i> <?php echo __('Team'); ?></a>
+  </ul>
+</div>
+<?php
+}
+
+// Mass Transfer
+if ($agent->hasPerm(Ticket::PERM_TRANSFER, false)) {?>
+<span class="action-button">
+ <a class="tickets-action" id="tickets-transfer" data-placement="bottom"
+    data-toggle="tooltip" title="<?php echo __('Transfer'); ?>"
+    href="#tickets/mass/transfer"><i class="icon-share"></i></a>
+</span>
+<?php
+}
+
+
+// Mass Delete
+if ($agent->hasPerm(Ticket::PERM_DELETE, false)) {?>
+<span class="red button action-button">
+ <a class="tickets-action" id="tickets-delete" data-placement="bottom"
+    data-toggle="tooltip" title="<?php echo __('Delete'); ?>"
+    href="#tickets/mass/delete"><i class="icon-trash"></i></a>
+</span>
+<?php
+}
+
+?>
+<script type="text/javascript">
+$(function() {
+
+    $(document).off('.tickets');
+    $(document).on('click.tickets', 'a.tickets-action', function(e) {
+        e.preventDefault();
+        var $form = $('form#tickets');
+        var count = checkbox_checker($form, 1);
+        if (count) {
+            var tids = $('.ckb:checked', $form).map(function() {
+                    return this.value;
+                }).get();
+            var url = 'ajax.php/'
+            +$(this).attr('href').substr(1)
+            +'?count='+count
+            +'&tids='+tids.join(',')
+            +'&_uid='+new Date().getTime();
+            console.log(tids);
+            $.dialog(url, [201], function (xhr) {
+                $.pjax.reload('#pjax-container');
+             });
+        }
+        return false;
+    });
+});
+</script>
diff --git a/include/staff/templates/tickets.tmpl.php b/include/staff/templates/tickets.tmpl.php
index ea0aae0b7e3b28be02252f2d245100b4b73cbddc..71dc0470a6524ed0a4f094c0affebeca37f6ea68 100644
--- a/include/staff/templates/tickets.tmpl.php
+++ b/include/staff/templates/tickets.tmpl.php
@@ -1,83 +1,108 @@
 <?php
+$args = array();
+parse_str($_SERVER['QUERY_STRING'], $args);
+$args['t'] = 'tickets';
+unset($args['p'], $args['_pjax']);
 
-$select ='SELECT ticket.ticket_id,ticket.`number`,ticket.dept_id,ticket.staff_id,ticket.team_id, ticket.user_id '
-        .' ,dept.dept_name,status.name as status,ticket.source,ticket.isoverdue,ticket.isanswered,ticket.created '
-        .' ,CAST(GREATEST(IFNULL(ticket.lastmessage, 0), IFNULL(ticket.reopened, 0), ticket.created) as datetime) as effective_date '
-        .' ,CONCAT_WS(" ", staff.firstname, staff.lastname) as staff, team.name as team '
-        .' ,IF(staff.staff_id IS NULL,team.name,CONCAT_WS(" ", staff.lastname, staff.firstname)) as assigned '
-        .' ,IF(ptopic.topic_pid IS NULL, topic.topic, CONCAT_WS(" / ", ptopic.topic, topic.topic)) as helptopic '
-        .' ,cdata.priority as priority_id, cdata.subject, user.name, email.address as email';
+$tickets = TicketModel::objects();
 
-$from =' FROM '.TICKET_TABLE.' ticket '
-      .' LEFT JOIN '.TICKET_STATUS_TABLE.' status
-        ON status.id = ticket.status_id '
-      .' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id '
-      .' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id '
-      .' LEFT JOIN '.USER_ACCOUNT_TABLE.' account ON (ticket.user_id=account.user_id) '
-      .' LEFT JOIN '.DEPT_TABLE.' dept ON ticket.dept_id=dept.dept_id '
-      .' LEFT JOIN '.STAFF_TABLE.' staff ON (ticket.staff_id=staff.staff_id) '
-      .' LEFT JOIN '.TEAM_TABLE.' team ON (ticket.team_id=team.team_id) '
-      .' LEFT JOIN '.TOPIC_TABLE.' topic ON (ticket.topic_id=topic.topic_id) '
-      .' LEFT JOIN '.TOPIC_TABLE.' ptopic ON (ptopic.topic_id=topic.topic_pid) '
-      .' LEFT JOIN '.TABLE_PREFIX.'ticket__cdata cdata ON (cdata.ticket_id = ticket.ticket_id) '
-      .' LEFT JOIN '.PRIORITY_TABLE.' pri ON (pri.priority_id = cdata.priority)';
+if ($user) {
+    $filter = $tickets->copy()
+        ->values_flat('ticket_id')
+        ->filter(array('user_id' => $user->getId()))
+        ->union($tickets->copy()
+            ->values_flat('ticket_id')
+            ->filter(array('thread__collaborators__user_id' => $user->getId()))
+        , false);
+} elseif ($org) {
+    $filter = $tickets->copy()
+        ->values_flat('ticket_id')
+        ->filter(array('user__org' => $org));
+}
 
-if ($user)
-    $where = 'WHERE ticket.user_id = '.db_input($user->getId());
-elseif ($org)
-    $where = 'WHERE user.org_id = '.db_input($org->getId());
+// Apply filter
+$tickets->filter(array('ticket_id__in' => $filter));
 
+// Apply staff visibility
+if (!$thisstaff->hasPerm(SearchBackend::PERM_EVERYTHING)) {
+    // -- Open and assigned to me
+    $visibility = array(
+        new Q(array('status__state'=>'open', 'staff_id' => $thisstaff->getId()))
+    );
+    // -- Routed to a department of mine
+    if (!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts()))
+        $visibility[] = new Q(array('dept_id__in' => $depts));
+    // -- Open and assigned to a team of mine
+    if (($teams = $thisstaff->getTeams()) && count(array_filter($teams)))
+        $visibility[] = new Q(array(
+            'team_id__in' => array_filter($teams), 'status__state'=>'open'
+        ));
+    $tickets->filter(Q::any($visibility));
+}
 
-TicketForm::ensureDynamicDataView();
+$tickets->constrain(array('lock' => array(
+                'lock__expire__gt' => SqlFunction::NOW())));
 
-$query ="$select $from $where ORDER BY ticket.created DESC";
+// Group by ticket_id.
+$tickets->distinct('ticket_id');
 
-// Fetch the results
-$results = array();
-$res = db_query($query);
-while ($row = db_fetch_array($res))
-    $results[$row['ticket_id']] = $row;
+// Save the query to the session for exporting
+$queue = sprintf(':%s:tickets', $user ? 'U' : 'O');
+$_SESSION[$queue] = $tickets;
 
-if ($results) {
-    $counts_sql = 'SELECT ticket.ticket_id,
-        count(DISTINCT attach.attach_id) as attachments,
-        count(DISTINCT thread.id) as thread_count,
-        count(DISTINCT collab.id) as collaborators
-        FROM '.TICKET_TABLE.' ticket
-        LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON (ticket.ticket_id=attach.ticket_id) '
-     .' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON ( ticket.ticket_id=thread.ticket_id) '
-     .' LEFT JOIN '.TICKET_COLLABORATOR_TABLE.' collab
-            ON ( ticket.ticket_id=collab.ticket_id) '
-     .' WHERE ticket.ticket_id IN ('.implode(',', db_input(array_keys($results))).')
-        GROUP BY ticket.ticket_id';
-    $ids_res = db_query($counts_sql);
-    while ($row = db_fetch_array($ids_res)) {
-        $results[$row['ticket_id']] += $row;
-    }
-}
+// Apply pagination
+$total = $tickets->count();
+$page = ($_GET['p'] && is_numeric($_GET['p'])) ? $_GET['p'] : 1;
+$pageNav = new Pagenate($total, $page, PAGE_LIMIT);
+$pageNav->setURL(($user ? 'users.php' : 'orgs.php'), $args);
+$tickets = $pageNav->paginate($tickets);
+
+$tickets->annotate(array(
+    'collab_count' => SqlAggregate::COUNT('thread__collaborators', true),
+    'attachment_count' => SqlAggregate::COUNT(SqlCase::N()
+       ->when(new SqlField('thread__entries__attachments__inline'), null)
+       ->otherwise(new SqlField('thread__entries__attachments')),
+        true
+    ),
+    'thread_count' => SqlAggregate::COUNT(SqlCase::N()
+        ->when(
+            new Q(array('thread__entries__flags__hasbit'=>ThreadEntry::FLAG_HIDDEN)),
+            null)
+        ->otherwise(new SqlField('thread__entries__id')),
+       true
+    ),
+));
+
+$tickets->values('staff_id', 'staff__firstname', 'staff__lastname', 'team__name', 'team_id', 'lock__lock_id', 'lock__staff_id', 'isoverdue', 'status_id', 'status__name', 'status__state', 'number', 'cdata__subject', 'ticket_id', 'source', 'dept_id', 'dept__name', 'user_id', 'user__default_email__address', 'user__name', 'lastupdate');
+
+$tickets->order_by('-created');
+
+TicketForm::ensureDynamicDataView();
+// Fetch the results
 ?>
-<div style="width:700px;" class="pull-left">
+<div class="pull-left" style="margin-top:5px;">
    <?php
-    if ($results) {
-        echo '<strong>'.sprintf(_N('Showing %d ticket', 'Showing %d tickets',
-            count($results)), count($results)).'</strong>';
+    if ($total) {
+        echo '<strong>'.$pageNav->showing().'</strong>';
     } else {
         echo sprintf(__('%s does not have any tickets'), $user? 'User' : 'Organization');
     }
    ?>
 </div>
-<div class="pull-right flush-right" style="padding-right:5px;">
-    <?php
-    if ($user) { ?>
-    <b><a class="Icon newTicket" href="tickets.php?a=open&uid=<?php echo $user->getId(); ?>">
-    <?php print __('Create New Ticket'); ?></a></b>
-    <?php
-    } ?>
+<div style="margin-bottom:10px;">
+    <div class="pull-right flush-right">
+        <?php
+        if ($user) { ?>
+            <a class="green button action-button" href="tickets.php?a=open&uid=<?php echo $user->getId(); ?>">
+                <i class="icon-plus"></i> <?php print __('Create New Ticket'); ?></a>
+        <?php
+        } ?>
+    </div>
 </div>
 <br/>
 <div>
 <?php
-if ($results) { ?>
+if ($total) { ?>
 <form action="users.php" method="POST" name='tickets' style="padding-top:10px;">
 <?php csrf_token(); ?>
  <input type="hidden" name="a" value="mass_process" >
@@ -87,81 +112,106 @@ if ($results) { ?>
         <tr>
             <?php
             if (0) {?>
-            <th width="8px">&nbsp;</th>
+            <th width="4%">&nbsp;</th>
             <?php
             } ?>
-            <th width="70"><?php echo __('Ticket'); ?></th>
-            <th width="100"><?php echo __('Date'); ?></th>
-            <th width="100"><?php echo __('Status'); ?></th>
-            <th width="300"><?php echo __('Subject'); ?></th>
+            <th width="10%"><?php echo __('Ticket'); ?></th>
+            <th width="18%"><?php echo __('Last Updated'); ?></th>
+            <th width="8%"><?php echo __('Status'); ?></th>
+            <th width="30%"><?php echo __('Subject'); ?></th>
             <?php
             if ($user) { ?>
-            <th width="200"><?php echo __('Department'); ?></th>
-            <th width="200"><?php echo __('Assignee'); ?></th>
+            <th width="15%"><?php echo __('Department'); ?></th>
+            <th width="15%"><?php echo __('Assignee'); ?></th>
             <?php
             } else { ?>
-            <th width="400"><?php echo __('User'); ?></th>
+            <th width="30%"><?php echo __('User'); ?></th>
             <?php
             } ?>
         </tr>
     </thead>
     <tbody>
     <?php
-    foreach($results as $row) {
+    $subject_field = TicketForm::objects()->one()->getField('subject');
+    $user_id = $user ? $user->getId() : 0;
+    foreach($tickets as $T) {
         $flag=null;
-        if ($row['lock_id'])
+        if ($T['lock__lock_id'] && $T['lock__staff_id'] != $thisstaff->getId())
             $flag='locked';
-        elseif ($row['isoverdue'])
+        elseif ($T['isoverdue'])
             $flag='overdue';
 
         $assigned='';
-        if ($row['staff_id'])
-            $assigned=sprintf('<span class="Icon staffAssigned">%s</span>',Format::truncate($row['staff'],40));
-        elseif ($row['team_id'])
-            $assigned=sprintf('<span class="Icon teamAssigned">%s</span>',Format::truncate($row['team'],40));
+        if ($T['staff_id'])
+            $assigned = new AgentsName(array(
+                'first' => $T['staff__firstname'],
+                'last' => $T['staff__lastname']
+            ));
+        elseif ($T['team_id'])
+            $assigned = Team::getLocalById($T['team_id'], 'name', $T['team__name']);
         else
             $assigned=' ';
 
-        $status = ucfirst($row['status']);
-        $tid=$row['number'];
-        $subject = Format::htmlchars(Format::truncate($row['subject'],40));
-        $threadcount=$row['thread_count'];
+        $status = TicketStatus::getLocalById($T['status_id'], 'value', $T['status__name']);
+        $tid = $T['number'];
+        $subject = $subject_field->display($subject_field->to_php($T['cdata__subject']));
+        $threadcount = $T['thread_count'];
         ?>
-        <tr id="<?php echo $row['ticket_id']; ?>">
+        <tr id="<?php echo $T['ticket_id']; ?>">
             <?php
             //Implement mass  action....if need be.
             if (0) { ?>
             <td align="center" class="nohover">
-                <input class="ckb" type="checkbox" name="tids[]" value="<?php echo $row['ticket_id']; ?>" <?php echo $sel?'checked="checked"':''; ?>>
+                <input class="ckb" type="checkbox" name="tids[]" value="<?php echo $T['ticket_id']; ?>" <?php echo $sel?'checked="checked"':''; ?>>
             </td>
             <?php
             } ?>
-            <td align="center" nowrap>
-              <a class="Icon <?php echo strtolower($row['source']); ?>Ticket ticketPreview"
+            <td nowrap>
+              <a class="Icon <?php
+                echo strtolower($T['source']); ?>Ticket preview"
                 title="<?php echo __('Preview Ticket'); ?>"
-                href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $tid; ?></a></td>
-            <td align="center" nowrap><?php echo Format::db_datetime($row['effective_date']); ?></td>
+                href="tickets.php?id=<?php echo $T['ticket_id']; ?>"
+                data-preview="#tickets/<?php echo $T['ticket_id']; ?>/preview"><?php
+                echo $tid; ?></a>
+               <?php
+                if ($user_id && $user_id != $T['user_id'])
+                    echo '<span class="pull-right faded-more" data-toggle="tooltip" title="'
+                            .__('Collaborator').'"><i class="icon-eye-open"></i></span>';
+            ?></td>
+            <td nowrap><?php echo Format::datetime($T['lastupdate']); ?></td>
             <td><?php echo $status; ?></td>
-            <td><a <?php if ($flag) { ?> class="Icon <?php echo $flag; ?>Ticket" title="<?php echo ucfirst($flag); ?> Ticket" <?php } ?>
-                href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $subject; ?></a>
+            <td><a class="truncate <?php if ($flag) { ?> Icon <?php echo $flag; ?>Ticket" title="<?php echo ucfirst($flag); ?> Ticket<?php } ?>"
+                style="max-width: 230px;"
+                href="tickets.php?id=<?php echo $T['ticket_id']; ?>"><?php echo $subject; ?></a>
                  <?php
-                    if ($threadcount>1)
-                        echo "<small>($threadcount)</small>&nbsp;".'<i
-                            class="icon-fixed-width icon-comments-alt"></i>&nbsp;';
-                    if ($row['collaborators'])
-                        echo '<i class="icon-fixed-width icon-group faded"></i>&nbsp;';
-                    if ($row['attachments'])
-                        echo '<i class="icon-fixed-width icon-paperclip"></i>&nbsp;';
+                    if ($T['attachment_count'])
+                        echo '<i class="small icon-paperclip icon-flip-horizontal" data-toggle="tooltip" title="'
+                            .$T['attachment_count'].'"></i>';
+                    if ($threadcount > 1) { ?>
+                            <span class="pull-right faded-more"><i class="icon-comments-alt"></i>
+                            <small><?php echo $threadcount; ?></small></span>
+<?php               }
+                    if ($T['attachments'])
+                        echo '<i class="small icon-paperclip icon-flip-horizontal"></i>';
+                    if ($T['collab_count'])
+                        echo '<span class="faded-more" data-toggle="tooltip" title="'
+                            .$T['collab_count'].'"><i class="icon-group"></i></span>';
                 ?>
             </td>
             <?php
-            if ($user) { ?>
-            <td><?php echo Format::truncate($row['dept_name'], 40); ?></td>
-            <td>&nbsp;<?php echo $assigned; ?></td>
+            if ($user) {
+                $dept = Dept::getLocalById($T['dept_id'], 'name', $T['dept__name']); ?>
+            <td><span class="truncate" style="max-wdith:125px"><?php
+                echo Format::htmlchars($dept); ?></span></td>
+            <td><span class="truncate" style="max-width:125px"><?php
+                echo Format::htmlchars($assigned); ?></span></td>
             <?php
             } else { ?>
-            <td>&nbsp;<?php echo sprintf('<a href="users.php?id=%d">%s <em> &lt;%s&gt;</em></a>',
-                    $row['user_id'], $row['name'], $row['email']); ?></td>
+            <td><a class="truncate" style="max-width:250px" href="users.php?id="<?php
+                echo $T['user_id']; ?>><?php echo Format::htmlchars($T['user__name']);
+                    ?> <em>&lt;<?php echo Format::htmlchars($T['user__default_email__address']);
+                ?>&gt;</em></a>
+            </td>
             <?php
             } ?>
         </tr>
@@ -170,6 +220,18 @@ if ($results) { ?>
     ?>
     </tbody>
 </table>
+<?php
+if ($total>0) {
+    echo '<div>';
+    echo __('Page').':'.$pageNav->getPageLinks('tickets', '#tickets').'&nbsp;';
+    echo sprintf('<a class="export-csv no-pjax" href="?%s">%s</a>',
+            Http::build_query(array(
+                    'id' => $user ? $user->getId(): $org->getId(),
+                    'a' => 'export',
+                    't' => 'tickets')),
+            __('Export'));
+    echo '</div>';
+} ?>
 </form>
 <?php
  } ?>
diff --git a/include/staff/templates/timezone.tmpl.php b/include/staff/templates/timezone.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..edc834050ae8825153abc30524e910a9d00f5764
--- /dev/null
+++ b/include/staff/templates/timezone.tmpl.php
@@ -0,0 +1,39 @@
+<?php
+$TZ_NAME = @$TZ_NAME ?: 'timezone';
+$TZ_ALLOW_DEFAULT = isset($TZ_ALLOW_DEFAULT) ? $TZ_ALLOW_DEFAULT : true;
+$TZ_PLACEHOLDER = @$TZ_PLACEHOLDER ?: __('System Default');
+$TZ_TIMEZONE = @$TZ_TIMEZONE ?: '';
+?>
+<select name="<?php echo $TZ_NAME; ?>" id="timezone-dropdown"
+        data-placeholder="<?php echo $TZ_PLACEHOLDER; ?>">
+<?php if ($TZ_ALLOW_DEFAULT) { ?>
+        <option value=""></option>
+<?php }
+    foreach (DateTimeZone::listIdentifiers() as $zone) { ?>
+        <option value="<?php echo $zone; ?>" <?php
+        if ($TZ_TIMEZONE == $zone)
+            echo 'selected="selected"';
+        ?>><?php echo str_replace('/',' / ',$zone); ?></option>
+<?php } ?>
+    </select>
+    <button type="button" class="action-button" onclick="javascript:
+$('head').append($('<script>').attr('src', '<?php
+    echo ROOT_PATH; ?>js/jstz.min.js'));
+var recheck = setInterval(function() {
+    if (window.jstz !== undefined) {
+        clearInterval(recheck);
+        var zone = jstz.determine();
+        $('#timezone-dropdown').val(zone.name()).trigger('change');
+
+    }
+}, 100);
+return false;" style="vertical-align:middle"><i class="icon-map-marker"></i> <?php echo __('Auto Detect'); ?></button>
+
+<script type="text/javascript">
+$(function() {
+    $('#timezone-dropdown').select2({
+        allowClear: <?php echo $TZ_ALLOW_DEFAULT ? 'true' : 'false'; ?>,
+        width: '300px'
+    });
+});
+</script>
diff --git a/include/staff/templates/transfer.tmpl.php b/include/staff/templates/transfer.tmpl.php
new file mode 100644
index 0000000000000000000000000000000000000000..8472f2179f5a994bbb3f949b37a3664d0cf8813a
--- /dev/null
+++ b/include/staff/templates/transfer.tmpl.php
@@ -0,0 +1,62 @@
+<?php
+global $cfg;
+
+$form = $form ?: TransferForm::instantiate($info);
+?>
+<h3 class="drag-handle"><?php echo $info[':title']; ?></h3>
+<b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
+<div class="clear"></div>
+<hr/>
+<?php
+if ($info['error']) {
+    echo sprintf('<p id="msg_error">%s</p>', $info['error']);
+} elseif ($info['warn']) {
+    echo sprintf('<p id="msg_warning">%s</p>', $info['warn']);
+} elseif ($info['msg']) {
+    echo sprintf('<p id="msg_notice">%s</p>', $info['msg']);
+} elseif ($info['notice']) {
+   echo sprintf('<p id="msg_info"><i class="icon-info-sign"></i> %s</p>',
+           $info['notice']);
+}
+
+$action = $info[':action'] ?: ('#');
+?>
+<div style="display:block; margin:5px;">
+<form method="post" name="transfer" id="transfer"
+    class="mass-action"
+    action="<?php echo $action; ?>">
+    <table width="100%">
+        <?php
+        if ($info[':extra']) {
+            ?>
+        <tbody>
+            <tr><td colspan="2"><strong><?php echo $info[':extra'];
+            ?></strong></td> </tr>
+        </tbody>
+        <?php
+        }
+       ?>
+        <tbody>
+            <tr><td colspan=2>
+             <?php
+             $options = array('template' => 'simple', 'form_id' => 'transfer');
+             $form->render($options);
+             ?>
+            </td> </tr>
+        </tbody>
+    </table>
+    <hr>
+    <p class="full-width">
+        <span class="buttons pull-left">
+            <input type="reset" value="<?php echo __('Reset'); ?>">
+            <input type="button" name="cancel" class="close"
+            value="<?php echo __('Cancel'); ?>">
+        </span>
+        <span class="buttons pull-right">
+            <input type="submit" value="<?php
+            echo $verb ?: __('Transfer'); ?>">
+        </span>
+     </p>
+</form>
+</div>
+<div class="clear"></div>
diff --git a/include/staff/templates/user-account.tmpl.php b/include/staff/templates/user-account.tmpl.php
index e68261a29174f86f190826f672713e02aa72b012..ce1605f961cd27542a63df8589941f05469a8bd4 100644
--- a/include/staff/templates/user-account.tmpl.php
+++ b/include/staff/templates/user-account.tmpl.php
@@ -5,7 +5,7 @@ $access = (isset($info['_target']) && $info['_target'] == 'access');
 if (!$info['title'])
     $info['title'] = Format::htmlchars($user->getName());
 ?>
-<h3><?php echo $info['title']; ?></h3>
+<h3 class="drag-handle"><?php echo $info['title']; ?></h3>
 <b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
 <div class="clear"></div>
 <hr/>
@@ -15,16 +15,17 @@ if ($info['error']) {
 } elseif ($info['msg']) {
     echo sprintf('<p id="msg_notice">%s</p>', $info['msg']);
 } ?>
-<ul class="tabs">
-    <li><a href="#user-account" <?php echo !$access? 'class="active"' : ''; ?>
+<form method="post" class="user" action="#users/<?php echo $user->getId(); ?>/manage" >
+<ul class="tabs" id="user-account-tabs">
+    <li <?php echo !$access? 'class="active"' : ''; ?>><a href="#user-account"
         ><i class="icon-user"></i>&nbsp;<?php echo __('User Information'); ?></a></li>
-    <li><a href="#user-access" <?php echo $access? 'class="active"' : ''; ?>
+    <li <?php echo $access? 'class="active"' : ''; ?>><a href="#user-access"
         ><i class="icon-fixed-width icon-lock faded"></i>&nbsp;<?php echo __('Manage Access'); ?></a></li>
 </ul>
 
 
-<form method="post" class="user" action="#users/<?php echo $user->getId(); ?>/manage" >
  <input type="hidden" name="id" value="<?php echo $user->getId(); ?>" />
+<div id="user-account-tabs_container">
  <div class="tab_content"  id="user-account" style="display:<?php echo $access? 'none' : 'block'; ?>; margin:5px;">
     <form method="post" class="user" action="#users/<?php echo $user->getId(); ?>/manage" >
         <input type="hidden" name="id" value="<?php echo $user->getId(); ?>" />
@@ -60,30 +61,17 @@ if ($info['error']) {
         <tbody>
             <tr>
                 <th colspan="2"><em><strong><?php echo __('User Preferences'); ?></strong></em></th>
-            </tr>
-                <td><?php echo __('Time Zone'); ?>:</td>
-                <td>
-                    <select name="timezone_id" id="timezone_id">
-                        <?php
-                        $sql='SELECT id, offset,timezone FROM '.TIMEZONE_TABLE.' ORDER BY id';
-                        if(($res=db_query($sql)) && db_num_rows($res)){
-                            while(list($id,$offset, $tz)=db_fetch_row($res)){
-                                $sel=($info['timezone_id']==$id)?'selected="selected"':'';
-                                echo sprintf('<option value="%d" %s>GMT %s - %s</option>',$id,$sel,$offset,$tz);
-                            }
-                        }
-                        ?>
-                    </select>
-                    &nbsp;<span class="error"><?php echo $errors['timezone_id']; ?></span>
-                </td>
             </tr>
             <tr>
                 <td width="180">
-                   <?php echo __('Daylight Saving'); ?>:
+                    <?php echo __('Time Zone');?>:
                 </td>
                 <td>
-                    <input type="checkbox" name="dst" value="1" <?php echo $info['dst']?'checked="checked"':''; ?>>
-                    <?php echo __('Observe daylight saving'); ?>
+                    <?php
+                    $TZ_NAME = 'timezone';
+                    $TZ_TIMEZONE = $info['timezone'];
+                    include STAFFINC_DIR.'templates/timezone.tmpl.php'; ?>
+                    <div class="error"><?php echo $errors['timezone']; ?></div>
                 </td>
             </tr>
         </tbody>
@@ -110,7 +98,7 @@ if ($info['error']) {
                     data-content="<?php echo sprintf('%s: %s',
                         __('Users can always sign in with their email address'),
                         $user->getEmail()); ?>"></i>
-                    <div class="error">&nbsp;<?php echo $errors['username']; ?></div>
+                    <div class="error"><?php echo $errors['username']; ?></div>
                 </td>
             </tr>
             <tr>
@@ -157,6 +145,7 @@ if ($info['error']) {
         </tbody>
         </table>
    </div>
+   </div>
    <hr>
    <p class="full-width">
         <span class="buttons pull-left">
diff --git a/include/staff/templates/user-delete.tmpl.php b/include/staff/templates/user-delete.tmpl.php
index b563643d284146896f33ce2df58532d6da5d4554..dff5b7f2cdd42782fe6cdb8ce7d62ab24d64a3f1 100644
--- a/include/staff/templates/user-delete.tmpl.php
+++ b/include/staff/templates/user-delete.tmpl.php
@@ -6,7 +6,7 @@ if (!$info['title'])
 $info['warn'] = __('Deleted users and tickets CANNOT be recovered');
 
 ?>
-<h3><?php echo $info['title']; ?></h3>
+<h3 class="drag-handle"><?php echo $info['title']; ?></h3>
 <b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
 <hr/>
 <?php
@@ -20,8 +20,17 @@ if ($info['error']) {
 } ?>
 
 <div id="user-profile" style="margin:5px;">
+<?php
+if ($user) { ?>
+    <div class="avatar pull-left" style="margin: 0 10px;">
+    <?php echo $user->getAvatar(); ?>
+    </div>
+<?php
+}
+else { ?>
     <i class="icon-user icon-4x pull-left icon-border"></i>
-    <?php
+<?php
+}
     // TODO: Implement change of ownership
     if (0 && $user->getNumTickets()) { ?>
     <a class="action-button pull-right change-user" style="overflow:inherit"
@@ -35,7 +44,7 @@ if ($info['error']) {
 <?php foreach ($user->getDynamicData() as $entry) {
 ?>
     <tr><td colspan="2" style="border-bottom: 1px dotted black"><strong><?php
-         echo $entry->getForm()->get('title'); ?></strong></td></tr>
+         echo $entry->getTitle(); ?></strong></td></tr>
 <?php foreach ($entry->getAnswers() as $a) { ?>
     <tr style="vertical-align:top"><td style="width:30%;border-bottom: 1px dotted #ccc"><?php echo Format::htmlchars($a->getField()->get('label'));
          ?>:</td>
diff --git a/include/staff/templates/user-import.tmpl.php b/include/staff/templates/user-import.tmpl.php
index 447f83ee2441b3764853e2f5e7098b800c6bc9db..a1c47340b131e4487dbcae4e21451c381175f84d 100644
--- a/include/staff/templates/user-import.tmpl.php
+++ b/include/staff/templates/user-import.tmpl.php
@@ -1,5 +1,5 @@
 <div id="the-lookup-form">
-<h3><?php echo $info['title']; ?></h3>
+<h3 class="drag-handle"><?php echo $info['title']; ?></h3>
 <b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
 <hr/>
 <?php
@@ -10,8 +10,8 @@ if ($info['error']) {
 } elseif ($info['msg']) {
     echo sprintf('<p id="msg_notice">%s</p>', $info['msg']);
 } ?>
-<ul class="tabs">
-    <li><a href="#copy-paste" class="active"
+<ul class="tabs" id="user-import-tabs">
+    <li class="active"><a href="#copy-paste"
         ><i class="icon-edit"></i>&nbsp;<?php echo __('Copy Paste'); ?></a></li>
     <li><a href="#upload"
         ><i class="icon-fixed-width icon-cloud-upload"></i>&nbsp;<?php echo __('Upload'); ?></a></li>
@@ -26,7 +26,7 @@ if ($info['error']) {
 if ($org_id) { ?>
     <input type="hidden" name="id" value="<?php echo $org_id; ?>"/>
 <?php } ?>
-
+<div id="user-import-tabs_container">
 <div class="tab_content" id="copy-paste" style="margin:5px;">
 <h2 style="margin-bottom:10px"><?php echo __('Name and Email'); ?></h2>
 <p><?php echo __(
@@ -39,7 +39,7 @@ if ($org_id) { ?>
 </textarea>
 </div>
 
-<div class="tab_content" id="upload" style="display:none;margin:5px;">
+<div class="hidden tab_content" id="upload" style="margin:5px;">
 <h2 style="margin-bottom:10px"><?php echo __('Import a CSV File'); ?></h2>
 <p>
 <em><?php echo sprintf(__(
@@ -71,6 +71,7 @@ if ($org_id) { ?>
 </tr></table>
 <br/>
 <input type="file" name="import"/>
+</div>
 </div>
     <hr>
     <p class="full-width">
diff --git a/include/staff/templates/user-lookup.tmpl.php b/include/staff/templates/user-lookup.tmpl.php
index fa18f885888041d997c5e75e192d5644e7fd35b2..cfeeaddf65b5ce7640cef5158eac44a642e5c202 100644
--- a/include/staff/templates/user-lookup.tmpl.php
+++ b/include/staff/templates/user-lookup.tmpl.php
@@ -1,16 +1,18 @@
 <div id="the-lookup-form">
-<h3><?php echo $info['title']; ?></h3>
+<h3 class="drag-handle"><?php echo $info['title']; ?></h3>
 <b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
 <hr/>
 <?php
 if (!isset($info['lookup']) || $info['lookup'] !== false) { ?>
-<div><p id="msg_info"><i class="icon-info-sign"></i>&nbsp; <?php echo __(
-'Search existing users or add a new user.'
-); ?></p></div>
+<div><p id="msg_info"><i class="icon-info-sign"></i>&nbsp; <?php echo
+    $thisstaff->hasPerm(User::PERM_CREATE)
+    ? __('Search existing users or add a new user.')
+    : __('Search existing users.');
+?></p></div>
 <div style="margin-bottom:10px;">
     <input type="text" class="search-input" style="width:100%;"
     placeholder="<?php echo __('Search by email, phone or name'); ?>" id="user-search"
-    autocorrect="off" autocomplete="off"/>
+    autofocus autocorrect="off" autocomplete="off"/>
 </div>
 <?php
 }
@@ -25,11 +27,23 @@ if ($info['error']) {
 <div id="selected-user-info" style="display:<?php echo $user ? 'block' :'none'; ?>;margin:5px;">
 <form method="post" class="user" action="<?php echo $info['action'] ?  $info['action'] : '#users/lookup'; ?>">
     <input type="hidden" id="user-id" name="id" value="<?php echo $user ? $user->getId() : 0; ?>"/>
+<?php
+if ($user) { ?>
+    <div class="avatar pull-left" style="margin: 0 10px;">
+    <?php echo $user->getAvatar(); ?>
+    </div>
+<?php
+}
+else { ?>
     <i class="icon-user icon-4x pull-left icon-border"></i>
+<?php
+}
+if ($thisstaff->hasPerm(User::PERM_CREATE)) { ?>
     <a class="action-button pull-right" style="overflow:inherit"
         id="unselect-user"  href="#"><i class="icon-remove"></i>
         <?php echo __('Add New User'); ?></a>
-<?php if ($user) { ?>
+<?php }
+if ($user) { ?>
     <div><strong id="user-name"><?php echo Format::htmlchars($user->getName()->getOriginal()); ?></strong></div>
     <div>&lt;<span id="user-email"><?php echo $user->getEmail(); ?></span>&gt;</div>
     <?php
@@ -40,7 +54,7 @@ if ($info['error']) {
     <table style="margin-top: 1em;">
 <?php foreach ($user->getDynamicData() as $entry) { ?>
     <tr><td colspan="2" style="border-bottom: 1px dotted black"><strong><?php
-         echo $entry->getForm()->get('title'); ?></strong></td></tr>
+         echo $entry->getTitle(); ?></strong></td></tr>
 <?php foreach ($entry->getAnswers() as $a) { ?>
     <tr style="vertical-align:top"><td style="width:30%;border-bottom: 1px dotted #ccc"><?php echo Format::htmlchars($a->getField()->get('label'));
          ?>:</td>
@@ -65,6 +79,7 @@ if ($info['error']) {
 </form>
 </div>
 <div id="new-user-form" style="display:<?php echo $user ? 'none' :'block'; ?>;">
+<?php if ($thisstaff->hasPerm(User::PERM_CREATE)) { ?>
 <form method="post" class="user" action="<?php echo $info['action'] ?: '#users/lookup/form'; ?>">
     <table width="100%" class="fixed">
     <?php
@@ -82,6 +97,15 @@ if ($info['error']) {
         </span>
      </p>
 </form>
+<?php }
+else { ?>
+    <hr/>
+    <p class="full-width">
+        <span class="buttons pull-left">
+            <input type="button" name="cancel" class="<?php echo $user ?  'cancel' : 'close' ?>"  value="<?php echo __('Cancel'); ?>">
+        </span>
+     </p>
+<?php } ?>
 </div>
 <div class="clear"></div>
 </div>
diff --git a/include/staff/templates/user-register.tmpl.php b/include/staff/templates/user-register.tmpl.php
index 015f82edf9fa5a66fe9d0c744deeb8a8eb665896..1fee0e6f30c21ec81bd4a0c84bf7e4748406fa68 100644
--- a/include/staff/templates/user-register.tmpl.php
+++ b/include/staff/templates/user-register.tmpl.php
@@ -5,18 +5,11 @@ if (!$info['title'])
     $info['title'] = sprintf(__('Register: %s'), Format::htmlchars($user->getName()));
 
 if (!$_POST) {
-
     $info['sendemail'] = true; // send email confirmation.
-
-    if (!isset($info['timezone_id']))
-        $info['timezone_id'] = $cfg->getDefaultTimezoneId();
-
-    if (!isset($info['dst']))
-        $info['dst'] = $cfg->observeDaylightSaving();
 }
 
 ?>
-<h3><?php echo $info['title']; ?></h3>
+<h3 class="drag-handle"><?php echo $info['title']; ?></h3>
 <b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
 <div class="clear"></div>
 <hr/>
@@ -137,28 +130,11 @@ echo sprintf(__(
             </tr>
                 <td><?php echo __('Time Zone'); ?>:</td>
                 <td>
-                    <select name="timezone_id" id="timezone_id">
-                        <?php
-                        $sql='SELECT id, offset, timezone FROM '.TIMEZONE_TABLE.' ORDER BY id';
-                        if(($res=db_query($sql)) && db_num_rows($res)){
-                            while(list($id, $offset, $tz) = db_fetch_row($res)) {
-                                $sel=($info['timezone_id']==$id) ? 'selected="selected"' : '';
-                                echo sprintf('<option value="%d" %s>GMT %s - %s</option>',
-                                        $id, $sel, $offset, $tz);
-                            }
-                        }
-                        ?>
-                    </select>
-                    &nbsp;<span class="error"><?php echo $errors['timezone_id']; ?></span>
-                </td>
-            </tr>
-            <tr>
-                <td width="180">
-                   <?php echo __('Daylight Saving'); ?>:
-                </td>
-                <td>
-                    <input type="checkbox" name="dst" value="1" <?php echo $info['dst'] ? 'checked="checked"' : ''; ?>>
-                    <?php echo __('Observe daylight saving'); ?>
+                    <?php
+                    $TZ_NAME = 'timezone';
+                    $TZ_TIMEZONE = $info['timezone'];
+                    include STAFFINC_DIR.'templates/timezone.tmpl.php'; ?>
+                    <div class="error"><?php echo $errors['timezone']; ?></div>
                 </td>
             </tr>
         </tbody>
diff --git a/include/staff/templates/user.tmpl.php b/include/staff/templates/user.tmpl.php
index af21b01286708196a9d3ebfce6b4b08849f9954f..b1debfd90653b08fb1669c8a410c09460af73630 100644
--- a/include/staff/templates/user.tmpl.php
+++ b/include/staff/templates/user.tmpl.php
@@ -3,7 +3,7 @@ if (!isset($info['title']))
     $info['title'] = Format::htmlchars($user->getName());
 
 if ($info['title']) { ?>
-<h3><?php echo $info['title']; ?></h3>
+<h3 class="drag-handle"><?php echo $info['title']; ?></h3>
 <b><a class="close" href="#"><i class="icon-remove-circle"></i></a></b>
 <hr>
 <?php
@@ -16,7 +16,9 @@ if ($info['error']) {
     echo sprintf('<p id="msg_notice">%s</p>', $info['msg']);
 } ?>
 <div id="user-profile" style="display:<?php echo $forms ? 'none' : 'block'; ?>;margin:5px;">
-    <i class="icon-user icon-4x pull-left icon-border"></i>
+    <div class="avatar pull-left" style="margin: 0 10px;">
+    <?php echo $user->getAvatar(); ?>
+    </div>
     <?php
     if ($ticket) { ?>
     <a class="action-button pull-right change-user" style="overflow:inherit"
@@ -34,11 +36,11 @@ if ($info['error']) {
     } ?>
 
 <div class="clear"></div>
-<ul class="tabs" style="margin-top:5px">
-    <li><a href="#info-tab" class="active"
+<ul class="tabs" id="user_tabs" style="margin-top:5px">
+    <li class="active"><a href="#info-tab"
         ><i class="icon-info-sign"></i>&nbsp;<?php echo __('User'); ?></a></li>
 <?php if ($org) { ?>
-    <li><a href="#organization-tab"
+    <li><a href="#org-tab"
         ><i class="icon-fixed-width icon-building"></i>&nbsp;<?php echo __('Organization'); ?></a></li>
 <?php }
     $ext_id = "U".$user->getId();
@@ -47,17 +49,22 @@ if ($info['error']) {
         ><i class="icon-fixed-width icon-pushpin"></i>&nbsp;<?php echo __('Notes'); ?></a></li>
 </ul>
 
+<div id="user_tabs_container">
 <div class="tab_content" id="info-tab">
 <div class="floating-options">
+<?php if ($thisstaff->hasPerm(User::PERM_EDIT)) { ?>
     <a href="<?php echo $info['useredit'] ?: '#'; ?>" id="edituser" class="action" title="<?php echo __('Edit'); ?>"><i class="icon-edit"></i></a>
+<?php }
+      if ($thisstaff->hasPerm(User::PERM_DIRECTORY)) { ?>
     <a href="users.php?id=<?php echo $user->getId(); ?>" title="<?php
         echo __('Manage User'); ?>" class="action"><i class="icon-share"></i></a>
+<?php } ?>
 </div>
     <table class="custom-info" width="100%">
 <?php foreach ($user->getDynamicData() as $entry) {
 ?>
     <tr><th colspan="2"><strong><?php
-         echo $entry->getForm()->get('title'); ?></strong></td></tr>
+         echo $entry->getTitle(); ?></strong></td></tr>
 <?php foreach ($entry->getAnswers() as $a) { ?>
     <tr><td style="width:30%;"><?php echo Format::htmlchars($a->getField()->get('label'));
          ?>:</td>
@@ -70,16 +77,18 @@ if ($info['error']) {
 </div>
 
 <?php if ($org) { ?>
-<div class="tab_content" id="organization-tab" style="display:none">
+<div class="hidden tab_content" id="org-tab">
+<?php if ($thisstaff->hasPerm(User::PERM_DIRECTORY)) { ?>
 <div class="floating-options">
     <a href="orgs.php?id=<?php echo $org->getId(); ?>" title="<?php
     echo __('Manage Organization'); ?>" class="action"><i class="icon-share"></i></a>
 </div>
+<?php } ?>
     <table class="custom-info" width="100%">
 <?php foreach ($org->getDynamicData() as $entry) {
 ?>
     <tr><th colspan="2"><strong><?php
-         echo $entry->getForm()->get('title'); ?></strong></td></tr>
+         echo $entry->getTitle(); ?></strong></td></tr>
 <?php foreach ($entry->getAnswers() as $a) { ?>
     <tr><td style="width:30%"><?php echo Format::htmlchars($a->getField()->get('label'));
          ?>:</td>
@@ -92,7 +101,7 @@ if ($info['error']) {
 </div>
 <?php } # endif ($org) ?>
 
-<div class="tab_content" id="notes-tab" style="display:none">
+<div class="hidden tab_content" id="notes-tab">
 <?php $show_options = true;
 foreach ($notes as $note)
     include STAFFINC_DIR . 'templates/note.tmpl.php';
@@ -107,6 +116,7 @@ foreach ($notes as $note)
 </div>
 </div>
 </div>
+</div>
 
 </div>
 <div id="user-form" style="display:<?php echo $forms ? 'block' : 'none'; ?>;">
diff --git a/include/staff/templates/users.tmpl.php b/include/staff/templates/users.tmpl.php
index f65f529eef1b3b019988d480aede6c1be64f7ac3..7d56f26ef739863fd02ec9ccfdfdd89c82b3d9ef 100644
--- a/include/staff/templates/users.tmpl.php
+++ b/include/staff/templates/users.tmpl.php
@@ -56,20 +56,22 @@ else
     $showing .= __("This organization doesn't have any users yet");
 
 ?>
-<div style="width:700px;" class="pull-left"><b><?php echo $showing; ?></b></div>
-<div class="pull-right flush-right" style="padding-right:5px;">
-    <b><a href="#orgs/<?php echo $org->getId(); ?>/add-user" class="Icon newstaff add-user"
-        ><?php echo __('Add User'); ?></a></b>
-    |
-    <b><a href="#orgs/<?php echo $org->getId(); ?>/import-users" class="add-user">
-    <i class="icon-cloud-upload icon-large"></i>
-    <?php echo __('Import'); ?></a></b>
+<form action="orgs.php?id=<?php echo $org->getId(); ?>" method="POST" name="users" >
+
+<div style="margin-top:5px;" class="pull-left"><b><?php echo $showing; ?></b></div>
+<?php if ($thisstaff->hasPerm(User::PERM_EDIT)) { ?>
+<div class="pull-right flush-right" style="margin-bottom:10px;">
+    <a href="#orgs/<?php echo $org->getId(); ?>/add-user" class="green button action-button add-user"
+        ><i class="icon-plus"></i> <?php echo __('Add User'); ?></a>
+    <a href="#orgs/<?php echo $org->getId(); ?>/import-users" class="button action-button add-user">
+        <i class="icon-cloud-upload icon-large"></i>
+    <?php echo __('Import'); ?></a>
+    <button id="actions" class="red button action-button" type="submit" name="remove-users"><i class="icon-trash"></i> <?php echo __('Remove'); ?></button>
 </div>
+<?php } ?>
 <div class="clear"></div>
-<br/>
 <?php
 if ($num) { ?>
-<form action="orgs.php?id=<?php echo $org->getId(); ?>" method="POST" name="users" >
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="mass_process" >
  <input type="hidden" id="id" name="id" value="<?php echo $org->getId(); ?>" >
@@ -77,11 +79,11 @@ if ($num) { ?>
  <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
     <thead>
         <tr>
-            <th width="7px">&nbsp;</th>
-            <th width="350"><?php echo __('Name'); ?></th>
-            <th width="300"><?php echo __('Email'); ?></th>
-            <th width="100"><?php echo __('Status'); ?></th>
-            <th width="100"><?php echo __('Created'); ?></th>
+            <th width="4%">&nbsp;</th>
+            <th width="38%"><?php echo __('Name'); ?></th>
+            <th width="35%"><?php echo __('Email'); ?></th>
+            <th width="8%"><?php echo __('Status'); ?></th>
+            <th width="15%"><?php echo __('Created'); ?></th>
         </tr>
     </thead>
     <tbody>
@@ -90,20 +92,22 @@ if ($num) { ?>
             $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null;
             while ($row = db_fetch_array($res)) {
 
-                $name = new PersonsName($row['name']);
+                $name = new UsersName($row['name']);
                 $status = 'Active';
                 $sel=false;
                 if($ids && in_array($row['id'], $ids))
                     $sel=true;
                 ?>
                <tr id="<?php echo $row['id']; ?>">
-                <td width=7px>
+                <td align="center">
                   <input type="checkbox" class="ckb" name="ids[]"
                     value="<?php echo $row['id']; ?>" <?php echo $sel?'checked="checked"':''; ?> >
                 </td>
                 <td>&nbsp;
-                    <a class="userPreview"
-                        href="users.php?id=<?php echo $row['id']; ?>"><?php
+                    <a class="preview"
+                        href="users.php?id=<?php echo $row['id']; ?>"
+                        data-preview="#users/<?php
+                        echo $row['id']; ?>/preview" ><?php
                         echo Format::htmlchars($name); ?></a>
                     &nbsp;
                     <?php
@@ -114,7 +118,7 @@ if ($num) { ?>
                 </td>
                 <td><?php echo Format::htmlchars($row['email']); ?></td>
                 <td><?php echo $status; ?></td>
-                <td><?php echo Format::db_date($row['created']); ?></td>
+                <td><?php echo Format::date($row['created']); ?></td>
                </tr>
             <?php
             } //end of while.
@@ -144,9 +148,7 @@ if ($res && $num) { //Show options..
     echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;</div>';
 
     ?>
-    <p class="centered" id="actions">
-        <input class="button" type="submit" name="remove-users" value="<?php echo __('Remove'); ?>" >
-    </p>
+
 <?php
 }
 ?>
@@ -155,7 +157,7 @@ if ($res && $num) { //Show options..
 } ?>
 
 <div style="display:none;" class="dialog" id="confirm-action">
-    <h3><?php echo __('Please Confirm'); ?></h3>
+    <h3 class="drag-handle"><?php echo __('Please Confirm'); ?></h3>
     <a class="close" href=""><i class="icon-remove-circle"></i></a>
     <hr/>
     <p class="confirm-action" style="display:none;" id="remove-users-confirm">
diff --git a/include/staff/ticket-edit.inc.php b/include/staff/ticket-edit.inc.php
index 96d1abe6fc943457bf67cbfd3459582dd5b0da2c..37697ea8c5059688306ceef2b077df57e1f1bf13 100644
--- a/include/staff/ticket-edit.inc.php
+++ b/include/staff/ticket-edit.inc.php
@@ -1,25 +1,33 @@
 <?php
-if(!defined('OSTSCPINC') || !$thisstaff || !$thisstaff->canEditTickets() || !$ticket) die('Access Denied');
+if (!defined('OSTSCPINC')
+        || !$ticket
+        || !($ticket->checkStaffPerm($thisstaff, TicketModel::PERM_EDIT)))
+    die('Access Denied');
 
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$ticket->getUpdateInfo());
 if ($_POST)
-    $info['duedate'] = Format::date($cfg->getDateFormat(),
-       strtotime($info['duedate']));
+    // Reformat duedate to the display standard (but don't convert to local
+    // timezone)
+    $info['duedate'] = Format::date(strtotime($info['duedate']), false, false, 'UTC');
 ?>
 <form action="tickets.php?id=<?php echo $ticket->getId(); ?>&a=edit" method="post" id="save"  enctype="multipart/form-data">
- <?php csrf_token(); ?>
- <input type="hidden" name="do" value="update">
- <input type="hidden" name="a" value="edit">
- <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>">
- <h2><?php echo sprintf(__('Update Ticket #%s'),$ticket->getNumber());?></h2>
- <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
-    <tbody>
-        <tr>
-            <th colspan="2">
-                <em><strong><?php echo __('User Information'); ?></strong>: <?php echo __('Currently selected user'); ?></em>
-            </th>
-        </tr>
-    <?php
+    <?php csrf_token(); ?>
+    <input type="hidden" name="do" value="update">
+    <input type="hidden" name="a" value="edit">
+    <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>">
+    <div style="margin-bottom:20px; padding-top:5px;">
+        <div class="pull-left flush-left">
+            <h2><?php echo sprintf(__('Update Ticket #%s'),$ticket->getNumber());?></h2>
+        </div>
+    </div>
+    <table class="form_table" width="940" border="0" cellspacing="0" cellpadding="2">
+        <tbody>
+            <tr>
+                <th colspan="2">
+                    <em><strong><?php echo __('User Information'); ?></strong>: <?php echo __('Currently selected user'); ?></em>
+                </th>
+            </tr>
+        <?php
     if(!$info['user_id'] || !($user = User::lookup($info['user_id'])))
         $user = $ticket->getUser();
     ?>
@@ -36,7 +44,7 @@ if ($_POST)
             <span id="client-name"><?php echo Format::htmlchars($user->getName()); ?></span>
             &lt;<span id="client-email"><?php echo $user->getEmail(); ?></span>&gt;
             </a>
-            <a class="action-button" style="overflow:inherit" href="#"
+            <a class="inline action-button" style="overflow:inherit" href="#"
                 onclick="javascript:
                     $.userLookup('ajax.php/tickets/<?php echo $ticket->getId(); ?>/change-user',
                             function(user) {
@@ -62,12 +70,17 @@ if ($_POST)
             </td>
             <td>
                 <select name="source">
-                    <option value="" selected >&mdash; <?php echo __('Select Source');?> &mdash;</option>
-                    <option value="Phone" <?php echo ($info['source']=='Phone')?'selected="selected"':''; ?>><?php echo __('Phone');?></option>
-                    <option value="Email" <?php echo ($info['source']=='Email')?'selected="selected"':''; ?>><?php echo __('Email');?></option>
-                    <option value="Web"   <?php echo ($info['source']=='Web')?'selected="selected"':''; ?>><?php echo __('Web');?></option>
-                    <option value="API"   <?php echo ($info['source']=='API')?'selected="selected"':''; ?>><?php echo __('API');?></option>
-                    <option value="Other" <?php echo ($info['source']=='Other')?'selected="selected"':''; ?>><?php echo __('Other');?></option>
+                    <option value="" selected >&mdash; <?php
+                        echo __('Select Source');?> &mdash;</option>
+                    <?php
+                    $source = $info['source'] ?: 'Phone';
+                    foreach (Ticket::getSources() as $k => $v) {
+                        echo sprintf('<option value="%s" %s>%s</option>',
+                                $k,
+                                ($source == $k ) ? 'selected="selected"' : '',
+                                $v);
+                    }
+                    ?>
                 </select>
                 &nbsp;<font class="error"><b>*</b>&nbsp;<?php echo $errors['source']; ?></font>
             </td>
@@ -125,7 +138,8 @@ if ($_POST)
                 echo Misc::timeDropdown($hr, $min, 'time');
                 ?>
                 &nbsp;<font class="error">&nbsp;<?php echo $errors['duedate']; ?>&nbsp;<?php echo $errors['time']; ?></font>
-                <em><?php echo __('Time is based on your time zone');?> (GMT <?php echo $thisstaff->getTZoffset(); ?>)</em>
+                <em><?php echo __('Time is based on your time zone');?>
+                    (<?php echo $cfg->getTimezone($thisstaff); ?>)</em>
             </td>
         </tr>
     </tbody>
@@ -152,7 +166,7 @@ if ($_POST)
         </tr>
     </tbody>
 </table>
-<p style="padding-left:250px;">
+<p style="text-align:center;">
     <input type="submit" name="submit" value="<?php echo __('Save');?>">
     <input type="reset"  name="reset"  value="<?php echo __('Reset');?>">
     <input type="button" name="cancel" value="<?php echo __('Cancel');?>" onclick='window.location.href="tickets.php?id=<?php echo $ticket->getId(); ?>"'>
@@ -162,17 +176,24 @@ if ($_POST)
     <div class="body"></div>
 </div>
 <script type="text/javascript">
-$('table.dynamic-forms').sortable({
-  items: 'tbody',
-  handle: 'th',
-  helper: function(e, ui) {
-    ui.children().each(function() {
-      $(this).children().each(function() {
-        $(this).width($(this).width());
-      });
++(function() {
+  var I = setInterval(function() {
+    if (!$.fn.sortable)
+      return;
+    clearInterval(I);
+    $('table.dynamic-forms').sortable({
+      items: 'tbody',
+      handle: 'th',
+      helper: function(e, ui) {
+        ui.children().each(function() {
+          $(this).children().each(function() {
+            $(this).width($(this).width());
+          });
+        });
+        ui=ui.clone().css({'background-color':'white', 'opacity':0.8});
+        return ui;
+      }
     });
-    ui=ui.clone().css({'background-color':'white', 'opacity':0.8});
-    return ui;
-  }
-});
+  }, 20);
+})();
 </script>
diff --git a/include/staff/ticket-open.inc.php b/include/staff/ticket-open.inc.php
index 6041f5573480b67ee10c94d26a52e81050aed2f8..c7680704fe2c8e8f9b6ec8d2100cb5592023b0cf 100644
--- a/include/staff/ticket-open.inc.php
+++ b/include/staff/ticket-open.inc.php
@@ -1,46 +1,52 @@
 <?php
-if(!defined('OSTSCPINC') || !$thisstaff || !$thisstaff->canCreateTickets()) die('Access Denied');
+if (!defined('OSTSCPINC') || !$thisstaff
+        || !$thisstaff->hasPerm(TicketModel::PERM_CREATE, false))
+        die('Access Denied');
+
 $info=array();
 $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
 
 if (!$info['topicId'])
     $info['topicId'] = $cfg->getDefaultTopicId();
 
-$form = null;
+$forms = array();
 if ($info['topicId'] && ($topic=Topic::lookup($info['topicId']))) {
-    $form = $topic->getForm();
-    if ($_POST && $form) {
-        $form = $form->instanciate();
-        $form->isValid();
+    foreach ($topic->getForms() as $F) {
+        if (!$F->hasAnyVisibleFields())
+            continue;
+        if ($_POST) {
+            $F = $F->instanciate();
+            $F->isValidForClient();
+        }
+        $forms[] = $F;
     }
 }
 
 if ($_POST)
-    $info['duedate'] = Format::date($cfg->getDateFormat(),
-       strtotime($info['duedate']));
+    $info['duedate'] = Format::date(strtotime($info['duedate']), false, false, 'UTC');
 ?>
 <form action="tickets.php?a=open" method="post" id="save"  enctype="multipart/form-data">
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="create">
  <input type="hidden" name="a" value="open">
- <h2><?php echo __('Open a New Ticket');?></h2>
+<div style="margin-bottom:20px; padding-top:5px;">
+    <div class="pull-left flush-left">
+        <h2><?php echo __('Open a New Ticket');?></h2>
+    </div>
+</div>
  <table class="form_table fixed" width="940" border="0" cellspacing="0" cellpadding="2">
     <thead>
     <!-- This looks empty - but beware, with fixed table layout, the user
          agent will usually only consult the cells in the first row to
          construct the column widths of the entire toable. Therefore, the
          first row needs to have two cells -->
-        <tr><td></td><td></td></tr>
-        <tr>
-            <th colspan="2">
-                <h4><?php echo __('New Ticket');?></h4>
-            </th>
-        </tr>
+        <tr><td style="padding:0;"></td><td style="padding:0;"></td></tr>
     </thead>
     <tbody>
         <tr>
             <th colspan="2">
                 <em><strong><?php echo __('User Information'); ?></strong>: </em>
+                <div class="error"><?php echo $errors['user']; ?></div>
             </th>
         </tr>
         <?php
@@ -59,7 +65,7 @@ if ($_POST)
                 <span id="user-name"><?php echo Format::htmlchars($user->getName()); ?></span>
                 &lt;<span id="user-email"><?php echo $user->getEmail(); ?></span>&gt;
                 </a>
-                <a class="action-button" style="overflow:inherit" href="#"
+                <a class="inline button" style="overflow:inherit" href="#"
                     onclick="javascript:
                         $.userLookup('ajax.php/users/select/'+$('input#uid').val(),
                             function(user) {
@@ -68,7 +74,7 @@ if ($_POST)
                                 $('#user-email').text('<'+user.email+'>');
                         });
                         return false;
-                    "><i class="icon-edit"></i> <?php echo __('Change'); ?></a>
+                    "><i class="icon-retweet"></i> <?php echo __('Change'); ?></a>
             </div>
         </td></tr>
         <?php
@@ -77,10 +83,14 @@ if ($_POST)
         <tr>
             <td width="160" class="required"> <?php echo __('Email Address'); ?>: </td>
             <td>
-                <span style="display:inline-block;">
-                    <input type="text" size=45 name="email" id="user-email"
+                <div class="attached input">
+                    <input type="text" size=45 name="email" id="user-email" class="attached"
                         autocomplete="off" autocorrect="off" value="<?php echo $info['email']; ?>" /> </span>
-                <font class="error">* <?php echo $errors['email']; ?></font>
+                <a href="?a=open&amp;uid={id}" data-dialog="ajax.php/users/lookup/form"
+                    class="attached button"><i class="icon-search"></i></a>
+                </div>
+                <span class="error">*</span>
+                <div class="error"><?php echo $errors['email']; ?></div>
             </td>
         </tr>
         <tr>
@@ -88,7 +98,8 @@ if ($_POST)
             <td>
                 <span style="display:inline-block;">
                     <input type="text" size=45 name="name" id="user-name" value="<?php echo $info['name']; ?>" /> </span>
-                <font class="error">* <?php echo $errors['name']; ?></font>
+                <span class="error">*</span>
+                <div class="error"><?php echo $errors['name']; ?></div>
             </td>
         </tr>
         <?php
@@ -118,9 +129,16 @@ if ($_POST)
             </td>
             <td>
                 <select name="source">
-                    <option value="Phone" selected="selected"><?php echo __('Phone'); ?></option>
-                    <option value="Email" <?php echo ($info['source']=='Email')?'selected="selected"':''; ?>><?php echo __('Email'); ?></option>
-                    <option value="Other" <?php echo ($info['source']=='Other')?'selected="selected"':''; ?>><?php echo __('Other'); ?></option>
+                    <?php
+                    $source = $info['source'] ?: 'Phone';
+                    $sources = Ticket::getSources();
+                    unset($sources['Web'], $sources['API']);
+                    foreach ($sources as $k => $v)
+                        echo sprintf('<option value="%s" %s>%s</option>',
+                                $k,
+                                ($source == $k ) ? 'selected="selected"' : '',
+                                $v);
+                    ?>
                 </select>
                 &nbsp;<font class="error"><b>*</b>&nbsp;<?php echo $errors['source']; ?></font>
             </td>
@@ -143,7 +161,7 @@ if ($_POST)
                             }
                           });">
                     <?php
-                    if ($topics=Topic::getHelpTopics()) {
+                    if ($topics=Topic::getHelpTopics(false, false, true)) {
                         if (count($topics) == 1)
                             $selected = 'selected="selected"';
                         else { ?>
@@ -154,9 +172,9 @@ if ($_POST)
                                 $id, ($info['topicId']==$id)?'selected="selected"':'',
                                 $selected, $name);
                         }
-                        if (count($topics) == 1 && !$form) {
+                        if (count($topics) == 1 && !$forms) {
                             if (($T = Topic::lookup($id)))
-                                $form =  $T->getForm();
+                                $forms =  $T->getForms();
                         }
                     }
                     ?>
@@ -172,8 +190,14 @@ if ($_POST)
                 <select name="deptId">
                     <option value="" selected >&mdash; <?php echo __('Select Department'); ?>&mdash;</option>
                     <?php
-                    if($depts=Dept::getDepartments()) {
+                    if($depts=Dept::getDepartments(array('dept_id' => $thisstaff->getDepts()))) {
                         foreach($depts as $id =>$name) {
+                            if (!($role = $thisstaff->getRole($id))
+                                || !$role->hasPerm(Ticket::PERM_CREATE)
+                            ) {
+                                // No access to create tickets in this dept
+                                continue;
+                            }
                             echo sprintf('<option value="%d" %s>%s</option>',
                                     $id, ($info['deptId']==$id)?'selected="selected"':'',$name);
                         }
@@ -219,12 +243,12 @@ if ($_POST)
                 echo Misc::timeDropdown($hr, $min, 'time');
                 ?>
                 &nbsp;<font class="error">&nbsp;<?php echo $errors['duedate']; ?> &nbsp; <?php echo $errors['time']; ?></font>
-                <em><?php echo __('Time is based on your time zone');?> (GMT <?php echo $thisstaff->getTZoffset(); ?>)</em>
+                <em><?php echo __('Time is based on your time zone');?> (GMT <?php echo Format::date(false, false, 'ZZZ'); ?>)</em>
             </td>
         </tr>
 
         <?php
-        if($thisstaff->canAssignTickets()) { ?>
+        if($thisstaff->hasPerm(TicketModel::PERM_ASSIGN, false)) { ?>
         <tr>
             <td width="160"><?php echo __('Assign To');?>:</td>
             <td>
@@ -258,23 +282,16 @@ if ($_POST)
         </tbody>
         <tbody id="dynamic-form">
         <?php
-            if ($form) {
+            foreach ($forms as $form) {
                 print $form->getForm()->getMedia();
                 include(STAFFINC_DIR .  'templates/dynamic-form.tmpl.php');
             }
         ?>
         </tbody>
-        <tbody> <?php
-        $tform = TicketForm::getInstance();
-        if ($_POST && !$tform->errors())
-            $tform->isValidForStaff();
-        $tform->render(true);
-        ?>
-        </tbody>
         <tbody>
         <?php
         //is the user allowed to post replies??
-        if($thisstaff->canPostReply()) { ?>
+        if ($thisstaff->getRole()->hasPerm(TicketModel::PERM_REPLY)) { ?>
         <tr>
             <th colspan="2">
                 <em><strong><?php echo __('Response');?></strong>: <?php echo __('Optional response to the above issue.');?></em>
@@ -295,22 +312,25 @@ if ($_POST)
                         }
                         ?>
                     </select>
-                    &nbsp;&nbsp;&nbsp;
-                    <label><input type='checkbox' value='1' name="append" id="append" checked="checked"><?php echo __('Append');?></label>
+                    &nbsp;&nbsp;
+                    <label class="checkbox inline"><input type='checkbox' value='1' name="append" id="append" checked="checked"><?php echo __('Append');?></label>
                 </div>
             <?php
             }
                 $signature = '';
                 if ($thisstaff->getDefaultSignatureType() == 'mine')
                     $signature = $thisstaff->getSignature(); ?>
-                <textarea class="richtext ifhtml draft draft-delete"
-                    data-draft-namespace="ticket.staff.response"
-                    data-signature="<?php
+                <textarea
+                    class="<?php if ($cfg->isRichTextEnabled()) echo 'richtext';
+                        ?> draft draft-delete" data-signature="<?php
                         echo Format::htmlchars(Format::viewableImages($signature)); ?>"
                     data-signature-field="signature" data-dept-field="deptId"
                     placeholder="<?php echo __('Initial response for the ticket'); ?>"
                     name="response" id="response" cols="21" rows="8"
-                    style="width:80%;"><?php echo $info['response']; ?></textarea>
+                    style="width:80%;" <?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('ticket.staff.response', false, $info['response']);
+    echo $attrs; ?>><?php echo $_POST ? $info['response'] : $draft;
+                ?></textarea>
                     <div class="attachments">
 <?php
 print $response_form->getField('attachments')->render();
@@ -325,7 +345,7 @@ print $response_form->getField('attachments')->render();
                     <?php
                     $statusId = $info['statusId'] ?: $cfg->getDefaultTicketStatusId();
                     $states = array('open');
-                    if ($thisstaff->canCloseTickets())
+                    if ($thisstaff->hasPerm(TicketModel::PERM_CLOSE, false))
                         $states = array_merge($states, array('closed'));
                     foreach (TicketStatusList::getStatuses(
                                 array('states' => $states)) as $s) {
@@ -372,11 +392,14 @@ print $response_form->getField('attachments')->render();
         </tr>
         <tr>
             <td colspan=2>
-                <textarea class="richtext ifhtml draft draft-delete"
+                <textarea
+                    class="<?php if ($cfg->isRichTextEnabled()) echo 'richtext';
+                        ?> draft draft-delete"
                     placeholder="<?php echo __('Optional internal note (recommended on assignment)'); ?>"
-                    data-draft-namespace="ticket.staff.note" name="note"
-                    cols="21" rows="6" style="width:80%;"
-                    ><?php echo $info['note']; ?></textarea>
+                    name="note" cols="21" rows="6" style="width:80%;" <?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('ticket.staff.note', false, $info['note']);
+    echo $attrs; ?>><?php echo $_POST ? $info['note'] : $draft;
+                ?></textarea>
             </td>
         </tr>
     </tbody>
diff --git a/include/staff/ticket-tasks.inc.php b/include/staff/ticket-tasks.inc.php
new file mode 100644
index 0000000000000000000000000000000000000000..a260f0f05b07e2427b973ccfd68f44ea9ce0e30c
--- /dev/null
+++ b/include/staff/ticket-tasks.inc.php
@@ -0,0 +1,200 @@
+<?php
+global $thisstaff;
+
+$role = $thisstaff->getRole($ticket->getDeptId());
+
+$tasks = Task::objects()
+    ->select_related('dept', 'staff', 'team')
+    ->order_by('-created');
+
+$tasks->filter(array(
+            'object_id' => $ticket->getId(),
+            'object_type' => 'T'));
+
+$count = $tasks->count();
+$pageNav = new Pagenate($count,1, 100000); //TODO: support ajax based pages
+$showing = $pageNav->showing().' '._N('task', 'tasks', $count);
+
+?>
+<div id="tasks_content" style="display:block;">
+<div class="pull-left">
+   <?php
+    if ($count) {
+        echo '<strong>'.$showing.'</strong>';
+    } else {
+        echo sprintf(__('%s does not have any tasks'), $ticket? 'This ticket' :
+                'System');
+    }
+   ?>
+</div>
+<div class="pull-right">
+    <?php
+    if ($role && $role->hasPerm(Task::PERM_CREATE)) { ?>
+        <a
+        class="green button action-button ticket-task-action"
+        data-url="tickets.php?id=<?php echo $ticket->getId(); ?>#tasks"
+        data-dialog-config='{"size":"large"}'
+        href="#tickets/<?php
+            echo $ticket->getId(); ?>/add-task">
+            <i class="icon-plus-sign"></i> <?php
+            print __('Add New Task'); ?></a>
+    <?php
+    }
+    if ($count)
+        Task::getAgentActions($thisstaff, array(
+                    'container' => '#tasks_content',
+                    'callback_url' => sprintf('ajax.php/tickets/%d/tasks',
+                        $ticket->getId()),
+                    'morelabel' => __('Options')));
+    ?>
+</div>
+<div class="clear"></div>
+<div>
+<?php
+if ($count) { ?>
+<form action="#tickets/<?php echo $ticket->getId(); ?>/tasks" method="POST"
+    name='tasks' id="tasks" style="padding-top:7px;">
+<?php csrf_token(); ?>
+ <input type="hidden" name="a" value="mass_process" >
+ <input type="hidden" name="do" id="action" value="" >
+ <table class="list" border="0" cellspacing="1" cellpadding="2" width="940">
+    <thead>
+        <tr>
+            <?php
+            if (1) {?>
+            <th width="8px">&nbsp;</th>
+            <?php
+            } ?>
+            <th width="70"><?php echo __('Number'); ?></th>
+            <th width="100"><?php echo __('Date'); ?></th>
+            <th width="100"><?php echo __('Status'); ?></th>
+            <th width="300"><?php echo __('Title'); ?></th>
+            <th width="200"><?php echo __('Department'); ?></th>
+            <th width="200"><?php echo __('Assignee'); ?></th>
+        </tr>
+    </thead>
+    <tbody class="tasks">
+    <?php
+    foreach($tasks as $task) {
+        $id = $task->getId();
+        $access = $task->checkStaffPerm($thisstaff);
+        $assigned='';
+        if ($task->staff)
+            $assigned=sprintf('<span class="Icon staffAssigned">%s</span>',
+                    Format::truncate($task->staff->getName(),40));
+
+        $status = $task->isOpen() ? '<strong>open</strong>': 'closed';
+
+        $title = Format::htmlchars(Format::truncate($task->getTitle(),40));
+        $threadcount = $task->getThread() ?
+            $task->getThread()->getNumEntries() : 0;
+
+        if ($access)
+            $viewhref = sprintf('#tickets/%d/tasks/%d/view', $ticket->getId(), $id);
+        else
+            $viewhref = '#';
+
+        ?>
+        <tr id="<?php echo $id; ?>">
+            <td align="center" class="nohover">
+                <input class="ckb" type="checkbox" name="tids[]"
+                value="<?php echo $id; ?>" <?php echo $sel?'checked="checked"':''; ?>>
+            </td>
+            <td align="center" nowrap>
+              <a class="Icon no-pjax preview"
+                title="<?php echo __('Preview Task'); ?>"
+                href="<?php echo $viewhref; ?>"
+                data-preview="#tasks/<?php echo $id; ?>/preview"
+                ><?php echo $task->getNumber(); ?></a></td>
+            <td align="center" nowrap><?php echo
+            Format::datetime($task->created); ?></td>
+            <td><?php echo $status; ?></td>
+            <td>
+                <?php
+                if ($access) { ?>
+                    <a <?php if ($flag) { ?> class="no-pjax"
+                        title="<?php echo ucfirst($flag); ?> Task" <?php } ?>
+                        href="<?php echo $viewhref; ?>"><?php
+                    echo $title; ?></a>
+                 <?php
+                } else {
+                     echo $title;
+                }
+                    if ($threadcount>1)
+                        echo "<small>($threadcount)</small>&nbsp;".'<i
+                            class="icon-fixed-width icon-comments-alt"></i>&nbsp;';
+                    if ($row['collaborators'])
+                        echo '<i class="icon-fixed-width icon-group faded"></i>&nbsp;';
+                    if ($row['attachments'])
+                        echo '<i class="icon-fixed-width icon-paperclip"></i>&nbsp;';
+                ?>
+            </td>
+            <td><?php echo Format::truncate($task->dept->getName(), 40); ?></td>
+            <td>&nbsp;<?php echo $assigned; ?></td>
+        </tr>
+   <?php
+    }
+    ?>
+    </tbody>
+</table>
+</form>
+<?php
+ } ?>
+</div>
+</div>
+<div id="task_content" style="display:none;">
+</div>
+<script type="text/javascript">
+$(function() {
+
+    $(document).off('click.taskv');
+    $(document).on('click.taskv', 'tbody.tasks a, a#reload-task', function(e) {
+        e.preventDefault();
+        e.stopImmediatePropagation();
+        if ($(this).attr('href').length > 1) {
+            var url = 'ajax.php/'+$(this).attr('href').substr(1);
+            var $container = $('div#task_content');
+            var $stop = $('ul#ticket_tabs').offset().top;
+            $.pjax({url: url, container: $container, push: false, scrollTo: $stop})
+            .done(
+                function() {
+                $container.show();
+                $('.tip_box').remove();
+                $('div#tasks_content').hide();
+                });
+        } else {
+            $(this).trigger('mouseenter');
+        }
+
+        return false;
+     });
+    // Ticket Tasks
+    $(document).off('.ticket-task-action');
+    $(document).on('click.ticket-task-action', 'a.ticket-task-action', function(e) {
+        e.preventDefault();
+        var url = 'ajax.php/'
+        +$(this).attr('href').substr(1)
+        +'?_uid='+new Date().getTime();
+        var $redirect = $(this).data('href');
+        var $options = $(this).data('dialogConfig');
+        $.dialog(url, [201], function (xhr) {
+            var tid = parseInt(xhr.responseText);
+            if (tid) {
+                var url = 'ajax.php/tickets/'+<?php echo $ticket->getId();
+                ?>+'/tasks';
+                var $container = $('div#task_content');
+                $container.load(url+'/'+tid+'/view', function () {
+                    $('.tip_box').remove();
+                    $('div#tasks_content').hide();
+                    $.pjax({url: url, container: '#tasks_content', push: false});
+                }).show();
+            } else {
+                window.location.href = $redirect ? $redirect : window.location.href;
+            }
+        }, $options);
+        return false;
+    });
+
+    $('#ticket-tasks-count').html(<?php echo $count; ?>);
+});
+</script>
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index 138b0af4ccc87f68f69118bd67c091f3b1337bf2..782151d32172f9abd5245f0ee2db4513d2c7e7a3 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -3,22 +3,22 @@
 if(!defined('OSTSCPINC') || !$thisstaff || !is_object($ticket) || !$ticket->getId()) die('Invalid path');
 
 //Make sure the staff is allowed to access the page.
-if(!@$thisstaff->isStaff() || !$ticket->checkStaffAccess($thisstaff)) die('Access Denied');
+if(!@$thisstaff->isStaff() || !$ticket->checkStaffPerm($thisstaff)) die('Access Denied');
 
 //Re-use the post info on error...savekeyboards.org (Why keyboard? -> some people care about objects than users!!)
 $info=($_POST && $errors)?Format::input($_POST):array();
 
-//Auto-lock the ticket if locking is enabled.. If already locked by the user then it simply renews.
-if($cfg->getLockTime() && !$ticket->acquireLock($thisstaff->getId(),$cfg->getLockTime()))
-    $warn.=__('Unable to obtain a lock on the ticket');
-
 //Get the goodies.
 $dept  = $ticket->getDept();  //Dept
+$role  = $thisstaff->getRole($dept);
 $staff = $ticket->getStaff(); //Assigned or closed by..
 $user  = $ticket->getOwner(); //Ticket User (EndUser)
 $team  = $ticket->getTeam();  //Assigned team.
 $sla   = $ticket->getSLA();
 $lock  = $ticket->getLock();  //Ticket lock obj
+if (!$lock && $cfg->getTicketLockMode() == Lock::MODE_ON_VIEW)
+    $lock = $ticket->acquireLock($thisstaff->getId());
+$mylock = ($lock && $lock->getStaffId() == $thisstaff->getId()) ? $lock : null;
 $id    = $ticket->getId();    //Ticket ID.
 
 //Useful warnings and errors the user might want to know!
@@ -40,7 +40,7 @@ if (!$errors['err']) {
     if ($lock && $lock->getStaffId()!=$thisstaff->getId())
         $errors['err'] = sprintf(__('This ticket is currently locked by %s'),
                 $lock->getStaffName());
-    elseif (($emailBanned=TicketFilter::isBanned($ticket->getEmail())))
+    elseif (($emailBanned=Banlist::isBanned($ticket->getEmail())))
         $errors['err'] = __('Email is in banlist! Must be removed before any reply/response');
     elseif (!Validator::is_valid_email($ticket->getEmail()))
         $errors['err'] = __('EndUser email address is not valid! Consider updating it before responding');
@@ -52,45 +52,28 @@ if($ticket->isOverdue())
     $warn.='&nbsp;&nbsp;<span class="Icon overdueTicket">'.__('Marked overdue!').'</span>';
 
 ?>
-<table width="940" cellpadding="2" cellspacing="0" border="0">
-    <tr>
-        <td width="20%" class="has_bottom_border">
-             <h2><a href="tickets.php?id=<?php echo $ticket->getId(); ?>"
-             title="<?php echo __('Reload'); ?>"><i class="icon-refresh"></i>
-             <?php echo sprintf(__('Ticket #%s'), $ticket->getNumber()); ?></a></h2>
-        </td>
-        <td width="auto" class="flush-right has_bottom_border">
+<div>
+    <div class="sticky bar">
+       <div class="content">
+        <div class="pull-right flush-right">
             <?php
-            if ($thisstaff->canBanEmails()
-                    || $thisstaff->canEditTickets()
+            if ($thisstaff->hasPerm(Email::PERM_BANLIST)
+                    || $role->hasPerm(TicketModel::PERM_EDIT)
                     || ($dept && $dept->isManager($thisstaff))) { ?>
-            <span class="action-button pull-right" data-dropdown="#action-dropdown-more">
+            <span class="action-button pull-right" data-placement="bottom" data-dropdown="#action-dropdown-more" data-toggle="tooltip" title="<?php echo __('More');?>">
                 <i class="icon-caret-down pull-right"></i>
-                <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+                <span ><i class="icon-cog"></i></span>
             </span>
             <?php
             }
-            // Status change options
-            echo TicketStatus::status_options();
-
-            if ($thisstaff->canEditTickets()) { ?>
-                <a class="action-button pull-right" href="tickets.php?id=<?php echo $ticket->getId(); ?>&a=edit"><i class="icon-edit"></i> <?php
-                    echo __('Edit'); ?></a>
-            <?php
-            }
-            if ($ticket->isOpen()
-                    && !$ticket->isAssigned()
-                    && $thisstaff->canAssignTickets()
-                    && $ticket->getDept()->isMember($thisstaff)) {?>
-                <a id="ticket-claim" class="action-button pull-right confirm-action" href="#claim"><i class="icon-user"></i> <?php
-                    echo __('Claim'); ?></a>
 
+            if ($role->hasPerm(TicketModel::PERM_EDIT)) { ?>
+                <span class="action-button pull-right"><a data-placement="bottom" data-toggle="tooltip" title="<?php echo __('Edit'); ?>" href="tickets.php?id=<?php echo $ticket->getId(); ?>&a=edit"><i class="icon-edit"></i></a></span>
             <?php
-            }?>
-            <span class="action-button pull-right" data-dropdown="#action-dropdown-print">
+            } ?>
+            <span class="action-button pull-right" data-placement="bottom" data-dropdown="#action-dropdown-print" data-toggle="tooltip" title="<?php echo __('Print'); ?>">
                 <i class="icon-caret-down pull-right"></i>
-                <a id="ticket-print" href="tickets.php?id=<?php echo $ticket->getId(); ?>&a=print"><i class="icon-print"></i> <?php
-                    echo __('Print'); ?></a>
+                <a id="ticket-print" href="tickets.php?id=<?php echo $ticket->getId(); ?>&a=print"><i class="icon-print"></i></a>
             </span>
             <div id="action-dropdown-print" class="action-dropdown anchor-right">
               <ul>
@@ -100,24 +83,68 @@ if($ticket->isOverdue())
                  class="icon-file-text-alt"></i> <?php echo __('Thread + Internal Notes'); ?></a>
               </ul>
             </div>
+            <?php
+            // Transfer
+            if ($role->hasPerm(TicketModel::PERM_TRANSFER)) {?>
+            <span class="action-button pull-right">
+            <a class="ticket-action" id="ticket-transfer" data-placement="bottom" data-toggle="tooltip" title="<?php echo __('Transfer'); ?>"
+                data-redirect="tickets.php"
+                href="#tickets/<?php echo $ticket->getId(); ?>/transfer"><i class="icon-share"></i></a>
+            </span>
+            <?php
+            } ?>
+
+            <?php
+            // Assign
+            if ($ticket->isOpen() && $role->hasPerm(TicketModel::PERM_ASSIGN)) {?>
+            <span class="action-button pull-right"
+                data-dropdown="#action-dropdown-assign"
+                data-placement="bottom"
+                data-toggle="tooltip"
+                title=" <?php echo $ticket->isAssigned() ? __('Assign') : __('Reassign'); ?>"
+                >
+                <i class="icon-caret-down pull-right"></i>
+                <a class="ticket-action" id="ticket-assign"
+                    data-redirect="tickets.php"
+                    href="#tickets/<?php echo $ticket->getId(); ?>/assign"><i class="icon-user"></i></a>
+            </span>
+            <div id="action-dropdown-assign" class="action-dropdown anchor-right">
+              <ul>
+                <?php
+                // Agent can claim team assigned ticket
+                if (!$ticket->getStaff()
+                        && (!$dept->assignMembersOnly()
+                            || $dept->isMember($thisstaff))
+                        ) { ?>
+                 <li><a class="no-pjax ticket-action"
+                    data-redirect="tickets.php"
+                    href="#tickets/<?php echo $ticket->getId(); ?>/claim"><i
+                    class="icon-chevron-sign-down"></i> <?php echo __('Claim'); ?></a>
+                <?php
+                } ?>
+                 <li><a class="no-pjax ticket-action"
+                    data-redirect="tickets.php"
+                    href="#tickets/<?php echo $ticket->getId(); ?>/assign/agents"><i
+                    class="icon-user"></i> <?php echo __('Agent'); ?></a>
+                 <li><a class="no-pjax ticket-action"
+                    data-redirect="tickets.php"
+                    href="#tickets/<?php echo $ticket->getId(); ?>/assign/teams"><i
+                    class="icon-group"></i> <?php echo __('Team'); ?></a>
+              </ul>
+            </div>
+            <?php
+            } ?>
             <div id="action-dropdown-more" class="action-dropdown anchor-right">
               <ul>
                 <?php
-                 if($thisstaff->canEditTickets()) { ?>
+                 if ($role->hasPerm(TicketModel::PERM_EDIT)) { ?>
                     <li><a class="change-user" href="#tickets/<?php
                     echo $ticket->getId(); ?>/change-user"><i class="icon-user"></i> <?php
                     echo __('Change Owner'); ?></a></li>
                 <?php
                  }
-                 if($thisstaff->canDeleteTickets()) {
-                     ?>
-                    <li><a class="ticket-action" href="#tickets/<?php
-                    echo $ticket->getId(); ?>/status/delete"
-                    data-href="tickets.php"><i class="icon-trash"></i> <?php
-                    echo __('Delete Ticket'); ?></a></li>
-                <?php
-                 }
-                if($ticket->isOpen() && ($dept && $dept->isManager($thisstaff))) {
+
+                 if($ticket->isOpen() && ($dept && $dept->isManager($thisstaff))) {
 
                     if($ticket->isAssigned()) { ?>
                         <li><a  class="confirm-action" id="ticket-release" href="#release"><i class="icon-user"></i> <?php
@@ -141,13 +168,17 @@ if($ticket->isOverdue())
                     <?php
                     }
                 } ?>
+                <?php
+                if ($role->hasPerm(Ticket::PERM_EDIT)) { ?>
                 <li><a href="#ajax.php/tickets/<?php echo $ticket->getId();
                     ?>/forms/manage" onclick="javascript:
                     $.dialog($(this).attr('href').substr(1), 201);
                     return false"
                     ><i class="icon-paste"></i> <?php echo __('Manage Forms'); ?></a></li>
+                <?php
+                } ?>
 
-<?php           if($thisstaff->canBanEmails()) {
+<?php           if ($thisstaff->hasPerm(Email::PERM_BANLIST)) {
                      if(!$emailBanned) {?>
                         <li><a class="confirm-action" id="ticket-banemail"
                             href="#banemail"><i class="icon-ban-circle"></i> <?php echo sprintf(
@@ -161,19 +192,54 @@ if($ticket->isOverdue())
                                 $ticket->getEmail()); ?></a></li>
                     <?php
                      }
-                }?>
+                  }
+                  if ($role->hasPerm(TicketModel::PERM_DELETE)) {
+                     ?>
+                    <li class="danger"><a class="ticket-action" href="#tickets/<?php
+                    echo $ticket->getId(); ?>/status/delete"
+                    data-redirect="tickets.php"><i class="icon-trash"></i> <?php
+                    echo __('Delete Ticket'); ?></a></li>
+                <?php
+                 }
+                ?>
               </ul>
             </div>
-        </td>
-    </tr>
-</table>
+                <?php
+                if ($role->hasPerm(TicketModel::PERM_REPLY)) { ?>
+                <a href="#post-reply" class="post-response action-button"
+                data-placement="bottom" data-toggle="tooltip"
+                title="<?php echo __('Post Reply'); ?>"><i class="icon-mail-reply"></i></a>
+                <?php
+                } ?>
+                <a href="#post-note" id="post-note" class="post-response action-button"
+                data-placement="bottom" data-toggle="tooltip"
+                title="<?php echo __('Post Internal Note'); ?>"><i class="icon-file-text"></i></a>
+                <?php // Status change options
+                echo TicketStatus::status_options();
+                ?>
+           </div>
+        <div class="flush-left">
+             <h2><a href="tickets.php?id=<?php echo $ticket->getId(); ?>"
+             title="<?php echo __('Reload'); ?>"><i class="icon-refresh"></i>
+             <?php echo sprintf(__('Ticket #%s'), $ticket->getNumber()); ?></a>
+            </h2>
+        </div>
+    </div>
+  </div>
+</div>
+<div class="clear tixTitle has_bottom_border">
+    <h3>
+    <?php $subject_field = TicketForm::getInstance()->getField('subject');
+        echo $subject_field->display($ticket->getSubject()); ?>
+    </h3>
+</div>
 <table class="ticket_info" cellspacing="0" cellpadding="0" width="940" border="0">
     <tr>
         <td width="50%">
             <table border="0" cellspacing="" cellpadding="4" width="100%">
                 <tr>
                     <th width="100"><?php echo __('Status');?>:</th>
-                    <td><?php echo $ticket->getStatus(); ?></td>
+                    <td><?php echo ($S = $ticket->getStatus()) ? $S->getLocalName() : ''; ?></td>
                 </tr>
                 <tr>
                     <th><?php echo __('Priority');?>:</th>
@@ -185,7 +251,7 @@ if($ticket->isOverdue())
                 </tr>
                 <tr>
                     <th><?php echo __('Create Date');?>:</th>
-                    <td><?php echo Format::db_datetime($ticket->getCreateDate()); ?></td>
+                    <td><?php echo Format::datetime($ticket->getCreateDate()); ?></td>
                 </tr>
             </table>
         </td>
@@ -207,10 +273,13 @@ if($ticket->isOverdue())
                             ><?php echo Format::htmlchars($ticket->getName());
                         ?></span></a>
                         <?php
-                        if($user) {
-                            echo sprintf('&nbsp;&nbsp;<a href="tickets.php?a=search&uid=%d" title="%s" data-dropdown="#action-dropdown-stats">(<b>%d</b>)</a>',
-                                    urlencode($user->getId()), __('Related Tickets'), $user->getNumTickets());
-                        ?>
+                        if ($user) { ?>
+                            <a href="tickets.php?<?php echo Http::build_query(array(
+                                'status'=>'open', 'a'=>'search', 'uid'=> $user->getId()
+                            )); ?>" title="<?php echo __('Related Tickets'); ?>"
+                            data-dropdown="#action-dropdown-stats">
+                            (<b><?php echo $user->getNumTickets(); ?></b>)
+                            </a>
                             <div id="action-dropdown-stats" class="action-dropdown anchor-right">
                                 <ul>
                                     <?php
@@ -224,19 +293,14 @@ if($ticket->isOverdue())
                                                 $user->getId(), sprintf(_N('%d Closed Ticket', '%d Closed Tickets', $closed), $closed));
                                     ?>
                                     <li><a href="tickets.php?a=search&uid=<?php echo $ticket->getOwnerId(); ?>"><i class="icon-double-angle-right icon-fixed-width"></i> <?php echo __('All Tickets'); ?></a></li>
+<?php   if ($thisstaff->hasPerm(User::PERM_DIRECTORY)) { ?>
                                     <li><a href="users.php?id=<?php echo
                                     $user->getId(); ?>"><i class="icon-user
                                     icon-fixed-width"></i> <?php echo __('Manage User'); ?></a></li>
-<?php if ($user->getOrgId()) { ?>
-                                    <li><a href="orgs.php?id=<?php echo $user->getOrgId(); ?>"><i
-                                        class="icon-building icon-fixed-width"></i> <?php
-                                        echo __('Manage Organization'); ?></a></li>
-<?php } ?>
+<?php   } ?>
                                 </ul>
                             </div>
-                    <?php
-                        }
-                    ?>
+<?php                   } # end if ($user) ?>
                     </td>
                 </tr>
                 <tr>
@@ -245,18 +309,52 @@ if($ticket->isOverdue())
                         <span id="user-<?php echo $ticket->getOwnerId(); ?>-email"><?php echo $ticket->getEmail(); ?></span>
                     </td>
                 </tr>
+<?php   if ($user->getOrgId()) { ?>
                 <tr>
-                    <th><?php echo __('Phone'); ?>:</th>
-                    <td>
-                        <span id="user-<?php echo $ticket->getOwnerId(); ?>-phone"><?php echo $ticket->getPhoneNumber(); ?></span>
-                    </td>
-                </tr>
+                    <th><?php echo __('Organization'); ?>:</th>
+                    <td><i class="icon-building"></i>
+                    <?php echo Format::htmlchars($user->getOrganization()->getName()); ?>
+                        <a href="tickets.php?<?php echo Http::build_query(array(
+                            'status'=>'open', 'a'=>'search', 'orgid'=> $user->getOrgId()
+                        )); ?>" title="<?php echo __('Related Tickets'); ?>"
+                        data-dropdown="#action-dropdown-org-stats">
+                        (<b><?php echo $user->getNumOrganizationTickets(); ?></b>)
+                        </a>
+                            <div id="action-dropdown-org-stats" class="action-dropdown anchor-right">
+                                <ul>
+<?php   if ($open = $user->getNumOpenOrganizationTickets()) { ?>
+                                    <li><a href="tickets.php?<?php echo Http::build_query(array(
+                                        'a' => 'search', 'status' => 'open', 'orgid' => $user->getOrgId()
+                                    )); ?>"><i class="icon-folder-open-alt icon-fixed-width"></i>
+                                    <?php echo sprintf(_N('%d Open Ticket', '%d Open Tickets', $open), $open); ?>
+                                    </a></li>
+<?php   }
+        if ($closed = $user->getNumClosedOrganizationTickets()) { ?>
+                                    <li><a href="tickets.php?<?php echo Http::build_query(array(
+                                        'a' => 'search', 'status' => 'closed', 'orgid' => $user->getOrgId()
+                                    )); ?>"><i class="icon-folder-close-alt icon-fixed-width"></i>
+                                    <?php echo sprintf(_N('%d Closed Ticket', '%d Closed Tickets', $closed), $closed); ?>
+                                    </a></li>
+                                    <li><a href="tickets.php?<?php echo Http::build_query(array(
+                                        'a' => 'search', 'orgid' => $user->getOrgId()
+                                    )); ?>"><i class="icon-double-angle-right icon-fixed-width"></i> <?php echo __('All Tickets'); ?></a></li>
+<?php   }
+        if ($thisstaff->hasPerm(User::PERM_DIRECTORY)) { ?>
+                                    <li><a href="orgs.php?id=<?php echo $user->getOrgId(); ?>"><i
+                                        class="icon-building icon-fixed-width"></i> <?php
+                                        echo __('Manage Organization'); ?></a></li>
+<?php   } ?>
+                                </ul>
+                            </div>
+                        </td>
+                    </tr>
+<?php   } # end if (user->org) ?>
                 <tr>
                     <th><?php echo __('Source'); ?>:</th>
                     <td><?php
                         echo Format::htmlchars($ticket->getSource());
 
-                        if($ticket->getIP())
+                        if (!strcasecmp($ticket->getSource(), 'Web') && $ticket->getIP())
                             echo '&nbsp;&nbsp; <span class="faded">('.$ticket->getIP().')</span>';
                         ?>
                     </td>
@@ -306,13 +404,13 @@ if($ticket->isOverdue())
                 if($ticket->isOpen()){ ?>
                 <tr>
                     <th><?php echo __('Due Date');?>:</th>
-                    <td><?php echo Format::db_datetime($ticket->getEstDueDate()); ?></td>
+                    <td><?php echo Format::datetime($ticket->getEstDueDate()); ?></td>
                 </tr>
                 <?php
                 }else { ?>
                 <tr>
                     <th><?php echo __('Close Date');?>:</th>
-                    <td><?php echo Format::db_datetime($ticket->getCloseDate()); ?></td>
+                    <td><?php echo Format::datetime($ticket->getCloseDate()); ?></td>
                 </tr>
                 <?php
                 }
@@ -327,155 +425,133 @@ if($ticket->isOverdue())
                 </tr>
                 <tr>
                     <th nowrap><?php echo __('Last Message');?>:</th>
-                    <td><?php echo Format::db_datetime($ticket->getLastMsgDate()); ?></td>
+                    <td><?php echo Format::datetime($ticket->getLastMsgDate()); ?></td>
                 </tr>
                 <tr>
                     <th nowrap><?php echo __('Last Response');?>:</th>
-                    <td><?php echo Format::db_datetime($ticket->getLastRespDate()); ?></td>
+                    <td><?php echo Format::datetime($ticket->getLastRespDate()); ?></td>
                 </tr>
             </table>
         </td>
     </tr>
 </table>
 <br>
-<table class="ticket_info" cellspacing="0" cellpadding="0" width="940" border="0">
 <?php
-$idx = 0;
 foreach (DynamicFormEntry::forTicket($ticket->getId()) as $form) {
     // Skip core fields shown earlier in the ticket view
     // TODO: Rewrite getAnswers() so that one could write
     //       ->getAnswers()->filter(not(array('field__name__in'=>
     //           array('email', ...))));
-    $answers = array_filter($form->getAnswers(), function ($a) {
-        return !in_array($a->getField()->get('name'),
-                array('email','subject','name','priority'));
-        });
-    if (count($answers) == 0)
+    $answers = $form->getAnswers()->exclude(Q::any(array(
+        'field__flags__hasbit' => DynamicFormField::FLAG_EXT_STORED,
+        'field__name__in' => array('subject', 'priority')
+    )));
+    $displayed = array();
+    foreach($answers as $a) {
+        if (!($v = $a->display()))
+            continue;
+        $displayed[] = array($a->getLocal('label'), $v);
+    }
+    if (count($displayed) == 0)
         continue;
     ?>
+    <table class="ticket_info custom-data" cellspacing="0" cellpadding="0" width="940" border="0">
+    <thead>
+        <th colspan="2"><?php echo Format::htmlchars($form->getTitle()); ?></th>
+    </thead>
+    <tbody>
+<?php
+    foreach ($displayed as $stuff) {
+        list($label, $v) = $stuff;
+?>
         <tr>
-        <td colspan="2">
-            <table cellspacing="0" cellpadding="4" width="100%" border="0">
-            <?php foreach($answers as $a) {
-                if (!($v = $a->display())) continue; ?>
-                <tr>
-                    <th width="100"><?php
-    echo $a->getField()->get('label');
-                    ?>:</th>
-                    <td><?php
-    echo $v;
-                    ?></td>
-                </tr>
-                <?php } ?>
-            </table>
-        </td>
+            <td width="200"><?php
+echo Format::htmlchars($label);
+            ?>:</th>
+            <td><?php
+echo $v;
+            ?></td>
         </tr>
-    <?php
-    $idx++;
-    } ?>
-</table>
+<?php } ?>
+    </tbody>
+    </table>
+<?php } ?>
 <div class="clear"></div>
-<h2 style="padding:10px 0 5px 0; font-size:11pt;"><?php echo Format::htmlchars($ticket->getSubject()); ?></h2>
+
 <?php
-$tcount = $ticket->getThreadCount();
-$tcount+= $ticket->getNumNotes();
+$tcount = $ticket->getThreadEntries($types)->count();
 ?>
-<ul id="threads">
-    <li><a class="active" id="toggle_ticket_thread" href="#"><?php echo sprintf(__('Ticket Thread (%d)'), $tcount); ?></a></li>
+<ul  class="tabs clean threads" id="ticket_tabs" >
+    <li class="active"><a id="ticket-thread-tab" href="#ticket_thread"><?php
+        echo sprintf(__('Ticket Thread (%d)'), $tcount); ?></a></li>
+    <li><a id="ticket-tasks-tab" href="#tasks"
+            data-url="<?php
+        echo sprintf('#tickets/%d/tasks', $ticket->getId()); ?>"><?php
+        echo __('Tasks');
+        if ($ticket->getNumTasks())
+            echo sprintf('&nbsp;(<span id="ticket-tasks-count">%d</span>)', $ticket->getNumTasks());
+        ?></a></li>
 </ul>
-<div id="ticket_thread">
-    <?php
-    $threadTypes=array('M'=>'message','R'=>'response', 'N'=>'note');
-    /* -------- Messages & Responses & Notes (if inline)-------------*/
-    $types = array('M', 'R', 'N');
-    if(($thread=$ticket->getThreadEntries($types))) {
-       foreach($thread as $entry) { ?>
-        <table class="thread-entry <?php echo $threadTypes[$entry['thread_type']]; ?>" cellspacing="0" cellpadding="1" width="940" border="0">
-            <tr>
-                <th colspan="4" width="100%">
-                <div>
-                    <span class="pull-left">
-                    <span style="display:inline-block"><?php
-                        echo Format::db_datetime($entry['created']);?></span>
-                    <span style="display:inline-block;padding:0 1em" class="faded title"><?php
-                        echo Format::truncate($entry['title'], 100); ?></span>
-                    </span>
-                    <span class="pull-right" style="white-space:no-wrap;display:inline-block">
-                        <span style="vertical-align:middle;" class="textra"></span>
-                        <span style="vertical-align:middle;"
-                            class="tmeta faded title"><?php
-                            echo Format::htmlchars($entry['name'] ?: $entry['poster']); ?></span>
-                    </span>
-                </div>
-                </th>
-            </tr>
-            <tr><td colspan="4" class="thread-body" id="thread-id-<?php
-                echo $entry['id']; ?>"><div><?php
-                echo $entry['body']->toHtml(); ?></div></td></tr>
-            <?php
-            if($entry['attachments']
-                    && ($tentry = $ticket->getThreadEntry($entry['id']))
-                    && ($urls = $tentry->getAttachmentUrls())
-                    && ($links = $tentry->getAttachmentsLinks())) {?>
-            <tr>
-                <td class="info" colspan="4"><?php echo $tentry->getAttachmentsLinks(); ?></td>
-            </tr> <?php
-            }
-            if ($urls) { ?>
-                <script type="text/javascript">
-                    $('#thread-id-<?php echo $entry['id']; ?>')
-                        .data('urls', <?php
-                            echo JsonDataEncoder::encode($urls); ?>)
-                        .data('id', <?php echo $entry['id']; ?>);
-                </script>
+
+<div id="ticket_tabs_container">
+<div id="ticket_thread" class="tab_content">
+
 <?php
-            } ?>
-        </table>
-        <?php
-        if($entry['thread_type']=='M')
-            $msgId=$entry['id'];
-       }
-    } else {
-        echo '<p>'.__('Error fetching ticket thread - get technical help.').'</p>';
-    }?>
-</div>
-<div class="clear" style="padding-bottom:10px;"></div>
-<?php if($errors['err']) { ?>
-    <div id="msg_error"><?php echo $errors['err']; ?></div>
-<?php }elseif($msg) { ?>
+    // Render ticket thread
+    $ticket->getThread()->render(
+            array('M', 'R', 'N'),
+            array(
+                'html-id'   => 'ticketThread',
+                'mode'      => Thread::MODE_STAFF,
+                'sort'      => $thisstaff->thread_view_order
+                )
+            );
+?>
+<div class="clear"></div>
+<?php
+if ($errors['err'] && isset($_POST['a'])) {
+    // Reflect errors back to the tab.
+    $errors[$_POST['a']] = $errors['err'];
+} elseif($msg) { ?>
     <div id="msg_notice"><?php echo $msg; ?></div>
-<?php }elseif($warn) { ?>
+<?php
+} elseif($warn) { ?>
     <div id="msg_warning"><?php echo $warn; ?></div>
-<?php } ?>
+<?php
+} ?>
 
-<div id="response_options">
-    <ul class="tabs">
+<div class="sticky bar stop actions" id="response_options"
+>
+    <ul class="tabs" id="response-tabs">
         <?php
-        if($thisstaff->canPostReply()) { ?>
-        <li><a id="reply_tab" href="#reply"><?php echo __('Post Reply');?></a></li>
-        <?php
-        } ?>
-        <li><a id="note_tab" href="#note"><?php echo __('Post Internal Note');?></a></li>
-        <?php
-        if($thisstaff->canTransferTickets()) { ?>
-        <li><a id="transfer_tab" href="#transfer"><?php echo __('Department Transfer');?></a></li>
-        <?php
-        }
-
-        if($thisstaff->canAssignTickets()) { ?>
-        <li><a id="assign_tab" href="#assign"><?php echo $ticket->isAssigned()?__('Reassign Ticket'):__('Assign Ticket'); ?></a></li>
+        if ($role->hasPerm(TicketModel::PERM_REPLY)) { ?>
+        <li class="active <?php
+            echo isset($errors['reply']) ? 'error' : ''; ?>"><a
+            href="#reply" id="post-reply-tab"><?php echo __('Post Reply');?></a></li>
         <?php
         } ?>
+        <li><a href="#note" <?php
+            echo isset($errors['postnote']) ?  'class="error"' : ''; ?>
+            id="post-note-tab"><?php echo __('Post Internal Note');?></a></li>
     </ul>
     <?php
-    if($thisstaff->canPostReply()) { ?>
-    <form id="reply" action="tickets.php?id=<?php echo $ticket->getId(); ?>#reply" name="reply" method="post" enctype="multipart/form-data">
+    if ($role->hasPerm(TicketModel::PERM_REPLY)) { ?>
+    <form id="reply" class="tab_content spellcheck exclusive"
+        data-lock-object-id="ticket/<?php echo $ticket->getId(); ?>"
+        data-lock-id="<?php echo $mylock ? $mylock->getId() : ''; ?>"
+        action="tickets.php?id=<?php
+        echo $ticket->getId(); ?>#reply" name="reply" method="post" enctype="multipart/form-data">
         <?php csrf_token(); ?>
         <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>">
         <input type="hidden" name="msgId" value="<?php echo $msgId; ?>">
         <input type="hidden" name="a" value="reply">
-        <span class="error"></span>
+        <input type="hidden" name="lockCode" value="<?php echo $mylock ? $mylock->getCode() : ''; ?>">
         <table style="width:100%" border="0" cellspacing="0" cellpadding="3">
+            <?php
+            if ($errors['reply']) {?>
+            <tr><td width="120">&nbsp;</td><td class="error"><?php echo $errors['reply']; ?>&nbsp;</td></tr>
+            <?php
+            }?>
            <tbody id="to_sec">
             <tr>
                 <td width="120">
@@ -509,18 +585,19 @@ $tcount+= $ticket->getNumNotes();
                 <td>
                     <input type='checkbox' value='1' name="emailcollab" id="emailcollab"
                         <?php echo ((!$info['emailcollab'] && !$errors) || isset($info['emailcollab']))?'checked="checked"':''; ?>
-                        style="display:<?php echo $ticket->getNumCollaborators() ? 'inline-block': 'none'; ?>;"
+                        style="display:<?php echo $ticket->getThread()->getNumCollaborators() ? 'inline-block': 'none'; ?>;"
                         >
                     <?php
                     $recipients = __('Add Recipients');
-                    if ($ticket->getNumCollaborators())
+                    if ($ticket->getThread()->getNumCollaborators())
                         $recipients = sprintf(__('Recipients (%d of %d)'),
-                                $ticket->getNumActiveCollaborators(),
-                                $ticket->getNumCollaborators());
+                                $ticket->getThread()->getNumActiveCollaborators(),
+                                $ticket->getThread()->getNumCollaborators());
 
                     echo sprintf('<span><a class="collaborators preview"
-                            href="#tickets/%d/collaborators"><span id="recipients">%s</span></a></span>',
-                            $ticket->getId(),
+                            href="#thread/%d/collaborators"><span id="t%d-recipients">%s</span></a></span>',
+                            $ticket->getThreadId(),
+                            $ticket->getThreadId(),
                             $recipients);
                    ?>
                 </td>
@@ -567,21 +644,22 @@ $tcount+= $ticket->getNumNotes();
                     } ?>
                     <input type="hidden" name="draft_id" value=""/>
                     <textarea name="response" id="response" cols="50"
-                        data-draft-namespace="ticket.response"
                         data-signature-field="signature" data-dept-id="<?php echo $dept->getId(); ?>"
                         data-signature="<?php
                             echo Format::htmlchars(Format::viewableImages($signature)); ?>"
                         placeholder="<?php echo __(
                         'Start writing your response here. Use canned responses from the drop-down above'
                         ); ?>"
-                        data-draft-object-id="<?php echo $ticket->getId(); ?>"
                         rows="9" wrap="soft"
-                        class="richtext ifhtml draft draft-delete"><?php
-                        echo $info['response']; ?></textarea>
+                        class="<?php if ($cfg->isRichTextEnabled()) echo 'richtext';
+                            ?> draft draft-delete" <?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('ticket.response', $ticket->getId(), $info['response']);
+    echo $attrs; ?>><?php echo $_POST ? $info['response'] : $draft;
+                    ?></textarea>
                 <div id="reply_form_attachments" class="attachments">
-<?php
-print $response_form->getField('attachments')->render();
-?>
+                <?php
+                    print $response_form->getField('attachments')->render();
+                ?>
                 </div>
                 </td>
             </tr>
@@ -610,15 +688,22 @@ print $response_form->getField('attachments')->render();
                 </td>
             </tr>
             <tr>
-                <td width="120">
+                <td width="120" style="vertical-align:top">
                     <label><strong><?php echo __('Ticket Status');?>:</strong></label>
                 </td>
                 <td>
+                    <?php
+                    $outstanding = false;
+                    if ($role->hasPerm(TicketModel::PERM_CLOSE)
+                            && is_string($warning=$ticket->isCloseable())) {
+                        $outstanding =  true;
+                        echo sprintf('<div class="warning-banner">%s</div>', $warning);
+                    } ?>
                     <select name="reply_status_id">
                     <?php
                     $statusId = $info['reply_status_id'] ?: $ticket->getStatusId();
                     $states = array('open');
-                    if ($thisstaff->canCloseTickets())
+                    if ($role->hasPerm(TicketModel::PERM_CLOSE) && !$outstanding)
                         $states = array_merge($states, array('closed'));
 
                     foreach (TicketStatusList::getStatuses(
@@ -640,18 +725,23 @@ print $response_form->getField('attachments')->render();
             </tr>
          </tbody>
         </table>
-        <p  style="padding:0 165px;">
-            <input class="btn_sm" type="submit" value="<?php echo __('Post Reply');?>">
-            <input class="btn_sm" type="reset" value="<?php echo __('Reset');?>">
+        <p  style="text-align:center;">
+            <input class="save pending" type="submit" value="<?php echo __('Post Reply');?>">
+            <input class="" type="reset" value="<?php echo __('Reset');?>">
         </p>
     </form>
     <?php
     } ?>
-    <form id="note" action="tickets.php?id=<?php echo $ticket->getId(); ?>#note" name="note" method="post" enctype="multipart/form-data">
+    <form id="note" class="hidden tab_content spellcheck exclusive"
+        data-lock-object-id="ticket/<?php echo $ticket->getId(); ?>"
+        data-lock-id="<?php echo $mylock ? $mylock->getId() : ''; ?>"
+        action="tickets.php?id=<?php echo $ticket->getId(); ?>#note"
+        name="note" method="post" enctype="multipart/form-data">
         <?php csrf_token(); ?>
         <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>">
-        <input type="hidden" name="locktime" value="<?php echo $cfg->getLockTime(); ?>">
+        <input type="hidden" name="locktime" value="<?php echo $cfg->getLockTime() * 60; ?>">
         <input type="hidden" name="a" value="postnote">
+        <input type="hidden" name="lockCode" value="<?php echo $mylock ? $mylock->getCode() : ''; ?>">
         <table width="100%" border="0" cellspacing="0" cellpadding="3">
             <?php
             if($errors['postnote']) {?>
@@ -677,14 +767,16 @@ print $response_form->getField('attachments')->render();
                     <div class="error"><?php echo $errors['note']; ?></div>
                     <textarea name="note" id="internal_note" cols="80"
                         placeholder="<?php echo __('Note details'); ?>"
-                        rows="9" wrap="soft" data-draft-namespace="ticket.note"
-                        data-draft-object-id="<?php echo $ticket->getId(); ?>"
-                        class="richtext ifhtml draft draft-delete"><?php echo $info['note'];
+                        rows="9" wrap="soft"
+                        class="<?php if ($cfg->isRichTextEnabled()) echo 'richtext';
+                            ?> draft draft-delete" <?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('ticket.note', $ticket->getId(), $info['note']);
+    echo $attrs; ?>><?php echo $_POST ? $info['note'] : $draft;
                         ?></textarea>
                 <div class="attachments">
-<?php
-print $note_form->getField('attachments')->render();
-?>
+                <?php
+                    print $note_form->getField('attachments')->render();
+                ?>
                 </div>
                 </td>
             </tr>
@@ -699,7 +791,8 @@ print $note_form->getField('attachments')->render();
                         <?php
                         $statusId = $info['note_status_id'] ?: $ticket->getStatusId();
                         $states = array('open');
-                        if ($thisstaff->canCloseTickets())
+                        if ($ticket->isCloseable() === true
+                                && $role->hasPerm(TicketModel::PERM_CLOSE))
                             $states = array_merge($states, array('closed'));
                         foreach (TicketStatusList::getStatuses(
                                     array('states' => $states)) as $s) {
@@ -719,180 +812,28 @@ print $note_form->getField('attachments')->render();
             </tr>
         </table>
 
-       <p  style="padding-left:165px;">
-           <input class="btn_sm" type="submit" value="<?php echo __('Post Note');?>">
-           <input class="btn_sm" type="reset" value="<?php echo __('Reset');?>">
+       <p style="text-align:center;">
+           <input class="save pending" type="submit" value="<?php echo __('Post Note');?>">
+           <input class="" type="reset" value="<?php echo __('Reset');?>">
        </p>
    </form>
-    <?php
-    if($thisstaff->canTransferTickets()) { ?>
-    <form id="transfer" action="tickets.php?id=<?php echo $ticket->getId(); ?>#transfer" name="transfer" method="post" enctype="multipart/form-data">
-        <?php csrf_token(); ?>
-        <input type="hidden" name="ticket_id" value="<?php echo $ticket->getId(); ?>">
-        <input type="hidden" name="a" value="transfer">
-        <table width="100%" border="0" cellspacing="0" cellpadding="3">
-            <?php
-            if($errors['transfer']) {
-                ?>
-            <tr>
-                <td width="120">&nbsp;</td>
-                <td class="error"><?php echo $errors['transfer']; ?></td>
-            </tr>
-            <?php
-            } ?>
-            <tr>
-                <td width="120">
-                    <label for="deptId"><strong><?php echo __('Department');?>:</strong></label>
-                </td>
-                <td>
-                    <?php
-                        echo sprintf('<span class="faded">'.__('Ticket is currently in <b>%s</b> department.').'</span>', $ticket->getDeptName());
-                    ?>
-                    <br>
-                    <select id="deptId" name="deptId">
-                        <option value="0" selected="selected">&mdash; <?php echo __('Select Target Department');?> &mdash;</option>
-                        <?php
-                        if($depts=Dept::getDepartments()) {
-                            foreach($depts as $id =>$name) {
-                                if($id==$ticket->getDeptId()) continue;
-                                echo sprintf('<option value="%d" %s>%s</option>',
-                                        $id, ($info['deptId']==$id)?'selected="selected"':'',$name);
-                            }
-                        }
-                        ?>
-                    </select>&nbsp;<span class='error'>*&nbsp;<?php echo $errors['deptId']; ?></span>
-                </td>
-            </tr>
-            <tr>
-                <td width="120" style="vertical-align:top">
-                    <label><strong><?php echo __('Comments'); ?>:</strong><span class='error'>&nbsp;*</span></label>
-                </td>
-                <td>
-                    <textarea name="transfer_comments" id="transfer_comments"
-                        placeholder="<?php echo __('Enter reasons for the transfer'); ?>"
-                        class="richtext ifhtml no-bar" cols="80" rows="7" wrap="soft"><?php
-                        echo $info['transfer_comments']; ?></textarea>
-                    <span class="error"><?php echo $errors['transfer_comments']; ?></span>
-                </td>
-            </tr>
-        </table>
-        <p style="padding-left:165px;">
-           <input class="btn_sm" type="submit" value="<?php echo __('Transfer');?>">
-           <input class="btn_sm" type="reset" value="<?php echo __('Reset');?>">
-        </p>
-    </form>
-    <?php
-    } ?>
-    <?php
-    if($thisstaff->canAssignTickets()) { ?>
-    <form id="assign" action="tickets.php?id=<?php echo $ticket->getId(); ?>#assign" name="assign" method="post" enctype="multipart/form-data">
-        <?php csrf_token(); ?>
-        <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>">
-        <input type="hidden" name="a" value="assign">
-        <table style="width:100%" border="0" cellspacing="0" cellpadding="3">
-
-            <?php
-            if($errors['assign']) {
-                ?>
-            <tr>
-                <td width="120">&nbsp;</td>
-                <td class="error"><?php echo $errors['assign']; ?></td>
-            </tr>
-            <?php
-            } ?>
-            <tr>
-                <td width="120" style="vertical-align:top">
-                    <label for="assignId"><strong><?php echo __('Assignee');?>:</strong></label>
-                </td>
-                <td>
-                    <select id="assignId" name="assignId">
-                        <option value="0" selected="selected">&mdash; <?php echo __('Select an Agent OR a Team');?> &mdash;</option>
-                        <?php
-                        if ($ticket->isOpen()
-                                && !$ticket->isAssigned()
-                                && $ticket->getDept()->isMember($thisstaff))
-                            echo sprintf('<option value="%d">'.__('Claim Ticket (comments optional)').'</option>', $thisstaff->getId());
-
-                        $sid=$tid=0;
-
-                        if ($dept->assignMembersOnly())
-                            $users = $dept->getAvailableMembers();
-                        else
-                            $users = Staff::getAvailableStaffMembers();
-
-                        if ($users) {
-                            echo '<OPTGROUP label="'.sprintf(__('Agents (%d)'), count($users)).'">';
-                            $staffId=$ticket->isAssigned()?$ticket->getStaffId():0;
-                            foreach($users as $id => $name) {
-                                if($staffId && $staffId==$id)
-                                    continue;
-
-                                if (!is_object($name))
-                                    $name = new PersonsName($name);
-
-                                $k="s$id";
-                                echo sprintf('<option value="%s" %s>%s</option>',
-                                        $k,(($info['assignId']==$k)?'selected="selected"':''), $name);
-                            }
-                            echo '</OPTGROUP>';
-                        }
-
-                        if(($teams=Team::getActiveTeams())) {
-                            echo '<OPTGROUP label="'.sprintf(__('Teams (%d)'), count($teams)).'">';
-                            $teamId=(!$sid && $ticket->isAssigned())?$ticket->getTeamId():0;
-                            foreach($teams as $id => $name) {
-                                if($teamId && $teamId==$id)
-                                    continue;
-
-                                $k="t$id";
-                                echo sprintf('<option value="%s" %s>%s</option>',
-                                        $k,(($info['assignId']==$k)?'selected="selected"':''),$name);
-                            }
-                            echo '</OPTGROUP>';
-                        }
-                        ?>
-                    </select>&nbsp;<span class='error'>*&nbsp;<?php echo $errors['assignId']; ?></span>
-                    <?php
-                    if ($ticket->isAssigned() && $ticket->isOpen()) { ?>
-                        <div class="faded"><?php echo sprintf(__('Ticket is currently assigned to %s'),
-                            sprintf('<b>%s</b>', $ticket->getAssignee())); ?></div> <?php
-                    } elseif ($ticket->isClosed()) { ?>
-                        <div class="faded"><?php echo __('Assigning a closed ticket will <b>reopen</b> it!'); ?></div>
-                    <?php } ?>
-                </td>
-            </tr>
-            <tr>
-                <td width="120" style="vertical-align:top">
-                    <label><strong><?php echo __('Comments');?>:</strong><span class='error'>&nbsp;</span></label>
-                </td>
-                <td>
-                    <textarea name="assign_comments" id="assign_comments"
-                        cols="80" rows="7" wrap="soft"
-                        placeholder="<?php echo __('Enter reasons for the assignment or instructions for assignee'); ?>"
-                        class="richtext ifhtml no-bar"><?php echo $info['assign_comments']; ?></textarea>
-                    <span class="error"><?php echo $errors['assign_comments']; ?></span><br>
-                </td>
-            </tr>
-        </table>
-        <p  style="padding-left:165px;">
-            <input class="btn_sm" type="submit" value="<?php echo $ticket->isAssigned()?__('Reassign'):__('Assign'); ?>">
-            <input class="btn_sm" type="reset" value="<?php echo __('Reset');?>">
-        </p>
-    </form>
-    <?php
-    } ?>
+ </div>
+ </div>
 </div>
 <div style="display:none;" class="dialog" id="print-options">
     <h3><?php echo __('Ticket Print Options');?></h3>
     <a class="close" href=""><i class="icon-remove-circle"></i></a>
     <hr/>
-    <form action="tickets.php?id=<?php echo $ticket->getId(); ?>" method="post" id="print-form" name="print-form">
+    <form action="tickets.php?id=<?php echo $ticket->getId(); ?>"
+        method="post" id="print-form" name="print-form" target="_blank">
         <?php csrf_token(); ?>
         <input type="hidden" name="a" value="print">
         <input type="hidden" name="id" value="<?php echo $ticket->getId(); ?>">
         <fieldset class="notes">
             <label class="fixed-size" for="notes"><?php echo __('Print Notes');?>:</label>
+            <label class="inline checkbox">
             <input type="checkbox" id="notes" name="notes" value="1"> <?php echo __('Print <b>Internal</b> Notes/Comments');?>
+            </label>
         </fieldset>
         <fieldset>
             <label class="fixed-size" for="psize"><?php echo __('Paper Size');?>:</label>
@@ -997,19 +938,30 @@ $(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 } ?>
+
+    // Post Reply or Note action buttons.
+    $('a.post-response').click(function (e) {
+        var $r = $('ul.tabs > li > a'+$(this).attr('href')+'-tab');
+        if ($r.length) {
+            // Make sure ticket thread tab is visiable.
+            var $t = $('ul#ticket_tabs > li > a#ticket-thread-tab');
+            if ($t.length && !$t.hasClass('active'))
+                $t.trigger('click');
+            // Make the target response tab active.
+            if (!$r.hasClass('active'))
+                $r.trigger('click');
+
+            // Scroll to the response section.
+            var $stop = $(document).height();
+            var $s = $('div#response_options');
+            if ($s.length)
+                $stop = $s.offset().top-125
+
+            $('html, body').animate({scrollTop: $stop}, 'fast');
+        }
+
+        return false;
+    });
+
 });
 </script>
diff --git a/include/staff/tickets.inc.php b/include/staff/tickets.inc.php
index 6a30ac094d0a37fda3b7a63e2e220f3bcc278af6..99bfb2381c0efd0b717b705181edf92379051284 100644
--- a/include/staff/tickets.inc.php
+++ b/include/staff/tickets.inc.php
@@ -1,358 +1,453 @@
 <?php
-if(!defined('OSTSCPINC') || !$thisstaff || !@$thisstaff->isStaff()) die('Access Denied');
+$search = SavedSearch::create();
+$tickets = TicketModel::objects();
+$clear_button = false;
+$view_all_tickets = $date_header = $date_col = false;
 
-$qs= array(); //Query string collector
-if($_REQUEST['status']) { //Query string status has nothing to do with the real status used below; gets overloaded.
-    $qs += array('status' => $_REQUEST['status']);
-}
+// Make sure the cdata materialized view is available
+TicketForm::ensureDynamicDataView();
 
-//See if this is a search
-$search=($_REQUEST['a']=='search');
-$searchTerm='';
-//make sure the search query is 3 chars min...defaults to no query with warning message
-if($search) {
-  $searchTerm=$_REQUEST['query'];
-  if( ($_REQUEST['query'] && strlen($_REQUEST['query'])<3)
-      || (!$_REQUEST['query'] && isset($_REQUEST['basic_search'])) ){ //Why do I care about this crap...
-      $search=false; //Instead of an error page...default back to regular query..with no search.
-      $errors['err']=__('Search term must be more than 3 chars');
-      $searchTerm='';
-  }
-}
-$showoverdue=$showanswered=false;
-$staffId=0; //Nothing for now...TODO: Allow admin and manager to limit tickets to single staff level.
-$showassigned= true; //show Assigned To column - defaults to true
-
-//Get status we are actually going to use on the query...making sure it is clean!
-$status=null;
-switch(strtolower($_REQUEST['status'])){ //Status is overloaded
-    case 'open':
-        $status='open';
-		$results_type=__('Open Tickets');
-        break;
-    case 'closed':
-        $status='closed';
-		$results_type=__('Closed Tickets');
-        $showassigned=true; //closed by.
-        break;
-    case 'overdue':
-        $status='open';
-        $showoverdue=true;
-        $results_type=__('Overdue Tickets');
+// Figure out REFRESH url — which might not be accurate after posting a
+// response
+list($path,) = explode('?', $_SERVER['REQUEST_URI'], 2);
+$args = array();
+parse_str($_SERVER['QUERY_STRING'], $args);
+
+// Remove commands from query
+unset($args['id']);
+if ($args['a'] !== 'search') unset($args['a']);
+
+$refresh_url = $path . '?' . http_build_query($args);
+
+$sort_options = array(
+    'priority,updated' =>   __('Priority + Most Recently Updated'),
+    'updated' =>            __('Most Recently Updated'),
+    'priority,created' =>   __('Priority + Most Recently Created'),
+    'due' =>                __('Due Date'),
+    'priority,due' =>       __('Priority + Due Date'),
+    'number' =>             __('Ticket Number'),
+    'answered' =>           __('Most Recently Answered'),
+    'closed' =>             __('Most Recently Closed'),
+    'hot' =>                __('Longest Thread'),
+    'relevance' =>          __('Relevance'),
+);
+
+// Queues columns
+
+$queue_columns = array(
+        'number' => array(
+            'width' => '7.4%',
+            'heading' => __('Number'),
+            ),
+        'date' => array(
+            'width' => '14.6%',
+            'heading' => __('Date Created'),
+            'sort_col' => 'created',
+            ),
+        'subject' => array(
+            'width' => '29.8%',
+            'heading' => __('Subject'),
+            'sort_col' => 'cdata__subject',
+            ),
+        'name' => array(
+            'width' => '18.1%',
+            'heading' => __('From'),
+            'sort_col' =>  'user__name',
+            ),
+        'status' => array(
+            'width' => '8.4%',
+            'heading' => __('Status'),
+            'sort_col' => 'status_id',
+            ),
+        'priority' => array(
+            'width' => '8.4%',
+            'heading' => __('Priority'),
+            'sort_col' => 'cdata__priority__priority_urgency',
+            ),
+        'assignee' => array(
+            'width' => '16%',
+            'heading' => __('Agent'),
+            ),
+        'dept' => array(
+            'width' => '16%',
+            'heading' => __('Department'),
+            'sort_col'  => 'dept__name',
+            ),
+        );
+
+$use_subquery = true;
+
+// Figure out the queue we're viewing
+$queue_key = sprintf('::Q:%s', ObjectModel::OBJECT_TYPE_TICKET);
+$queue_name = $_SESSION[$queue_key] ?: '';
+
+switch ($queue_name) {
+case 'closed':
+    $status='closed';
+    $results_type=__('Closed Tickets');
+    $showassigned=true; //closed by.
+    $queue_sort_options = array('closed', 'priority,due', 'due',
+        'priority,updated', 'priority,created', 'answered', 'number', 'hot');
+    break;
+case 'overdue':
+    $status='open';
+    $results_type=__('Overdue Tickets');
+    $tickets->filter(array('isoverdue'=>1));
+    $queue_sort_options = array('priority,due', 'due', 'priority,updated',
+        'updated', 'answered', 'priority,created', 'number', 'hot');
+    break;
+case 'assigned':
+    $status='open';
+    $staffId=$thisstaff->getId();
+    $results_type=__('My Tickets');
+    $tickets->filter(Q::any(array(
+        'staff_id'=>$thisstaff->getId(),
+        Q::all(array('staff_id' => 0, 'team_id__gt' => 0)),
+    )));
+    $queue_sort_options = array('updated', 'priority,updated',
+        'priority,created', 'priority,due', 'due', 'answered', 'number',
+        'hot');
+    break;
+case 'answered':
+    $status='open';
+    $showanswered=true;
+    $results_type=__('Answered Tickets');
+    $tickets->filter(array('isanswered'=>1));
+    $queue_sort_options = array('answered', 'priority,updated', 'updated',
+        'priority,created', 'priority,due', 'due', 'number', 'hot');
+    break;
+default:
+case 'search':
+    $queue_sort_options = array('priority,updated', 'priority,created',
+        'priority,due', 'due', 'updated', 'answered',
+        'closed', 'number', 'hot');
+    // Consider basic search
+    if ($_REQUEST['query']) {
+        $results_type=__('Search Results');
+        // Use an index if possible
+        if ($_REQUEST['search-type'] == 'typeahead') {
+            if (Validator::is_email($_REQUEST['query'])) {
+                $tickets = $tickets->filter(array(
+                    'user__emails__address' => $_REQUEST['query'],
+                ));
+            }
+            elseif ($_REQUEST['query']) {
+                $tickets = $tickets->filter(array(
+                    'number' => $_REQUEST['query'],
+                ));
+            }
+        }
+        elseif (isset($_REQUEST['query'])
+            && ($q = trim($_REQUEST['query']))
+            && strlen($q) > 2
+        ) {
+            // [Search] click, consider keywords
+            $__tickets = $ost->searcher->find($q, $tickets);
+            if (!count($__tickets) && preg_match('`\w$`u', $q)) {
+                // Do wildcard search if no hits
+                $__tickets = $ost->searcher->find($q.'*', $tickets);
+            }
+            $tickets = $__tickets;
+            $has_relevance = true;
+        }
+        // Clear sticky search queue
+        unset($_SESSION[$queue_key]);
         break;
-    case 'assigned':
-        $status='open';
-        $staffId=$thisstaff->getId();
-        $results_type=__('My Tickets');
+    }
+    // Apply user filter
+    elseif (isset($_GET['uid']) && ($user = User::lookup($_GET['uid']))) {
+        $tickets->filter(array('user__id'=>$_GET['uid']));
+        $results_type = sprintf('%s — %s', __('Search Results'),
+            $user->getName());
+        if (isset($_GET['status']))
+            $status = $_GET['status'];
+        // Don't apply normal open ticket
         break;
-    case 'answered':
-        $status='open';
-        $showanswered=true;
-        $results_type=__('Answered Tickets');
+    }
+    elseif (isset($_GET['orgid']) && ($org = Organization::lookup($_GET['orgid']))) {
+        $tickets->filter(array('user__org_id'=>$_GET['orgid']));
+        $results_type = sprintf('%s — %s', __('Search Results'),
+            $org->getName());
+        if (isset($_GET['status']))
+            $status = $_GET['status'];
+        // Don't apply normal open ticket
         break;
-    default:
-        if (!$search && !isset($_REQUEST['advsid'])) {
-            $_REQUEST['status']=$status='open';
-            $results_type=__('Open Tickets');
+    } elseif (isset($_SESSION['advsearch'])) {
+        $form = $search->getFormFromSession('advsearch');
+        $tickets = $search->mangleQuerySet($tickets, $form);
+        $view_all_tickets = $thisstaff->hasPerm(SearchBackend::PERM_EVERYTHING);
+        $results_type=__('Advanced Search')
+            . '<a class="action-button" style="font-size: 15px;" href="?clear_filter"><i style="top:0" class="icon-ban-circle"></i> <em>' . __('clear') . '</em></a>';
+        foreach ($form->getFields() as $sf) {
+            if ($sf->get('name') == 'keywords' && $sf->getClean()) {
+                $has_relevance = true;
+                break;
+            }
         }
+        break;
+    }
+    // Fall-through and show open tickets
+case 'open':
+    $status='open';
+    $queue_name = $queue_name ?: 'open';
+    $results_type=__('Open Tickets');
+    if (!$cfg->showAnsweredTickets())
+        $tickets->filter(array('isanswered'=>0));
+    $queue_sort_options = array('priority,updated', 'updated',
+        'priority,due', 'due', 'priority,created', 'answered', 'number',
+        'hot');
+    break;
 }
 
-// Stash current queue view
-$_SESSION['::Q'] = $_REQUEST['status'];
-
-$qwhere ='';
-/*
-   STRICT DEPARTMENTS BASED PERMISSION!
-   User can also see tickets assigned to them regardless of the ticket's dept.
-*/
-
-$depts=$thisstaff->getDepts();
-$qwhere =' WHERE ( '
-        .'  ( ticket.staff_id='.db_input($thisstaff->getId())
-        .' AND status.state="open") ';
-
-if(!$thisstaff->showAssignedOnly())
-    $qwhere.=' OR ticket.dept_id IN ('.($depts?implode(',', db_input($depts)):0).')';
-
-if(($teams=$thisstaff->getTeams()) && count(array_filter($teams)))
-    $qwhere.=' OR (ticket.team_id IN ('.implode(',', db_input(array_filter($teams)))
-            .') AND status.state="open") ';
-
-$qwhere .= ' )';
-
-//STATUS to states
-$states = array(
-    'open' => array('open'),
-    'closed' => array('closed'));
-
-if($status && isset($states[$status])) {
-    $qwhere.=' AND status.state IN (
-                '.implode(',', db_input($states[$status])).' ) ';
+// Open queues _except_ assigned should respect showAssignedTickets()
+// settings
+if ($status != 'closed' && $queue_name != 'assigned') {
+    $hideassigned = ($cfg && !$cfg->showAssignedTickets()) && !$thisstaff->showAssignedTickets();
+    $showassigned = !$hideassigned;
+    if ($queue_name == 'open' && $hideassigned)
+        $tickets->filter(array('staff_id'=>0, 'team_id'=>0));
 }
 
-if (isset($_REQUEST['uid']) && $_REQUEST['uid']) {
-    $qwhere .= ' AND (ticket.user_id='.db_input($_REQUEST['uid'])
-            .' OR collab.user_id='.db_input($_REQUEST['uid']).') ';
-    $qs += array('uid' => $_REQUEST['uid']);
-}
+// Apply primary ticket status
+if ($status)
+    $tickets->filter(array('status__state'=>$status));
 
-//Queues: Overloaded sub-statuses  - you've got to just have faith!
-if($staffId && ($staffId==$thisstaff->getId())) { //My tickets
-    $results_type=__('Assigned Tickets');
-    $qwhere.=' AND ticket.staff_id='.db_input($staffId);
-    $showassigned=false; //My tickets...already assigned to the staff.
-}elseif($showoverdue) { //overdue
-    $qwhere.=' AND ticket.isoverdue=1 ';
-}elseif($showanswered) { ////Answered
-    $qwhere.=' AND ticket.isanswered=1 ';
-}elseif(!strcasecmp($status, 'open') && !$search) { //Open queue (on search OPEN means all open tickets - regardless of state).
-    //Showing answered tickets on open queue??
-    if(!$cfg->showAnsweredTickets())
-        $qwhere.=' AND ticket.isanswered=0 ';
-
-    /* Showing assigned tickets on open queue?
-       Don't confuse it with show assigned To column -> F'it it's confusing - just trust me!
-     */
-    if(!($cfg->showAssignedTickets() || $thisstaff->showAssignedTickets())) {
-        $qwhere.=' AND ticket.staff_id=0 '; //XXX: NOT factoring in team assignments - only staff assignments.
-        $showassigned=false; //Not showing Assigned To column since assigned tickets are not part of open queue
-    }
-}
+// Impose visibility constraints
+// ------------------------------------------------------------
+if (!$view_all_tickets) {
+    // -- Open and assigned to me
+    $assigned = Q::any(array(
+        'staff_id' => $thisstaff->getId(),
+    ));
+    // -- Open and assigned to a team of mine
+    if ($teams = array_filter($thisstaff->getTeams()))
+        $assigned->add(array('team_id__in' => $teams));
 
-//Search?? Somebody...get me some coffee
-$deep_search=false;
-$order_by=$order=null;
-if($search):
-    $qs += array('a' => $_REQUEST['a'], 't' => $_REQUEST['t']);
-    //query
-    if($searchTerm){
-        $qs += array('query' => $searchTerm);
-        $queryterm=db_real_escape($searchTerm,false); //escape the term ONLY...no quotes.
-        if (is_numeric($searchTerm)) {
-            $qwhere.=" AND ticket.`number` LIKE '$queryterm%'";
-        } elseif (strpos($searchTerm,'@') && Validator::is_email($searchTerm)) {
-            //pulling all tricks!
-            # XXX: What about searching for email addresses in the body of
-            #      the thread message
-            $qwhere.=" AND email.address='$queryterm'";
-        } else {//Deep search!
-            //This sucks..mass scan! search anything that moves!
-            require_once(INCLUDE_DIR.'ajax.tickets.php');
-
-            $tickets = TicketsAjaxApi::_search(array('query'=>$queryterm));
-            if (count($tickets)) {
-                $ticket_ids = implode(',',db_input($tickets));
-                $qwhere .= ' AND ticket.ticket_id IN ('.$ticket_ids.')';
-                $order_by = 'FIELD(ticket.ticket_id, '.$ticket_ids.')';
-                $order = ' ';
-            }
-            else
-                // No hits -- there should be an empty list of results
-                $qwhere .= ' AND false';
-        }
-   }
+    $visibility = Q::any(new Q(array('status__state'=>'open', $assigned)));
 
-endif;
+    // -- Routed to a department of mine
+    if (!$thisstaff->showAssignedOnly() && ($depts=$thisstaff->getDepts()))
+        $visibility->add(array('dept_id__in' => $depts));
 
-if ($_REQUEST['advsid'] && isset($_SESSION['adv_'.$_REQUEST['advsid']])) {
-    $ticket_ids = implode(',', db_input($_SESSION['adv_'.$_REQUEST['advsid']]));
-    $qs += array('advsid' => $_REQUEST['advsid']);
-    $qwhere .= ' AND ticket.ticket_id IN ('.$ticket_ids.')';
-    // Thanks, http://stackoverflow.com/a/1631794
-    $order_by = 'FIELD(ticket.ticket_id, '.$ticket_ids.')';
-    $order = ' ';
+    $tickets->filter(Q::any($visibility));
 }
 
-$sortOptions=array('date'=>'effective_date','ID'=>'ticket.`number`*1',
-    'pri'=>'pri.priority_urgency','name'=>'user.name','subj'=>'cdata.subject',
-    'status'=>'status.name','assignee'=>'assigned','staff'=>'staff',
-    'dept'=>'dept.dept_name');
+// TODO :: Apply requested quick filter
 
-$orderWays=array('DESC'=>'DESC','ASC'=>'ASC');
+// Apply requested pagination
+$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
+$count = $tickets->count();
+$pageNav = new Pagenate($count, $page, PAGE_LIMIT);
+$pageNav->setURL('tickets.php', $args);
+$tickets = $pageNav->paginate($tickets);
 
-//Sorting options...
-$queue = isset($_REQUEST['status'])?strtolower($_REQUEST['status']):$status;
-if($_REQUEST['sort'] && $sortOptions[$_REQUEST['sort']])
-    $order_by =$sortOptions[$_REQUEST['sort']];
-elseif($sortOptions[$_SESSION[$queue.'_tickets']['sort']]) {
-    $_REQUEST['sort'] = $_SESSION[$queue.'_tickets']['sort'];
-    $_REQUEST['order'] = $_SESSION[$queue.'_tickets']['order'];
+// Apply requested sorting
+$queue_sort_key = sprintf(':Q%s:%s:sort', ObjectModel::OBJECT_TYPE_TICKET, $queue_name);
 
-    $order_by = $sortOptions[$_SESSION[$queue.'_tickets']['sort']];
-    $order = $_SESSION[$queue.'_tickets']['order'];
+// If relevance is available, use it as the default
+if ($has_relevance) {
+    array_unshift($queue_sort_options, 'relevance');
 }
-
-if($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])])
-    $order=$orderWays[strtoupper($_REQUEST['order'])];
-
-//Save sort order for sticky sorting.
-if($_REQUEST['sort'] && $queue) {
-    $_SESSION[$queue.'_tickets']['sort'] = $_REQUEST['sort'];
-    $_SESSION[$queue.'_tickets']['order'] = $_REQUEST['order'];
+elseif ($_SESSION[$queue_sort_key][0] == 'relevance') {
+    unset($_SESSION[$queue_sort_key]);
 }
 
-//Set default sort by columns.
-if(!$order_by ) {
-    if($showanswered)
-        $order_by='ticket.lastresponse, ticket.created'; //No priority sorting for answered tickets.
-    elseif(!strcasecmp($status,'closed'))
-        $order_by='ticket.closed, ticket.created'; //No priority sorting for closed tickets.
-    elseif($showoverdue) //priority> duedate > age in ASC order.
-        $order_by='pri.priority_urgency ASC, ISNULL(ticket.duedate) ASC, ticket.duedate ASC, effective_date ASC, ticket.created';
-    else //XXX: Add due date here?? No -
-        $order_by='pri.priority_urgency ASC, effective_date DESC, ticket.created';
+if (isset($_GET['sort'])) {
+    $_SESSION[$queue_sort_key] = array($_GET['sort'], $_GET['dir']);
+}
+elseif (!isset($_SESSION[$queue_sort_key])) {
+    $_SESSION[$queue_sort_key] = array($queue_sort_options[0], 0);
 }
 
-$order=$order?$order:'DESC';
-if($order_by && strpos($order_by,',') && $order)
-    $order_by=preg_replace('/(?<!ASC|DESC),/', " $order,", $order_by);
-
-$sort=$_REQUEST['sort']?strtolower($_REQUEST['sort']):'pri.priority_urgency'; //Urgency is not on display table.
-$x=$sort.'_sort';
-$$x=' class="'.strtolower($order).'" ';
-
-if($_GET['limit'])
-    $qs += array('limit' => $_GET['limit']);
-
-$qselect ='SELECT ticket.ticket_id,tlock.lock_id,ticket.`number`,ticket.dept_id,ticket.staff_id,ticket.team_id '
-    .' ,user.name'
-    .' ,email.address as email, dept.dept_name, status.state '
-         .' ,status.name as status,ticket.source,ticket.isoverdue,ticket.isanswered,ticket.created ';
-
-$qfrom=' FROM '.TICKET_TABLE.' ticket '.
-       ' LEFT JOIN '.TICKET_STATUS_TABLE. ' status
-            ON (status.id = ticket.status_id) '.
-       ' LEFT JOIN '.USER_TABLE.' user ON user.id = ticket.user_id'.
-       ' LEFT JOIN '.USER_EMAIL_TABLE.' email ON user.id = email.user_id'.
-       ' LEFT JOIN '.DEPT_TABLE.' dept ON ticket.dept_id=dept.dept_id ';
+list($sort_cols, $sort_dir) = $_SESSION[$queue_sort_key];
+$orm_dir = $sort_dir ? QuerySet::ASC : QuerySet::DESC;
+$orm_dir_r = $sort_dir ? QuerySet::DESC : QuerySet::ASC;
+
+switch ($sort_cols) {
+case 'number':
+    $queue_columns['number']['sort_dir'] = $sort_dir;
+    $tickets->extra(array(
+        'order_by'=>array(
+            array(SqlExpression::times(new SqlField('number'), 1), $orm_dir)
+        )
+    ));
+    break;
+
+case 'priority,created':
+    $tickets->order_by(($sort_dir ? '-' : '') . 'cdata__:priority__priority_urgency');
+    // Fall through to columns for `created`
+case 'created':
+    $queue_columns['date']['heading'] = __('Date Created');
+    $queue_columns['date']['sort_col'] = $date_col = 'created';
+    $tickets->values('created');
+    $tickets->order_by($sort_dir ? 'created' : '-created');
+    break;
+
+case 'priority,due':
+    $tickets->order_by('cdata__:priority__priority_urgency', $orm_dir_r);
+    // Fall through to add in due date filter
+case 'due':
+    $queue_columns['date']['heading'] = __('Due Date');
+    $queue_columns['date']['sort'] = 'due';
+    $queue_columns['date']['sort_col'] = $date_col = 'est_duedate';
+    $tickets->values('est_duedate');
+    $tickets->order_by(SqlFunction::COALESCE(new SqlField('est_duedate'), 'zzz'), $orm_dir_r);
+    break;
+
+case 'closed':
+    $queue_columns['date']['heading'] = __('Date Closed');
+    $queue_columns['date']['sort'] = $sort_cols;
+    $queue_columns['date']['sort_col'] = $date_col = 'closed';
+    $queue_columns['date']['sort_dir'] = $sort_dir;
+    $tickets->values('closed');
+    $tickets->order_by('closed', $orm_dir);
+    break;
+
+case 'answered':
+    $queue_columns['date']['heading'] = __('Last Response');
+    $queue_columns['date']['sort'] = $sort_cols;
+    $queue_columns['date']['sort_col'] = $date_col = 'thread__lastresponse';
+    $queue_columns['date']['sort_dir'] = $sort_dir;
+    $date_fallback = '<em class="faded">'.__('unanswered').'</em>';
+    $tickets->order_by('thread__lastresponse', $orm_dir);
+    $tickets->values('thread__lastresponse');
+    break;
+
+case 'hot':
+    $tickets->order_by('thread_count', $orm_dir);
+    $tickets->annotate(array(
+        'thread_count' => SqlAggregate::COUNT('thread__entries'),
+    ));
+    break;
+
+case 'relevance':
+    $tickets->order_by(new SqlCode('__relevance__'), $orm_dir);
+    break;
+
+case 'assignee':
+    $tickets->order_by('staff__lastname', $orm_dir);
+    $tickets->order_by('staff__firstname', $orm_dir);
+    $tickets->order_by('team__name', $orm_dir);
+    $queue_columns['assignee']['sort_dir'] = $sort_dir;
+    break;
+
+default:
+    if ($sort_cols && isset($queue_columns[$sort_cols])) {
+        $queue_columns[$sort_cols]['sort_dir'] = $sort_dir;
+        if (isset($queue_columns[$sort_cols]['sort_col']))
+            $sort_cols = $queue_columns[$sort_cols]['sort_col'];
+        $tickets->order_by($sort_cols, $orm_dir);
+        break;
+    }
 
-if ($_REQUEST['uid'])
-    $qfrom.=' LEFT JOIN '.TICKET_COLLABORATOR_TABLE.' collab
-        ON (ticket.ticket_id = collab.ticket_id )';
+case 'priority,updated':
+    $tickets->order_by('cdata__:priority__priority_urgency', $orm_dir_r);
+    // Fall through for columns defined for `updated`
+case 'updated':
+    $queue_columns['date']['heading'] = __('Last Updated');
+    $queue_columns['date']['sort'] = $sort_cols;
+    $queue_columns['date']['sort_col'] = $date_col = 'lastupdate';
+    $tickets->order_by('lastupdate', $orm_dir);
+    break;
+}
 
+if (in_array($sort_cols, array('created', 'due', 'updated')))
+    $queue_columns['date']['sort_dir'] = $sort_dir;
 
-$sjoin='';
+// Rewrite $tickets to use a nested query, which will include the LIMIT part
+// in order to speed the result
+$orig_tickets = clone $tickets;
+$tickets2 = TicketModel::objects();
+$tickets2->values = $tickets->values;
+$tickets2->filter(array('ticket_id__in' => $tickets->values_flat('ticket_id')));
 
-if($search && $deep_search) {
-    $sjoin.=' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON (ticket.ticket_id=thread.ticket_id )';
-}
+// Transfer the order_by from the original tickets
+$tickets2->order_by($orig_tickets->getSortFields());
+$tickets = $tickets2;
 
-//get ticket count based on the query so far..
-$total=db_count("SELECT count(DISTINCT ticket.ticket_id) $qfrom $sjoin $qwhere");
-//pagenate
-$pagelimit=($_GET['limit'] && is_numeric($_GET['limit']))?$_GET['limit']:PAGE_LIMIT;
-$page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
-$pageNav=new Pagenate($total,$page,$pagelimit);
-
-$qstr = '&amp;'.http::build_query($qs);
-$qs += array('sort' => $_REQUEST['sort'], 'order' => $_REQUEST['order']);
-$pageNav->setURL('tickets.php', $qs);
-
-//ADD attachment,priorities, lock and other crap
-$qselect.=' ,IF(ticket.duedate IS NULL,IF(sla.id IS NULL, NULL, DATE_ADD(ticket.created, INTERVAL sla.grace_period HOUR)), ticket.duedate) as duedate '
-         .' ,CAST(GREATEST(IFNULL(ticket.lastmessage, 0), IFNULL(ticket.closed, 0), IFNULL(ticket.reopened, 0), ticket.created) as datetime) as effective_date '
-         .' ,ticket.created as ticket_created, CONCAT_WS(" ", staff.firstname, staff.lastname) as staff, team.name as team '
-         .' ,IF(staff.staff_id IS NULL,team.name,CONCAT_WS(" ", staff.lastname, staff.firstname)) as assigned '
-         .' ,IF(ptopic.topic_pid IS NULL, topic.topic, CONCAT_WS(" / ", ptopic.topic, topic.topic)) as helptopic '
-         .' ,cdata.priority as priority_id, cdata.subject, pri.priority_desc, pri.priority_color';
-
-$qfrom.=' LEFT JOIN '.TICKET_LOCK_TABLE.' tlock ON (ticket.ticket_id=tlock.ticket_id AND tlock.expire>NOW()
-               AND tlock.staff_id!='.db_input($thisstaff->getId()).') '
-       .' LEFT JOIN '.STAFF_TABLE.' staff ON (ticket.staff_id=staff.staff_id) '
-       .' LEFT JOIN '.TEAM_TABLE.' team ON (ticket.team_id=team.team_id) '
-       .' LEFT JOIN '.SLA_TABLE.' sla ON (ticket.sla_id=sla.id AND sla.isactive=1) '
-       .' LEFT JOIN '.TOPIC_TABLE.' topic ON (ticket.topic_id=topic.topic_id) '
-       .' LEFT JOIN '.TOPIC_TABLE.' ptopic ON (ptopic.topic_id=topic.topic_pid) '
-       .' LEFT JOIN '.TABLE_PREFIX.'ticket__cdata cdata ON (cdata.ticket_id = ticket.ticket_id) '
-       .' LEFT JOIN '.PRIORITY_TABLE.' pri ON (pri.priority_id = cdata.priority)';
+// Save the query to the session for exporting
+$_SESSION[':Q:tickets'] = $tickets;
 
 TicketForm::ensureDynamicDataView();
 
-$query="$qselect $qfrom $qwhere ORDER BY $order_by $order LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
-//echo $query;
-$hash = md5($query);
-$_SESSION['search_'.$hash] = $query;
-$res = db_query($query);
-$showing=db_num_rows($res)? ' &mdash; '.$pageNav->showing():"";
-if(!$results_type)
-    $results_type = sprintf(__('%s Tickets' /* %s will be a status such as 'open' */),
-        mb_convert_case($status, MB_CASE_TITLE));
-
-if($search)
-    $results_type.= ' ('.__('Search Results').')';
-
-$negorder=$order=='DESC'?'ASC':'DESC'; //Negate the sorting..
-
-// Fetch the results
-$results = array();
-while ($row = db_fetch_array($res)) {
-    $results[$row['ticket_id']] = $row;
-}
-
-// Fetch attachment and thread entry counts
-if ($results) {
-    $counts_sql = 'SELECT ticket.ticket_id,
-        count(DISTINCT attach.attach_id) as attachments,
-        count(DISTINCT thread.id) as thread_count,
-        count(DISTINCT collab.id) as collaborators
-        FROM '.TICKET_TABLE.' ticket
-        LEFT JOIN '.TICKET_ATTACHMENT_TABLE.' attach ON (ticket.ticket_id=attach.ticket_id) '
-     .' LEFT JOIN '.TICKET_THREAD_TABLE.' thread ON ( ticket.ticket_id=thread.ticket_id) '
-     .' LEFT JOIN '.TICKET_COLLABORATOR_TABLE.' collab
-            ON ( ticket.ticket_id=collab.ticket_id) '
-     .' WHERE ticket.ticket_id IN ('.implode(',', db_input(array_keys($results))).')
-        GROUP BY ticket.ticket_id';
-    $ids_res = db_query($counts_sql);
-    while ($row = db_fetch_array($ids_res)) {
-        $results[$row['ticket_id']] += $row;
-    }
-}
+// Select pertinent columns
+// ------------------------------------------------------------
+$tickets->values('lock__staff_id', 'staff_id', 'isoverdue', 'team_id',
+'ticket_id', 'number', 'cdata__subject', 'user__default_email__address',
+'source', 'cdata__:priority__priority_color', 'cdata__:priority__priority_desc', 'status_id', 'status__name', 'status__state', 'dept_id', 'dept__name', 'user__name', 'lastupdate', 'isanswered', 'staff__firstname', 'staff__lastname', 'team__name');
+
+// Add in annotations
+$tickets->annotate(array(
+    'collab_count' => TicketThread::objects()
+        ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
+        ->aggregate(array('count' => SqlAggregate::COUNT('collaborators__id'))),
+    'attachment_count' => TicketThread::objects()
+        ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
+        ->filter(array('entries__attachments__inline' => 0))
+        ->aggregate(array('count' => SqlAggregate::COUNT('entries__attachments__id'))),
+    'thread_count' => TicketThread::objects()
+        ->filter(array('ticket__ticket_id' => new SqlField('ticket_id', 1)))
+        ->exclude(array('entries__flags__hasbit' => ThreadEntry::FLAG_HIDDEN))
+        ->aggregate(array('count' => SqlAggregate::COUNT('entries__id'))),
+));
+
+
+// Make sure we're only getting active locks
+$tickets->constrain(array('lock' => array(
+                'lock__expire__gt' => SqlFunction::NOW())));
 
-//YOU BREAK IT YOU FIX IT.
 ?>
+
 <!-- SEARCH FORM START -->
 <div id='basic_search'>
-    <form action="tickets.php" method="get">
-    <?php csrf_token(); ?>
+  <div class="pull-right" style="height:25px">
+    <span class="valign-helper"></span>
+    <?php
+    require STAFFINC_DIR.'templates/queue-sort.tmpl.php';
+    ?>
+  </div>
+    <form action="tickets.php" method="get" onsubmit="javascript:
+  $.pjax({
+    url:$(this).attr('action') + '?' + $(this).serialize(),
+    container:'#pjax-container',
+    timeout: 2000
+  });
+return false;">
     <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'],
-            true); ?>"
-                autocomplete="off" autocorrect="off" autocapitalize="off"></td>
-            <td><input type="submit" name="basic_search" class="button" value="<?php echo __('Search'); ?>"></td>
-            <td>&nbsp;&nbsp;<a href="#" id="go-advanced">[<?php echo __('advanced'); ?>]</a>&nbsp;<i class="help-tip icon-question-sign" href="#advanced"></i></td>
-        </tr>
-    </table>
+    <input type="hidden" name="search-type" value=""/>
+    <div class="attached input">
+      <input type="text" class="basic-search" data-url="ajax.php/tickets/lookup" name="query"
+        autofocus size="30" value="<?php echo Format::htmlchars($_REQUEST['query'], true); ?>"
+        autocomplete="off" autocorrect="off" autocapitalize="off">
+      <button type="submit" class="attached button"><i class="icon-search"></i>
+      </button>
+    </div>
+    <a href="#" onclick="javascript:
+        $.dialog('ajax.php/tickets/search', 201);"
+        >[<?php echo __('advanced'); ?>]</a>
+        <i class="help-tip icon-question-sign" href="#advanced"></i>
     </form>
 </div>
 <!-- SEARCH FORM END -->
 <div class="clear"></div>
-<div style="margin-bottom:20px; padding-top:10px;">
-<div>
-        <div class="pull-left flush-left">
-            <h2><a href="<?php echo Format::htmlchars($_SERVER['REQUEST_URI']); ?>"
-                title="<?php echo __('Refresh'); ?>"><i class="icon-refresh"></i> <?php echo
-                $results_type.$showing; ?></a></h2>
-        </div>
-        <div class="pull-right flush-right">
-
+<div style="margin-bottom:20px; padding-top:5px;">
+    <div class="sticky bar opaque">
+        <div class="content">
+            <div class="pull-left flush-left">
+                <h2><a href="<?php echo $refresh_url; ?>"
+                    title="<?php echo __('Refresh'); ?>"><i class="icon-refresh"></i> <?php echo
+                    $results_type; ?></a></h2>
+            </div>
+            <div class="pull-right flush-right">
             <?php
-            if ($thisstaff->canDeleteTickets()) { ?>
-            <a id="tickets-delete" class="action-button pull-right tickets-action"
-                href="#tickets/status/delete"><i
-            class="icon-trash"></i> <?php echo __('Delete'); ?></a>
-            <?php
-            } ?>
-            <?php
-            if ($thisstaff->canManageTickets()) {
-                echo TicketStatus::status_options();
-            }
-            ?>
+            if ($count) {
+                Ticket::agentActions($thisstaff, array('status' => $status));
+            }?>
+            </div>
         </div>
+    </div>
 </div>
-<div class="clear" style="margin-bottom:10px;"></div>
+<div class="clear"></div>
 <form action="tickets.php" method="POST" name='tickets' id="tickets">
 <?php csrf_token(); ?>
  <input type="hidden" name="a" value="mass_process" >
@@ -362,147 +457,153 @@ if ($results) {
  <table class="list" border="0" cellspacing="1" cellpadding="2" width="940">
     <thead>
         <tr>
-            <?php if($thisstaff->canManageTickets()) { ?>
-	        <th width="8px">&nbsp;</th>
-            <?php } ?>
-	        <th width="70">
-                <a <?php echo $id_sort; ?> href="tickets.php?sort=ID&order=<?php echo $negorder; ?><?php echo $qstr; ?>"
-                    title="<?php echo sprintf(__('Sort by %s %s'), __('Ticket ID'), __($negorder)); ?>"><?php echo __('Ticket'); ?></a></th>
-	        <th width="70">
-                <a  <?php echo $date_sort; ?> href="tickets.php?sort=date&order=<?php echo $negorder; ?><?php echo $qstr; ?>"
-                    title="<?php echo sprintf(__('Sort by %s %s'), __('Date'), __($negorder)); ?>"><?php echo __('Date'); ?></a></th>
-	        <th width="280">
-                 <a <?php echo $subj_sort; ?> href="tickets.php?sort=subj&order=<?php echo $negorder; ?><?php echo $qstr; ?>"
-                    title="<?php echo sprintf(__('Sort by %s %s'), __('Subject'), __($negorder)); ?>"><?php echo __('Subject'); ?></a></th>
-            <th width="170">
-                <a <?php echo $name_sort; ?> href="tickets.php?sort=name&order=<?php echo $negorder; ?><?php echo $qstr; ?>"
-                     title="<?php echo sprintf(__('Sort by %s %s'), __('Name'), __($negorder)); ?>"><?php echo __('From');?></a></th>
-            <?php
-            if($search && !$status) { ?>
-                <th width="60">
-                    <a <?php echo $status_sort; ?> href="tickets.php?sort=status&order=<?php echo $negorder; ?><?php echo $qstr; ?>"
-                        title="<?php echo sprintf(__('Sort by %s %s'), __('Status'), __($negorder)); ?>"><?php echo __('Status');?></a></th>
             <?php
-            } else { ?>
-                <th width="60" <?php echo $pri_sort;?>>
-                    <a <?php echo $pri_sort; ?> href="tickets.php?sort=pri&order=<?php echo $negorder; ?><?php echo $qstr; ?>"
-                        title="<?php echo sprintf(__('Sort by %s %s'), __('Priority'), __($negorder)); ?>"><?php echo __('Priority');?></a></th>
-            <?php
-            }
+            if ($thisstaff->canManageTickets()) { ?>
+	        <th width="2%">&nbsp;</th>
+            <?php } ?>
 
-            if($showassigned ) {
-                //Closed by
-                if(!strcasecmp($status,'closed')) { ?>
-                    <th width="150">
-                        <a <?php echo $staff_sort; ?> href="tickets.php?sort=staff&order=<?php echo $negorder; ?><?php echo $qstr; ?>"
-                            title="<?php echo sprintf(__('Sort by %s %s'), __("Closing Agent's Name"), __($negorder)); ?>"><?php echo __('Closed By'); ?></a></th>
-                <?php
-                } else { //assigned to ?>
-                    <th width="150">
-                        <a <?php echo $assignee_sort; ?> href="tickets.php?sort=assignee&order=<?php echo $negorder; ?><?php echo $qstr; ?>"
-                            title="<?php echo sprintf(__('Sort by %s %s'), __('Assignee'), __($negorder)); ?>"><?php echo __('Assigned To'); ?></a></th>
-                <?php
-                }
-            } else { ?>
-                <th width="150">
-                    <a <?php echo $dept_sort; ?> href="tickets.php?sort=dept&order=<?php echo $negorder;?><?php echo $qstr; ?>"
-                        title="<?php echo sprintf(__('Sort by %s %s'), __('Department'), __($negorder)); ?>"><?php echo __('Department');?></a></th>
             <?php
-            } ?>
+            // Swap some columns based on the queue.
+            if ($showassigned ) {
+                unset($queue_columns['dept']);
+                if (!strcasecmp($status,'closed'))
+                    $queue_columns['assignee']['heading'] =  __('Closed By');
+                else
+                    $queue_columns['assignee']['heading'] =  __('Assigned To');
+            } else {
+                unset($queue_columns['assignee']);
+            }
+            if ($search && !$status)
+                unset($queue_columns['priority']);
+            else
+                unset($queue_columns['status']);
+
+            // Query string
+            unset($args['sort'], $args['dir'], $args['_pjax']);
+            $qstr = Http::build_query($args);
+            // Show headers
+            foreach ($queue_columns as $k => $column) {
+                echo sprintf( '<th width="%s"><a href="?sort=%s&dir=%s&%s"
+                        class="%s">%s</a></th>',
+                        $column['width'],
+                        $column['sort'] ?: $k,
+                        $column['sort_dir'] ? 0 : 1,
+                        $qstr,
+                        isset($column['sort_dir'])
+                        ? ($column['sort_dir'] ? 'asc': 'desc') : '',
+                        $column['heading']);
+            }
+            ?>
         </tr>
      </thead>
      <tbody>
         <?php
         // Setup Subject field for display
-        $subject_field = TicketForm::objects()->one()->getField('subject');
+        $subject_field = TicketForm::getInstance()->getField('subject');
         $class = "row1";
         $total=0;
-        if($res && ($num=count($results))):
-            $ids=($errors && $_POST['tids'] && is_array($_POST['tids']))?$_POST['tids']:null;
-            foreach ($results as $row) {
-                $tag=$row['staff_id']?'assigned':'openticket';
+        $ids=($errors && $_POST['tids'] && is_array($_POST['tids']))?$_POST['tids']:null;
+        foreach ($tickets as $T) {
+            $total += 1;
+                $tag=$T['staff_id']?'assigned':'openticket';
                 $flag=null;
-                if($row['lock_id'])
+                if($T['lock__staff_id'] && $T['lock__staff_id'] != $thisstaff->getId())
                     $flag='locked';
-                elseif($row['isoverdue'])
+                elseif($T['isoverdue'])
                     $flag='overdue';
 
                 $lc='';
-                if($showassigned) {
-                    if($row['staff_id'])
-                        $lc=sprintf('<span class="Icon staffAssigned">%s</span>',Format::truncate($row['staff'],40));
-                    elseif($row['team_id'])
-                        $lc=sprintf('<span class="Icon teamAssigned">%s</span>',Format::truncate($row['team'],40));
-                    else
-                        $lc=' ';
-                }else{
-                    $lc=Format::truncate($row['dept_name'],40);
+                if ($showassigned) {
+                    if ($T['staff_id'])
+                        $lc = new AgentsName($T['staff__firstname'].' '.$T['staff__lastname']);
+                    elseif ($T['team_id'])
+                        $lc = Team::getLocalById($T['team_id'], 'name', $T['team__name']);
                 }
-                $tid=$row['number'];
-
-                $subject = Format::truncate($subject_field->display(
-                    $subject_field->to_php($row['subject']) ?: $row['subject']
-                ), 40);
-                $threadcount=$row['thread_count'];
-                if(!strcasecmp($row['state'],'open') && !$row['isanswered'] && !$row['lock_id']) {
+                else {
+                    $lc = Dept::getLocalById($T['dept_id'], 'name', $T['dept__name']);
+                }
+                $tid=$T['number'];
+                $subject = $subject_field->display($subject_field->to_php($T['cdata__subject']));
+                $threadcount=$T['thread_count'];
+                if(!strcasecmp($T['status__state'],'open') && !$T['isanswered'] && !$T['lock__staff_id']) {
                     $tid=sprintf('<b>%s</b>',$tid);
                 }
                 ?>
-            <tr id="<?php echo $row['ticket_id']; ?>">
+            <tr id="<?php echo $T['ticket_id']; ?>">
                 <?php if($thisstaff->canManageTickets()) {
 
                     $sel=false;
-                    if($ids && in_array($row['ticket_id'], $ids))
+                    if($ids && in_array($T['ticket_id'], $ids))
                         $sel=true;
                     ?>
                 <td align="center" class="nohover">
                     <input class="ckb" type="checkbox" name="tids[]"
-                        value="<?php echo $row['ticket_id']; ?>" <?php echo $sel?'checked="checked"':''; ?>>
+                        value="<?php echo $T['ticket_id']; ?>" <?php echo $sel?'checked="checked"':''; ?>>
                 </td>
                 <?php } ?>
-                <td title="<?php echo $row['email']; ?>" nowrap>
-                  <a class="Icon <?php echo strtolower($row['source']); ?>Ticket ticketPreview"
-                    title="<?php echo __('Preview Ticket'); ?>"
-                    href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $tid; ?></a></td>
-                <td align="center" nowrap><?php echo Format::db_datetime($row['effective_date']); ?></td>
-                <td><a <?php if ($flag) { ?> class="Icon <?php echo $flag; ?>Ticket" title="<?php echo ucfirst($flag); ?> Ticket" <?php } ?>
-                    href="tickets.php?id=<?php echo $row['ticket_id']; ?>"><?php echo $subject; ?></a>
-                     <?php
-                        if ($threadcount>1)
-                            echo "<small>($threadcount)</small>&nbsp;".'<i
-                                class="icon-fixed-width icon-comments-alt"></i>&nbsp;';
-                        if ($row['collaborators'])
-                            echo '<i class="icon-fixed-width icon-group faded"></i>&nbsp;';
-                        if ($row['attachments'])
-                            echo '<i class="icon-fixed-width icon-paperclip"></i>&nbsp;';
-                    ?>
+                <td title="<?php echo $T['user__default_email__address']; ?>" nowrap>
+                  <a class="Icon <?php echo strtolower($T['source']); ?>Ticket preview"
+                    title="Preview Ticket"
+                    href="tickets.php?id=<?php echo $T['ticket_id']; ?>"
+                    data-preview="#tickets/<?php echo $T['ticket_id']; ?>/preview"
+                    ><?php echo $tid; ?></a></td>
+                <td align="center" nowrap><?php echo Format::datetime($T[$date_col ?: 'lastupdate']) ?: $date_fallback; ?></td>
+                <td><div style="max-width: <?php
+                    $base = 279;
+                    // Make room for the paperclip and some extra
+                    if ($T['attachment_count']) $base -= 18;
+                    // Assume about 8px per digit character
+                    if ($threadcount > 1) $base -= 20 + ((int) log($threadcount, 10) + 1) * 8;
+                    // Make room for overdue flag and friends
+                    if ($flag) $base -= 20;
+                    echo $base; ?>px; max-height: 1.2em"
+                    class="<?php if ($flag) { ?>Icon <?php echo $flag; ?>Ticket <?php } ?>link truncate"
+                    <?php if ($flag) { ?> title="<?php echo ucfirst($flag); ?> Ticket" <?php } ?>
+                    href="tickets.php?id=<?php echo $T['ticket_id']; ?>"><?php echo $subject; ?></div>
+<?php               if ($T['attachment_count'])
+                        echo '<i class="small icon-paperclip icon-flip-horizontal" data-toggle="tooltip" title="'
+                            .$T['attachment_count'].'"></i>';
+                    if ($threadcount > 1) { ?>
+                        <span class="pull-right faded-more"><i class="icon-comments-alt"></i>
+                            <small><?php echo $threadcount; ?></small>
+                        </span>
+                    <?php } ?>
                 </td>
-                <td nowrap>&nbsp;<?php echo Format::htmlchars(
-                        Format::truncate($row['name'], 22, strpos($row['name'], '@'))); ?>&nbsp;</td>
+                <td nowrap><div><?php
+                    if ($T['collab_count'])
+                        echo '<span class="pull-right faded-more" data-toggle="tooltip" title="'
+                            .$T['collab_count'].'"><i class="icon-group"></i></span>';
+                    ?><span class="truncate" style="max-width:<?php
+                        echo $T['collab_count'] ? '150px' : '170px'; ?>"><?php
+                    $un = new UsersName($T['user__name']);
+                        echo Format::htmlchars($un);
+                    ?></span></div></td>
                 <?php
                 if($search && !$status){
-                    $displaystatus=ucfirst($row['status']);
-                    if(!strcasecmp($row['state'],'open'))
+                    $displaystatus=TicketStatus::getLocalById($T['status_id'], 'value', $T['status__name']);
+                    if(!strcasecmp($T['status__state'],'open'))
                         $displaystatus="<b>$displaystatus</b>";
                     echo "<td>$displaystatus</td>";
                 } else { ?>
-                <td class="nohover" align="center" style="background-color:<?php echo $row['priority_color']; ?>;">
-                    <?php echo $row['priority_desc']; ?></td>
+                <td class="nohover" align="center"
+                    style="background-color:<?php echo $T['cdata__:priority__priority_color']; ?>;">
+                    <?php echo $T['cdata__:priority__priority_desc']; ?></td>
                 <?php
                 }
                 ?>
-                <td nowrap>&nbsp;<?php echo $lc; ?></td>
+                <td nowrap><span class="truncate" style="max-width: 169px"><?php
+                    echo Format::htmlchars($lc); ?></span></td>
             </tr>
             <?php
-            } //end of while.
-        else: //not tickets found!! set fetch error.
+            } //end of foreach
+        if (!$total)
             $ferror=__('There are no tickets matching your criteria.');
-        endif; ?>
+        ?>
     </tbody>
     <tfoot>
      <tr>
         <td colspan="7">
-            <?php if($res && $num && $thisstaff->canManageTickets()){ ?>
+            <?php if($total && $thisstaff->canManageTickets()){ ?>
             <?php echo __('Select');?>:&nbsp;
             <a id="selectAll" href="#ckb"><?php echo __('All');?></a>&nbsp;&nbsp;
             <a id="selectNone" href="#ckb"><?php echo __('None');?></a>&nbsp;&nbsp;
@@ -517,8 +618,11 @@ if ($results) {
     </tfoot>
     </table>
     <?php
-    if ($num>0) { //if we actually had any tickets returned.
-        echo '<div>&nbsp;'.__('Page').':'.$pageNav->getPageLinks().'&nbsp;';
+    if ($total>0) { //if we actually had any tickets returned.
+?>      <div>
+            <span class="faded pull-right"><?php echo $pageNav->showing(); ?></span>
+<?php
+        echo __('Page').':'.$pageNav->getPageLinks().'&nbsp;';
         echo sprintf('<a class="export-csv no-pjax" href="?%s">%s</a>',
                 Http::build_query(array(
                         'a' => 'export', 'h' => $hash,
@@ -548,160 +652,9 @@ if ($results) {
      </p>
     <div class="clear"></div>
 </div>
-
-<div class="dialog" style="display:none;" id="advanced-search">
-    <h3><?php echo __('Advanced Ticket Search');?></h3>
-    <a class="close" href=""><i class="icon-remove-circle"></i></a>
-    <hr/>
-    <form action="tickets.php" method="post" id="search" name="search">
-        <input type="hidden" name="a" value="search">
-        <fieldset class="query">
-            <input type="input" id="query" name="query" size="20" placeholder="<?php echo __('Keywords') . ' &mdash; ' . __('Optional'); ?>">
-        </fieldset>
-        <fieldset class="span6">
-            <label for="statusId"><?php echo __('Statuses');?>:</label>
-            <select id="statusId" name="statusId">
-                 <option value="">&mdash; <?php echo __('Any Status');?> &mdash;</option>
-                <?php
-                foreach (TicketStatusList::getStatuses(
-                            array('states' => array('open', 'closed'))) as $s) {
-                    echo sprintf('<option data-state="%s" value="%d">%s</option>',
-                            $s->getState(), $s->getId(), __($s->getName()));
-                }
-                ?>
-            </select>
-        </fieldset>
-        <fieldset class="span6">
-            <label for="deptId"><?php echo __('Departments');?>:</label>
-            <select id="deptId" name="deptId">
-                <option value="">&mdash; <?php echo __('All Departments');?> &mdash;</option>
-                <?php
-                if(($mydepts = $thisstaff->getDepts()) && ($depts=Dept::getDepartments())) {
-                    foreach($depts as $id =>$name) {
-                        if(!in_array($id, $mydepts)) continue;
-                        echo sprintf('<option value="%d">%s</option>', $id, $name);
-                    }
-                }
-                ?>
-            </select>
-        </fieldset>
-        <fieldset class="span6">
-            <label for="flag"><?php echo __('Flags');?>:</label>
-            <select id="flag" name="flag">
-                 <option value="">&mdash; <?php echo __('Any Flags');?> &mdash;</option>
-                 <?php
-                 if (!$cfg->showAnsweredTickets()) { ?>
-                 <option data-state="open" value="answered"><?php echo __('Answered');?></option>
-                 <?php
-                 } ?>
-                 <option data-state="open" value="overdue"><?php echo __('Overdue');?></option>
-            </select>
-        </fieldset>
-        <fieldset class="owner span6">
-            <label for="assignee"><?php echo __('Assigned To');?>:</label>
-            <select id="assignee" name="assignee">
-                <option value="">&mdash; <?php echo __('Anyone');?> &mdash;</option>
-                <option value="s0">&mdash; <?php echo __('Unassigned');?> &mdash;</option>
-                <option value="s<?php echo $thisstaff->getId(); ?>"><?php echo __('Me');?></option>
-                <?php
-                if(($users=Staff::getStaffMembers())) {
-                    echo '<OPTGROUP label="'.sprintf(__('Agents (%d)'),count($users)-1).'">';
-                    foreach($users as $id => $name) {
-                        if ($id == $thisstaff->getId())
-                            continue;
-                        $k="s$id";
-                        echo sprintf('<option value="%s">%s</option>', $k, $name);
-                    }
-                    echo '</OPTGROUP>';
-                }
-
-                if(($teams=Team::getTeams())) {
-                    echo '<OPTGROUP label="'.__('Teams').' ('.count($teams).')">';
-                    foreach($teams as $id => $name) {
-                        $k="t$id";
-                        echo sprintf('<option value="%s">%s</option>', $k, $name);
-                    }
-                    echo '</OPTGROUP>';
-                }
-                ?>
-            </select>
-        </fieldset>
-        <fieldset class="span6">
-            <label for="topicId"><?php echo __('Help Topics');?>:</label>
-            <select id="topicId" name="topicId">
-                <option value="" selected >&mdash; <?php echo __('All Help Topics');?> &mdash;</option>
-                <?php
-                if($topics=Topic::getHelpTopics()) {
-                    foreach($topics as $id =>$name)
-                        echo sprintf('<option value="%d" >%s</option>', $id, $name);
-                }
-                ?>
-            </select>
-        </fieldset>
-        <fieldset class="owner span6">
-            <label for="staffId"><?php echo __('Closed By');?>:</label>
-            <select id="staffId" name="staffId">
-                <option value="0">&mdash; <?php echo __('Anyone');?> &mdash;</option>
-                <option value="<?php echo $thisstaff->getId(); ?>"><?php echo __('Me');?></option>
-                <?php
-                if(($users=Staff::getStaffMembers())) {
-                    foreach($users as $id => $name)
-                        echo sprintf('<option value="%d">%s</option>', $id, $name);
-                }
-                ?>
-            </select>
-        </fieldset>
-        <fieldset class="date_range">
-            <label><?php echo __('Date Range').' &mdash; '.__('Create Date');?>:</label>
-            <input class="dp" type="input" size="20" name="startDate">
-            <span class="between"><?php echo __('TO');?></span>
-            <input class="dp" type="input" size="20" name="endDate">
-        </fieldset>
-        <?php
-        $tform = TicketForm::objects()->one();
-        echo $tform->getForm()->getMedia();
-        foreach ($tform->getInstance()->getFields() as $f) {
-            if (!$f->hasData())
-                continue;
-            elseif (!$f->getImpl()->hasSpecialSearch())
-                continue;
-            ?><fieldset class="span6">
-            <label><?php echo $f->getLabel(); ?>:</label><div><?php
-                     $f->render('search'); ?></div>
-            </fieldset>
-        <?php } ?>
-        <hr/>
-        <div id="result-count" class="clear"></div>
-        <p>
-            <span class="buttons pull-right">
-                <input type="submit" value="<?php echo __('Search');?>">
-            </span>
-            <span class="buttons pull-left">
-                <input type="reset" value="<?php echo __('Reset');?>">
-                <input type="button" value="<?php echo __('Cancel');?>" class="close">
-            </span>
-            <span class="spinner">
-                <img src="./images/ajax-loader.gif" width="16" height="16">
-            </span>
-        </p>
-    </form>
-</div>
 <script type="text/javascript">
 $(function() {
-    $(document).off('.tickets');
-    $(document).on('click.tickets', 'a.tickets-action', function(e) {
-        e.preventDefault();
-        var count = checkbox_checker($('form#tickets'), 1);
-        if (count) {
-            var url = 'ajax.php/'
-            +$(this).attr('href').substr(1)
-            +'?count='+count
-            +'&_uid='+new Date().getTime();
-            $.dialog(url, [201], function (xhr) {
-                window.location.href = window.location.href;
-             });
-        }
-        return false;
-    });
+    $('[data-toggle=tooltip]').tooltip();
 });
 </script>
+
diff --git a/include/staff/tpl.inc.php b/include/staff/tpl.inc.php
index fe048decd06309b9f2f3383aa3950bb0bcbed687..7f0839f8b8e05538f66d04e0eaaf4d135280d165 100644
--- a/include/staff/tpl.inc.php
+++ b/include/staff/tpl.inc.php
@@ -35,13 +35,10 @@ $tpl=$msgtemplates[$selected];
 
 ?>
 <form method="get" action="templates.php?">
-<h2><span><?php echo __('Email Template Set');
-    ?> &nbsp;/&nbsp; <span><a href="templates.php?tpl_id=<?php echo $tpl_id; ?>"><?php echo $name; ?></a>
-    <input type="hidden" name="a" value="manage">
-    <input type="hidden" name="tpl_id" value="<?php echo $tpl_id; ?>">
+<h2>
 <div class="pull-right">
     <span style="font-size:10pt"><?php echo __('Viewing'); ?>:</span>
-    <select id="tpl_options" name="id" style="width:300px;">
+    <select id="tpl_options" name="id" style="width:250px;">
         <option value="">&mdash; <?php echo __('Select Setting Group'); ?> &mdash;</option>
         <?php
         $impl = $group->getTemplates();
@@ -70,9 +67,13 @@ $tpl=$msgtemplates[$selected];
             echo "</optgroup>";
         ?>
     </select>
-    <input type="submit" value="Go">
     </div>
+
+    <span><?php echo __('Email Template Set'); ?></span>
+    <small> — <a href="templates.php?tpl_id=<?php echo $tpl_id; ?>"><?php echo $name; ?></a></small>
 </h2>
+    <input type="hidden" name="a" value="manage">
+    <input type="hidden" name="tpl_id" value="<?php echo $tpl_id; ?>">
 </form>
 <hr/>
 <form action="templates.php?id=<?php echo $id; ?>&amp;a=manage" method="post" id="save">
@@ -99,6 +100,19 @@ $tpl=$msgtemplates[$selected];
 <?php } ?>
 </div>
 
+<?php
+$invalid = array();
+if ($template instanceof EmailTemplate) {
+    if ($invalid = $template->getInvalidVariableUsage()) {
+    $invalid = array_unique($invalid); ?>
+    <div class="warning-banner"><?php echo
+        __('Some variables may not be a valid for this context. Please check for spelling errors and correct usage for this template.') ?>
+    <br/>
+    <code><?php echo implode(', ', $invalid); ?></code>
+</div>
+<?php }
+} ?>
+
 <div style="padding-bottom:3px;" class="faded"><strong><?php echo __('Email Subject and Body'); ?>:</strong></div>
 <div id="toolbar"></div>
 <div id="save" style="padding-top:5px;">
@@ -108,9 +122,11 @@ $tpl=$msgtemplates[$selected];
     </div>
     <input type="hidden" name="draft_id" value=""/>
     <textarea name="body" cols="21" rows="16" style="width:98%;" wrap="soft"
-        data-toolbar-external="#toolbar"
-        class="richtext draft" data-draft-namespace="tpl.<?php echo Format::htmlchars($selected); ?>"
-        data-draft-object-id="<?php echo $tpl_id; ?>"><?php echo $info['body']; ?></textarea>
+        data-root-context="<?php echo $selected; ?>"
+        data-toolbar-external="#toolbar" class="richtext draft" <?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('tpl.'.$selected, $tpl_id, $info['body']);
+    echo $attrs; ?>><?php echo $draft ?: $info['body'];
+    ?></textarea>
 </div>
 
 <p style="text-align:center">
diff --git a/include/staff/user-view.inc.php b/include/staff/user-view.inc.php
index 73142d895b6b8e764350fd0c8ef4c140afa7ad08..837d8a1cda8e34a86fa228c67a53e5832740f8b9 100644
--- a/include/staff/user-view.inc.php
+++ b/include/staff/user-view.inc.php
@@ -13,13 +13,19 @@ $org = $user->getOrganization();
              title="Reload"><i class="icon-refresh"></i> <?php echo Format::htmlchars($user->getName()); ?></a></h2>
         </td>
         <td width="50%" class="right_align has_bottom_border">
+<?php if (($account && $account->isConfirmed())
+    || $thisstaff->hasPerm(User::PERM_EDIT)) { ?>
             <span class="action-button pull-right" data-dropdown="#action-dropdown-more">
                 <i class="icon-caret-down pull-right"></i>
-                <span ><i class="icon-cog"></i> <?php echo __('More'); ?></span>
+                <span><i class="icon-cog"></i> <?php echo __('More'); ?></span>
             </span>
-            <a id="user-delete" class="action-button pull-right user-action"
+<?php }
+    if ($thisstaff->hasPerm(User::PERM_DELETE)) { ?>
+            <a id="user-delete" class="red button action-button pull-right user-action"
             href="#users/<?php echo $user->getId(); ?>/delete"><i class="icon-trash"></i>
             <?php echo __('Delete User'); ?></a>
+<?php } ?>
+<?php if ($thisstaff->hasPerm(User::PERM_MANAGE)) { ?>
             <?php
             if ($account) { ?>
             <a id="user-manage" class="action-button pull-right user-action"
@@ -32,6 +38,7 @@ $org = $user->getOrganization();
             <?php echo __('Register'); ?></a>
             <?php
             } ?>
+<?php } ?>
             <div id="action-dropdown-more" class="action-dropdown anchor-right">
               <ul>
                 <?php
@@ -48,36 +55,49 @@ $org = $user->getOrganization();
                         <?php echo __('Send Password Reset Email'); ?></a></li>
                     <?php
                     } ?>
+<?php if ($thisstaff->hasPerm(User::PERM_MANAGE)) { ?>
                     <li><a class="user-action"
                         href="#users/<?php echo $user->getId(); ?>/manage/access"><i
                         class="icon-lock"></i>
                         <?php echo __('Manage Account Access'); ?></a></li>
                 <?php
-
+}
                 } ?>
+<?php if ($thisstaff->hasPerm(User::PERM_EDIT)) { ?>
                 <li><a href="#ajax.php/users/<?php echo $user->getId();
                     ?>/forms/manage" onclick="javascript:
                     $.dialog($(this).attr('href').substr(1), 201);
                     return false"
                     ><i class="icon-paste"></i>
                     <?php echo __('Manage Forms'); ?></a></li>
+<?php } ?>
 
               </ul>
             </div>
         </td>
     </tr>
 </table>
-<table class="ticket_info" cellspacing="0" cellpadding="0" width="940" border="0">
+<div class="avatar pull-left" style="margin: 10px; width: 80px;">
+    <?php echo $user->getAvatar(); ?>
+</div>
+<table class="ticket_info" cellspacing="0" cellpadding="0" width="830" border="0">
     <tr>
         <td width="50%">
             <table border="0" cellspacing="" cellpadding="4" width="100%">
                 <tr>
                     <th width="150"><?php echo __('Name'); ?>:</th>
-                    <td><b><a href="#users/<?php echo $user->getId();
+                    <td>
+<?php
+if ($thisstaff->hasPerm(User::PERM_EDIT)) { ?>
+                    <b><a href="#users/<?php echo $user->getId();
                     ?>/edit" class="user-action"><i
-                    class="icon-edit"></i>&nbsp;<?php echo
-                    Format::htmlchars($user->getName()->getOriginal());
-                    ?></a></td>
+                        class="icon-edit"></i>
+<?php }
+                    echo Format::htmlchars($user->getName()->getOriginal());
+if ($thisstaff->hasPerm(User::PERM_EDIT)) { ?>
+                        </a></b>
+<?php } ?>
+                    </td>
                 </tr>
                 <tr>
                     <th><?php echo __('Email'); ?>:</th>
@@ -93,11 +113,12 @@ $org = $user->getOrganization();
                             if ($org)
                                 echo sprintf('<a href="#users/%d/org" class="user-action">%s</a>',
                                         $user->getId(), $org->getName());
-                            else
+                            elseif ($thisstaff->hasPerm(User::PERM_EDIT)) {
                                 echo sprintf(
                                     '<a href="#users/%d/org" class="user-action">%s</a>',
                                     $user->getId(),
                                     __('Add Organization'));
+                            }
                         ?>
                         </span>
                     </td>
@@ -113,11 +134,11 @@ $org = $user->getOrganization();
                 </tr>
                 <tr>
                     <th><?php echo __('Created'); ?>:</th>
-                    <td><?php echo Format::db_datetime($user->getCreateDate()); ?></td>
+                    <td><?php echo Format::datetime($user->getCreateDate()); ?></td>
                 </tr>
                 <tr>
                     <th><?php echo __('Updated'); ?>:</th>
-                    <td><?php echo Format::db_datetime($user->getUpdateDate()); ?></td>
+                    <td><?php echo Format::datetime($user->getUpdateDate()); ?></td>
                 </tr>
             </table>
         </td>
@@ -125,27 +146,28 @@ $org = $user->getOrganization();
 </table>
 <br>
 <div class="clear"></div>
-<ul class="tabs">
-    <li><a class="active" id="tickets_tab" href="#tickets"><i
-    class="icon-list-alt"></i>&nbsp;<?php echo __('User Tickets'); ?></a></li>
-    <li><a id="notes_tab" href="#notes"><i
+<ul class="clean tabs" id="user-view-tabs">
+    <li class="active"><a href="#tickets"><i
+    class="icon-list-alt"></i>&nbsp;<?php echo __('Tickets'); ?></a></li>
+    <li><a href="#notes"><i
     class="icon-pushpin"></i>&nbsp;<?php echo __('Notes'); ?></a></li>
 </ul>
-<div id="tickets" class="tab_content">
-<?php
-include STAFFINC_DIR . 'templates/tickets.tmpl.php';
-?>
-</div>
+<div id="user-view-tabs_container">
+    <div id="tickets" class="tab_content">
+    <?php
+    include STAFFINC_DIR . 'templates/tickets.tmpl.php';
+    ?>
+    </div>
 
-<div class="tab_content" id="notes" style="display:none">
-<?php
-$notes = QuickNote::forUser($user);
-$create_note_url = 'users/'.$user->getId().'/note';
-include STAFFINC_DIR . 'templates/notes.tmpl.php';
-?>
+    <div class="hidden tab_content" id="notes">
+    <?php
+    $notes = QuickNote::forUser($user);
+    $create_note_url = 'users/'.$user->getId().'/note';
+    include STAFFINC_DIR . 'templates/notes.tmpl.php';
+    ?>
+    </div>
 </div>
-
-<div style="display:none;" class="dialog" id="confirm-action">
+<div class="hidden dialog" id="confirm-action">
     <h3><?php echo __('Please Confirm'); ?></h3>
     <a class="close" href=""><i class="icon-remove-circle"></i></a>
     <hr/>
diff --git a/include/staff/users.inc.php b/include/staff/users.inc.php
index ab21a960653dc950c66ede49df993c4eceaae6e4..afb54e57b51daeb501d6340d22e5052a3ff193f7 100644
--- a/include/staff/users.inc.php
+++ b/include/staff/users.inc.php
@@ -1,218 +1,213 @@
 <?php
 if(!defined('OSTSCPINC') || !$thisstaff) die('Access Denied');
 
-$qs = array();
-
-$select = 'SELECT user.*, email.address as email, org.name as organization
-          , account.id as account_id, account.status as account_status ';
-
-$from = 'FROM '.USER_TABLE.' user '
-      . 'LEFT JOIN '.USER_EMAIL_TABLE.' email ON (user.id = email.user_id) '
-      . 'LEFT JOIN '.ORGANIZATION_TABLE.' org ON (user.org_id = org.id) '
-      . 'LEFT JOIN '.USER_ACCOUNT_TABLE.' account ON (account.user_id = user.id) ';
-
-$where='WHERE 1 ';
+// Ensure cdata
+UserForm::ensureDynamicDataView();
 
+$qs = array();
+$users = User::objects()
+    ->annotate(array('ticket_count'=>SqlAggregate::COUNT('tickets')));
 
 if ($_REQUEST['query']) {
-
-    $from .=' LEFT JOIN '.FORM_ENTRY_TABLE.' entry
-                ON (entry.object_type=\'U\' AND entry.object_id = user.id)
-              LEFT JOIN '.FORM_ANSWER_TABLE.' value
-                ON (value.entry_id=entry.id) ';
-
-    $search = db_input(strtolower($_REQUEST['query']), false);
-    $where .= ' AND (
-                    email.address LIKE \'%'.$search.'%\'
-                    OR user.name LIKE \'%'.$search.'%\'
-                    OR org.name LIKE \'%'.$search.'%\'
-                    OR value.value LIKE \'%'.$search.'%\'
-                )';
-
+    $search = $_REQUEST['query'];
+    $users->filter(Q::any(array(
+        'emails__address__contains' => $search,
+        'name__contains' => $search,
+        'org__name__contains' => $search,
+        // TODO: Add search for cdata
+    )));
     $qs += array('query' => $_REQUEST['query']);
 }
 
-$sortOptions = array('name' => 'user.name',
-                     'email' => 'email.address',
-                     'status' => 'account_status',
-                     'create' => 'user.created',
-                     'update' => 'user.updated');
-$orderWays = array('DESC'=>'DESC','ASC'=>'ASC');
+$sortOptions = array('name' => 'name',
+                     'email' => 'emails__address',
+                     'status' => 'account__status',
+                     'create' => 'created',
+                     'update' => 'updated');
+$orderWays = array('DESC'=>'-','ASC'=>'');
 $sort= ($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])]) ? strtolower($_REQUEST['sort']) : 'name';
 //Sorting options...
 if ($sort && $sortOptions[$sort])
     $order_column =$sortOptions[$sort];
 
-$order_column = $order_column ?: 'user.name';
+$order_column = $order_column ?: 'name';
 
 if ($_REQUEST['order'] && $orderWays[strtoupper($_REQUEST['order'])])
     $order = $orderWays[strtoupper($_REQUEST['order'])];
 
-$order=$order ?: 'ASC';
 if ($order_column && strpos($order_column,','))
     $order_column = str_replace(','," $order,",$order_column);
 
 $x=$sort.'_sort';
-$$x=' class="'.strtolower($order).'" ';
-$order_by="$order_column $order ";
+$$x=' class="'.($order == '' ? 'asc' : 'desc').'" ';
 
-$total=db_count('SELECT count(DISTINCT user.id) '.$from.' '.$where);
+$total = $users->count();
 $page=($_GET['p'] && is_numeric($_GET['p']))?$_GET['p']:1;
 $pageNav=new Pagenate($total,$page,PAGE_LIMIT);
+$pageNav->paginate($users);
+
 $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');
-
-$select .= ', count(DISTINCT ticket.ticket_id) as tickets ';
-
-$from .= ' LEFT JOIN '.TICKET_TABLE.' ticket ON (ticket.user_id = user.id) ';
+$qstr.='&amp;order='.($order=='-' ? 'ASC' : 'DESC');
 
-
-$query="$select $from $where GROUP BY user.id ORDER BY $order_by LIMIT ".$pageNav->getStart().",".$pageNav->getLimit();
 //echo $query;
-$qhash = md5($query);
-$_SESSION['users_qs_'.$qhash] = $query;
+$_SESSION[':Q:users'] = $users;
 
+$users->values('id', 'name', 'default_email__address', 'account__id',
+    'account__status', 'created', 'updated');
+$users->order_by($order . $order_column);
 ?>
-<h2><?php echo __('User Directory'); ?></h2>
-<div class="pull-left">
-    <form action="users.php" method="get">
-        <?php csrf_token(); ?>
-        <input type="hidden" name="a" value="search">
-        <table>
-            <tr>
-                <td><input type="text" id="basic-user-search" name="query" size=30 value="<?php echo Format::htmlchars($_REQUEST['query']); ?>"
-                autocomplete="off" autocorrect="off" autocapitalize="off"></td>
-                <td><input type="submit" name="basic_search" class="button" value="<?php echo __('Search'); ?>"></td>
-                <!-- <td>&nbsp;&nbsp;<a href="" id="advanced-user-search">[advanced]</a></td> -->
-            </tr>
-        </table>
-    </form>
+<div id="basic_search">
+    <div style="min-height:25px;">
+        <form action="users.php" method="get">
+            <?php csrf_token(); ?>
+            <input type="hidden" name="a" value="search">
+            <div class="attached input">
+                <input type="text" class="basic-search" id="basic-user-search" name="query"
+                         size="30" value="<?php echo Format::htmlchars($_REQUEST['query']); ?>"
+                        autocomplete="off" autocorrect="off" autocapitalize="off">
+            <!-- <td>&nbsp;&nbsp;<a href="" id="advanced-user-search">[advanced]</a></td> -->
+                <button type="submit" class="attached button"><i class="icon-search"></i>
+                </button>
+            </div>
+        </form>
+    </div>
  </div>
+<form id="users-list" action="users.php" method="POST" name="staff" >
 
-<div class="pull-right">
-    <a class="action-button popup-dialog"
-        href="#users/add">
-        <i class="icon-plus-sign"></i>
-        <?php echo __('Add User'); ?>
-    </a>
-    <a class="action-button popup-dialog"
-        href="#users/import">
-        <i class="icon-upload"></i>
-        <?php echo __('Import'); ?>
-    </a>
-    <span class="action-button" data-dropdown="#action-dropdown-more"
-        style="/*DELME*/ vertical-align:top; margin-bottom:0">
-        <i class="icon-caret-down pull-right"></i>
-        <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
-    </span>
-    <div id="action-dropdown-more" class="action-dropdown anchor-right">
-        <ul>
-            <li><a class="users-action" href="#delete">
-                <i class="icon-trash icon-fixed-width"></i>
-                <?php echo __('Delete'); ?></a></li>
-            <li><a href="#orgs/lookup/form" onclick="javascript:
-$.dialog('ajax.php/orgs/lookup/form', 201);
-return false;">
-                <i class="icon-group icon-fixed-width"></i>
-                <?php echo __('Add to Organization'); ?></a></li>
-<?php
-if ('disabled' != $cfg->getClientRegistrationMode()) { ?>
-            <li><a class="users-action" href="#reset">
-                <i class="icon-envelope icon-fixed-width"></i>
-                <?php echo __('Send Password Reset Email'); ?></a></li>
-            <li><a class="users-action" href="#register">
-                <i class="icon-smile icon-fixed-width"></i>
-                <?php echo __('Register'); ?></a></li>
-            <li><a class="users-action" href="#lock">
-                <i class="icon-lock icon-fixed-width"></i>
-                <?php echo __('Lock'); ?></a></li>
-            <li><a class="users-action" href="#unlock">
-                <i class="icon-unlock icon-fixed-width"></i>
-                <?php echo __('Unlock'); ?></a></li>
-<?php } # end of registration-enabled? ?>
-        </ul>
+<div style="margin-bottom:20px; padding-top:5px;">
+    <div class="sticky bar opaque">
+        <div class="content">
+            <div class="pull-left flush-left">
+                <h2><?php echo __('User Directory'); ?></h2>
+            </div>
+            <div class="pull-right">
+                <?php if ($thisstaff->hasPerm(User::PERM_CREATE)) { ?>
+                <a class="green button action-button popup-dialog"
+                   href="#users/add">
+                    <i class="icon-plus-sign"></i>
+                    <?php echo __('Add User'); ?>
+                </a>
+                <a class="action-button popup-dialog"
+                   href="#users/import">
+                    <i class="icon-upload"></i>
+                    <?php echo __('Import'); ?>
+                </a>
+                <?php } ?>
+                <span class="action-button" data-dropdown="#action-dropdown-more"
+                      style="/*DELME*/ vertical-align:top; margin-bottom:0">
+                    <i class="icon-caret-down pull-right"></i>
+                    <span ><i class="icon-cog"></i> <?php echo __('More');?></span>
+                </span>
+                <div id="action-dropdown-more" class="action-dropdown anchor-right">
+                    <ul>
+                        <?php if ($thisstaff->hasPerm(User::PERM_EDIT)) { ?>
+                        <li><a href="#add-to-org" class="users-action">
+                            <i class="icon-group icon-fixed-width"></i>
+                            <?php echo __('Add to Organization'); ?></a></li>
+                        <?php
+                            }
+                        if ('disabled' != $cfg->getClientRegistrationMode()) { ?>
+                        <li><a class="users-action" href="#reset">
+                            <i class="icon-envelope icon-fixed-width"></i>
+                            <?php echo __('Send Password Reset Email'); ?></a></li>
+                        <?php if ($thisstaff->hasPerm(User::PERM_MANAGE)) { ?>
+                        <li><a class="users-action" href="#register">
+                            <i class="icon-smile icon-fixed-width"></i>
+                            <?php echo __('Register'); ?></a></li>
+                        <li><a class="users-action" href="#lock">
+                            <i class="icon-lock icon-fixed-width"></i>
+                            <?php echo __('Lock'); ?></a></li>
+                        <li><a class="users-action" href="#unlock">
+                            <i class="icon-unlock icon-fixed-width"></i>
+                            <?php echo __('Unlock'); ?></a></li>
+                        <?php }
+                        if ($thisstaff->hasPerm(User::PERM_DELETE)) { ?>
+                        <li class="danger"><a class="users-action" href="#delete">
+                            <i class="icon-trash icon-fixed-width"></i>
+                            <?php echo __('Delete'); ?></a></li>
+                        <?php }
+                        } # end of registration-enabled? ?>
+                    </ul>
+                </div>
+            </div>
+        </div>
     </div>
 </div>
-
 <div class="clear"></div>
 <?php
 $showing = $search ? __('Search Results').': ' : '';
-$res = db_query($query);
-if($res && ($num=db_num_rows($res)))
+if($users->exists(true))
     $showing .= $pageNav->showing();
 else
     $showing .= __('No users found!');
 ?>
-<form id="users-list" action="users.php" method="POST" name="staff" >
  <?php csrf_token(); ?>
  <input type="hidden" name="do" value="mass_process" >
  <input type="hidden" id="action" name="a" value="" >
  <input type="hidden" id="selected-count" name="count" value="" >
  <input type="hidden" id="org_id" name="org_id" value="" >
  <table class="list" border="0" cellspacing="1" cellpadding="0" width="940">
-    <caption><?php echo $showing; ?></caption>
     <thead>
         <tr>
-            <th nowrap width="12"> </th>
-            <th width="350"><a <?php echo $name_sort; ?> href="users.php?<?php
+            <th nowrap width="4%">&nbsp;</th>
+            <th><a <?php echo $name_sort; ?> href="users.php?<?php
                 echo $qstr; ?>&sort=name"><?php echo __('Name'); ?></a></th>
-            <th width="250"><a  <?php echo $status_sort; ?> href="users.php?<?php
+            <th width="22%"><a  <?php echo $status_sort; ?> href="users.php?<?php
                 echo $qstr; ?>&sort=status"><?php echo __('Status'); ?></a></th>
-            <th width="100"><a <?php echo $create_sort; ?> href="users.php?<?php
+            <th width="20%"><a <?php echo $create_sort; ?> href="users.php?<?php
                 echo $qstr; ?>&sort=create"><?php echo __('Created'); ?></a></th>
-            <th width="145"><a <?php echo $update_sort; ?> href="users.php?<?php
+            <th width="20%"><a <?php echo $update_sort; ?> href="users.php?<?php
                 echo $qstr; ?>&sort=update"><?php echo __('Updated'); ?></a></th>
         </tr>
     </thead>
     <tbody>
     <?php
-        if($res && db_num_rows($res)):
-            $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null;
-            while ($row = db_fetch_array($res)) {
+        $ids=($errors && is_array($_POST['ids']))?$_POST['ids']:null;
+        foreach ($users as $U) {
                 // Default to email address mailbox if no name specified
-                if (!$row['name'])
-                    list($name) = explode('@', $row['email']);
+                if (!$U['name'])
+                    list($name) = explode('@', $U['default_email__address']);
                 else
-                    $name = new PersonsName($row['name']);
+                    $name = new UsersName($U['name']);
 
                 // Account status
-                if ($row['account_id'])
-                    $status = new UserAccountStatus($row['account_status']);
+                if ($U['account__id'])
+                    $status = new UserAccountStatus($U['account__status']);
                 else
                     $status = __('Guest');
 
                 $sel=false;
-                if($ids && in_array($row['id'], $ids))
+                if($ids && in_array($U['id'], $ids))
                     $sel=true;
                 ?>
-               <tr id="<?php echo $row['id']; ?>">
-                <td nowrap>
-                    <input type="checkbox" value="<?php echo $row['id']; ?>" class="ckb mass nowarn"/>
+               <tr id="<?php echo $U['id']; ?>">
+                <td nowrap align="center">
+                    <input type="checkbox" value="<?php echo $U['id']; ?>" class="ckb mass nowarn"/>
                 </td>
                 <td>&nbsp;
-                    <a class="userPreview" href="users.php?id=<?php echo $row['id']; ?>"><?php
+                    <a class="preview"
+                        href="users.php?id=<?php echo $U['id']; ?>"
+                        data-preview="#users/<?php echo $U['id']; ?>/preview"><?php
                         echo Format::htmlchars($name); ?></a>
                     &nbsp;
                     <?php
-                    if ($row['tickets'])
+                    if ($U['ticket_count'])
                          echo sprintf('<i class="icon-fixed-width icon-file-text-alt"></i>
-                             <small>(%d)</small>', $row['tickets']);
+                             <small>(%d)</small>', $U['ticket_count']);
                     ?>
                 </td>
                 <td><?php echo $status; ?></td>
-                <td><?php echo Format::db_date($row['created']); ?></td>
-                <td><?php echo Format::db_datetime($row['updated']); ?>&nbsp;</td>
+                <td><?php echo Format::date($U['created']); ?></td>
+                <td><?php echo Format::datetime($U['updated']); ?>&nbsp;</td>
                </tr>
-            <?php
-            } //end of while.
-        endif; ?>
+<?php   } //end of foreach. ?>
     </tbody>
     <tfoot>
      <tr>
         <td colspan="7">
-            <?php if ($res && $num) { ?>
+            <?php if ($total) { ?>
             <?php echo __('Select');?>:&nbsp;
             <a id="selectAll" href="#ckb"><?php echo __('All');?></a>&nbsp;&nbsp;
             <a id="selectNone" href="#ckb"><?php echo __('None');?></a>&nbsp;&nbsp;
@@ -227,12 +222,12 @@ else
     </tfoot>
 </table>
 <?php
-if($res && $num): //Show options..
+if ($total) {
     echo sprintf('<div>&nbsp;'.__('Page').': %s &nbsp; <a class="no-pjax"
             href="users.php?a=export&qh=%s">'.__('Export').'</a></div>',
             $pageNav->getPageLinks(),
             $qhash);
-endif;
+}
 ?>
 </form>
 
@@ -273,14 +268,36 @@ $(function() {
             ids.push($(this).val());
         });
         if (ids.length) {
-          var submit = function() {
+          var submit = function(data) {
             $form.find('#action').val(action);
             $.each(ids, function() { $form.append($('<input type="hidden" name="ids[]">').val(this)); });
+            if (data)
+              $.each(data, function() { $form.append($('<input type="hidden">').attr('name', this.name).val(this.value)); });
             $form.find('#selected-count').val(ids.length);
             $form.submit();
           };
+          var options = {};
+          if (action === 'delete') {
+              options['deletetickets']
+                =  __('Also delete all associated tickets and attachments');
+          }
+          else if (action === 'add-to-org') {
+            $.dialog('ajax.php/orgs/lookup/form', 201, function(xhr, json) {
+              var $form = $('form#users-list');
+              try {
+                  var json = $.parseJSON(json),
+                      org_id = $form.find('#org_id');
+                  if (json.id) {
+                      org_id.val(json.id);
+                      goBaby('setorg', true);
+                  }
+              }
+              catch (e) { }
+            });
+            return;
+          }
           if (!confirmed)
-              $.confirm(__('You sure?')).then(submit);
+              $.confirm(__('You sure?'), undefined, options).then(submit);
           else
               submit();
         }
@@ -294,18 +311,6 @@ $(function() {
         goBaby($(this).attr('href').substr(1));
         return false;
     });
-    $(document).on('dialog:close', function(e, json) {
-        $form = $('form#users-list');
-        try {
-            var json = $.parseJSON(json),
-                org_id = $form.find('#org_id');
-            if (json.id) {
-                org_id.val(json.id);
-                goBaby('setorg', true);
-            }
-        }
-        catch (e) { }
-    });
 });
 </script>
 
diff --git a/include/upgrader/aborted.inc.php b/include/upgrader/aborted.inc.php
index 5e4b1e9f74e8ef35fa66a0412214aec07537e6f7..846d5cd7238567a9243b92e1cc65ff711e4e9a7b 100644
--- a/include/upgrader/aborted.inc.php
+++ b/include/upgrader/aborted.inc.php
@@ -27,7 +27,7 @@ if(!defined('OSTSCPINC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access D
     </div>
     <p><strong><?php echo __('Need Help?');?></strong> <?php echo sprintf(__('We provide %1$s professional upgrade services %2$s and commercial support.'), '<a target="_blank" href="http://osticket.com/support/professional_services.php"><u>','</u></a>'); echo sprintf(__('%1$s Contact us %2$s today for <u>expedited</u> help.'), '<a target="_blank" href="http://osticket.com/support/">','</a>');?></p>
   </div>
-  <div id="sidebar">
+  <div class="sidebar">
     <h3><?php echo __('What to do?');?></h3>
     <p><?php echo sprintf(__('Restore your previous version from backup and try again or %1$s seek help %2$s.'), '<a target="_blank" href="http://osticket.com/support/">','</a>');?></p>
   </div>
diff --git a/include/upgrader/done.inc.php b/include/upgrader/done.inc.php
index 4542b6f4c9804a82d7b573a7b3285376f90f41f2..58edda1cb125c666acc72c9627170b0e9da9c292 100644
--- a/include/upgrader/done.inc.php
+++ b/include/upgrader/done.inc.php
@@ -21,11 +21,11 @@ $_SESSION['ost_upgrader']=null;
         <br>
         <p><b><?php echo __('PS');?></b>: <?php echo __("Don't just make customers happy, make happy customers!");?></p>
     </div>
-    <div id="sidebar">
+    <div class="sidebar">
             <h3><?php echo __("What's Next?");?></h3>
             <p><b><?php echo __('Post-upgrade');?></b>: <?php
             echo sprintf(__('You can now go to %s to enable the system and explore the new features. For complete and up-to-date release notes see the %s'),
-                sprintf('<a href="scp/settings.php" target="_blank">%s</a>', __('Admin Panel')),
+                sprintf('<a href="'. ROOT_PATH . 'scp/settings.php" target="_blank">%s</a>', __('Admin Panel')),
                 sprintf('<a href="http://osticket.com/wiki/Release_Notes" target="_blank">%s</a>', __('osTicket Wiki')));?></p>
             <p><b><?php echo __('Stay up to date');?></b>: <?php echo __("It's important to keep your osTicket installation up to date. Get announcements, security updates and alerts delivered directly to you!");?>
             <?php echo sprintf(__('%1$s Get in the loop %2$s today and stay informed!'), '<a target="_blank" href="http://osticket.com/subscribe.php">', '</a>');?></p>
diff --git a/include/upgrader/prereq.inc.php b/include/upgrader/prereq.inc.php
index 8cdd98e97e87ed89ac94d4a4ecdfa78bc4c30375..7ab7b4c5b5b98dd7cb272b34854fecb69c4fe558 100644
--- a/include/upgrader/prereq.inc.php
+++ b/include/upgrader/prereq.inc.php
@@ -15,7 +15,7 @@ if(!defined('OSTSCPINC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access D
             <?php echo __('These items are necessary in order to run the latest version of osTicket.');?>
             <ul class="progress">
                 <li class="<?php echo $upgrader->check_php()?'yes':'no'; ?>">
-                <?php echo sprintf(__('%s or later'), 'PHP v5.3'); ?> - (<small><b><?php echo PHP_VERSION; ?></b></small>)</li>
+                <?php echo sprintf(__('%s or later'), 'PHP v5.4'); ?> - (<small><b><?php echo PHP_VERSION; ?></b></small>)</li>
                 <li class="<?php echo $upgrader->check_mysql()?'yes':'no'; ?>">
                 <?php echo __('MySQLi extension for PHP'); ?>- (<small><b><?php
                     echo extension_loaded('mysqli')?__('module loaded'):__('missing!'); ?></b></small>)</li>
@@ -36,7 +36,7 @@ if(!defined('OSTSCPINC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access D
                 </form>
             </div>
     </div>
-    <div id="sidebar">
+    <div class="sidebar pull-right">
             <h3><?php echo __('Upgrade Tips');?></h3>
             <p>1. <?php echo __('Remember to back up your osTicket database');?></p>
             <p>2. <?php echo sprintf(__('Refer to %1$s Upgrade Guide %2$s for the latest tips'), '<a href="http://osticket.com/wiki/Upgrade_and_Migration" target="_blank">', '</a>');?></p>
diff --git a/include/upgrader/rename.inc.php b/include/upgrader/rename.inc.php
index 408c2f75a06a066f39c3f1a8c7ed93b525557ed9..c553f173fe5e788a5f4250897a7321b1e98b873e 100644
--- a/include/upgrader/rename.inc.php
+++ b/include/upgrader/rename.inc.php
@@ -24,7 +24,7 @@ if(!defined('OSTSCPINC') || !$thisstaff || !$thisstaff->isAdmin()) die('Access D
                 </form>
             </div>
     </div>
-    <div id="sidebar">
+    <div class="sidebar">
             <h3><?php echo __('Need Help?');?></h3>
             <p>
             <?php echo __('If you are looking for a greater level of support, we provide <u>professional upgrade</u> and commercial support with guaranteed response times and access to the core development team. We can also help customize osTicket or even add new features to the system to meet your unique needs. <a target="_blank" href="http://osticket.com/support">Learn More!</a>'); ?>
diff --git a/include/upgrader/streams/core.sig b/include/upgrader/streams/core.sig
index 3c66643467933d668c14629d21d672cb2db1eaf3..f6aa807b2faf040f3616a572ef0292b9182d2662 100644
--- a/include/upgrader/streams/core.sig
+++ b/include/upgrader/streams/core.sig
@@ -1 +1 @@
-b26f29a6bb5dbb3510b057632182d138
+98ad7d550c26ac44340350912296e673
diff --git a/include/upgrader/streams/core/03ff59bf-b26f29a6.patch.sql b/include/upgrader/streams/core/03ff59bf-b26f29a6.patch.sql
index ca097c81066c4ca07bc3706868fa4c3d2378f9ec..277b10950b97645e82920fb39aaaed4fced83ed1 100644
--- a/include/upgrader/streams/core/03ff59bf-b26f29a6.patch.sql
+++ b/include/upgrader/streams/core/03ff59bf-b26f29a6.patch.sql
@@ -1,7 +1,7 @@
 /**
  * @version v1.9.4
  * @signature b26f29a6bb5dbb3510b057632182d138
- * @title Add properties filed and drop 'resolved' state
+ * @title Add properties field and drop 'resolved' state
  *
  * This patch drops resolved state and any associated statuses
  *
diff --git a/include/upgrader/streams/core/0d6099a6-98ad7d55.cleanup.sql b/include/upgrader/streams/core/0d6099a6-98ad7d55.cleanup.sql
new file mode 100644
index 0000000000000000000000000000000000000000..dcc52a01494d34084f3d099983a802bdc518a6fd
--- /dev/null
+++ b/include/upgrader/streams/core/0d6099a6-98ad7d55.cleanup.sql
@@ -0,0 +1,40 @@
+/**
+ * @signature 4e9f2e2441e82ba393df94647a1ec9ea
+ * @version v1.10.0
+ * @title Access Control 2.0
+ *
+ */
+
+DROP TABLE IF EXISTS `%TABLE_PREFIX%group_dept_access`;
+DROP TABLE IF EXISTS `%TABLE_PREFIX%group`;
+
+-- Drop `updated` if it exists (it stayed in the install script after it was
+-- removed from the update path
+SET @s = (SELECT IF(
+    (SELECT COUNT(*)
+        FROM INFORMATION_SCHEMA.COLUMNS
+        WHERE table_name = '%TABLE_PREFIX%team_member'
+        AND table_schema = DATABASE()
+        AND column_name = 'updated'
+    ) > 0,
+    "ALTER TABLE `%TABLE_PREFIX%team_member` DROP `updated`",
+    "SELECT 1"
+));
+PREPARE stmt FROM @s;
+EXECUTE stmt;
+
+-- Drop `views` and `score` from 1ee831c8 as it cannot handle translations
+SET @s = (SELECT IF(
+    (SELECT COUNT(*)
+        FROM INFORMATION_SCHEMA.COLUMNS
+        WHERE table_name = '%TABLE_PREFIX%faq'
+        AND table_schema = DATABASE()
+        AND column_name = 'views'
+    ) > 0,
+    "ALTER TABLE `%TABLE_PREFIX%faq` DROP `views`, DROP `score`",
+    "SELECT 1"
+));
+PREPARE stmt FROM @s;
+EXECUTE stmt;
+
+ALTER TABLE `%TABLE_PREFIX%ticket` DROP `lastmessage`, DROP `lastresponse`;
diff --git a/include/upgrader/streams/core/0d6099a6-98ad7d55.patch.sql b/include/upgrader/streams/core/0d6099a6-98ad7d55.patch.sql
new file mode 100644
index 0000000000000000000000000000000000000000..c388747703e7d831fe9da1d02bb38c71be2290b8
--- /dev/null
+++ b/include/upgrader/streams/core/0d6099a6-98ad7d55.patch.sql
@@ -0,0 +1,67 @@
+/**
+ * @signature 98ad7d550c26ac44340350912296e673
+ * @version v1.10.0
+ * @title Access Control 2.0
+ *
+ */
+
+DROP TABLE IF EXISTS `%TABLE_PREFIX%staff_dept_access`;
+CREATE TABLE `%TABLE_PREFIX%staff_dept_access` (
+  `staff_id` int(10) unsigned NOT NULL DEFAULT 0,
+  `dept_id` int(10) unsigned NOT NULL DEFAULT 0,
+  `role_id` int(10) unsigned NOT NULL DEFAULT 0,
+  `flags` int(10) unsigned NOT NULL DEFAULT 1,
+  PRIMARY KEY `staff_dept` (`staff_id`,`dept_id`),
+  KEY `dept_id` (`dept_id`)
+) DEFAULT CHARSET=utf8;
+
+-- Expand staff -> group -> dept_access to staff -> dept_access
+-- At the same time, drop primary department from staff_dept_access
+INSERT INTO `%TABLE_PREFIX%staff_dept_access`
+  (`staff_id`, `dept_id`, `role_id`)
+  SELECT A1.`staff_id`, A2.`dept_id`,
+    CASE WHEN A2.`role_id` = 0 THEN A3.`role_id` ELSE A2.`role_id` END
+  FROM `%TABLE_PREFIX%staff` A1
+  JOIN `%TABLE_PREFIX%group_dept_access` A2 ON (A1.`group_id` = A2.`group_id`)
+  JOIN `%TABLE_PREFIX%group` A3 ON (A3.`id` = A1.`group_id`)
+  WHERE A2.`dept_id` != A1.`dept_id`;
+
+ALTER TABLE `%TABLE_PREFIX%staff`
+  DROP `group_id`,
+  ADD `permissions` text AFTER `extra`;
+
+ALTER TABLE `%TABLE_PREFIX%team_member`
+  ADD `flags` int(10) unsigned NOT NULL DEFAULT 1 AFTER `staff_id`;
+
+ALTER TABLE `%TABLE_PREFIX%task`
+  ADD `closed` datetime DEFAULT NULL AFTER `duedate`;
+
+ALTER TABLE `%TABLE_PREFIX%thread`
+  ADD `lastresponse` datetime DEFAULT NULL AFTER `extra`,
+  ADD `lastmessage` datetime DEFAULT NULL AFTER `lastresponse`;
+
+UPDATE `%TABLE_PREFIX%thread` A1
+  JOIN `%TABLE_PREFIX%ticket` A2 ON (A2.`ticket_id` = A1.`object_id` AND A1.`object_type` = 'T')
+  SET A1.`lastresponse` = A2.`lastresponse`,
+      A1.`lastmessage` = A2.`lastmessage`;
+
+-- Mark `message` field as externally stored
+-- DynamicFormField::FLAG_EXT_STORED = 0x00002;
+UPDATE `%TABLE_PREFIX%form_field` A1
+  JOIN `%TABLE_PREFIX%form` A2 ON (A2.`id` = A1.`form_id`)
+  SET A1.`flags` = A1.`flags` | 0x00002
+  WHERE A2.`type` = 'T' AND A1.`name` = 'message';
+
+-- Change storage type for `DatetimeField` values to Y-m-d format
+UPDATE `%TABLE_PREFIX%form_entry_values` A1
+  JOIN `%TABLE_PREFIX%form_field` A2 ON (A2.`id` = A1.`field_id`)
+  SET A1.`value` = DATE_FORMAT(FROM_UNIXTIME(A1.`value`), '%Y-%m-%d %H:%i:%s')
+  WHERE A2.`type` = 'datetime';
+
+-- Updates should happen in the %cdata tables too; however, those are more
+-- complex and the tables are being dropped anyway
+
+-- Finished with patch
+UPDATE `%TABLE_PREFIX%config`
+    SET `value` = '98ad7d550c26ac44340350912296e673'
+    WHERE `key` = 'schema_signature' AND `namespace` = 'core';
diff --git a/include/upgrader/streams/core/0d6099a6-98ad7d55.task.php b/include/upgrader/streams/core/0d6099a6-98ad7d55.task.php
new file mode 100644
index 0000000000000000000000000000000000000000..abcc0498cc9f2b911eef970b8d69813d76e99763
--- /dev/null
+++ b/include/upgrader/streams/core/0d6099a6-98ad7d55.task.php
@@ -0,0 +1,43 @@
+<?php
+
+class StaffPermissions extends MigrationTask {
+    var $description = "Add staff permissions";
+
+    function run($time) {
+        foreach (Staff::objects() as $staff) {
+            $role = $staff->getRole();
+            if ($role)
+                $role_perms = $role->getPermission();
+            else
+                $role_perms = new RolePermission(null);
+            $perms = array(
+                User::PERM_CREATE,
+                User::PERM_EDIT,
+                User::PERM_DELETE,
+                User::PERM_MANAGE,
+                User::PERM_DIRECTORY,
+                Organization::PERM_CREATE,
+                Organization::PERM_EDIT,
+                Organization::PERM_DELETE,
+            );
+            if ($role_perms->has(FAQ::PERM_MANAGE))
+                $perms[] = FAQ::PERM_MANAGE;
+            if ($role_perms->has(Email::PERM_BANLIST))
+                $perms[] = Email::PERM_BANLIST;
+
+            $errors = array();
+            $staff->updatePerms($perms, $errors);
+            $staff->save();
+        }
+
+        // Update user's with <div> in their name (regression from v1.9.9)
+        foreach (
+            User::objects()->filter(array('name__startswith' => ' <div>'))
+            as $user
+        ) {
+            $user->name = ltrim(str_replace(' <div>', '', $user->name));
+            $user->save();
+        }
+    }
+}
+return 'StaffPermissions';
diff --git a/include/upgrader/streams/core/15b30765-dd0022fb.patch.sql b/include/upgrader/streams/core/15b30765-dd0022fb.patch.sql
index 796fbf296b6c5b7e60995c655d0c73262d6036a1..9fbb35360d09e53ca5d8856351b81e46fd1b780f 100644
--- a/include/upgrader/streams/core/15b30765-dd0022fb.patch.sql
+++ b/include/upgrader/streams/core/15b30765-dd0022fb.patch.sql
@@ -18,7 +18,11 @@ INSERT INTO `%TABLE_PREFIX%file_chunk` (`file_id`, `chunk_id`, `filedata`)
     SELECT `id`, 0, `filedata`
     FROM `%TABLE_PREFIX%file`;
 
-ALTER TABLE `%TABLE_PREFIX%file` DROP COLUMN `filedata`;
+ALTER TABLE `%TABLE_PREFIX%file`
+    DROP COLUMN `filedata`,
+    ADD `bk` CHAR(1) NOT NULL DEFAULT 'D' AFTER `id`,
+    ADD `attrs` VARCHAR(255) AFTER `name`;
+
 OPTIMIZE TABLE `%TABLE_PREFIX%file`;
 
 -- Finished with patch
diff --git a/include/upgrader/streams/core/15b30765-dd0022fb.task.php b/include/upgrader/streams/core/15b30765-dd0022fb.task.php
index 4d7eac01ff94a7622a16b118b5c567d41d3a9af0..beafd9d7d03b693cd24fa0619983af19e2d14b38 100644
--- a/include/upgrader/streams/core/15b30765-dd0022fb.task.php
+++ b/include/upgrader/streams/core/15b30765-dd0022fb.task.php
@@ -18,6 +18,9 @@
 require_once INCLUDE_DIR.'class.migrater.php';
 require_once(INCLUDE_DIR.'class.file.php');
 
+// Later version of osTicket dropped/undefined the table
+@define('TICKET_ATTACHMENT_TABLE', TABLE_PREFIX.'ticket_attachment');
+
 class AttachmentMigrater extends MigrationTask {
     var $description = "Attachment migration from disk to database";
 
@@ -92,37 +95,33 @@ class AttachmentMigrater extends MigrationTask {
         # need to be recalculated for every shift() operation.
         $info = array_pop($this->queue);
         # Attach file to the ticket
-        if (!($info['data'] = @file_get_contents($info['path']))) {
+        if (!@is_readable($info['path'])) {
             # Continue with next file
             return $this->skip($info['attachId'],
                 sprintf('%s: Cannot read file contents', $info['path']));
         }
         # Get the mime/type of each file
         # XXX: Use finfo_buffer for PHP 5.3+
-        if(function_exists('mime_content_type')) {
-            //XXX: function depreciated in newer versions of PHP!!!!!
-            $info['type'] = mime_content_type($info['path']);
-        } elseif (function_exists('finfo_file')) { // PHP 5.3.0+
+        if (function_exists('finfo_file')) { // PHP 5.3.0+
             $finfo = finfo_open(FILEINFO_MIME_TYPE);
             $info['type'] = finfo_file($finfo, $info['path']);
         }
+        elseif (function_exists('mime_content_type')) {
+            //XXX: function depreciated in newer versions of PHP!!!!!
+            $info['type'] = mime_content_type($info['path']);
+        }
         # TODO: Add extension-based mime-type lookup
 
-        if (!($fileId = $this->saveAttachment($info))) {
+        $file = $this->saveAttachment($info);
+        if (!$file)
             return $this->skip($info['attachId'],
                 sprintf('%s: Unable to migrate attachment', $info['path']));
-        }
+
         # Update the ATTACHMENT_TABLE record to set file_id
         db_query('update '.TICKET_ATTACHMENT_TABLE
-                .' set file_id='.db_input($fileId)
+                .' set file_id='.db_input($file->id)
                 .' where attach_id='.db_input($info['attachId']));
-        # Remove disk image of the file. If this fails, the migration for
-        # this file would not be retried, because the file_id in the
-        # TICKET_ATTACHMENT_TABLE has a nonzero value now
-        if (!@unlink($info['path'])) //XXX: what should we do on failure?
-            $this->error(
-                sprintf('%s: Unable to remove file from disk',
-                $info['path']));
+
         # TODO: Log an internal note to the ticket?
         return true;
     }
@@ -188,7 +187,7 @@ class AttachmentMigrater extends MigrationTask {
             # TODO: Get the size and mime/type of each file.
             #
             # NOTE: If filesize() fails and file_get_contents() doesn't,
-            # then the AttachmentFile::save() method will automatically
+            # then the AttachmentFile::create() method will automatically
             # estimate the filesize based on the length of the string data
             # received in $info['data'] -- ie. no need to do that here.
             #
@@ -204,7 +203,7 @@ class AttachmentMigrater extends MigrationTask {
             $this->enqueue($info);
         }
 
-        return $this->queueAttachments($limit);
+        return $this->getQueueLength();
     }
 
     function skip($attachId, $error) {
@@ -228,47 +227,45 @@ class AttachmentMigrater extends MigrationTask {
         return $this->errorList;
     }
 
-    // This is the AttachmentFile::save() method from osTicket 1.7.6. It's
-    // been ported here so that further changes to the %file table and the
-    // AttachmentFile::save() method do not affect upgrades from osTicket
-    // 1.6 to osTicket 1.8 and beyond.
+    // This is (similar to) the AttachmentFile::create() method from
+    // osTicket 1.7.6. It's been ported here so that further changes to the
+    // %file table and the AttachmentFile::create() method do not affect
+    // upgrades from osTicket 1.6 to osTicket 1.8 and beyond.
     function saveAttachment($file) {
 
-        if(!$file['hash'])
+        if (!$file['hash'])
             $file['hash']=MD5(md5_file($file['path']).time());
-        $file['data'] = file_get_contents($file['path']);
-        if(!$file['size'])
-            $file['size']=strlen($file['data']);
-
-        $sql='INSERT INTO '.FILE_TABLE.' SET created=NOW() '
-            .',type='.db_input($file['type'])
-            .',size='.db_input($file['size'])
-            .',name='.db_input($file['name'])
-            .',hash='.db_input($file['hash']);
-
-        if (!(db_query($sql) && ($id=db_insert_id())))
-            return false;
-
-        $f = new CompatAttachmentFile($id);
-        $bk = new AttachmentChunkedData($f);
-        if (!$bk->write($file['data']))
-            return false;
-
-        return $id;
+        if (!$file['size'])
+            $file['size'] = filesize($file['path']);
+
+        return OldOneSixFile::create(array(
+            'name' => $file['name'],
+            'size' => $file['size'],
+            'type' => $file['type'],
+            'hash' => $file['hash'],
+            'bk' => '6',
+            'attrs' => $file['path'],
+            'created' => date('Y-m-d H:i:s', Misc::dbtime(filemtime($file['path']))),
+        ));
     }
 }
 
-class CompatAttachmentFile {
-    var $id;
-
-    function __construct($id) {
-        $this->id = $id;
-    }
-
-    function getId() {
-        return $this->id;
+class OldOneSixFile extends VerySimpleModel {
+    static $meta = array(
+        'table' => FILE_TABLE,
+        'pk' => array('id'),
+        'joins' => array(
+            'attachments' => array(
+                'reverse' => 'Attachment.file'
+            ),
+        ),
+    );
+
+    static function create($info) {
+        $I = new static($info);
+        $I->save();
+        return $I;
     }
 }
 
 return 'AttachmentMigrater';
-?>
diff --git a/include/upgrader/streams/core/1ee831c8-36f6b328.cleanup.sql b/include/upgrader/streams/core/1ee831c8-36f6b328.cleanup.sql
new file mode 100644
index 0000000000000000000000000000000000000000..2bbe6cba034b1486977c4f1fbec13403a1807b6f
--- /dev/null
+++ b/include/upgrader/streams/core/1ee831c8-36f6b328.cleanup.sql
@@ -0,0 +1,2 @@
+-- drop useless updated column
+ALTER TABLE  `%TABLE_PREFIX%team_member` DROP  `updated`;
diff --git a/include/upgrader/streams/core/1ee831c8-36f6b328.patch.sql b/include/upgrader/streams/core/1ee831c8-36f6b328.patch.sql
new file mode 100644
index 0000000000000000000000000000000000000000..a60fae20cf7a1181530723eca71433e470336948
--- /dev/null
+++ b/include/upgrader/streams/core/1ee831c8-36f6b328.patch.sql
@@ -0,0 +1,42 @@
+/**
+ * @signature 36f6b32893c2b97c5104ab5302d2dd2e
+ * @version v1.10.0
+ * @title Add role-based access
+ *
+ * This patch adds support for role based access to group and departments
+ *
+ */
+
+CREATE TABLE `%TABLE_PREFIX%role` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `flags` int(10) unsigned NOT NULL DEFAULT '1',
+  `name` varchar(64) DEFAULT NULL,
+  `permissions` text,
+  `notes` text,
+  `created` datetime NOT NULL,
+  `updated` datetime NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`)
+) DEFAULT CHARSET=utf8;
+
+ALTER TABLE  `%TABLE_PREFIX%group_dept_access`
+    ADD  `role_id` INT UNSIGNED NOT NULL DEFAULT  '0';
+
+ALTER TABLE `%TABLE_PREFIX%staff`
+    ADD `role_id` INT(10) UNSIGNED NOT NULL DEFAULT '0' AFTER `dept_id`;
+
+ALTER TABLE  `%TABLE_PREFIX%groups`
+    RENAME TO `%TABLE_PREFIX%group`,
+    CHANGE  `group_id`  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
+    ADD  `role_id` INT UNSIGNED NOT NULL DEFAULT  '0' AFTER  `id`;
+
+-- department changes
+ALTER TABLE  `%TABLE_PREFIX%department`
+    CHANGE  `dept_id`  `id` INT( 11 ) UNSIGNED NOT NULL AUTO_INCREMENT,
+    CHANGE  `dept_signature`  `signature` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
+    CHANGE  `dept_name`  `name` VARCHAR( 128 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT  '';
+
+-- Finished with patch
+UPDATE `%TABLE_PREFIX%config`
+    SET `value`='36f6b32893c2b97c5104ab5302d2dd2e'
+    WHERE `key` = 'schema_signature' AND `namespace` = 'core';
diff --git a/include/upgrader/streams/core/1ee831c8-36f6b328.task.php b/include/upgrader/streams/core/1ee831c8-36f6b328.task.php
new file mode 100644
index 0000000000000000000000000000000000000000..982f690e70f3e0250cd7c351fad7fca0592c12a9
--- /dev/null
+++ b/include/upgrader/streams/core/1ee831c8-36f6b328.task.php
@@ -0,0 +1,83 @@
+<?php
+define('GROUP_TABLE', TABLE_PREFIX.'group');
+define('GROUP_DEPT_TABLE', TABLE_PREFIX.'group_dept_access');
+class Group extends VerySimpleModel {
+    static $meta = array(
+        'table' => GROUP_TABLE,
+        'pk' => array('id'),
+    );
+    const FLAG_ENABLED = 0x0001;
+
+    function getName() {
+        return $this->group_name;
+    }
+    function getId() {
+        return $this->id;
+    }
+}
+
+Staff::getMeta()->addJoin('group', array(
+    'constraint' => array('group_id' => 'Group.id'),
+));
+
+class GroupRoles extends MigrationTask {
+    var $description = "Migrate permissions from Group to Role";
+
+    static $pmap = array(
+            'ticket.create' => 'can_create_tickets',
+            'ticket.edit' => 'can_edit_tickets',
+            'ticket.reply' => 'can_post_ticket_reply',
+            'ticket.delete' => 'can_delete_tickets',
+            'ticket.close' => 'can_close_tickets',
+            'ticket.assign' => 'can_assign_tickets',
+            'ticket.transfer' => 'can_transfer_tickets',
+            'task.create' => 'can_create_tickets',
+            'task.edit' => 'can_edit_tickets',
+            'task.reply' => 'can_post_ticket_reply',
+            'task.delete' => 'can_delete_tickets',
+            'task.close' => 'can_close_tickets',
+            'task.assign' => 'can_assign_tickets',
+            'task.transfer' => 'can_transfer_tickets',
+            'emails.banlist' => 'can_ban_emails',
+            'canned.manage' => 'can_manage_premade',
+            'faq.manage' => 'can_manage_faq',
+            'stats.agents' => 'can_view_staff_stats',
+    );
+
+    function run($max_time) {
+        global $cfg;
+        // Select existing groups and create roles matching the current
+        // settings
+        foreach (Group::objects() as $group) {
+            $ht=array(
+                    'flags' => Group::FLAG_ENABLED,
+                    'name' => sprintf('%s %s', $group->getName(),
+                        // XXX: Translate based on the system language, not
+                        //      the current agent's
+                        __('Role')),
+                    'notes' => $group->getName()
+                    );
+            $perms = array();
+            foreach (self::$pmap as  $v => $k) {
+                if ($group->{$k})
+                    $perms[] = $v;
+            }
+
+            $ht['permissions'] = $perms;
+
+            $errors = array();
+            $role = Role::__create($ht, $errors);
+            $group->role_id =  $role->getId();
+            $group->save();
+        }
+
+        // Copy group default role to the agent for the respective primary
+        // department role
+        foreach (Staff::objects()->select_related('group') as $staff) {
+            $staff->role_id = $staff->group->role_id;
+            $staff->save();
+        }
+    }
+}
+
+return 'GroupRoles';
diff --git a/include/upgrader/streams/core/2d590ffa-9143a511.patch.sql b/include/upgrader/streams/core/2d590ffa-9143a511.patch.sql
new file mode 100644
index 0000000000000000000000000000000000000000..5ffa7338516c054605d5e4ca0e1a7c10578ebca9
--- /dev/null
+++ b/include/upgrader/streams/core/2d590ffa-9143a511.patch.sql
@@ -0,0 +1,56 @@
+/**
+ * @version v1.10.0
+ * @title Add collaborators to tasks
+ * @signature 9143a511719555e8f8f09b49523bd022
+ *
+ * This patch renames the %ticket_lock table to just %lock, which allows for
+ * it to be considered more flexible. Instead, it joins the lock to the
+ * ticket and task objects directly.
+ *
+ * It also redefines the collaborator table to link to a thread rather than
+ * to a ticket, which allows any object in the system with a thread to
+ * theoretically have collaborators.
+ */
+
+ALTER TABLE `%TABLE_PREFIX%ticket`
+  ADD `lock_id` int(11) unsigned NOT NULL default '0' AFTER `email_id`;
+
+RENAME TABLE `%TABLE_PREFIX%ticket_lock` TO `%TABLE_PREFIX%lock`;
+ALTER TABLE `%TABLE_PREFIX%lock`
+  DROP COLUMN `ticket_id`,
+  ADD `code` varchar(20) AFTER `expire`;
+
+-- Drop all the current locks as they do not point to anything now
+TRUNCATE TABLE `%TABLE_PREFIX%lock`;
+
+CREATE TABLE `%TABLE_PREFIX%thread_collaborator` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `isactive` tinyint(1) NOT NULL DEFAULT '1',
+  `thread_id` int(11) unsigned NOT NULL DEFAULT '0',
+  `user_id` int(11) unsigned NOT NULL DEFAULT '0',
+  -- M => (message) clients, N => (note) 3rd-Party, R => (reply) external authority
+  `role` char(1) NOT NULL DEFAULT 'M',
+  `created` datetime NOT NULL,
+  `updated` datetime NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `collab` (`thread_id`,`user_id`),
+  KEY `user_id` (`user_id`)
+) DEFAULT CHARSET=utf8;
+
+-- Drop zombie collaborators from tickets which were deleted and had
+-- collaborators and the collaborators were not removed
+INSERT INTO `%TABLE_PREFIX%thread_collaborator`
+  (`id`, `isactive`, `thread_id`, `user_id`, `role`, `created`, `updated`)
+  SELECT t1.`id`, t1.`isactive`, t2.`id`, t1.`user_id`, t1.`role`, t2.`created`, t1.`updated`
+  FROM `%TABLE_PREFIX%ticket_collaborator` t1
+  JOIN `%TABLE_PREFIX%thread` t2 ON (t2.`object_id` = t1.`ticket_id`  and t2.`object_type` = 'T');
+
+DROP TABLE `%TABLE_PREFIX%ticket_collaborator`;
+
+ALTER TABLE `%TABLE_PREFIX%task`
+  ADD `lock_id` int(11) unsigned NOT NULL DEFAULT '0' AFTER `team_id`;
+
+-- Finished with patch
+UPDATE `%TABLE_PREFIX%config`
+    SET `value` = '9143a511719555e8f8f09b49523bd022'
+    WHERE `key` = 'schema_signature' AND `namespace` = 'core';
diff --git a/include/upgrader/streams/core/36f6b328-5cd0a25a.cleanup.sql b/include/upgrader/streams/core/36f6b328-5cd0a25a.cleanup.sql
new file mode 100644
index 0000000000000000000000000000000000000000..5345ffbdd7d925b2a785c03e0a930190f350c59e
--- /dev/null
+++ b/include/upgrader/streams/core/36f6b328-5cd0a25a.cleanup.sql
@@ -0,0 +1,18 @@
+-- Drop `tid` from thread (if it exists)
+SET @s = (SELECT IF(
+    (SELECT COUNT(*)
+        FROM INFORMATION_SCHEMA.COLUMNS
+        WHERE table_name = '%TABLE_PREFIX%thread'
+        AND table_schema = DATABASE()
+        AND column_name = 'tid'
+    ) > 0,
+    "ALTER TABLE `%TABLE_PREFIX%thread` DROP COLUMN `tid`",
+    "SELECT 1"
+));
+PREPARE stmt FROM @s;
+EXECUTE stmt;
+
+DROP TABLE `%TABLE_PREFIX%ticket_attachment`;
+
+OPTIMIZE TABLE `%TABLE_PREFIX%ticket`;
+OPTIMIZE TABLE `%TABLE_PREFIX%thread`;
diff --git a/include/upgrader/streams/core/36f6b328-5cd0a25a.patch.sql b/include/upgrader/streams/core/36f6b328-5cd0a25a.patch.sql
new file mode 100644
index 0000000000000000000000000000000000000000..1d88fea216ed6a49b357a60b7f31767531408273
--- /dev/null
+++ b/include/upgrader/streams/core/36f6b328-5cd0a25a.patch.sql
@@ -0,0 +1,153 @@
+/**
+ * @version v1.10.0
+ * @signature 5cd0a25a54fd27ed95f00d62edda4c6d
+ * @title Add support for ticket tasks
+ *
+ * This patch adds ability to thread anything and introduces tasks
+ *
+ */
+
+-- Add thread table
+DROP TABLE IF EXISTS `%TABLE_PREFIX%thread`;
+CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%thread` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `object_id` int(11) unsigned NOT NULL,
+  `object_type` char(1) NOT NULL,
+  `extra` text,
+  `created` datetime NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `object_id` (`object_id`),
+  KEY `object_type` (`object_type`)
+) DEFAULT CHARSET=utf8;
+
+-- create threads
+INSERT INTO `%TABLE_PREFIX%thread`
+    (`object_id`, `object_type`, `created`)
+    SELECT t1.ticket_id, 'T', t1.created
+        FROM `%TABLE_PREFIX%ticket_thread` t1
+        JOIN (
+            SELECT ticket_id, MIN(id) as id
+            FROM `%TABLE_PREFIX%ticket_thread`
+            WHERE `thread_type` = 'M'
+            GROUP BY ticket_id
+    ) t2
+    ON (t1.ticket_id=t2.ticket_id and t1.id=t2.id)
+    ORDER BY t1.created;
+
+-- convert ticket_thread to thread_entry
+CREATE TABLE `%TABLE_PREFIX%thread_entry` (
+  `id` int(11) unsigned NOT NULL auto_increment,
+  `pid` int(11) unsigned NOT NULL default '0',
+  `thread_id` int(11) unsigned NOT NULL default '0',
+  `staff_id` int(11) unsigned NOT NULL default '0',
+  `user_id` int(11) unsigned not null default 0,
+  `type` char(1) NOT NULL default '',
+  `flags` int(11) unsigned NOT NULL default '0',
+  `poster` varchar(128) NOT NULL default '',
+  `editor` int(10) unsigned NULL,
+  `editor_type` char(1) NULL,
+  `source` varchar(32) NOT NULL default '',
+  `title` varchar(255),
+  `body` text NOT NULL,
+  `format` varchar(16) NOT NULL default 'html',
+  `ip_address` varchar(64) NOT NULL default '',
+  `created` datetime NOT NULL,
+  `updated` datetime NOT NULL,
+  PRIMARY KEY  (`id`),
+  KEY `pid` (`pid`),
+  KEY `thread_id` (`thread_id`),
+  KEY `staff_id` (`staff_id`),
+  KEY `type` (`type`)
+) DEFAULT CHARSET=utf8;
+
+-- Set the ORIGINAL_MESSAGE flag to all the first messages of each thread
+CREATE TABLE `%TABLE_PREFIX%_orig_msg_ids`
+  (`id` INT NOT NULL, PRIMARY KEY (id))
+  SELECT MIN(id) AS `id` FROM `%TABLE_PREFIX%ticket_thread`
+  WHERE `thread_type` = 'M'
+  GROUP BY `ticket_id`;
+
+INSERT INTO `%TABLE_PREFIX%thread_entry`
+  (`id`, `pid`, `thread_id`, `staff_id`, `user_id`, `type`, `flags`,
+    `poster`, `source`, `title`, `body`, `format`, `ip_address`, `created`,
+    `updated`)
+  SELECT t1.`id`, t1.`pid`, t2.`id`, t1.`staff_id`, t1.`user_id`, t1.`thread_type`,
+    CASE WHEN t3.`id` IS NULL THEN 0 ELSE 1 END,
+    t1.`poster`, t1.`source`, t1.`title`, t1.`body`, t1.`format`, t1.`ip_address`,
+    t1.`created`, t1.`updated`
+  FROM `%TABLE_PREFIX%ticket_thread` t1
+  LEFT JOIN `%TABLE_PREFIX%thread` t2 ON (t2.object_id = t1.ticket_id AND t2.object_type = 'T')
+  LEFT JOIN `%TABLE_PREFIX%_orig_msg_ids` t3 ON (t1.id = t3.id);
+
+DROP TABLE `%TABLE_PREFIX%ticket_thread`;
+DROP TABLE `%TABLE_PREFIX%_orig_msg_ids`;
+
+-- move records in ticket_attachment to generic attachment table
+ALTER TABLE  `%TABLE_PREFIX%attachment`
+    DROP PRIMARY KEY,
+    ADD  `id` INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST,
+    ADD UNIQUE  `file-type` (`object_id`, `file_id`, `type`);
+
+INSERT INTO `%TABLE_PREFIX%attachment`
+    (`object_id`, `type`, `file_id`, `inline`)
+    SELECT `ref_id`, 'H', `file_id`, `inline`
+    FROM `%TABLE_PREFIX%ticket_attachment` A
+    WHERE A.file_id > 0;
+
+-- convert ticket_email_info to thread_entry_email
+ALTER TABLE  `%TABLE_PREFIX%ticket_email_info`
+    ADD INDEX (  `thread_id` );
+
+ALTER TABLE  `%TABLE_PREFIX%ticket_email_info`
+    CHANGE  `thread_id`  `thread_entry_id` INT( 11 ) UNSIGNED NOT NULL,
+    CHANGE  `email_mid`  `mid` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;
+
+RENAME TABLE `%TABLE_PREFIX%ticket_email_info` TO  `%TABLE_PREFIX%thread_entry_email`;
+
+-- create task task
+CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%task` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `object_id` int(11) NOT NULL DEFAULT '0',
+  `object_type` char(1) NOT NULL,
+  `number` varchar(20) DEFAULT NULL,
+  `dept_id` int(10) unsigned NOT NULL DEFAULT '0',
+  `sla_id` int(10) unsigned NOT NULL DEFAULT '0',
+  `staff_id` int(10) unsigned NOT NULL DEFAULT '0',
+  `team_id` int(10) unsigned NOT NULL DEFAULT '0',
+  `flags` int(10) unsigned NOT NULL DEFAULT '0',
+  `duedate` datetime DEFAULT NULL,
+  `created` datetime NOT NULL,
+  `updated` datetime NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `dept_id` (`dept_id`),
+  KEY `staff_id` (`staff_id`),
+  KEY `team_id` (`team_id`),
+  KEY `created` (`created`),
+  KEY `sla_id` (`sla_id`),
+  KEY `object` (`object_id`,`object_type`)
+) DEFAULT CHARSET=utf8;
+
+-- rename ticket sequence numbering
+
+UPDATE `%TABLE_PREFIX%config`
+    SET `key` = 'ticket_number_format'
+    WHERE `key` = 'number_format'  AND `namespace` = 'core';
+
+UPDATE `%TABLE_PREFIX%config`
+    SET `key` = 'ticket_sequence_id'
+    WHERE `key` = 'sequence_id'  AND `namespace` = 'core';
+
+-- add parent department support
+ALTER TABLE `%TABLE_PREFIX%department`
+  DROP INDEX  `dept_name`,
+  ADD `pid` int(11) unsigned default NULL AFTER `id`,
+  ADD `path` varchar(128) NOT NULL default '/' AFTER `message_auto_response`,
+  ADD UNIQUE  `name` (  `name` ,  `pid` );
+
+UPDATE `%TABLE_PREFIX%department`
+  SET `path` = CONCAT('/', id, '/');
+
+-- Set new schema signature
+UPDATE `%TABLE_PREFIX%config`
+    SET `value` = '5cd0a25a54fd27ed95f00d62edda4c6d'
+    WHERE `key` = 'schema_signature' AND `namespace` = 'core';
diff --git a/include/upgrader/streams/core/36f6b328-5cd0a25a.task.php b/include/upgrader/streams/core/36f6b328-5cd0a25a.task.php
new file mode 100644
index 0000000000000000000000000000000000000000..e7d11a8561aa0105bac141ac882799842db919e6
--- /dev/null
+++ b/include/upgrader/streams/core/36f6b328-5cd0a25a.task.php
@@ -0,0 +1,58 @@
+<?php
+/*
+ * Import initial form for task
+ *
+ */
+
+class TaskLoader extends MigrationTask {
+    var $description = "Loading initial data for tasks";
+    static $pmap = array(
+            'ticket.create'     => 'task.create',
+            'ticket.edit'       => 'task.edit',
+            'ticket.reply'      => 'task.reply',
+            'ticket.delete'     => 'task.delete',
+            'ticket.close'      => 'task.close',
+            'ticket.assign'     => 'task.assign',
+            'ticket.transfer'   => 'task.transfer',
+    );
+
+    function run($max_time) {
+        global $cfg;
+
+        // Load task form
+        require_once INCLUDE_DIR.'class.task.php';
+        Task::__loadDefaultForm();
+        // Load sequence for the task
+        $i18n = new Internationalization($cfg->get('system_language', 'en_US'));
+        $sequences = $i18n->getTemplate('sequence.yaml')->getData();
+        foreach ($sequences as $s) {
+            if ($s['id'] != 2) continue;
+            unset($s['id']);
+            $sq=Sequence::create($s);
+            $sq->save();
+            $sql= 'INSERT INTO '.CONFIG_TABLE
+                .' (`namespace`, `key`, `value`) '
+                .' VALUES
+                    ("core", "task_number_format", "###"),
+                    ("core", "task_sequence_id",'.db_input($sq->id).')';
+            db_query($sql);
+            break;
+        }
+
+        // Copy ticket permissions
+        foreach (Role::objects() as $role) {
+            $perms = $role->getPermissionInfo();
+            foreach (self::$pmap as  $k => $v) {
+                if (in_array($k, $perms))
+                    $perms[] = $v;
+            }
+            $role->updatePerms($perms);
+            $role->save();
+        }
+
+    }
+}
+
+return 'TaskLoader';
+
+?>
diff --git a/include/upgrader/streams/core/435c62c3-2e7531a2.task.php b/include/upgrader/streams/core/435c62c3-2e7531a2.task.php
index d80bc23a2e616248409c3c3780e3eedfca975799..bdfb105b6d2fd3cc9de5b6aa6331ea63cb2dbe9e 100644
--- a/include/upgrader/streams/core/435c62c3-2e7531a2.task.php
+++ b/include/upgrader/streams/core/435c62c3-2e7531a2.task.php
@@ -1,6 +1,10 @@
 <?php
 require_once INCLUDE_DIR.'class.migrater.php';
 
+// Replaced in v1.10 for STAFF_DEPT_TABLE
+define('GROUP_TABLE', TABLE_PREFIX.'groups');
+define('GROUP_DEPT_TABLE', TABLE_PREFIX.'group_dept_access');
+
 class MigrateGroupDeptAccess extends MigrationTask {
     var $description = "Migrate department access for groups from v1.6";
 
diff --git a/include/upgrader/streams/core/5cd0a25a-2d590ffa.cleanup.sql b/include/upgrader/streams/core/5cd0a25a-2d590ffa.cleanup.sql
new file mode 100644
index 0000000000000000000000000000000000000000..409ff700c9de2ed17bb740b268cd37efa8a3c601
--- /dev/null
+++ b/include/upgrader/streams/core/5cd0a25a-2d590ffa.cleanup.sql
@@ -0,0 +1,21 @@
+/**
+ * @signature 2d590ffab4a6a928f08cc97aace1399e
+ * @version v1.9.6
+ * @title Make fields disable-able per help topic
+ */
+
+ALTER TABLE `%TABLE_PREFIX%help_topic`
+    DROP `form_id`;
+
+ALTER TABLE `%TABLE_PREFIX%filter`
+  DROP `reject_ticket`,
+  DROP `use_replyto_email`,
+  DROP `disable_autoresponder`,
+  DROP `canned_response_id`,
+  DROP `status_id`,
+  DROP `priority_id`,
+  DROP `dept_id`,
+  DROP `staff_id`,
+  DROP `team_id`,
+  DROP `sla_id`,
+  DROP `form_id`;
diff --git a/include/upgrader/streams/core/5cd0a25a-2d590ffa.patch.sql b/include/upgrader/streams/core/5cd0a25a-2d590ffa.patch.sql
new file mode 100644
index 0000000000000000000000000000000000000000..7fa6e361fcaa1d6e4cf2e91ac6ac4ed3fbf7ead7
--- /dev/null
+++ b/include/upgrader/streams/core/5cd0a25a-2d590ffa.patch.sql
@@ -0,0 +1,136 @@
+/**
+ * @signature 2d590ffab4a6a928f08cc97aace1399e
+ * @version v1.10.0
+ * @title Make fields disable-able per help topic
+ *
+ * This patch adds the ability to associate more than one extra form with a
+ * help topic, allows specifying the sort order of each form, including the
+ * main ticket details forms, and also allows disabling any of the fields on
+ * any of the associated forms, including the issue details field.
+ *
+ * This patch migrates the columnar layout of the %filter table into a new
+ * %filter_action table. The cleanup portion of the script will drop the old
+ * columns from the %filter table.
+ */
+
+DROP TABLE IF EXISTS `%TABLE_PREFIX%filter_action`;
+CREATE TABLE `%TABLE_PREFIX%filter_action` (
+  `id` int(11) unsigned NOT NULL auto_increment,
+  `filter_id` int(10) unsigned NOT NULL,
+  `sort` int(10) unsigned NOT NULL default 0,
+  `type` varchar(24) NOT NULL,
+  `configuration` text,
+  `updated` datetime NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `filter_id` (`filter_id`)
+) DEFAULT CHARSET=utf8;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'reject', '', `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `reject_ticket` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'replyto', '{"enable":true}', `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `use_replyto_email` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'noresp', '{"enable":true}', `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `disable_autoresponder` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'canned', CONCAT('{"canned_id":',`canned_response_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `canned_response_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'dept', CONCAT('{"dept_id":',`dept_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `dept_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'pri', CONCAT('{"priority_id":',`priority_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `priority_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'sla', CONCAT('{"sla_id":',`sla_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `sla_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'team', CONCAT('{"team_id":',`team_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `team_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'agent', CONCAT('{"staff_id":',`staff_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `staff_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'topic', CONCAT('{"topic_id":',`topic_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `topic_id` != 0;
+
+INSERT INTO `%TABLE_PREFIX%filter_action`
+    (`filter_id`, `type`, `configuration`, `updated`)
+    SELECT `id`, 'status', CONCAT('{"status_id":',`status_id`,'}'), `updated`
+    FROM `%TABLE_PREFIX%filter`
+    WHERE `status_id` != 0;
+
+ALTER TABLE `%TABLE_PREFIX%form`
+    ADD `pid` int(10) unsigned DEFAULT NULL AFTER `id`,
+    ADD `name` varchar(64) NOT NULL DEFAULT '' AFTER `instructions`;
+
+ALTER TABLE `%TABLE_PREFIX%form_entry`
+    ADD `extra` text AFTER `sort`;
+
+CREATE TABLE `%TABLE_PREFIX%help_topic_form` (
+  `id` int(11) unsigned NOT NULL auto_increment,
+  `topic_id` int(11) unsigned NOT NULL default 0,
+  `form_id` int(10) unsigned NOT NULL default 0,
+  `sort` int(10) unsigned NOT NULL default 1,
+  `extra` text,
+  PRIMARY KEY (`id`),
+  KEY `topic-form` (`topic_id`, `form_id`)
+) DEFAULT CHARSET=utf8;
+
+-- Handle A4 / A3 / A2 / A1 help topics. For these, consider the forms
+-- associated with each, which should sort above the ticket details form, as
+-- the graphical interface rendered it suchly. Then, consider cascaded
+-- forms, where the parent form was specified on a child.
+insert into `%TABLE_PREFIX%help_topic_form`
+    (`topic_id`, `form_id`, `sort`)
+    select A1.topic_id, case
+        when A3.form_id = 4294967295 then A4.form_id
+        when A2.form_id = 4294967295 then A3.form_id
+        when A1.form_id = 4294967295 then A2.form_id
+        else COALESCE(A4.form_id, A3.form_id, A2.form_id, A1.form_id) end as form_id, 1 as `sort`
+    from `%TABLE_PREFIX%help_topic` A1
+    left join `%TABLE_PREFIX%help_topic` A2 on (A2.topic_id = A1.topic_pid)
+    left join `%TABLE_PREFIX%help_topic` A3 on (A3.topic_id = A2.topic_pid)
+    left join `%TABLE_PREFIX%help_topic` A4 on (A4.topic_id = A3.topic_pid)
+    having `form_id` > 0
+    union
+    select A2.topic_id, id as `form_id`, 2 as `sort`
+    from `%TABLE_PREFIX%form` A1
+    join `%TABLE_PREFIX%help_topic` A2
+    where A1.`type` = 'T';
+
+-- Finished with patch
+UPDATE `%TABLE_PREFIX%config`
+    SET `value` = '2d590ffab4a6a928f08cc97aace1399e'
+    WHERE `key` = 'schema_signature' AND `namespace` = 'core';
diff --git a/include/upgrader/streams/core/5cd0a25a-2d590ffa.task.php b/include/upgrader/streams/core/5cd0a25a-2d590ffa.task.php
new file mode 100644
index 0000000000000000000000000000000000000000..34c59835bb94514662c264efbdbd6d4914e88e5e
--- /dev/null
+++ b/include/upgrader/streams/core/5cd0a25a-2d590ffa.task.php
@@ -0,0 +1,18 @@
+<?php
+
+class InstructionsPorter extends MigrationTask {
+    var $description = "Converting custom form instructions to HTML";
+
+    function run($max_time) {
+        foreach (DynamicForm::objects() as $F) {
+            $F->instructions = Format::htmlchars($F->get('instructions'));
+            $F->save();
+        }
+        foreach (DynamicFormField::objects() as $F){
+            $F->hint = Format::htmlchars($F->get('hint'));
+            $F->save();
+        }
+    }
+}
+
+return 'InstructionsPorter';
diff --git a/include/upgrader/streams/core/8f99b8bf-03ff59bf.cleanup.sql b/include/upgrader/streams/core/8f99b8bf-03ff59bf.cleanup.sql
index dbeb1fa39788d22ebe82ba7ef6c559d2de363bcf..3982ee61ebeb21a904e71152c2b3bd90754e379b 100644
--- a/include/upgrader/streams/core/8f99b8bf-03ff59bf.cleanup.sql
+++ b/include/upgrader/streams/core/8f99b8bf-03ff59bf.cleanup.sql
@@ -5,6 +5,6 @@ ALTER TABLE `%TABLE_PREFIX%ticket`
     DROP COLUMN `status`;
 
 -- Regenerate the CDATA table with the new format for 1.9.4
-DROP TABLE `%TABLE_PREFIX%ticket__cdata`;
+DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket__cdata`;
 
 OPTIMIZE TABLE `%TABLE_PREFIX%ticket`;
diff --git a/include/upgrader/streams/core/8f99b8bf-03ff59bf.task.php b/include/upgrader/streams/core/8f99b8bf-03ff59bf.task.php
index f7bb36f7db2da78056a307b730543f6cc43ad5b0..cd41e131bdc7617503aa789d731e538ad0fb7db5 100644
--- a/include/upgrader/streams/core/8f99b8bf-03ff59bf.task.php
+++ b/include/upgrader/streams/core/8f99b8bf-03ff59bf.task.php
@@ -19,17 +19,8 @@ class SequenceLoader extends MigrationTask {
             .'(SELECT MAX(ticket_id)+1 FROM '.TICKET_TABLE.') '
             .'WHERE `id`=1');
 
-        require_once(INCLUDE_DIR . 'class.list.php');
-
-        $lists = $i18n->getTemplate('list.yaml')->getData();
-        foreach ($lists as $l) {
-            DynamicList::create($l);
-        }
-
-        $statuses = $i18n->getTemplate('ticket_status.yaml')->getData();
-        foreach ($statuses as $s) {
-            TicketStatus::__create($s);
-        }
+        // list.yaml and ticket_status.yaml import moved to
+        // core/b26f29a6-1ee831c8.task.php
 
         // Initialize MYSQL search backend
         MysqlSearchBackend::__init();
diff --git a/include/upgrader/streams/core/9143a511-0d6099a6.cleanup.sql b/include/upgrader/streams/core/9143a511-0d6099a6.cleanup.sql
new file mode 100644
index 0000000000000000000000000000000000000000..91793e7b6285005bb50dfe7119851b94a3fb50de
--- /dev/null
+++ b/include/upgrader/streams/core/9143a511-0d6099a6.cleanup.sql
@@ -0,0 +1,72 @@
+/**
+ * @signature 959aca6ed189cd918d227a3ea8a135a3
+ * @version v1.9.6
+ * @title Retire `private`, `required`, and `edit_mask` for fields
+ *
+ */
+
+ALTER TABLE `%TABLE_PREFIX%form_field`
+    DROP `private`,
+    DROP `required`,
+    DROP `edit_mask`;
+
+ALTER TABLE `%TABLE_PREFIX%content`
+    DROP `lang`;
+
+-- DROP IF EXISTS `%content.content_id`
+SET @s = (SELECT IF(
+    (SELECT COUNT(*)
+        FROM INFORMATION_SCHEMA.COLUMNS
+        WHERE table_name = '%TABLE_PREFIX%content'
+        AND table_schema = DATABASE()
+        AND column_name = 'content_id'
+    ) > 0,
+    "ALTER TABLE `%TABLE_PREFIX%content` DROP `content_id`",
+    "SELECT 1"
+));
+PREPARE stmt FROM @s;
+EXECUTE stmt;
+
+-- DROP IF EXISTS `%task.sla_id`
+SET @s = (SELECT IF(
+    (SELECT COUNT(*)
+        FROM INFORMATION_SCHEMA.COLUMNS
+        WHERE table_name = '%TABLE_PREFIX%task'
+        AND table_schema = DATABASE()
+        AND column_name = 'sla_id'
+    ) > 0,
+    "ALTER TABLE `%TABLE_PREFIX%task` DROP `sla_id`",
+    "SELECT 1"
+));
+PREPARE stmt FROM @s;
+EXECUTE stmt;
+
+-- Retire %team.[flag fields]
+ALTER TABLE `%TABLE_PREFIX%team`
+    DROP `isenabled`,
+    DROP `noalerts`;
+
+-- Retire %dept.[flag fields]
+DELETE FROM `%TABLE_PREFIX%config`
+WHERE `key`='assign_members_only' AND `namespace` LIKE 'dept.%';
+
+-- Retire %sla.[flag fields]
+ALTER TABLE `%TABLE_PREFIX%sla`
+  DROP `isactive`,
+  DROP `enable_priority_escalation`,
+  DROP `disable_overdue_alerts`;
+
+DELETE FROM `%TABLE_PREFIX%config`
+WHERE `key`='transient' AND `namespace` LIKE 'sla.%';
+
+DELETE FROM `%TABLE_PREFIX%config`
+WHERE `key`='configuration' AND `namespace` LIKE 'list.%';
+
+DELETE FROM `%TABLE_PREFIX%config`
+WHERE `key`='name_format' AND `namespace` = 'core';
+
+-- Orphan users who don't know they're orphans
+UPDATE `%TABLE_PREFIX%user` A1
+  LEFT JOIN `%TABLE_PREFIX%organization` A2 ON (A1.`org_id` = A2.`id`)
+  SET A1.`org_id` = 0
+  WHERE A2.`id` IS NULL;
diff --git a/include/upgrader/streams/core/9143a511-0d6099a6.patch.sql b/include/upgrader/streams/core/9143a511-0d6099a6.patch.sql
new file mode 100644
index 0000000000000000000000000000000000000000..d5a151c9ec7ba6d8f686347d492387130a773a9d
--- /dev/null
+++ b/include/upgrader/streams/core/9143a511-0d6099a6.patch.sql
@@ -0,0 +1,115 @@
+/**
+ * @signature 0d6099a650cc7884eb59a040feab2ce8
+ * @version v1.10.0
+ * @title Add events to the ticket thread
+ *
+ */
+
+ALTER TABLE `%TABLE_PREFIX%ticket_event`
+  ADD `id` int(10) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST,
+  CHANGE `ticket_id` `thread_id` int(11) unsigned NOT NULL default '0',
+  CHANGE `staff` `username` varchar(128) NOT NULL default 'SYSTEM',
+  CHANGE `state` `state` enum('created','closed','reopened','assigned','transferred','overdue','edited','viewed','error','collab','resent') NOT NULL,
+  ADD `data` varchar(1024) DEFAULT NULL COMMENT 'Encoded differences' AFTER `state`,
+  ADD `uid` int(11) unsigned DEFAULT NULL AFTER `username`,
+  ADD `uid_type` char(1) NOT NULL DEFAULT 'S' AFTER `uid`,
+  RENAME TO `%TABLE_PREFIX%thread_event`;
+
+-- Change the `ticket_id` column to the values in `%thread`.`id`
+CREATE TABLE `%TABLE_PREFIX%_ticket_thread_evt`
+    (PRIMARY KEY (`object_id`))
+    SELECT `object_id`, `id` FROM `%TABLE_PREFIX%thread`
+    WHERE `object_type` = 'T';
+
+UPDATE `%TABLE_PREFIX%thread_event` A1
+    LEFT JOIN `%TABLE_PREFIX%_ticket_thread_evt` A2 ON (A1.`thread_id` = A2.`object_id`)
+    SET A1.`thread_id` = A2.`id`;
+
+DROP TABLE `%TABLE_PREFIX%_ticket_thread_evt`;
+
+-- Attempt to connect the `username` to the staff_id
+UPDATE `%TABLE_PREFIX%thread_event` A1
+    LEFT JOIN `%TABLE_PREFIX%staff` A2 ON (A2.`username` = A1.`username`)
+    SET A1.`uid` = A2.`staff_id`
+    WHERE A1.`username` != 'SYSTEM';
+
+ALTER TABLE `%TABLE_PREFIX%user_email`
+  ADD `flags` int(10) unsigned NOT NULL DEFAULT 0 AFTER `user_id`;
+
+ALTER TABLE `%TABLE_PREFIX%form`
+  CHANGE `deletable` `flags` int(10) unsigned NOT NULL DEFAULT 1;
+
+-- Previous versions did not correctly mark the internal forms as NOT deletable
+UPDATE `%TABLE_PREFIX%form`
+  SET `flags` = 0 WHERE `type` IN ('T','U','C','O','A');
+
+ALTER TABLE `%TABLE_PREFIX%team`
+  ADD `flags` int(10) unsigned NOT NULL default 1 AFTER `lead_id`;
+
+UPDATE `%TABLE_PREFIX%team`
+  SET `flags` = CASE WHEN `isenabled` THEN 1 ELSE 0 END
+              + CASE WHEN `noalerts` THEN 2 ELSE 0 END;
+
+-- Migrate %config[namespace=dept.x, key=alert_members_only]
+ALTER TABLE `%TABLE_PREFIX%department`
+  ADD `flags` int(10) unsigned NOT NULL default 0 AFTER `manager_id`;
+
+UPDATE `%TABLE_PREFIX%department` A1
+  JOIN `%TABLE_PREFIX%config` A2
+    ON (A2.`namespace` = CONCAT('dept.', A1.`id`) AND A2.`key` = 'assign_members_only')
+  SET A1.`flags` = 1 WHERE A2.`value` != '';
+
+-- Migrate %config[namespace=sla.x, key=transient]
+ALTER TABLE `%TABLE_PREFIX%sla`
+  ADD `flags` int(10) unsigned NOT NULL default 3 AFTER `id`;
+
+UPDATE `%TABLE_PREFIX%sla` A1
+  SET A1.`flags` =
+      (CASE WHEN A1.`isactive` THEN 1 ELSE 0 END)
+    | (CASE WHEN A1.`enable_priority_escalation` THEN 2 ELSE 0 END)
+    | (CASE WHEN A1.`disable_overdue_alerts` THEN 4 ELSE 0 END)
+    | (CASE WHEN (SELECT `value` FROM `%TABLE_PREFIX%config` `config`
+            WHERE`config`.`namespace` = CONCAT('sla.', A1.`id`) AND `config`.`key` = 'transient')
+            = '1' THEN 8 ELSE 0 END);
+
+ALTER TABLE `%TABLE_PREFIX%ticket`
+  ADD `source_extra` varchar(40) NULL default NULL AFTER `source`;
+
+-- Retire %config[namespace=list.x, key=configuration]
+ALTER TABLE `%TABLE_PREFIX%list`
+  ADD `configuration` text NOT NULL DEFAULT '' AFTER `type`;
+
+UPDATE `%TABLE_PREFIX%list` A1
+  JOIN `%TABLE_PREFIX%config` A2
+    ON (A2.`namespace` = CONCAT('list.', A1.`id`) AND A2.`key` = 'configuration')
+  SET A1.`configuration` = A2.`value`;
+
+-- Rebuild %ticket__cdata as UTF8
+DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket__cdata`;
+
+-- Move `enable_html_thread` to `enable_richtext`
+UPDATE `%TABLE_PREFIX%config`
+  SET `key` = 'enable_richtext'
+  WHERE `namespace` = 'core' AND `key` = 'enable_html_thread';
+
+SET @name_format = (SELECT `value` FROM `%TABLE_PREFIX%config` A1
+    WHERE A1.`namespace` = 'core' AND A1.`key` = 'name_format');
+INSERT INTO `%TABLE_PREFIX%config`
+    (`namespace`, `key`, `value`) VALUES
+    ('core', 'agent_name_format', @name_format),
+    ('core', 'client_name_format', @name_format);
+
+-- Drop search table and turn on reindexing
+DROP TABLE IF EXISTS `%TABLE_PREFIX%_search`;
+
+UPDATE `%TABLE_PREFIX%config` SET `value` = '1'
+  WHERE `key` = 'reindex' and `namespace` = 'mysqlsearch';
+
+-- Support varying names for duplicated content
+ALTER TABLE `%TABLE_PREFIX%attachment`
+  ADD `name` varchar(255) NULL default NULL AFTER `file_id`;
+
+-- Finished with patch
+UPDATE `%TABLE_PREFIX%config`
+    SET `value` = '0d6099a650cc7884eb59a040feab2ce8'
+    WHERE `key` = 'schema_signature' AND `namespace` = 'core';
diff --git a/include/upgrader/streams/core/9143a511-0d6099a6.task.php b/include/upgrader/streams/core/9143a511-0d6099a6.task.php
new file mode 100644
index 0000000000000000000000000000000000000000..4578329f6130ed943e486bbdb78ae9766630eb32
--- /dev/null
+++ b/include/upgrader/streams/core/9143a511-0d6099a6.task.php
@@ -0,0 +1,28 @@
+<?php
+
+class StatusListCreater extends MigrationTask {
+    var $description = "Add ticket statuses (if not already)";
+
+    function run($max_time) {
+        global $cfg;
+
+        // Moved here from core/8f99b8bf-03ff59bf.task.php
+        // Moved here from core/b26f29a6-1ee831c8.task.php
+        require_once(INCLUDE_DIR . 'class.list.php');
+        if ($list = DynamicList::objects()->filter(array('type' => 'ticket-status'))->first())
+            // Already have ticket statuses
+            return;
+
+        $i18n = new Internationalization($cfg->get('system_language', 'en_US'));
+        $lists = $i18n->getTemplate('list.yaml')->getData();
+        foreach ($lists as $l) {
+            DynamicList::create($l);
+        }
+
+        $statuses = $i18n->getTemplate('ticket_status.yaml')->getData();
+        foreach ($statuses as $s) {
+            TicketStatus::__create($s);
+        }
+    }
+}
+return 'StatusListCreater';
diff --git a/include/upgrader/streams/core/934954de-f1ccd3bb.patch.sql b/include/upgrader/streams/core/934954de-f1ccd3bb.patch.sql
index c6bb713e7cafd57b4ccc58bd8df41ddc23d9fe0c..9cc75cb6bf7a63ede826fd6e7e7695a795eba6c8 100644
--- a/include/upgrader/streams/core/934954de-f1ccd3bb.patch.sql
+++ b/include/upgrader/streams/core/934954de-f1ccd3bb.patch.sql
@@ -8,15 +8,30 @@
  */
 
 ALTER TABLE `%TABLE_PREFIX%file`
-    ADD `bk` CHAR(1) NOT NULL DEFAULT 'D' AFTER `ft`,
     -- RFC 4288, Section 4.2 declares max MIMEType at 255 ascii chars
     CHANGE `type` `type` varchar(255) collate ascii_general_ci NOT NULL default '',
     CHANGE `size` `size` BIGINT(20) NOT NULL DEFAULT 0,
     CHANGE `hash` `key` VARCHAR(86) COLLATE ascii_general_ci,
     ADD `signature` VARCHAR(86) COLLATE ascii_bin AFTER `key`,
-    ADD `attrs` VARCHAR(255) AFTER `name`,
     ADD INDEX (`signature`);
 
+-- dd0022fb14892c0bb6a9700392df2de7 added `bk` and `attrs` to facilitate
+-- upgrading from osTicket 1.6 without loading files into the database
+SET @s = (SELECT IF(
+    (SELECT COUNT(*)
+        FROM INFORMATION_SCHEMA.COLUMNS
+        WHERE table_name = '%TABLE_PREFIX%file'
+        AND table_schema = DATABASE()
+        AND column_name = 'bk'
+    ) > 0,
+    "SELECT 1",
+    "ALTER TABLE `%TABLE_PREFIX%file`
+        ADD `bk` CHAR(1) NOT NULL DEFAULT 'D' AFTER `ft`,
+        ADD `attrs` VARCHAR(255) AFTER `name`"
+));
+PREPARE stmt FROM @s;
+EXECUTE stmt;
+
 -- Finished with patch
 UPDATE `%TABLE_PREFIX%config`
     SET `value` = 'f1ccd3bb620e314b0ae1dbd0a1a99177'
diff --git a/include/upgrader/streams/core/934954de-f1ccd3bb.task.php b/include/upgrader/streams/core/934954de-f1ccd3bb.task.php
new file mode 100644
index 0000000000000000000000000000000000000000..041bfad9a33730283e1203d6711bbafcaa27a548
--- /dev/null
+++ b/include/upgrader/streams/core/934954de-f1ccd3bb.task.php
@@ -0,0 +1,24 @@
+<?php
+
+class FileImport extends MigrationTask {
+    var $description = "Import core osTicket attachment files";
+
+    function run($runtime) {
+        $i18n = new Internationalization('en_US');
+        $files = $i18n->getTemplate('file.yaml')->getData();
+        foreach ($files as $f) {
+            if (!($file = AttachmentFile::create($f)))
+                continue;
+
+            // Ensure the new files are never deleted (attached to Disk)
+            $sql ='INSERT INTO '.ATTACHMENT_TABLE
+                .' SET object_id=0, `type`=\'D\', inline=1'
+                .', file_id='.db_input($file->getId());
+            db_query($sql);
+        }
+    }
+}
+
+return 'FileImport';
+
+?>
diff --git a/include/upgrader/streams/core/b26f29a6-1ee831c8.cleanup.sql b/include/upgrader/streams/core/b26f29a6-1ee831c8.cleanup.sql
new file mode 100644
index 0000000000000000000000000000000000000000..97bd2d4927a5abd805fa2bb88bf4bcd127fad8c9
--- /dev/null
+++ b/include/upgrader/streams/core/b26f29a6-1ee831c8.cleanup.sql
@@ -0,0 +1,23 @@
+/**
+ * @signature 1ee831c854fe9f35115a3e672916bb91
+ * @version v1.10.0
+ * @title Make editable content translatable
+ *
+ * This patch adds support for translatable administratively editable
+ * content, such as help topic names, department and group names, site page
+ * and faq content, etc.
+ */
+
+DROP TABLE `%TABLE_PREFIX%timezone`;
+
+ALTER TABLE `%TABLE_PREFIX%staff`
+    DROP `timezone_id`,
+    DROP `daylight_saving`;
+
+ALTER TABLE `%TABLE_PREFIX%user_account`
+    DROP `timezone_id`,
+    DROP `dst`;
+
+DELETE FROM `%TABLE_PREFIX%config`
+    WHERE `key` IN ('enable_daylight_saving', 'default_timezone_id')
+      AND `namespace` = 'core';
diff --git a/include/upgrader/streams/core/b26f29a6-1ee831c8.patch.sql b/include/upgrader/streams/core/b26f29a6-1ee831c8.patch.sql
new file mode 100644
index 0000000000000000000000000000000000000000..84df9fc92396ad1169464db4aa637c135d98cf20
--- /dev/null
+++ b/include/upgrader/streams/core/b26f29a6-1ee831c8.patch.sql
@@ -0,0 +1,256 @@
+/**
+ * @signature 1ee831c854fe9f35115a3e672916bb91
+ * @version v1.10.0
+ * @title Make editable content translatable and add queues
+ *
+ * This patch adds support for translatable administratively editable
+ * content, such as help topic names, department and group names, site page
+ * and faq content, etc.
+ *
+ * This patch also transitions from the timezone table to the Olson timezone
+ * database available in PHP 5.3.
+ */
+
+ALTER TABLE `%TABLE_PREFIX%attachment`
+    ADD `lang` varchar(16) AFTER `inline`;
+
+ALTER TABLE `%TABLE_PREFIX%staff`
+    ADD `lang` varchar(16) DEFAULT NULL AFTER `signature`,
+    ADD `timezone` varchar(64) default NULL AFTER `lang`,
+    ADD `locale` varchar(16) DEFAULT NULL AFTER `timezone`,
+    ADD `extra` text AFTER `default_paper_size`;
+
+ALTER TABLE `%TABLE_PREFIX%user_account`
+    ADD `timezone` varchar(64) DEFAULT NULL AFTER `status`,
+    ADD `extra` text AFTER `backend`;
+
+CREATE TABLE `%TABLE_PREFIX%translation` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `object_hash` char(16) CHARACTER SET ascii DEFAULT NULL,
+  `type` enum('phrase','article','override') DEFAULT NULL,
+  `flags` int(10) unsigned NOT NULL DEFAULT '0',
+  `revision` int(11) unsigned DEFAULT NULL,
+  `agent_id` int(10) unsigned NOT NULL DEFAULT '0',
+  `lang` varchar(16) NOT NULL DEFAULT '',
+  `text` mediumtext NOT NULL,
+  `source_text` text,
+  `updated` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `type` (`type`,`lang`),
+  KEY `object_hash` (`object_hash`)
+) DEFAULT CHARSET=utf8;
+
+-- Transition the current Timezone configuration to Olsen database
+
+CREATE TABLE `%TABLE_PREFIX%_timezones` (
+    `offset` int,
+    `dst` tinyint(1) unsigned,
+    `south` tinyint(1) unsigned default 0,
+    `olson_name` varchar(32)
+) DEFAULT CHARSET=utf8;
+
+INSERT INTO `%TABLE_PREFIX%_timezones` (`offset`, `dst`, `olson_name`) VALUES
+    -- source borrowed from the jstz project
+    (-720, 0, 'Pacific/Majuro'),
+    (-660, 0, 'Pacific/Pago_Pago'),
+    (-600, 1, 'America/Adak'),
+    (-600, 0, 'Pacific/Honolulu'),
+    (-570, 0, 'Pacific/Marquesas'),
+    (-540, 0, 'Pacific/Gambier'),
+    (-540, 1, 'America/Anchorage'),
+    (-480, 1, 'America/Los_Angeles'),
+    (-480, 0, 'Pacific/Pitcairn'),
+    (-420, 0, 'America/Phoenix'),
+    (-420, 1, 'America/Denver'),
+    (-360, 0, 'America/Guatemala'),
+    (-360, 1, 'America/Chicago'),
+    (-300, 0, 'America/Bogota'),
+    (-300, 1, 'America/New_York'),
+    (-270, 0, 'America/Caracas'),
+    (-240, 1, 'America/Halifax'),
+    (-240, 0, 'America/Santo_Domingo'),
+    (-240, 1, 'America/Santiago'),
+    (-210, 1, 'America/St_Johns'),
+    (-180, 1, 'America/Godthab'),
+    (-180, 0, 'America/Argentina/Buenos_Aires'),
+    (-180, 1, 'America/Montevideo'),
+    (-120, 0, 'America/Noronha'),
+    (-120, 1, 'America/Noronha'),
+    (-60,  1, 'Atlantic/Azores'),
+    (-60,  0, 'Atlantic/Cape_Verde'),
+    (0,    0, 'UTC'),
+    (0,    1, 'Europe/London'),
+    (60,   1, 'Europe/Berlin'),
+    (60,   0, 'Africa/Lagos'),
+    (120,  1, 'Asia/Beirut'),
+    (120,  0, 'Africa/Johannesburg'),
+    (180,  0, 'Asia/Baghdad'),
+    (180,  1, 'Europe/Moscow'),
+    (210,  1, 'Asia/Tehran'),
+    (240,  0, 'Asia/Dubai'),
+    (240,  1, 'Asia/Baku'),
+    (270,  0, 'Asia/Kabul'),
+    (300,  1, 'Asia/Yekaterinburg'),
+    (300,  0, 'Asia/Karachi'),
+    (330,  0, 'Asia/Kolkata'),
+    (345,  0, 'Asia/Kathmandu'),
+    (360,  0, 'Asia/Dhaka'),
+    (360,  1, 'Asia/Omsk'),
+    (390,  0, 'Asia/Rangoon'),
+    (420,  1, 'Asia/Krasnoyarsk'),
+    (420,  0, 'Asia/Jakarta'),
+    (480,  0, 'Asia/Shanghai'),
+    (480,  1, 'Asia/Irkutsk'),
+    (525,  0, 'Australia/Eucla'),
+    (525,  1, 'Australia/Eucla'),
+    (540,  1, 'Asia/Yakutsk'),
+    (540,  0, 'Asia/Tokyo'),
+    (570,  0, 'Australia/Darwin'),
+    (570,  1, 'Australia/Adelaide'),
+    (600,  0, 'Australia/Brisbane'),
+    (600,  1, 'Asia/Vladivostok'),
+    (630,  1, 'Australia/Lord_Howe'),
+    (660,  1, 'Asia/Kamchatka'),
+    (660,  0, 'Pacific/Noumea'),
+    (690,  0, 'Pacific/Norfolk'),
+    (720,  1, 'Pacific/Auckland'),
+    (720,  0, 'Pacific/Tarawa'),
+    (765,  1, 'Pacific/Chatham'),
+    (780,  0, 'Pacific/Tongatapu'),
+    (780,  1, 'Pacific/Apia'),
+    (840,  0, 'Pacific/Kiritimati');
+
+-- XXX:
+-- These zone have opposite DST interpretations and also have norther
+-- hemisphere counterparts
+INSERT INTO `%TABLE_PREFIX%_timezones` (`offset`, `dst`, `south`, `olson_name`) VALUES
+    (-360, 1, 1, 'Pacific/Easter'),
+    (60,   1, 1, 'Africa/Windhoek'),
+    (600,  1, 1, 'Australia/Sydney');
+
+UPDATE `%TABLE_PREFIX%staff` A1
+    JOIN `%TABLE_PREFIX%timezone` A2 ON (A1.`timezone_id` = A2.`id`)
+    JOIN `%TABLE_PREFIX%_timezones` A3 ON (A2.`offset` * 60 = A3.`offset`
+        AND A1.`daylight_saving` = A3.`dst`
+        AND A3.`south` = 0)
+    SET A1.`timezone` = A3.`olson_name`;
+
+UPDATE `%TABLE_PREFIX%user_account` A1
+    JOIN `%TABLE_PREFIX%timezone` A2 ON (A1.`timezone_id` = A2.`id`)
+    JOIN `%TABLE_PREFIX%_timezones` A3 ON (A2.`offset` * 60 = A3.`offset`
+        AND A1.`dst` = A3.`dst`
+        AND A3.`south` = 0)
+    SET A1.`timezone` = A3.`olson_name`;
+
+-- Update system default timezone
+SET @default_timezone_id = (
+    SELECT `value` FROM `%TABLE_PREFIX%config` A1
+    WHERE A1.`key` = 'default_timezone_id'
+      AND A1.`namespace` = 'core'
+);
+SET @enable_daylight_saving = (
+    SELECT `value` FROM `%TABLE_PREFIX%config` A1
+    WHERE A1.`key` = 'enable_daylight_saving'
+      AND A1.`namespace` = 'core'
+);
+
+UPDATE `%TABLE_PREFIX%config` A1
+    JOIN `%TABLE_PREFIX%timezone` A2 ON (@default_timezone_id = A2.`id`)
+    JOIN `%TABLE_PREFIX%_timezones` A3 ON (A2.`offset` * 60 = A3.`offset`
+        AND @enable_daylight_saving = A3.`dst`
+        AND A3.`south` = 0)
+    SET A1.`value` = A3.`olson_name`
+    WHERE A1.`key` = 'default_timezone_id'
+      AND A1.`namespace` = 'core';
+
+UPDATE `%TABLE_PREFIX%config` A1
+    SET A1.`key` = 'default_timezone'
+    WHERE A1.`key` = 'default_timezone_id'
+      AND A1.`namespace` = 'core';
+
+DROP TABLE %TABLE_PREFIX%_timezones;
+
+ALTER TABLE `%TABLE_PREFIX%ticket`
+    ADD `est_duedate` datetime default NULL AFTER `duedate`,
+    ADD `lastupdate` datetime default NULL AFTER `lastresponse`;
+
+UPDATE `%TABLE_PREFIX%ticket` A1
+    LEFT JOIN `%TABLE_PREFIX%sla` A2 ON (A1.sla_id = A2.id)
+    SET A1.`est_duedate` =
+        COALESCE(A1.`duedate`, A1.`created` + INTERVAL A2.`grace_period` HOUR),
+      A1.`lastupdate` =
+        CAST(GREATEST(IFNULL(A1.lastmessage, 0), IFNULL(A1.closed, 0), IFNULL(A1.reopened, 0), A1.created) as DATETIME);
+
+CREATE TABLE `%TABLE_PREFIX%queue` (
+  `id` int(11) unsigned not null auto_increment,
+  `parent_id` int(11) unsigned not null default 0,
+  `flags` int(11) unsigned not null default 0,
+  `staff_id` int(11) unsigned not null default 0,
+  `sort` int(11) unsigned not null default 0,
+  `title` varchar(60),
+  `config` text,
+  `created` datetime not null,
+  `updated` datetime not null,
+  primary key (`id`)
+) DEFAULT CHARSET=utf8;
+
+-- Add flags field to form field
+ALTER TABLE  `%TABLE_PREFIX%form_field`
+    ADD  `flags` INT UNSIGNED NOT NULL DEFAULT  '1' AFTER  `form_id`;
+
+-- Flag field stored in the system elsewhere as nonstorable locally.
+UPDATE `%TABLE_PREFIX%form_field` A1 JOIN `%TABLE_PREFIX%form` A2 ON(A2.id=A1.form_id)
+    SET A1.`flags` = 3
+    WHERE A2.`type` = 'U' AND A1.`name` IN('name','email');
+
+UPDATE `%TABLE_PREFIX%form_field` A1 JOIN `%TABLE_PREFIX%form` A2 ON(A2.id=A1.form_id)
+    SET A1.`flags`=3
+    WHERE A2.`type`='O' AND A1.`name` IN('name');
+
+-- Thread entry field is stored externally
+UPDATE `%TABLE_PREFIX%form_field` A1 JOIN `%TABLE_PREFIX%form` A2 ON(A2.id=A1.form_id)
+    SET A1.`flags`=3
+    WHERE A2.`type`='T' AND A1.`name` IN ('message');
+
+-- Coalesce to zero here in case the config option has never been saved
+set @client_edit = coalesce(
+    (select value from `%TABLE_PREFIX%config` where `key` =
+    'allow_client_updates'), 0);
+
+-- Transfer previous visibility and requirement settings to new flag field
+UPDATE `%TABLE_PREFIX%form_field` SET `flags` = `flags` |
+     CASE WHEN `private` = 0 and @client_edit = 1 THEN CONV(3300, 16, 10)
+          WHEN `private` = 0 and @client_edit = 0 THEN CONV(3100, 16, 10)
+          WHEN `private` = 1 THEN CONV(3000, 16, 10)
+          WHEN `private` = 2 and @client_edit = 1 THEN CONV(300, 16, 10)
+          WHEN `private` = 2 and @client_edit = 0 THEN CONV(100, 16, 10) END
+   | CASE WHEN `required` = 0 THEN 0
+          WHEN `required` = 1 THEN CONV(4400, 16, 10)
+          WHEN `required` = 2 THEN CONV(400, 16, 10)
+          WHEN `required` = 3 THEN CONV(4000, 16, 10) END
+   | IF(`edit_mask` & 1, CONV(20, 16, 10), 0)
+   | IF(`edit_mask` & 2, CONV(40000, 16, 10), 0)
+   | IF(`edit_mask` & 4, CONV(10000, 16, 10), 0)
+   | IF(`edit_mask` & 8, CONV(20000, 16, 10), 0)
+   | IF(`edit_mask` & 16, CONV(10, 16, 10), 0)
+   | IF(`edit_mask` & 32, CONV(40, 16, 10), 0);
+
+-- Detect inline images not recorded as inline
+CREATE TABLE `%TABLE_PREFIX%_unknown_inlines` AS
+  SELECT A2.`attach_id`
+  FROM `%TABLE_PREFIX%file` A1
+  JOIN `%TABLE_PREFIX%ticket_attachment` A2 ON (A1.id = A2.file_id)
+  JOIN `%TABLE_PREFIX%ticket_thread` A3 ON (A3.ticket_id = A2.ticket_id)
+  WHERE A1.`type` LIKE 'image/%' AND A2.inline = 0
+    AND A3.body LIKE CONCAT('%"cid:', A1.key, '"%');
+
+UPDATE `%TABLE_PREFIX%ticket_attachment` A1
+  JOIN %TABLE_PREFIX%_unknown_inlines A2 ON (A1.attach_id = A2.attach_id)
+  SET A1.inline = 1;
+
+DROP TABLE `%TABLE_PREFIX%_unknown_inlines`;
+
+-- Finished with patch
+UPDATE `%TABLE_PREFIX%config`
+    SET `value` = '1ee831c854fe9f35115a3e672916bb91'
+    WHERE `key` = 'schema_signature' AND `namespace` = 'core';
diff --git a/include/upgrader/streams/core/b26f29a6-1ee831c8.task.php b/include/upgrader/streams/core/b26f29a6-1ee831c8.task.php
new file mode 100644
index 0000000000000000000000000000000000000000..ba6d63515b8bee4a3535185464103057ce66a7fa
--- /dev/null
+++ b/include/upgrader/streams/core/b26f29a6-1ee831c8.task.php
@@ -0,0 +1,75 @@
+<?php
+
+class IntlMigrator extends MigrationTask {
+    var $description = "Date format migration from date() to ICU";
+
+    static $dateToIntl = array(
+        'd' => 'dd',
+        'D' => 'EEE',
+        'j' => 'd',
+        'l' => 'EEEE',
+        'N' => 'e',
+        'w' => 'c',
+        'z' => 'D',
+
+        'W' => 'w',
+
+        'F' => 'MMMM',
+        'm' => 'MM',
+        'M' => 'MMM',
+        'n' => 'M',
+
+        'o' => 'Y',
+        'Y' => 'y',
+        'y' => 'yy',
+
+        'A' => 'a',
+        'g' => 'h',
+        'G' => 'H',
+        'h' => 'hh',
+        'H' => 'HH',
+        'i' => 'mm',
+        's' => 'ss',
+        'u' => 'SSSSSS',
+
+        'e' => 'VV',
+        'O' => 'ZZZ',
+        'P' => 'ZZZZZ',
+        'T' => 'z',
+
+        'c' => "yyyy-MM-dd'T'HH:mm:ssXXXXX",
+        'r' => 'EEE, d MMM yyyy HH:mm:ss XXXXX',
+    );
+
+    function run($max_time) {
+        global $cfg;
+
+        // Detect rough install date — rationale: the schema_signature is
+        // touched by the database migrater; however the updated timestamp
+        // associated with it is not touched.
+        $install_date = $cfg->lastModified('schema_signature');
+        $touched = false;
+
+        // Upgrade date formats
+        foreach (
+            array('datetime_format', 'daydatetime_format', 'time_format', 'date_format')
+            as $key
+        ) {
+            $new_format = '';
+            $format = $cfg->get($key);
+            foreach (str_split($format) as $char) {
+                $new_format .= @self::$dateToIntl[$char] ?: $char;
+            }
+            $cfg->set($key, $new_format);
+
+            // Consider the last-updated time of the key to see if the
+            // format has been modified since installation
+            $touched |= $cfg->lastModified($key) != $install_date;
+        }
+
+        // Add in new custom date format flag
+        $cfg->set('date_formats', $touched ? 'custom' : '' );
+    }
+}
+
+return 'IntlMigrator';
diff --git a/include/upgrader/streams/core/d51f303a-dad45ca2.task.php b/include/upgrader/streams/core/d51f303a-dad45ca2.task.php
index 309f68e836e9f808a47fd3e69f6f143642226597..03a77f61d6349db29cd3d058ffa629afc388f53f 100644
--- a/include/upgrader/streams/core/d51f303a-dad45ca2.task.php
+++ b/include/upgrader/streams/core/d51f303a-dad45ca2.task.php
@@ -16,16 +16,7 @@ class NewHtmlTemplate extends MigrationTask {
             EmailTemplateGroup::create($t, $errors);
         }
 
-        $files = $i18n->getTemplate('file.yaml')->getData();
-        foreach ($files as $f) {
-            $id = AttachmentFile::create($f, $errors);
-
-            // Ensure the new files are never deleted (attached to Disk)
-            $sql ='INSERT INTO '.ATTACHMENT_TABLE
-                .' SET object_id=0, `type`=\'D\', inline=1'
-                .', file_id='.db_input($id);
-            db_query($sql);
-        }
+        // NOTE: Core files import moved to 934954de-f1ccd3bb.task.php
     }
 }
 return 'NewHtmlTemplate';
diff --git a/include/upgrader/streams/core/dad45ca2-61c9d5d7.task.php b/include/upgrader/streams/core/dad45ca2-61c9d5d7.task.php
index 98d128fbdaed3a1a4a57fe3af06cd0fbb99c591e..9f046b3e68716184692821096d20498955be0a0a 100644
--- a/include/upgrader/streams/core/dad45ca2-61c9d5d7.task.php
+++ b/include/upgrader/streams/core/dad45ca2-61c9d5d7.task.php
@@ -11,9 +11,50 @@ class DynamicFormLoader extends MigrationTask {
     function run($max_time) {
         $i18n = new Internationalization('en_US');
         $forms = $i18n->getTemplate('form.yaml')->getData();
-        foreach ($forms as $f)
+        foreach ($forms as &$f) {
+            // Only import forms which exist at this stage.
+            if (!in_array($f['type'], array('U', 'T', 'C', 'O')))
+                continue;
+
+            if ($f['fields'])  {
+                foreach($f['fields'] as &$field) {
+                    $flags = $field['flags'];
+                    // Edit mask
+                    $field['edit_mask'] = $this->f2m($flags);
+                    // private
+                    if (!($flags & DynamicFormField::FLAG_CLIENT_VIEW))
+                        $field['private'] = true;
+                    // required
+                    if (($flags & DynamicFormField::FLAG_CLIENT_REQUIRED)
+                            || ($flags & DynamicFormField::FLAG_AGENT_REQUIRED))
+                        $field['required'] = true;
+
+                    unset($field['flags']);
+                }
+                unset($field);
+            }
+
             DynamicForm::create($f);
+        }
+        unset($f);
     }
+
+    function f2m($flags) {
+        $masks = array(
+                1 => DynamicFormField::FLAG_MASK_DELETE,
+                2 => DynamicFormField::FLAG_MASK_NAME,
+                4 => DynamicFormField::FLAG_MASK_VIEW,
+                8 => DynamicFormField::FLAG_MASK_REQUIRE
+               );
+
+        $mask = 0;
+        foreach ($masks as $k => $v)
+            if (($flags & $v) != 0)
+                $mask += $k;
+
+        return $mask;
+   }
+
 }
 
 return 'DynamicFormLoader';
diff --git a/include/upgrader/streams/core/f5692e24-4323a6a8.patch.sql b/include/upgrader/streams/core/f5692e24-4323a6a8.patch.sql
index 39d7cb4698d1aa81e7f3fc2e48b0a7943fe86931..5db5be880fadc8107b909d5df1d99d3016b85de1 100644
--- a/include/upgrader/streams/core/f5692e24-4323a6a8.patch.sql
+++ b/include/upgrader/streams/core/f5692e24-4323a6a8.patch.sql
@@ -66,14 +66,9 @@ ALTER TABLE `%TABLE_PREFIX%email`
 ALTER TABLE `%TABLE_PREFIX%help_topic`
   ADD `sort` int(10) unsigned NOT NULL default '0' AFTER `form_id`;
 
--- Add `content_id` to the content table to allow for translations
 RENAME TABLE `%TABLE_PREFIX%page` TO `%TABLE_PREFIX%content`;
 ALTER TABLE `%TABLE_PREFIX%content`
-  CHANGE `type` `type` varchar(32) NOT NULL default 'other',
-  ADD `content_id` int(10) unsigned NOT NULL default 0 AFTER `id`;
-
-UPDATE `%TABLE_PREFIX%content`
-  SET `content_id` = `id`;
+  CHANGE `type` `type` varchar(32) NOT NULL default 'other';
 
 DROP TABLE IF EXISTS `%TABLE_PREFIX%user_account`;
 CREATE TABLE `%TABLE_PREFIX%user_account` (
@@ -114,9 +109,6 @@ INSERT INTO `%TABLE_PREFIX%content`
         WHERE A3.`key` = 'default_template_id' and `namespace` = 'core')
     AND A1.`code_name` = 'user.accesslink';
 
-UPDATE `%TABLE_PREFIX%content` SET `content_id` = LAST_INSERT_ID()
-    WHERE `id` = LAST_INSERT_ID();
-
 -- Transfer staff password reset link
 INSERT INTO `%TABLE_PREFIX%content`
     (`name`, `body`, `type`, `isactive`, `created`, `updated`)
@@ -126,9 +118,6 @@ INSERT INTO `%TABLE_PREFIX%content`
         WHERE A3.`key` = 'default_template_id' and `namespace` = 'core')
     AND A1.`code_name` = 'staff.pwreset';
 
-UPDATE `%TABLE_PREFIX%content` SET `content_id` = LAST_INSERT_ID()
-    WHERE `id` = LAST_INSERT_ID();
-
 -- No longer saved in the email_template table
 DELETE FROM `%TABLE_PREFIX%email_template`
     WHERE `code_name` IN ('staff.pwreset', 'user.accesslink');
diff --git a/include/upgrader/streams/core/f5692e24-4323a6a8.task.php b/include/upgrader/streams/core/f5692e24-4323a6a8.task.php
index 566fab7a20ad09887f6e59cd30ff73c630a09f17..7a22a16146700fbcd06679b494e3bcad62b536f7 100644
--- a/include/upgrader/streams/core/f5692e24-4323a6a8.task.php
+++ b/include/upgrader/streams/core/f5692e24-4323a6a8.task.php
@@ -29,9 +29,6 @@ class TemplateContentLoader extends MigrationTask {
                 .', created=NOW(), updated=NOW(), isactive=1';
             db_query($sql);
         }
-        // Set the content_id for all the new items
-        db_query('UPDATE '.PAGE_TABLE
-            .' SET `content_id` = `id` WHERE `content_id` = 0');
     }
 }
 return 'TemplateContentLoader';
diff --git a/include/upgrader/upgrade.inc.php b/include/upgrader/upgrade.inc.php
index c6492c65cb311e404e9fd497f817e51136f13152..8588f742eb644904593c62a68125ce8940513b5e 100644
--- a/include/upgrader/upgrade.inc.php
+++ b/include/upgrader/upgrade.inc.php
@@ -37,11 +37,13 @@ $action=$upgrader->getNextAction();
                 </form>
             </div>
     </div>
-    <div id="sidebar">
+    <div class="sidebar">
+        <div class="content">
             <h3><?php echo __('Upgrade Tips');?></h3>
             <p>1. <?php echo __('Be patient the process will take a couple of minutes.');?></p>
             <p>2. <?php echo __('If you experience any problems, you can always restore your files/database backup.');?></p>
             <p>3. <?php echo sprintf(__('We can help. Feel free to %1$s contact us %2$s for professional help.'), '<a href="http://osticket.com/support" target="_blank">', '</a>');?></p>
+        </div>
     </div>
     <div class="clear"></div>
     <div id="upgrading">
diff --git a/index.php b/index.php
index 5a18fb1463d4b81ccce47236e23625b3af4d14b0..6c9558fa930d71173e59645febce619cf38651af 100644
--- a/index.php
+++ b/index.php
@@ -14,51 +14,69 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 require('client.inc.php');
+
+require_once INCLUDE_DIR . 'class.page.php';
+
 $section = 'home';
 require(CLIENTINC_DIR.'header.inc.php');
 ?>
 <div id="landing_page">
-    <?php
+<?php include CLIENTINC_DIR.'templates/sidebar.tmpl.php'; ?>
+<div class="main-content">
+<?php
+if ($cfg && $cfg->isKnowledgebaseEnabled()) { ?>
+<div class="search-form">
+    <form method="get" action="kb/faq.php">
+    <input type="hidden" name="a" value="search"/>
+    <input type="text" name="q" class="search" placeholder="Search our knowledge base"/>
+    <button type="submit" class="green button">Search</button>
+    </form>
+</div>
+    <div class="thread-body">
+<?php
+}
     if($cfg && ($page = $cfg->getLandingPage()))
         echo $page->getBodyWithImages();
     else
         echo  '<h1>'.__('Welcome to the Support Center').'</h1>';
     ?>
-    <div id="new_ticket" class="pull-left">
-        <h3><?php echo __('Open a New Ticket');?></h3>
-        <br>
-        <div><?php echo __('Please provide as much detail as possible so we can best assist you. To update a previously submitted ticket, please login.');?></div>
-    </div>
-
-    <div id="check_status" class="pull-right">
-        <h3><?php echo __('Check Ticket Status');?></h3>
-        <br>
-        <div><?php echo __('We provide archives and history of all your current and past support requests complete with responses.');?></div>
-    </div>
-
-    <div class="clear"></div>
-    <div class="front-page-button pull-left">
-        <p>
-            <a href="open.php" class="green button"><?php echo __('Open a New Ticket');?></a>
-        </p>
-    </div>
-    <div class="front-page-button pull-right">
-        <p>
-            <a href="<?php if(is_object($thisclient)){ echo 'tickets.php';} else {echo 'view.php';}?>" class="blue button"><?php echo __('Check Ticket Status');?></a>
-        </p>
     </div>
 </div>
 <div class="clear"></div>
+
+<div>
 <?php
 if($cfg && $cfg->isKnowledgebaseEnabled()){
     //FIXME: provide ability to feature or select random FAQs ??
 ?>
-<p><?php echo sprintf(
-    __('Be sure to browse our %s before opening a ticket'),
-    sprintf('<a href="kb/index.php">%s</a>',
-        __('Frequently Asked Questions (FAQs)')
-    )); ?></p>
-</div>
+<br/><br/>
+<?php
+$cats = Category::getFeatured();
+if ($cats->all()) { ?>
+<h1>Featured Knowledge Base Articles</h1>
+<?php
+}
+
+    foreach ($cats as $C) { ?>
+    <div class="featured-category front-page">
+        <i class="icon-folder-open icon-2x"></i>
+        <div class="category-name">
+            <?php echo $C->getName(); ?>
+        </div>
+<?php foreach ($C->getTopArticles() as $F) { ?>
+        <div class="article-headline">
+            <div class="article-title"><a href="<?php echo ROOT_PATH;
+                ?>kb/faq.php?id=<?php echo $F->getId(); ?>"><?php
+                echo $F->getQuestion(); ?></a></div>
+            <div class="article-teaser"><?php echo $F->getTeaser(); ?></div>
+        </div>
+<?php } ?>
+    </div>
 <?php
-} ?>
+    }
+}
+?>
+</div>
+</div>
+
 <?php require(CLIENTINC_DIR.'footer.inc.php'); ?>
diff --git a/js/fabric.min.js b/js/fabric.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..dbcd34b8697579c8b766d39959771260dfaa7ade
--- /dev/null
+++ b/js/fabric.min.js
@@ -0,0 +1,8 @@
+/* build: `node build.js modules=ALL exclude=gestures,cufon,json minifier=uglifyjs` *//*! Fabric.js Copyright 2008-2014, Printio (Juriy Zaytsev, Maxim Chernyak) */var fabric=fabric||{version:"1.4.13"};typeof exports!="undefined"&&(exports.fabric=fabric),typeof document!="undefined"&&typeof window!="undefined"?(fabric.document=document,fabric.window=window):(fabric.document=require("jsdom").jsdom("<!DOCTYPE html><html><head></head><body></body></html>"),fabric.document.createWindow?fabric.window=fabric.document.createWindow():fabric.window=fabric.document.parentWindow),fabric.isTouchSupported="ontouchstart"in fabric.document.documentElement,fabric.isLikelyNode=typeof Buffer!="undefined"&&typeof window=="undefined",fabric.SHARED_ATTRIBUTES=["display","transform","fill","fill-opacity","fill-rule","opacity","stroke","stroke-dasharray","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width"],fabric.DPI=96,fabric.reNum="(?:[-+]?(?:\\d+|\\d*\\.\\d+)(?:e[-+]?\\d+)?)",function(){function e(e,t){if(!this.__eventListeners[e])return;t?fabric.util.removeFromArray(this.__eventListeners[e],t):this.__eventListeners[e].length=0}function t(e,t){this.__eventListeners||(this.__eventListeners={});if(arguments.length===1)for(var n in e)this.on(n,e[n]);else this.__eventListeners[e]||(this.__eventListeners[e]=[]),this.__eventListeners[e].push(t);return this}function n(t,n){if(!this.__eventListeners)return;if(arguments.length===0)this.__eventListeners={};else if(arguments.length===1&&typeof arguments[0]=="object")for(var r in t)e.call(this,r,t[r]);else e.call(this,t,n);return this}function r(e,t){if(!this.__eventListeners)return;var n=this.__eventListeners[e];if(!n)return;for(var r=0,i=n.length;r<i;r++)n[r].call(this,t||{});return this}fabric.Observable={observe:t,stopObserving:n,fire:r,on:t,off:n,trigger:r}}(),fabric.Collection={add:function(){this._objects.push.apply(this._objects,arguments);for(var e=0,t=arguments.length;e<t;e++)this._onObjectAdded(arguments[e]);return this.renderOnAddRemove&&this.renderAll(),this},insertAt:function(e,t,n){var r=this.getObjects();return n?r[t]=e:r.splice(t,0,e),this._onObjectAdded(e),this.renderOnAddRemove&&this.renderAll(),this},remove:function(){var e=this.getObjects(),t;for(var n=0,r=arguments.length;n<r;n++)t=e.indexOf(arguments[n]),t!==-1&&(e.splice(t,1),this._onObjectRemoved(arguments[n]));return this.renderOnAddRemove&&this.renderAll(),this},forEachObject:function(e,t){var n=this.getObjects(),r=n.length;while(r--)e.call(t,n[r],r,n);return this},getObjects:function(e){return typeof e=="undefined"?this._objects:this._objects.filter(function(t){return t.type===e})},item:function(e){return this.getObjects()[e]},isEmpty:function(){return this.getObjects().length===0},size:function(){return this.getObjects().length},contains:function(e){return this.getObjects().indexOf(e)>-1},complexity:function(){return this.getObjects().reduce(function(e,t){return e+=t.complexity?t.complexity():0,e},0)}},function(e){var t=Math.sqrt,n=Math.atan2,r=Math.PI/180;fabric.util={removeFromArray:function(e,t){var n=e.indexOf(t);return n!==-1&&e.splice(n,1),e},getRandomInt:function(e,t){return Math.floor(Math.random()*(t-e+1))+e},degreesToRadians:function(e){return e*r},radiansToDegrees:function(e){return e/r},rotatePoint:function(e,t,n){var r=Math.sin(n),i=Math.cos(n);e.subtractEquals(t);var s=e.x*i-e.y*r,o=e.x*r+e.y*i;return(new fabric.Point(s,o)).addEquals(t)},transformPoint:function(e,t,n){return n?new fabric.Point(t[0]*e.x+t[1]*e.y,t[2]*e.x+t[3]*e.y):new fabric.Point(t[0]*e.x+t[1]*e.y+t[4],t[2]*e.x+t[3]*e.y+t[5])},invertTransform:function(e){var t=e.slice(),n=1/(e[0]*e[3]-e[1]*e[2]);t=[n*e[3],-n*e[1],-n*e[2],n*e[0],0,0];var r=fabric.util.transformPoint({x:e[4],y:e[5]},t);return t[4]=-r.x,t[5]=-r.y,t},toFixed:function(e,t){return parseFloat(Number(e).toFixed(t))},parseUnit:function(e,t){var n=/\D{0,2}$/.exec(e),r=parseFloat(e);t||(t=fabric.Text.DEFAULT_SVG_FONT_SIZE);switch(n[0]){case"mm":return r*fabric.DPI/25.4;case"cm":return r*fabric.DPI/2.54;case"in":return r*fabric.DPI;case"pt":return r*fabric.DPI/72;case"pc":return r*fabric.DPI/72*12;case"em":return r*t;default:return r}},falseFunction:function(){return!1},getKlass:function(e,t){return e=fabric.util.string.camelize(e.charAt(0).toUpperCase()+e.slice(1)),fabric.util.resolveNamespace(t)[e]},resolveNamespace:function(t){if(!t)return fabric;var n=t.split("."),r=n.length,i=e||fabric.window;for(var s=0;s<r;++s)i=i[n[s]];return i},loadImage:function(e,t,n,r){if(!e){t&&t.call(n,e);return}var i=fabric.util.createImage();i.onload=function(){t&&t.call(n,i),i=i.onload=i.onerror=null},i.onerror=function(){fabric.log("Error loading "+i.src),t&&t.call(n,null,!0),i=i.onload=i.onerror=null},e.indexOf("data")!==0&&typeof r!="undefined"&&(i.crossOrigin=r),i.src=e},enlivenObjects:function(e,t,n,r){function i(){++o===u&&t&&t(s)}e=e||[];var s=[],o=0,u=e.length;if(!u){t&&t(s);return}e.forEach(function(e,t){if(!e||!e.type){i();return}var o=fabric.util.getKlass(e.type,n);o.async?o.fromObject(e,function(n,o){o||(s[t]=n,r&&r(e,s[t])),i()}):(s[t]=o.fromObject(e),r&&r(e,s[t]),i())})},groupSVGElements:function(e,t,n){var r;return r=new fabric.PathGroup(e,t),typeof n!="undefined"&&r.setSourcePath(n),r},populateWithProperties:function(e,t,n){if(n&&Object.prototype.toString.call(n)==="[object Array]")for(var r=0,i=n.length;r<i;r++)n[r]in e&&(t[n[r]]=e[n[r]])},drawDashedLine:function(e,r,i,s,o,u){var a=s-r,f=o-i,l=t(a*a+f*f),c=n(f,a),h=u.length,p=0,d=!0;e.save(),e.translate(r,i),e.moveTo(0,0),e.rotate(c),r=0;while(l>r)r+=u[p++%h],r>l&&(r=l),e[d?"lineTo":"moveTo"](r,0),d=!d;e.restore()},createCanvasElement:function(e){return e||(e=fabric.document.createElement("canvas")),!e.getContext&&typeof G_vmlCanvasManager!="undefined"&&G_vmlCanvasManager.initElement(e),e},createImage:function(){return fabric.isLikelyNode?new(require("canvas").Image):fabric.document.createElement("img")},createAccessors:function(e){var t=e.prototype;for(var n=t.stateProperties.length;n--;){var r=t.stateProperties[n],i=r.charAt(0).toUpperCase()+r.slice(1),s="set"+i,o="get"+i;t[o]||(t[o]=function(e){return new Function('return this.get("'+e+'")')}(r)),t[s]||(t[s]=function(e){return new Function("value",'return this.set("'+e+'", value)')}(r))}},clipContext:function(e,t){t.save(),t.beginPath(),e.clipTo(t),t.clip()},multiplyTransformMatrices:function(e,t){var n=[[e[0],e[2],e[4]],[e[1],e[3],e[5]],[0,0,1]],r=[[t[0],t[2],t[4]],[t[1],t[3],t[5]],[0,0,1]],i=[];for(var s=0;s<3;s++){i[s]=[];for(var o=0;o<3;o++){var u=0;for(var a=0;a<3;a++)u+=n[s][a]*r[a][o];i[s][o]=u}}return[i[0][0],i[1][0],i[0][1],i[1][1],i[0][2],i[1][2]]},getFunctionBody:function(e){return(String(e).match(/function[^{]*\{([\s\S]*)\}/)||{})[1]},isTransparent:function(e,t,n,r){r>0&&(t>r?t-=r:t=0,n>r?n-=r:n=0);var i=!0,s=e.getImageData(t,n,r*2||1,r*2||1);for(var o=3,u=s.data.length;o<u;o+=4){var a=s.data[o];i=a<=0;if(i===!1)break}return s=null,i}}}(typeof exports!="undefined"?exports:this),function(){function i(t,n,i,u,a,f,l){var c=r.call(arguments);if(e[c])return e[c];var h=Math.PI,p=l*h/180,d=Math.sin(p),v=Math.cos(p),m=0,g=0;i=Math.abs(i),u=Math.abs(u);var y=-v*t*.5-d*n*.5,b=-v*n*.5+d*t*.5,w=i*i,E=u*u,S=b*b,x=y*y,T=w*E-w*S-E*x,N=0;if(T<0){var C=Math.sqrt(1-T/(w*E));i*=C,u*=C}else N=(a===f?-1:1)*Math.sqrt(T/(w*S+E*x));var k=N*i*b/u,L=-N*u*y/i,A=v*k-d*L+t*.5,O=d*k+v*L+n*.5,M=o(1,0,(y-k)/i,(b-L)/u),_=o((y-k)/i,(b-L)/u,(-y-k)/i,(-b-L)/u);f===0&&_>0?_-=2*h:f===1&&_<0&&(_+=2*h);var D=Math.ceil(Math.abs(_/h*2)),P=[],H=_/D,B=8/3*Math.sin(H/4)*Math.sin(H/4)/Math.sin(H/2),j=M+H;for(var F=0;F<D;F++)P[F]=s(M,j,v,d,i,u,A,O,B,m,g),m=P[F][4],g=P[F][5],M=j,j+=H;return e[c]=P,P}function s(e,n,i,s,o,u,a,f,l,c,h){var p=r.call(arguments);if(t[p])return t[p];var d=Math.cos(e),v=Math.sin(e),m=Math.cos(n),g=Math.sin(n),y=i*o*m-s*u*g+a,b=s*o*m+i*u*g+f,w=c+l*(-i*o*v-s*u*d),E=h+l*(-s*o*v+i*u*d),S=y+l*(i*o*g+s*u*m),x=b+l*(s*o*g-i*u*m);return t[p]=[w,E,S,x,y,b],t[p]}function o(e,t,n,r){var i=Math.atan2(t,e),s=Math.atan2(r,n);return s>=i?s-i:2*Math.PI-(i-s)}function u(e,t,i,s,o,u,a,f){var l=r.call(arguments);if(n[l])return n[l];var c=Math.sqrt,h=Math.min,p=Math.max,d=Math.abs,v=[],m=[[],[]],g,y,b,w,E,S,x,T;y=6*e-12*i+6*o,g=-3*e+9*i-9*o+3*a,b=3*i-3*e;for(var N=0;N<2;++N){N>0&&(y=6*t-12*s+6*u,g=-3*t+9*s-9*u+3*f,b=3*s-3*t);if(d(g)<1e-12){if(d(y)<1e-12)continue;w=-b/y,0<w&&w<1&&v.push(w);continue}x=y*y-4*b*g;if(x<0)continue;T=c(x),E=(-y+T)/(2*g),0<E&&E<1&&v.push(E),S=(-y-T)/(2*g),0<S&&S<1&&v.push(S)}var C,k,L=v.length,A=L,O;while(L--)w=v[L],O=1-w,C=O*O*O*e+3*O*O*w*i+3*O*w*w*o+w*w*w*a,m[0][L]=C,k=O*O*O*t+3*O*O*w*s+3*O*w*w*u+w*w*w*f,m[1][L]=k;m[0][A]=e,m[1][A]=t,m[0][A+1]=a,m[1][A+1]=f;var M=[{x:h.apply(null,m[0]),y:h.apply(null,m[1])},{x:p.apply(null,m[0]),y:p.apply(null,m[1])}];return n[l]=M,M}var e={},t={},n={},r=Array.prototype.join;fabric.util.drawArc=function(e,t,n,r){var s=r[0],o=r[1],u=r[2],a=r[3],f=r[4],l=r[5],c=r[6],h=[[],[],[],[]],p=i(l-t,c-n,s,o,a,f,u);for(var d=0,v=p.length;d<v;d++)h[d][0]=p[d][0]+t,h[d][1]=p[d][1]+n,h[d][2]=p[d][2]+t,h[d][3]=p[d][3]+n,h[d][4]=p[d][4]+t,h[d][5]=p[d][5]+n,e.bezierCurveTo.apply(e,h[d])},fabric.util.getBoundsOfArc=function(e,t,n,r,s,o,a,f,l){var c=0,h=0,p=[],d=[],v=i(f-e,l-t,n,r,o,a,s);for(var m=0,g=v.length;m<g;m++)p=u(c,h,v[m][0],v[m][1],v[m][2],v[m][3],v[m][4],v[m][5]),p[0].x+=e,p[0].y+=t,p[1].x+=e,p[1].y+=t,d.push(p[0]),d.push(p[1]),c=v[m][4],h=v[m][5];return d},fabric.util.getBoundsOfCurve=u}(),function(){function t(t,n){var r=e.call(arguments,2),i=[];for(var s=0,o=t.length;s<o;s++)i[s]=r.length?t[s][n].apply(t[s],r):t[s][n].call(t[s]);return i}function n(e,t){return i(e,t,function(e,t){return e>=t})}function r(e,t){return i(e,t,function(e,t){return e<t})}function i(e,t,n){if(!e||e.length===0)return;var r=e.length-1,i=t?e[r][t]:e[r];if(t)while(r--)n(e[r][t],i)&&(i=e[r][t]);else while(r--)n(e[r],i)&&(i=e[r]);return i}var e=Array.prototype.slice;Array.prototype.indexOf||(Array.prototype.indexOf=function(e){if(this===void 0||this===null)throw new TypeError;var t=Object(this),n=t.length>>>0;if(n===0)return-1;var r=0;arguments.length>0&&(r=Number(arguments[1]),r!==r?r=0:r!==0&&r!==Number.POSITIVE_INFINITY&&r!==Number.NEGATIVE_INFINITY&&(r=(r>0||-1)*Math.floor(Math.abs(r))));if(r>=n)return-1;var i=r>=0?r:Math.max(n-Math.abs(r),0);for(;i<n;i++)if(i in t&&t[i]===e)return i;return-1}),Array.prototype.forEach||(Array.prototype.forEach=function(e,t){for(var n=0,r=this.length>>>0;n<r;n++)n in this&&e.call(t,this[n],n,this)}),Array.prototype.map||(Array.prototype.map=function(e,t){var n=[];for(var r=0,i=this.length>>>0;r<i;r++)r in this&&(n[r]=e.call(t,this[r],r,this));return n}),Array.prototype.every||(Array.prototype.every=function(e,t){for(var n=0,r=this.length>>>0;n<r;n++)if(n in this&&!e.call(t,this[n],n,this))return!1;return!0}),Array.prototype.some||(Array.prototype.some=function(e,t){for(var n=0,r=this.length>>>0;n<r;n++)if(n in this&&e.call(t,this[n],n,this))return!0;return!1}),Array.prototype.filter||(Array.prototype.filter=function(e,t){var n=[],r;for(var i=0,s=this.length>>>0;i<s;i++)i in this&&(r=this[i],e.call(t,r,i,this)&&n.push(r));return n}),Array.prototype.reduce||(Array.prototype.reduce=function(e){var t=this.length>>>0,n=0,r;if(arguments.length>1)r=arguments[1];else do{if(n in this){r=this[n++];break}if(++n>=t)throw new TypeError}while(!0);for(;n<t;n++)n in this&&(r=e.call(null,r,this[n],n,this));return r}),fabric.util.array={invoke:t,min:r,max:n}}(),function(){function e(e,t){for(var n in t)e[n]=t[n];return e}function t(t){return e({},t)}fabric.util.object={extend:e,clone:t}}(),function(){function e(e){return e.replace(/-+(.)?/g,function(e,t){return t?t.toUpperCase():""})}function t(e,t){return e.charAt(0).toUpperCase()+(t?e.slice(1):e.slice(1).toLowerCase())}function n(e){return e.replace(/&/g,"&amp;").replace(/"/g,"&quot;").replace(/'/g,"&apos;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}String.prototype.trim||(String.prototype.trim=function(){return this.replace(/^[\s\xA0]+/,"").replace(/[\s\xA0]+$/,"")}),fabric.util.string={camelize:e,capitalize:t,escapeXml:n}}(),function(){var e=Array.prototype.slice,t=Function.prototype.apply,n=function(){};Function.prototype.bind||(Function.prototype.bind=function(r){var i=this,s=e.call(arguments,1),o;return s.length?o=function(){return t.call(i,this instanceof n?this:r,s.concat(e.call(arguments)))}:o=function(){return t.call(i,this instanceof n?this:r,arguments)},n.prototype=this.prototype,o.prototype=new n,o})}(),function(){function i(){}function s(t){var n=this.constructor.superclass.prototype[t];return arguments.length>1?n.apply(this,e.call(arguments,1)):n.call(this)}function o(){function u(){this.initialize.apply(this,arguments)}var n=null,o=e.call(arguments,0);typeof o[0]=="function"&&(n=o.shift()),u.superclass=n,u.subclasses=[],n&&(i.prototype=n.prototype,u.prototype=new i,n.subclasses.push(u));for(var a=0,f=o.length;a<f;a++)r(u,o[a],n);return u.prototype.initialize||(u.prototype.initialize=t),u.prototype.constructor=u,u.prototype.callSuper=s,u}var e=Array.prototype.slice,t=function(){},n=function(){for(var e in{toString:1})if(e==="toString")return!1;return!0}(),r=function(e,t,r){for(var i in t)i in e.prototype&&typeof e.prototype[i]=="function"&&(t[i]+"").indexOf("callSuper")>-1?e.prototype[i]=function(e){return function(){var n=this.constructor.superclass;this.constructor.superclass=r;var i=t[e].apply(this,arguments);this.constructor.superclass=n;if(e!=="initialize")return i}}(i):e.prototype[i]=t[i],n&&(t.toString!==Object.prototype.toString&&(e.prototype.toString=t.toString),t.valueOf!==Object.prototype.valueOf&&(e.prototype.valueOf=t.valueOf))};fabric.util.createClass=o}(),function(){function t(e){var t=Array.prototype.slice.call(arguments,1),n,r,i=t.length;for(r=0;r<i;r++){n=typeof e[t[r]];if(!/^(?:function|object|unknown)$/.test(n))return!1}return!0}function s(e,t){return{handler:t,wrappedHandler:o(e,t)}}function o(e,t){return function(r){t.call(n(e),r||fabric.window.event)}}function u(e,t){return function(n){if(c[e]&&c[e][t]){var r=c[e][t];for(var i=0,s=r.length;i<s;i++)r[i].call(this,n||fabric.window.event)}}}function d(t,n){t||(t=fabric.window.event);var r=t.target||(typeof t.srcElement!==e?t.srcElement:null),i=fabric.util.getScrollLeftTop(r,n);return{x:v(t)+i.left,y:m(t)+i.top}}function g(e,t,n){var r=e.type==="touchend"?"changedTouches":"touches";return e[r]&&e[r][0]?e[r][0][t]-(e[r][0][t]-e[r][0][n])||e[n]:e[n]}var e="unknown",n,r,i=function(){var e=0;return function(t){return t.__uniqueID||(t.__uniqueID="uniqueID__"+e++)}}();(function(){var e={};n=function(t){return e[t]},r=function(t,n){e[t]=n}})();var a=t(fabric.document.documentElement,"addEventListener","removeEventListener")&&t(fabric.window,"addEventListener","removeEventListener"),f=t(fabric.document.documentElement,"attachEvent","detachEvent")&&t(fabric.window,"attachEvent","detachEvent"),l={},c={},h,p;a?(h=function(e,t,n){e.addEventListener(t,n,!1)},p=function(e,t,n){e.removeEventListener(t,n,!1)}):f?(h=function(e,t,n){var o=i(e);r(o,e),l[o]||(l[o]={}),l[o][t]||(l[o][t]=[]);var u=s(o,n);l[o][t].push(u),e.attachEvent("on"+t,u.wrappedHandler)},p=function(e,t,n){var r=i(e),s;if(l[r]&&l[r][t])for(var o=0,u=l[r][t].length;o<u;o++)s=l[r][t][o],s&&s.handler===n&&(e.detachEvent("on"+t,s.wrappedHandler),l[r][t][o]=null)}):(h=function(e,t,n){var r=i(e);c[r]||(c[r]={});if(!c[r][t]){c[r][t]=[];var s=e["on"+t];s&&c[r][t].push(s),e["on"+t]=u(r,t)}c[r][t].push(n)},p=function(e,t,n){var r=i(e);if(c[r]&&c[r][t]){var s=c[r][t];for(var o=0,u=s.length;o<u;o++)s[o]===n&&s.splice(o,1)}}),fabric.util.addListener=h,fabric.util.removeListener=p;var v=function(t){return typeof t.clientX!==e?t.clientX:0},m=function(t){return typeof t.clientY!==e?t.clientY:0};fabric.isTouchSupported&&(v=function(e){return g(e,"pageX","clientX")},m=function(e){return g(e,"pageY","clientY")}),fabric.util.getPointer=d,fabric.util.object.extend(fabric.util,fabric.Observable)}(),function(){function e(e,t){var n=e.style;if(!n)return e;if(typeof t=="string")return e.style.cssText+=";"+t,t.indexOf("opacity")>-1?s(e,t.match(/opacity:\s*(\d?\.?\d*)/)[1]):e;for(var r in t)if(r==="opacity")s(e,t[r]);else{var i=r==="float"||r==="cssFloat"?typeof n.styleFloat=="undefined"?"cssFloat":"styleFloat":r;n[i]=t[r]}return e}var t=fabric.document.createElement("div"),n=typeof t.style.opacity=="string",r=typeof t.style.filter=="string",i=/alpha\s*\(\s*opacity\s*=\s*([^\)]+)\)/,s=function(e){return e};n?s=function(e,t){return e.style.opacity=t,e}:r&&(s=function(e,t){var n=e.style;return e.currentStyle&&!e.currentStyle.hasLayout&&(n.zoom=1),i.test(n.filter)?(t=t>=.9999?"":"alpha(opacity="+t*100+")",n.filter=n.filter.replace(i,t)):n.filter+=" alpha(opacity="+t*100+")",e}),fabric.util.setStyle=e}(),function(){function t(e){return typeof e=="string"?fabric.document.getElementById(e):e}function s(e,t){var n=fabric.document.createElement(e);for(var r in t)r==="class"?n.className=t[r]:r==="for"?n.htmlFor=t[r]:n.setAttribute(r,t[r]);return n}function o(e,t){e&&(" "+e.className+" ").indexOf(" "+t+" ")===-1&&(e.className+=(e.className?" ":"")+t)}function u(e,t,n){return typeof t=="string"&&(t=s(t,n)),e.parentNode&&e.parentNode.replaceChild(t,e),t.appendChild(e),t}function a(e,t){var n,r,i=0,s=0,o=fabric.document.documentElement,u=fabric.document.body||{scrollLeft:0,scrollTop:0};r=e;while(e&&e.parentNode&&!n)e=e.parentNode,e.nodeType===1&&fabric.util.getElementStyle(e,"position")==="fixed"&&(n=e),e.nodeType===1&&r!==t&&fabric.util.getElementStyle(e,"position")==="absolute"?(i=0,s=0):e===fabric.document?(i=u.scrollLeft||o.scrollLeft||0,s=u.scrollTop||o.scrollTop||0):(i+=e.scrollLeft||0,s+=e.scrollTop||0);return{left:i,top:s}}function f(e){var t,n=e&&e.ownerDocument,r={left:0,top:0},i={left:0,top:0},s,o={borderLeftWidth:"left",borderTopWidth:"top",paddingLeft:"left",paddingTop:"top"};if(!n)return{left:0,top:0};for(var u in o)i[o[u]]+=parseInt(l(e,u),10)||0;return t=n.documentElement,typeof e.getBoundingClientRect!="undefined"&&(r=e.getBoundingClientRect()),s=fabric.util.getScrollLeftTop(e,null),{left:r.left+s.left-(t.clientLeft||0)+i.left,top:r.top+s.top-(t.clientTop||0)+i.top}}var e=Array.prototype.slice,n,r=function(t){return e.call(t,0)};try{n=r(fabric.document.childNodes)instanceof Array}catch(i){}n||(r=function(e){var t=new Array(e.length),n=e.length;while(n--)t[n]=e[n];return t});var l;fabric.document.defaultView&&fabric.document.defaultView.getComputedStyle?l=function(e,t){var n=fabric.document.defaultView.getComputedStyle(e,null);return n?n[t]:undefined}:l=function(e,t){var n=e.style[t];return!n&&e.currentStyle&&(n=e.currentStyle[t]),n},function(){function n(e){return typeof e.onselectstart!="undefined"&&(e.onselectstart=fabric.util.falseFunction),t?e.style[t]="none":typeof e.unselectable=="string"&&(e.unselectable="on"),e}function r(e){return typeof e.onselectstart!="undefined"&&(e.onselectstart=null),t?e.style[t]="":typeof e.unselectable=="string"&&(e.unselectable=""),e}var e=fabric.document.documentElement.style,t="userSelect"in e?"userSelect":"MozUserSelect"in e?"MozUserSelect":"WebkitUserSelect"in e?"WebkitUserSelect":"KhtmlUserSelect"in e?"KhtmlUserSelect":"";fabric.util.makeElementUnselectable=n,fabric.util.makeElementSelectable=r}(),function(){function e(e,t){var n=fabric.document.getElementsByTagName("head")[0],r=fabric.document.createElement("script"),i=!0;r.onload=r.onreadystatechange=function(e){if(i){if(typeof this.readyState=="string"&&this.readyState!=="loaded"&&this.readyState!=="complete")return;i=!1,t(e||fabric.window.event),r=r.onload=r.onreadystatechange=null}},r.src=e,n.appendChild(r)}fabric.util.getScript=e}(),fabric.util.getById=t,fabric.util.toArray=r,fabric.util.makeElement=s,fabric.util.addClass=o,fabric.util.wrapElement=u,fabric.util.getScrollLeftTop=a,fabric.util.getElementOffset=f,fabric.util.getElementStyle=l}(),function(){function e(e,t){return e+(/\?/.test(e)?"&":"?")+t}function n(){}function r(r,i){i||(i={});var s=i.method?i.method.toUpperCase():"GET",o=i.onComplete||function(){},u=t(),a;return u.onreadystatechange=function(){u.readyState===4&&(o(u),u.onreadystatechange=n)},s==="GET"&&(a=null,typeof i.parameters=="string"&&(r=e(r,i.parameters))),u.open(s,r,!0),(s==="POST"||s==="PUT")&&u.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),u.send(a),u}var t=function(){var e=[function(){return new ActiveXObject("Microsoft.XMLHTTP")},function(){return new ActiveXObject("Msxml2.XMLHTTP")},function(){return new ActiveXObject("Msxml2.XMLHTTP.3.0")},function(){return new XMLHttpRequest}];for(var t=e.length;t--;)try{var n=e[t]();if(n)return e[t]}catch(r){}}();fabric.util.request=r}(),fabric.log=function(){},fabric.warn=function(){},typeof console!="undefined"&&["log","warn"].forEach(function(e){typeof console[e]!="undefined"&&typeof console[e].apply=="function"&&(fabric[e]=function(){return console[e].apply(console,arguments)})}),function(){function e(e){n(function(t){e||(e={});var r=t||+(new Date),i=e.duration||500,s=r+i,o,u=e.onChange||function(){},a=e.abort||function(){return!1},f=e.easing||function(e,t,n,r){return-n*Math.cos(e/r*(Math.PI/2))+n+t},l="startValue"in e?e.startValue:0,c="endValue"in e?e.endValue:100,h=e.byValue||c-l;e.onStart&&e.onStart(),function p(t){o=t||+(new Date);var c=o>s?i:o-r;if(a()){e.onComplete&&e.onComplete();return}u(f(c,l,h,i));if(o>s){e.onComplete&&e.onComplete();return}n(p)}(r)})}function n(){return t.apply(fabric.window,arguments)}var t=fabric.window.requestAnimationFrame||fabric.window.webkitRequestAnimationFrame||fabric.window.mozRequestAnimationFrame||fabric.window.oRequestAnimationFrame||fabric.window.msRequestAnimationFrame||function(e){fabric.window.setTimeout(e,1e3/60)};fabric.util.animate=e,fabric.util.requestAnimFrame=n}(),function(){function e(e,t,n,r){return e<Math.abs(t)?(e=t,r=n/4):r=n/(2*Math.PI)*Math.asin(t/e),{a:e,c:t,p:n,s:r}}function t(e,t,n){return e.a*Math.pow(2,10*(t-=1))*Math.sin((t*n-e.s)*2*Math.PI/e.p)}function n(e,t,n,r){return n*((e=e/r-1)*e*e+1)+t}function r(e,t,n,r){return e/=r/2,e<1?n/2*e*e*e+t:n/2*((e-=2)*e*e+2)+t}function i(e,t,n,r){return n*(e/=r)*e*e*e+t}function s(e,t,n,r){return-n*((e=e/r-1)*e*e*e-1)+t}function o(e,t,n,r){return e/=r/2,e<1?n/2*e*e*e*e+t:-n/2*((e-=2)*e*e*e-2)+t}function u(e,t,n,r){return n*(e/=r)*e*e*e*e+t}function a(e,t,n,r){return n*((e=e/r-1)*e*e*e*e+1)+t}function f(e,t,n,r){return e/=r/2,e<1?n/2*e*e*e*e*e+t:n/2*((e-=2)*e*e*e*e+2)+t}function l(e,t,n,r){return-n*Math.cos(e/r*(Math.PI/2))+n+t}function c(e,t,n,r){return n*Math.sin(e/r*(Math.PI/2))+t}function h(e,t,n,r){return-n/2*(Math.cos(Math.PI*e/r)-1)+t}function p(e,t,n,r){return e===0?t:n*Math.pow(2,10*(e/r-1))+t}function d(e,t,n,r){return e===r?t+n:n*(-Math.pow(2,-10*e/r)+1)+t}function v(e,t,n,r){return e===0?t:e===r?t+n:(e/=r/2,e<1?n/2*Math.pow(2,10*(e-1))+t:n/2*(-Math.pow(2,-10*--e)+2)+t)}function m(e,t,n,r){return-n*(Math.sqrt(1-(e/=r)*e)-1)+t}function g(e,t,n,r){return n*Math.sqrt(1-(e=e/r-1)*e)+t}function y(e,t,n,r){return e/=r/2,e<1?-n/2*(Math.sqrt(1-e*e)-1)+t:n/2*(Math.sqrt(1-(e-=2)*e)+1)+t}function b(n,r,i,s){var o=1.70158,u=0,a=i;if(n===0)return r;n/=s;if(n===1)return r+i;u||(u=s*.3);var f=e(a,i,u,o);return-t(f,n,s)+r}function w(t,n,r,i){var s=1.70158,o=0,u=r;if(t===0)return n;t/=i;if(t===1)return n+r;o||(o=i*.3);var a=e(u,r,o,s);return a.a*Math.pow(2,-10*t)*Math.sin((t*i-a.s)*2*Math.PI/a.p)+a.c+n}function E(n,r,i,s){var o=1.70158,u=0,a=i;if(n===0)return r;n/=s/2;if(n===2)return r+i;u||(u=s*.3*1.5);var f=e(a,i,u,o);return n<1?-0.5*t(f,n,s)+r:f.a*Math.pow(2,-10*(n-=1))*Math.sin((n*s-f.s)*2*Math.PI/f.p)*.5+f.c+r}function S(e,t,n,r,i){return i===undefined&&(i=1.70158),n*(e/=r)*e*((i+1)*e-i)+t}function x(e,t,n,r,i){return i===undefined&&(i=1.70158),n*((e=e/r-1)*e*((i+1)*e+i)+1)+t}function T(e,t,n,r,i){return i===undefined&&(i=1.70158),e/=r/2,e<1?n/2*e*e*(((i*=1.525)+1)*e-i)+t:n/2*((e-=2)*e*(((i*=1.525)+1)*e+i)+2)+t}function N(e,t,n,r){return n-C(r-e,0,n,r)+t}function C(e,t,n,r){return(e/=r)<1/2.75?n*7.5625*e*e+t:e<2/2.75?n*(7.5625*(e-=1.5/2.75)*e+.75)+t:e<2.5/2.75?n*(7.5625*(e-=2.25/2.75)*e+.9375)+t:n*(7.5625*(e-=2.625/2.75)*e+.984375)+t}function k(e,t,n,r){return e<r/2?N(e*2,0,n,r)*.5+t:C(e*2-r,0,n,r)*.5+n*.5+t}fabric.util.ease={easeInQuad:function(e,t,n,r){return n*(e/=r)*e+t},easeOutQuad:function(e,t,n,r){return-n*(e/=r)*(e-2)+t},easeInOutQuad:function(e,t,n,r){return e/=r/2,e<1?n/2*e*e+t:-n/2*(--e*(e-2)-1)+t},easeInCubic:function(e,t,n,r){return n*(e/=r)*e*e+t},easeOutCubic:n,easeInOutCubic:r,easeInQuart:i,easeOutQuart:s,easeInOutQuart:o,easeInQuint:u,easeOutQuint:a,easeInOutQuint:f,easeInSine:l,easeOutSine:c,easeInOutSine:h,easeInExpo:p,easeOutExpo:d,easeInOutExpo:v,easeInCirc:m,easeOutCirc:g,easeInOutCirc:y,easeInElastic:b,easeOutElastic:w,easeInOutElastic:E,easeInBack:S,easeOutBack:x,easeInOutBack:T,easeInBounce:N,easeOutBounce:C,easeInOutBounce:k}}(),function(e){"use strict";function l(e){return e in a?a[e]:e}function c(e,n,r,i){var s=Object.prototype.toString.call(n)==="[object Array]",a;return e!=="fill"&&e!=="stroke"||n!=="none"?e==="strokeDashArray"?n=n.replace(/,/g," ").split(/\s+/).map(function(e){return parseFloat(e)}):e==="transformMatrix"?r&&r.transformMatrix?n=u(r.transformMatrix,t.parseTransformAttribute(n)):n=t.parseTransformAttribute(n):e==="visible"?(n=n==="none"||n==="hidden"?!1:!0,r&&r.visible===!1&&(n=!1)):e==="originX"?n=n==="start"?"left":n==="end"?"right":"center":a=s?n.map(o):o(n,i):n="",!s&&isNaN(a)?n:a}function h(e){for(var n in f){if(!e[n]||typeof e[f[n]]=="undefined")continue;if(e[n].indexOf("url(")===0)continue;var r=new t.Color(e[n]);e[n]=r.setAlpha(s(r.getAlpha()*e[f[n]],2)).toRgba()}return e}function p(e,t){var n,r;e.replace(/;$/,"").split(";").forEach(function(e){var i=e.split(":");n=l(i[0].trim().toLowerCase()),r=c(n,i[1].trim()),t[n]=r})}function d(e,t){var n,r;for(var i in e){if(typeof e[i]=="undefined")continue;n=l(i.toLowerCase()),r=c(n,e[i]),t[n]=r}}function v(e,n){var r={};for(var i in t.cssRules[n])if(m(e,i.split(" ")))for(var s in t.cssRules[n][i])r[s]=t.cssRules[n][i][s];return r}function m(e,t){var n,r=!0;return n=y(e,t.pop()),n&&t.length&&(r=g(e,t)),n&&r&&t.length===0}function g(e,t){var n,r=!0;while(e.parentNode&&e.parentNode.nodeType===1&&t.length)r&&(n=t.pop()),e=e.parentNode,r=y(e,n);return t.length===0}function y(e,t){var n=e.nodeName,r=e.getAttribute("class"),i=e.getAttribute("id"),s;s=new RegExp("^"+n,"i"),t=t.replace(s,""),i&&t.length&&(s=new RegExp("#"+i+"(?![a-zA-Z\\-]+)","i"),t=t.replace(s,""));if(r&&t.length){r=r.split(" ");for(var o=r.length;o--;)s=new RegExp("\\."+r[o]+"(?![a-zA-Z\\-]+)","i"),t=t.replace(s,"")}return t.length===0}function b(e){var t=e.getElementsByTagName("use");while(t.length){var n=t[0],r=n.getAttribute("xlink:href").substr(1),i=n.getAttribute("x")||0,s=n.getAttribute("y")||0,o=e.getElementById(r).cloneNode(!0),u=(o.getAttribute("transform")||"")+" translate("+i+", "+s+")",a;for(var f=0,l=n.attributes,c=l.length;f<c;f++){var h=l.item(f);if(h.nodeName==="x"||h.nodeName==="y"||h.nodeName==="xlink:href")continue;h.nodeName==="transform"?u=h.nodeValue+" "+u:o.setAttribute(h.nodeName,h.nodeValue)}o.setAttribute("transform",u),o.setAttribute("instantiated_by_use","1"),o.removeAttribute("id"),a=n.parentNode,a.replaceChild(o,n)}}function w(e,n,r){var i=new RegExp("^\\s*("+t.reNum+"+)\\s*,?"+"\\s*("+t.reNum+"+)\\s*,?"+"\\s*("+t.reNum+"+)\\s*,?"+"\\s*("+t.reNum+"+)\\s*"+"$"),s=e.getAttribute("viewBox"),o=1,u=1,a=0,f=0,l,c,h,p;if(!s||!(s=s.match(i)))return;a=-parseFloat(s[1]),f=-parseFloat(s[2]),l=parseFloat(s[3]),c=parseFloat(s[4]),n&&n!==l&&(o=n/l),r&&r!==c&&(u=r/c),u=o=o>u?u:o;if(o===1&&u===1&&a===0&&f===0)return;h="matrix("+o+" 0"+" 0 "+u+" "+a*o+" "+f*u+")";if(e.tagName==="svg"){p=e.ownerDocument.createElement("g");while(e.firstChild!=null)p.appendChild(e.firstChild);e.appendChild(p)}else p=e,h+=p.getAttribute("transform");p.setAttribute("transform",h)}function S(e){var n=e.objects,i=e.options;return n=n.map(function(e){return t[r(e.type)].fromObject(e)}),{objects:n,options:i}}function x(e,t,n){t[n]&&t[n].toSVG&&e.push('<pattern x="0" y="0" id="',n,'Pattern" ','width="',t[n].source.width,'" height="',t[n].source.height,'" patternUnits="userSpaceOnUse">','<image x="0" y="0" ','width="',t[n].source.width,'" height="',t[n].source.height,'" xlink:href="',t[n].source.src,'"></image></pattern>')}var t=e.fabric||(e.fabric={}),n=t.util.object.extend,r=t.util.string.capitalize,i=t.util.object.clone,s=t.util.toFixed,o=t.util.parseUnit,u=t.util.multiplyTransformMatrices,a={cx:"left",x:"left",r:"radius",cy:"top",y:"top",display:"visible",visibility:"visible",transform:"transformMatrix","fill-opacity":"fillOpacity","fill-rule":"fillRule","font-family":"fontFamily","font-size":"fontSize","font-style":"fontStyle","font-weight":"fontWeight","stroke-dasharray":"strokeDashArray","stroke-linecap":"strokeLineCap","stroke-linejoin":"strokeLineJoin","stroke-miterlimit":"strokeMiterLimit","stroke-opacity":"strokeOpacity","stroke-width":"strokeWidth","text-decoration":"textDecoration","text-anchor":"originX"},f={stroke:"strokeOpacity",fill:"fillOpacity"};t.cssRules={},t.gradientDefs={},t.parseTransformAttribute=function(){function e(e,t){var n=t[0];e[0]=Math.cos(n),e[1]=Math.sin(n),e[2]=-Math.sin(n),e[3]=Math.cos(n)}function n(e,t){var n=t[0],r=t.length===2?t[1]:t[0];e[0]=n,e[3]=r}function r(e,n){e[2]=Math.tan(t.util.degreesToRadians(n[0]))}function i(e,n){e[1]=Math.tan(t.util.degreesToRadians(n[0]))}function s(e,t){e[4]=t[0],t.length===2&&(e[5]=t[1])}var o=[1,0,0,1,0,0],u=t.reNum,a="(?:\\s+,?\\s*|,\\s*)",f="(?:(skewX)\\s*\\(\\s*("+u+")\\s*\\))",l="(?:(skewY)\\s*\\(\\s*("+u+")\\s*\\))",c="(?:(rotate)\\s*\\(\\s*("+u+")(?:"+a+"("+u+")"+a+"("+u+"))?\\s*\\))",h="(?:(scale)\\s*\\(\\s*("+u+")(?:"+a+"("+u+"))?\\s*\\))",p="(?:(translate)\\s*\\(\\s*("+u+")(?:"+a+"("+u+"))?\\s*\\))",d="(?:(matrix)\\s*\\(\\s*("+u+")"+a+"("+u+")"+a+"("+u+")"+a+"("+u+")"+a+"("+u+")"+a+"("+u+")"+"\\s*\\))",v="(?:"+d+"|"+p+"|"+h+"|"+c+"|"+f+"|"+l+")",m="(?:"+v+"(?:"+a+v+")*"+")",g="^\\s*(?:"+m+"?)\\s*$",y=new RegExp(g),b=new RegExp(v,"g");return function(u){var a=o.concat(),f=[];if(!u||u&&!y.test(u))return a;u.replace(b,function(u){var l=(new RegExp(v)).exec(u).filter(function(e){return e!==""&&e!=null}),c=l[1],h=l.slice(2).map(parseFloat);switch(c){case"translate":s(a,h);break;case"rotate":h[0]=t.util.degreesToRadians(h[0]),e(a,h);break;case"scale":n(a,h);break;case"skewX":r(a,h);break;case"skewY":i(a,h);break;case"matrix":a=h}f.push(a.concat()),a=o.concat()});var l=f[0];while(f.length>1)f.shift(),l=t.util.multiplyTransformMatrices(l,f[0]);return l}}(),t.parseSVGDocument=function(){function r(e,t){while(e&&(e=e.parentNode))if(t.test(e.nodeName)&&!e.getAttribute("instantiated_by_use"))return!0;return!1}var e=/^(path|circle|polygon|polyline|ellipse|rect|line|image|text)$/,n=/^(symbol|image|marker|pattern|view)$/;return function(s,u,a){if(!s)return;b(s);var f=new Date,l=t.Object.__uid++,c=o(s.getAttribute("width")||"100%"),h=o(s.getAttribute("height")||"100%");w(s,c,h);var p=t.util.toArray(s.getElementsByTagName("*"));if(p.length===0&&t.isLikelyNode){p=s.selectNodes('//*[name(.)!="svg"]');var d=[];for(var v=0,m=p.length;v<m;v++)d[v]=p[v];p=d}var g=p.filter(function(t){return n.test(t.tagName)&&w(t,0,0),e.test(t.tagName)&&!r(t,/^(?:pattern|defs|symbol)$/)});if(!g||g&&!g.length){u&&u([],{});return}var y={width:c,height:h,widthAttr:c,heightAttr:h,svgUid:l};t.gradientDefs[l]=t.getGradientDefs(s),t.cssRules[l]=t.getCSSRules(s),t.parseElements(g,function(e){t.documentParsingTime=new Date-f,u&&u(e,y)},i(y),a)}}();var E={has:function(e,t){t(!1)},get:function(){},set:function(){}};n(t,{parseFontDeclaration:function(e,n){var r="(normal|italic)?\\s*(normal|small-caps)?\\s*(normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)?\\s*("+t.reNum+"(?:px|cm|mm|em|pt|pc|in)*)(?:\\/(normal|"+t.reNum+"))?\\s+(.*)",i=e.match(r);if(!i)return;var s=i[1],u=i[3],a=i[4],f=i[5],l=i[6];s&&(n.fontStyle=s),u&&(n.fontWeight=isNaN(parseFloat(u))?u:parseFloat(u)),a&&(n.fontSize=o(a)),l&&(n.fontFamily=l),f&&(n.lineHeight=f==="normal"?1:f)},getGradientDefs:function(e){var t=e.getElementsByTagName("linearGradient"),n=e.getElementsByTagName("radialGradient"),r,i,s=0,o,u,a=[],f={},l={};a.length=t.length+n.length,i=t.length;while(i--)a[s++]=t[i];i=n.length;while(i--)a[s++]=n[i];while(s--)r=a[s],u=r.getAttribute("xlink:href"),o=r.getAttribute("id"),u&&(l[o]=u.substr(1)),f[o]=r;for(o in l){var c=f[l[o]].cloneNode(!0);r=f[o];while(c.firstChild)r.appendChild(c.firstChild)}return f},parseAttributes:function(e,r,i){if(!e)return;var s,o={},u;typeof i=="undefined"&&(i=e.getAttribute("svgUid")),e.parentNode&&/^symbol|[g|a]$/i.test(e.parentNode.nodeName)&&(o=t.parseAttributes(e.parentNode,r,i)),u=o&&o.fontSize||e.getAttribute("font-size"
+)||t.Text.DEFAULT_SVG_FONT_SIZE;var a=r.reduce(function(t,n){return s=e.getAttribute(n),s&&(n=l(n),s=c(n,s,o,u),t[n]=s),t},{});return a=n(a,n(v(e,i),t.parseStyleAttribute(e))),a.font&&t.parseFontDeclaration(a.font,a),h(n(o,a))},parseElements:function(e,n,r,i){(new t.ElementsParser(e,n,r,i)).parse()},parseStyleAttribute:function(e){var t={},n=e.getAttribute("style");return n?(typeof n=="string"?p(n,t):d(n,t),t):t},parsePointsAttribute:function(e){if(!e)return null;e=e.replace(/,/g," ").trim(),e=e.split(/\s+/);var t=[],n,r;n=0,r=e.length;for(;n<r;n+=2)t.push({x:parseFloat(e[n]),y:parseFloat(e[n+1])});return t},getCSSRules:function(e){var n=e.getElementsByTagName("style"),r={},i;for(var s=0,o=n.length;s<o;s++){var u=n[s].textContent;u=u.replace(/\/\*[\s\S]*?\*\//g,"");if(u.trim()==="")continue;i=u.match(/[^{]*\{[\s\S]*?\}/g),i=i.map(function(e){return e.trim()}),i.forEach(function(e){var n=e.match(/([\s\S]*?)\s*\{([^}]*)\}/),i={},s=n[2].trim(),o=s.replace(/;$/,"").split(/\s*;\s*/);for(var u=0,a=o.length;u<a;u++){var f=o[u].split(/\s*:\s*/),h=l(f[0]),p=c(h,f[1],f[0]);i[h]=p}e=n[1],e.split(",").forEach(function(e){e=e.replace(/^svg/i,"").trim();if(e==="")return;r[e]=t.util.object.clone(i)})})}return r},loadSVGFromURL:function(e,n,r){function i(i){var s=i.responseXML;s&&!s.documentElement&&t.window.ActiveXObject&&i.responseText&&(s=new ActiveXObject("Microsoft.XMLDOM"),s.async="false",s.loadXML(i.responseText.replace(/<!DOCTYPE[\s\S]*?(\[[\s\S]*\])*?>/i,"")));if(!s||!s.documentElement)return;t.parseSVGDocument(s.documentElement,function(r,i){E.set(e,{objects:t.util.array.invoke(r,"toObject"),options:i}),n(r,i)},r)}e=e.replace(/^\n\s*/,"").trim(),E.has(e,function(r){r?E.get(e,function(e){var t=S(e);n(t.objects,t.options)}):new t.util.request(e,{method:"get",onComplete:i})})},loadSVGFromString:function(e,n,r){e=e.trim();var i;if(typeof DOMParser!="undefined"){var s=new DOMParser;s&&s.parseFromString&&(i=s.parseFromString(e,"text/xml"))}else t.window.ActiveXObject&&(i=new ActiveXObject("Microsoft.XMLDOM"),i.async="false",i.loadXML(e.replace(/<!DOCTYPE[\s\S]*?(\[[\s\S]*\])*?>/i,"")));t.parseSVGDocument(i.documentElement,function(e,t){n(e,t)},r)},createSVGFontFacesMarkup:function(e){var t="";for(var n=0,r=e.length;n<r;n++){if(e[n].type!=="text"||!e[n].path)continue;t+=["@font-face {","font-family: ",e[n].fontFamily,"; ","src: url('",e[n].path,"')","}"].join("")}return t&&(t=['<style type="text/css">',"<![CDATA[",t,"]]>","</style>"].join("")),t},createSVGRefElementsMarkup:function(e){var t=[];return x(t,e,"backgroundColor"),x(t,e,"overlayColor"),t.join("")}})}(typeof exports!="undefined"?exports:this),fabric.ElementsParser=function(e,t,n,r){this.elements=e,this.callback=t,this.options=n,this.reviver=r,this.svgUid=n&&n.svgUid||0},fabric.ElementsParser.prototype.parse=function(){this.instances=new Array(this.elements.length),this.numElements=this.elements.length,this.createObjects()},fabric.ElementsParser.prototype.createObjects=function(){for(var e=0,t=this.elements.length;e<t;e++)this.elements[e].setAttribute("svgUid",this.svgUid),function(e,t){setTimeout(function(){e.createObject(e.elements[t],t)},0)}(this,e)},fabric.ElementsParser.prototype.createObject=function(e,t){var n=fabric[fabric.util.string.capitalize(e.tagName)];if(n&&n.fromElement)try{this._createObject(n,e,t)}catch(r){fabric.log(r)}else this.checkIfDone()},fabric.ElementsParser.prototype._createObject=function(e,t,n){if(e.async)e.fromElement(t,this.createCallback(n,t),this.options);else{var r=e.fromElement(t,this.options);this.resolveGradient(r,"fill"),this.resolveGradient(r,"stroke"),this.reviver&&this.reviver(t,r),this.instances[n]=r,this.checkIfDone()}},fabric.ElementsParser.prototype.createCallback=function(e,t){var n=this;return function(r){n.resolveGradient(r,"fill"),n.resolveGradient(r,"stroke"),n.reviver&&n.reviver(t,r),n.instances[e]=r,n.checkIfDone()}},fabric.ElementsParser.prototype.resolveGradient=function(e,t){var n=e.get(t);if(!/^url\(/.test(n))return;var r=n.slice(5,n.length-1);fabric.gradientDefs[this.svgUid][r]&&e.set(t,fabric.Gradient.fromElement(fabric.gradientDefs[this.svgUid][r],e))},fabric.ElementsParser.prototype.checkIfDone=function(){--this.numElements===0&&(this.instances=this.instances.filter(function(e){return e!=null}),this.callback(this.instances))},function(e){"use strict";function n(e,t){this.x=e,this.y=t}var t=e.fabric||(e.fabric={});if(t.Point){t.warn("fabric.Point is already defined");return}t.Point=n,n.prototype={constructor:n,add:function(e){return new n(this.x+e.x,this.y+e.y)},addEquals:function(e){return this.x+=e.x,this.y+=e.y,this},scalarAdd:function(e){return new n(this.x+e,this.y+e)},scalarAddEquals:function(e){return this.x+=e,this.y+=e,this},subtract:function(e){return new n(this.x-e.x,this.y-e.y)},subtractEquals:function(e){return this.x-=e.x,this.y-=e.y,this},scalarSubtract:function(e){return new n(this.x-e,this.y-e)},scalarSubtractEquals:function(e){return this.x-=e,this.y-=e,this},multiply:function(e){return new n(this.x*e,this.y*e)},multiplyEquals:function(e){return this.x*=e,this.y*=e,this},divide:function(e){return new n(this.x/e,this.y/e)},divideEquals:function(e){return this.x/=e,this.y/=e,this},eq:function(e){return this.x===e.x&&this.y===e.y},lt:function(e){return this.x<e.x&&this.y<e.y},lte:function(e){return this.x<=e.x&&this.y<=e.y},gt:function(e){return this.x>e.x&&this.y>e.y},gte:function(e){return this.x>=e.x&&this.y>=e.y},lerp:function(e,t){return new n(this.x+(e.x-this.x)*t,this.y+(e.y-this.y)*t)},distanceFrom:function(e){var t=this.x-e.x,n=this.y-e.y;return Math.sqrt(t*t+n*n)},midPointFrom:function(e){return new n(this.x+(e.x-this.x)/2,this.y+(e.y-this.y)/2)},min:function(e){return new n(Math.min(this.x,e.x),Math.min(this.y,e.y))},max:function(e){return new n(Math.max(this.x,e.x),Math.max(this.y,e.y))},toString:function(){return this.x+","+this.y},setXY:function(e,t){this.x=e,this.y=t},setFromPoint:function(e){this.x=e.x,this.y=e.y},swap:function(e){var t=this.x,n=this.y;this.x=e.x,this.y=e.y,e.x=t,e.y=n}}}(typeof exports!="undefined"?exports:this),function(e){"use strict";function n(e){this.status=e,this.points=[]}var t=e.fabric||(e.fabric={});if(t.Intersection){t.warn("fabric.Intersection is already defined");return}t.Intersection=n,t.Intersection.prototype={appendPoint:function(e){this.points.push(e)},appendPoints:function(e){this.points=this.points.concat(e)}},t.Intersection.intersectLineLine=function(e,r,i,s){var o,u=(s.x-i.x)*(e.y-i.y)-(s.y-i.y)*(e.x-i.x),a=(r.x-e.x)*(e.y-i.y)-(r.y-e.y)*(e.x-i.x),f=(s.y-i.y)*(r.x-e.x)-(s.x-i.x)*(r.y-e.y);if(f!==0){var l=u/f,c=a/f;0<=l&&l<=1&&0<=c&&c<=1?(o=new n("Intersection"),o.points.push(new t.Point(e.x+l*(r.x-e.x),e.y+l*(r.y-e.y)))):o=new n}else u===0||a===0?o=new n("Coincident"):o=new n("Parallel");return o},t.Intersection.intersectLinePolygon=function(e,t,r){var i=new n,s=r.length;for(var o=0;o<s;o++){var u=r[o],a=r[(o+1)%s],f=n.intersectLineLine(e,t,u,a);i.appendPoints(f.points)}return i.points.length>0&&(i.status="Intersection"),i},t.Intersection.intersectPolygonPolygon=function(e,t){var r=new n,i=e.length;for(var s=0;s<i;s++){var o=e[s],u=e[(s+1)%i],a=n.intersectLinePolygon(o,u,t);r.appendPoints(a.points)}return r.points.length>0&&(r.status="Intersection"),r},t.Intersection.intersectPolygonRectangle=function(e,r,i){var s=r.min(i),o=r.max(i),u=new t.Point(o.x,s.y),a=new t.Point(s.x,o.y),f=n.intersectLinePolygon(s,u,e),l=n.intersectLinePolygon(u,o,e),c=n.intersectLinePolygon(o,a,e),h=n.intersectLinePolygon(a,s,e),p=new n;return p.appendPoints(f.points),p.appendPoints(l.points),p.appendPoints(c.points),p.appendPoints(h.points),p.points.length>0&&(p.status="Intersection"),p}}(typeof exports!="undefined"?exports:this),function(e){"use strict";function n(e){e?this._tryParsingColor(e):this.setSource([0,0,0,1])}function r(e,t,n){return n<0&&(n+=1),n>1&&(n-=1),n<1/6?e+(t-e)*6*n:n<.5?t:n<2/3?e+(t-e)*(2/3-n)*6:e}var t=e.fabric||(e.fabric={});if(t.Color){t.warn("fabric.Color is already defined.");return}t.Color=n,t.Color.prototype={_tryParsingColor:function(e){var t;e in n.colorNameMap&&(e=n.colorNameMap[e]);if(e==="transparent"){this.setSource([255,255,255,0]);return}t=n.sourceFromHex(e),t||(t=n.sourceFromRgb(e)),t||(t=n.sourceFromHsl(e)),t&&this.setSource(t)},_rgbToHsl:function(e,n,r){e/=255,n/=255,r/=255;var i,s,o,u=t.util.array.max([e,n,r]),a=t.util.array.min([e,n,r]);o=(u+a)/2;if(u===a)i=s=0;else{var f=u-a;s=o>.5?f/(2-u-a):f/(u+a);switch(u){case e:i=(n-r)/f+(n<r?6:0);break;case n:i=(r-e)/f+2;break;case r:i=(e-n)/f+4}i/=6}return[Math.round(i*360),Math.round(s*100),Math.round(o*100)]},getSource:function(){return this._source},setSource:function(e){this._source=e},toRgb:function(){var e=this.getSource();return"rgb("+e[0]+","+e[1]+","+e[2]+")"},toRgba:function(){var e=this.getSource();return"rgba("+e[0]+","+e[1]+","+e[2]+","+e[3]+")"},toHsl:function(){var e=this.getSource(),t=this._rgbToHsl(e[0],e[1],e[2]);return"hsl("+t[0]+","+t[1]+"%,"+t[2]+"%)"},toHsla:function(){var e=this.getSource(),t=this._rgbToHsl(e[0],e[1],e[2]);return"hsla("+t[0]+","+t[1]+"%,"+t[2]+"%,"+e[3]+")"},toHex:function(){var e=this.getSource(),t,n,r;return t=e[0].toString(16),t=t.length===1?"0"+t:t,n=e[1].toString(16),n=n.length===1?"0"+n:n,r=e[2].toString(16),r=r.length===1?"0"+r:r,t.toUpperCase()+n.toUpperCase()+r.toUpperCase()},getAlpha:function(){return this.getSource()[3]},setAlpha:function(e){var t=this.getSource();return t[3]=e,this.setSource(t),this},toGrayscale:function(){var e=this.getSource(),t=parseInt((e[0]*.3+e[1]*.59+e[2]*.11).toFixed(0),10),n=e[3];return this.setSource([t,t,t,n]),this},toBlackWhite:function(e){var t=this.getSource(),n=(t[0]*.3+t[1]*.59+t[2]*.11).toFixed(0),r=t[3];return e=e||127,n=Number(n)<Number(e)?0:255,this.setSource([n,n,n,r]),this},overlayWith:function(e){e instanceof n||(e=new n(e));var t=[],r=this.getAlpha(),i=.5,s=this.getSource(),o=e.getSource();for(var u=0;u<3;u++)t.push(Math.round(s[u]*(1-i)+o[u]*i));return t[3]=r,this.setSource(t),this}},t.Color.reRGBa=/^rgba?\(\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*,\s*(\d{1,3}(?:\.\d+)?\%?)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)$/,t.Color.reHSLa=/^hsla?\(\s*(\d{1,3})\s*,\s*(\d{1,3}\%)\s*,\s*(\d{1,3}\%)\s*(?:\s*,\s*(\d+(?:\.\d+)?)\s*)?\)$/,t.Color.reHex=/^#?([0-9a-f]{6}|[0-9a-f]{3})$/i,t.Color.colorNameMap={aqua:"#00FFFF",black:"#000000",blue:"#0000FF",fuchsia:"#FF00FF",gray:"#808080",green:"#008000",lime:"#00FF00",maroon:"#800000",navy:"#000080",olive:"#808000",orange:"#FFA500",purple:"#800080",red:"#FF0000",silver:"#C0C0C0",teal:"#008080",white:"#FFFFFF",yellow:"#FFFF00"},t.Color.fromRgb=function(e){return n.fromSource(n.sourceFromRgb(e))},t.Color.sourceFromRgb=function(e){var t=e.match(n.reRGBa);if(t){var r=parseInt(t[1],10)/(/%$/.test(t[1])?100:1)*(/%$/.test(t[1])?255:1),i=parseInt(t[2],10)/(/%$/.test(t[2])?100:1)*(/%$/.test(t[2])?255:1),s=parseInt(t[3],10)/(/%$/.test(t[3])?100:1)*(/%$/.test(t[3])?255:1);return[parseInt(r,10),parseInt(i,10),parseInt(s,10),t[4]?parseFloat(t[4]):1]}},t.Color.fromRgba=n.fromRgb,t.Color.fromHsl=function(e){return n.fromSource(n.sourceFromHsl(e))},t.Color.sourceFromHsl=function(e){var t=e.match(n.reHSLa);if(!t)return;var i=(parseFloat(t[1])%360+360)%360/360,s=parseFloat(t[2])/(/%$/.test(t[2])?100:1),o=parseFloat(t[3])/(/%$/.test(t[3])?100:1),u,a,f;if(s===0)u=a=f=o;else{var l=o<=.5?o*(s+1):o+s-o*s,c=o*2-l;u=r(c,l,i+1/3),a=r(c,l,i),f=r(c,l,i-1/3)}return[Math.round(u*255),Math.round(a*255),Math.round(f*255),t[4]?parseFloat(t[4]):1]},t.Color.fromHsla=n.fromHsl,t.Color.fromHex=function(e){return n.fromSource(n.sourceFromHex(e))},t.Color.sourceFromHex=function(e){if(e.match(n.reHex)){var t=e.slice(e.indexOf("#")+1),r=t.length===3,i=r?t.charAt(0)+t.charAt(0):t.substring(0,2),s=r?t.charAt(1)+t.charAt(1):t.substring(2,4),o=r?t.charAt(2)+t.charAt(2):t.substring(4,6);return[parseInt(i,16),parseInt(s,16),parseInt(o,16),1]}},t.Color.fromSource=function(e){var t=new n;return t.setSource(e),t}}(typeof exports!="undefined"?exports:this),function(){function e(e){var t=e.getAttribute("style"),n=e.getAttribute("offset"),r,i,s;n=parseFloat(n)/(/%$/.test(n)?100:1),n=n<0?0:n>1?1:n;if(t){var o=t.split(/\s*;\s*/);o[o.length-1]===""&&o.pop();for(var u=o.length;u--;){var a=o[u].split(/\s*:\s*/),f=a[0].trim(),l=a[1].trim();f==="stop-color"?r=l:f==="stop-opacity"&&(s=l)}}return r||(r=e.getAttribute("stop-color")||"rgb(0,0,0)"),s||(s=e.getAttribute("stop-opacity")),r=new fabric.Color(r),i=r.getAlpha(),s=isNaN(parseFloat(s))?1:parseFloat(s),s*=i,{offset:n,color:r.toRgb(),opacity:s}}function t(e){return{x1:e.getAttribute("x1")||0,y1:e.getAttribute("y1")||0,x2:e.getAttribute("x2")||"100%",y2:e.getAttribute("y2")||0}}function n(e){return{x1:e.getAttribute("fx")||e.getAttribute("cx")||"50%",y1:e.getAttribute("fy")||e.getAttribute("cy")||"50%",r1:0,x2:e.getAttribute("cx")||"50%",y2:e.getAttribute("cy")||"50%",r2:e.getAttribute("r")||"50%"}}function r(e,t,n){var r,i=0,s=1,o="";for(var u in t){r=parseFloat(t[u],10),typeof t[u]=="string"&&/^\d+%$/.test(t[u])?s=.01:s=1;if(u==="x1"||u==="x2"||u==="r2")s*=n==="objectBoundingBox"?e.width:1,i=n==="objectBoundingBox"?e.left||0:0;else if(u==="y1"||u==="y2")s*=n==="objectBoundingBox"?e.height:1,i=n==="objectBoundingBox"?e.top||0:0;t[u]=r*s+i}if(e.type==="ellipse"&&t.r2!==null&&n==="objectBoundingBox"&&e.rx!==e.ry){var a=e.ry/e.rx;o=" scale(1, "+a+")",t.y1&&(t.y1/=a),t.y2&&(t.y2/=a)}return o}fabric.Gradient=fabric.util.createClass({offsetX:0,offsetY:0,initialize:function(e){e||(e={});var t={};this.id=fabric.Object.__uid++,this.type=e.type||"linear",t={x1:e.coords.x1||0,y1:e.coords.y1||0,x2:e.coords.x2||0,y2:e.coords.y2||0},this.type==="radial"&&(t.r1=e.coords.r1||0,t.r2=e.coords.r2||0),this.coords=t,this.colorStops=e.colorStops.slice(),e.gradientTransform&&(this.gradientTransform=e.gradientTransform),this.offsetX=e.offsetX||this.offsetX,this.offsetY=e.offsetY||this.offsetY},addColorStop:function(e){for(var t in e){var n=new fabric.Color(e[t]);this.colorStops.push({offset:t,color:n.toRgb(),opacity:n.getAlpha()})}return this},toObject:function(){return{type:this.type,coords:this.coords,colorStops:this.colorStops,offsetX:this.offsetX,offsetY:this.offsetY}},toSVG:function(e){var t=fabric.util.object.clone(this.coords),n,r;this.colorStops.sort(function(e,t){return e.offset-t.offset});if(!e.group||e.group.type!=="path-group")for(var i in t)if(i==="x1"||i==="x2"||i==="r2")t[i]+=this.offsetX-e.width/2;else if(i==="y1"||i==="y2")t[i]+=this.offsetY-e.height/2;r='id="SVGID_'+this.id+'" gradientUnits="userSpaceOnUse"',this.gradientTransform&&(r+=' gradientTransform="matrix('+this.gradientTransform.join(" ")+')" '),this.type==="linear"?n=["<linearGradient ",r,' x1="',t.x1,'" y1="',t.y1,'" x2="',t.x2,'" y2="',t.y2,'">\n']:this.type==="radial"&&(n=["<radialGradient ",r,' cx="',t.x2,'" cy="',t.y2,'" r="',t.r2,'" fx="',t.x1,'" fy="',t.y1,'">\n']);for(var s=0;s<this.colorStops.length;s++)n.push("<stop ",'offset="',this.colorStops[s].offset*100+"%",'" style="stop-color:',this.colorStops[s].color,this.colorStops[s].opacity!=null?";stop-opacity: "+this.colorStops[s].opacity:";",'"/>\n');return n.push(this.type==="linear"?"</linearGradient>\n":"</radialGradient>\n"),n.join("")},toLive:function(e,t){var n,r=fabric.util.object.clone(this.coords);if(!this.type)return;if(t.group&&t.group.type==="path-group")for(var i in r)if(i==="x1"||i==="x2")r[i]+=-this.offsetX+t.width/2;else if(i==="y1"||i==="y2")r[i]+=-this.offsetY+t.height/2;this.type==="linear"?n=e.createLinearGradient(r.x1,r.y1,r.x2,r.y2):this.type==="radial"&&(n=e.createRadialGradient(r.x1,r.y1,r.r1,r.x2,r.y2,r.r2));for(var s=0,o=this.colorStops.length;s<o;s++){var u=this.colorStops[s].color,a=this.colorStops[s].opacity,f=this.colorStops[s].offset;typeof a!="undefined"&&(u=(new fabric.Color(u)).setAlpha(a).toRgba()),n.addColorStop(parseFloat(f),u)}return n}}),fabric.util.object.extend(fabric.Gradient,{fromElement:function(i,s){var o=i.getElementsByTagName("stop"),u=i.nodeName==="linearGradient"?"linear":"radial",a=i.getAttribute("gradientUnits")||"objectBoundingBox",f=i.getAttribute("gradientTransform"),l=[],c={},h;u==="linear"?c=t(i):u==="radial"&&(c=n(i));for(var p=o.length;p--;)l.push(e(o[p]));h=r(s,c,a);var d=new fabric.Gradient({type:u,coords:c,colorStops:l,offsetX:-s.left,offsetY:-s.top});if(f||h!=="")d.gradientTransform=fabric.parseTransformAttribute((f||"")+h);return d},forObject:function(e,t){return t||(t={}),r(e,t.coords,"userSpaceOnUse"),new fabric.Gradient(t)}})}(),fabric.Pattern=fabric.util.createClass({repeat:"repeat",offsetX:0,offsetY:0,initialize:function(e){e||(e={}),this.id=fabric.Object.__uid++;if(e.source)if(typeof e.source=="string")if(typeof fabric.util.getFunctionBody(e.source)!="undefined")this.source=new Function(fabric.util.getFunctionBody(e.source));else{var t=this;this.source=fabric.util.createImage(),fabric.util.loadImage(e.source,function(e){t.source=e})}else this.source=e.source;e.repeat&&(this.repeat=e.repeat),e.offsetX&&(this.offsetX=e.offsetX),e.offsetY&&(this.offsetY=e.offsetY)},toObject:function(){var e;return typeof this.source=="function"?e=String(this.source):typeof this.source.src=="string"&&(e=this.source.src),{source:e,repeat:this.repeat,offsetX:this.offsetX,offsetY:this.offsetY}},toSVG:function(e){var t=typeof this.source=="function"?this.source():this.source,n=t.width/e.getWidth(),r=t.height/e.getHeight(),i=this.offsetX/e.getWidth(),s=this.offsetY/e.getHeight(),o="";if(this.repeat==="repeat-x"||this.repeat==="no-repeat")r=1;if(this.repeat==="repeat-y"||this.repeat==="no-repeat")n=1;return t.src?o=t.src:t.toDataURL&&(o=t.toDataURL()),'<pattern id="SVGID_'+this.id+'" x="'+i+'" y="'+s+'" width="'+n+'" height="'+r+'">\n'+'<image x="0" y="0"'+' width="'+t.width+'" height="'+t.height+'" xlink:href="'+o+'"></image>\n'+"</pattern>\n"},toLive:function(e){var t=typeof this.source=="function"?this.source():this.source;if(!t)return"";if(typeof t.src!="undefined"){if(!t.complete)return"";if(t.naturalWidth===0||t.naturalHeight===0)return""}return e.createPattern(t,this.repeat)}}),function(e){"use strict";var t=e.fabric||(e.fabric={});if(t.Shadow){t.warn("fabric.Shadow is already defined.");return}t.Shadow=t.util.createClass({color:"rgb(0,0,0)",blur:0,offsetX:0,offsetY:0,affectStroke:!1,includeDefaultValues:!0,initialize:function(e){typeof e=="string"&&(e=this._parseShadow(e));for(var n in e)this[n]=e[n];this.id=t.Object.__uid++},_parseShadow:function(e){var n=e.trim(),r=t.Shadow.reOffsetsAndBlur.exec(n)||[],i=n.replace(t.Shadow.reOffsetsAndBlur,"")||"rgb(0,0,0)";return{color:i.trim(),offsetX:parseInt(r[1],10)||0,offsetY:parseInt(r[2],10)||0,blur:parseInt(r[3],10)||0}},toString:function(){return[this.offsetX,this.offsetY,this.blur,this.color].join("px ")},toSVG:function(e){var t="SourceAlpha",n=40,r=40;return e&&(e.fill===this.color||e.stroke===this.color)&&(t="SourceGraphic"),e.width&&e.height&&(n=Math.abs(this.offsetX/e.getWidth())*100+20,r=Math.abs(this.offsetY/e.getHeight())*100+20),'<filter id="SVGID_'+this.id+'" y="-'+r+'%" height="'+(100+2*r)+'%" '+'x="-'+n+'%" width="'+(100+2*n)+'%" '+">\n"+'	<feGaussianBlur in="'+t+'" stdDeviation="'+(this.blur?this.blur/3:0)+'"></feGaussianBlur>\n'+'	<feOffset dx="'+this.offsetX+'" dy="'+this.offsetY+'"></feOffset>\n'+"	<feMerge>\n"+"		<feMergeNode></feMergeNode>\n"+'		<feMergeNode in="SourceGraphic"></feMergeNode>\n'+"	</feMerge>\n"+"</filter>\n"},toObject:function(){if(this.includeDefaultValues)return{color:this.color,blur:this.blur,offsetX:this.offsetX,offsetY:this.offsetY};var e={},n=t.Shadow.prototype;return this.color!==n.color&&(e.color=this.color),this.blur!==n.blur&&(e.blur=this.blur),this.offsetX!==n.offsetX&&(e.offsetX=this.offsetX),this.offsetY!==n.offsetY&&(e.offsetY=this.offsetY),e}}),t.Shadow.reOffsetsAndBlur=/(?:\s|^)(-?\d+(?:px)?(?:\s?|$))?(-?\d+(?:px)?(?:\s?|$))?(\d+(?:px)?)?(?:\s?|$)(?:$|\s)/}(typeof exports!="undefined"?exports:this),function(){"use strict";if(fabric.StaticCanvas){fabric.warn("fabric.StaticCanvas is already defined.");return}var e=fabric.util.object.extend,t=fabric.util.getElementOffset,n=fabric.util.removeFromArray,r=new Error("Could not initialize `canvas` element");fabric.StaticCanvas=fabric.util.createClass({initialize:function(e,t){t||(t={}),this._initStatic(e,t),fabric.StaticCanvas.activeInstance=this},backgroundColor:"",backgroundImage:null,overlayColor:"",overlayImage:null,includeDefaultValues:!0,stateful:!0,renderOnAddRemove:!0,clipTo:null,controlsAboveOverlay:!1,allowTouchScrolling:!1,imageSmoothingEnabled:!0,preserveObjectStacking:!1,viewportTransform:[1,0,0,1,0,0],onBeforeScaleRotate:function(){},_initStatic:function(e,t){this._objects=[],this._createLowerCanvas(e),this._initOptions(t),this._setImageSmoothing(),t.overlayImage&&this.setOverlayImage(t.overlayImage,this.renderAll.bind(this)),t.backgroundImage&&this.setBackgroundImage(t.backgroundImage,this.renderAll.bind(this)),t.backgroundColor&&this.setBackgroundColor(t.backgroundColor,this.renderAll.bind(this)),t.overlayColor&&this.setOverlayColor(t.overlayColor,this.renderAll.bind(this)),this.calcOffset()},calcOffset:function(){return this._offset=t(this.lowerCanvasEl),this},setOverlayImage:function(e,t,n){return this.__setBgOverlayImage("overlayImage",e,t,n)},setBackgroundImage:function(e,t,n){return this.__setBgOverlayImage("backgroundImage",e,t,n)},setOverlayColor:function(e,t){return this.__setBgOverlayColor("overlayColor",e,t)},setBackgroundColor:function(e,t){return this.__setBgOverlayColor("backgroundColor",e,t)},_setImageSmoothing:function(){var e=this.getContext();e.imageSmoothingEnabled=this.imageSmoothingEnabled,e.webkitImageSmoothingEnabled=this.imageSmoothingEnabled,e.mozImageSmoothingEnabled=this.imageSmoothingEnabled,e.msImageSmoothingEnabled=this.imageSmoothingEnabled,e.oImageSmoothingEnabled=this.imageSmoothingEnabled},__setBgOverlayImage:function(e,t,n,r){return typeof t=="string"?fabric.util.loadImage(t,function(t){this[e]=new fabric.Image(t,r),n&&n()},this,r&&r.crossOrigin):(this[e]=t,n&&n()),this},__setBgOverlayColor:function(e,t,n){if(t&&t.source){var r=this;fabric.util.loadImage(t.source,function(i){r[e]=new fabric.Pattern({source:i,repeat:t.repeat,offsetX:t.offsetX,offsetY:t.offsetY}),n&&n()})}else this[e]=t,n&&n();return this},_createCanvasElement:function(){var e=fabric.document.createElement("canvas");e.style||(e.style={});if(!e)throw r;return this._initCanvasElement(e),e},_initCanvasElement:function(e){fabric.util.createCanvasElement(e);if(typeof e.getContext=="undefined")throw r},_initOptions:function(e){for(var t in e)this[t]=e[t];this.width=this.width||parseInt(this.lowerCanvasEl.width,10)||0,this.height=this.height||parseInt(this.lowerCanvasEl.height,10)||0;if(!this.lowerCanvasEl.style)return;this.lowerCanvasEl.width=this.width,this.lowerCanvasEl.height=this.height,this.lowerCanvasEl.style.width=this.width+"px",this.lowerCanvasEl.style.height=this.height+"px",this.viewportTransform=this.viewportTransform.slice()},_createLowerCanvas:function(e){this.lowerCanvasEl=fabric.util.getById(e)||this._createCanvasElement(),this._initCanvasElement(this.lowerCanvasEl),fabric.util.addClass(this.lowerCanvasEl,"lower-canvas"),this.interactive&&this._applyCanvasStyle(this.lowerCanvasEl),this.contextContainer=this.lowerCanvasEl.getContext("2d")},getWidth:function(){return this.width},getHeight:function(){return this.height},setWidth:function(e,t){return this.setDimensions({width:e},t)},setHeight:function(e,t){return this.setDimensions({height:e},t)},setDimensions:function(e,t){var n;t=t||{};for(var r in e)n=e[r],t.cssOnly||(this._setBackstoreDimension(r,e[r]),n+="px"),t.backstoreOnly||this._setCssDimension(r,n);return t.cssOnly||this.renderAll(),this.calcOffset(),this},_setBackstoreDimension:function(e,t){return this.lowerCanvasEl[e]=t,this.upperCanvasEl&&(this.upperCanvasEl[e]=t),this.cacheCanvasEl&&(this.cacheCanvasEl[e]=t),this[e]=t,this},_setCssDimension:function(e,t){return this.lowerCanvasEl.style[e]=t,this.upperCanvasEl&&(this.upperCanvasEl.style[e]=t),this.wrapperEl&&(this.wrapperEl.style[e]=t),this},getZoom:function(){return Math.sqrt(this.viewportTransform[0]*this.viewportTransform[3])},setViewportTransform:function(e){this.viewportTransform=e,this.renderAll();for(var t=0,n=this._objects.length;t<n;t++)this._objects[t].setCoords();return this},zoomToPoint:function(e,t){var n=e;e=fabric.util.transformPoint(e,fabric.util.invertTransform(this.viewportTransform)),this.viewportTransform[0]=t,this.viewportTransform[3]=t;var r=fabric.util.transformPoint(e,this.viewportTransform);this.viewportTransform[4]+=n.x-r.x,this.viewportTransform[5]+=n.y-r.y,this.renderAll();for(var i=0,s=this._objects.length;i<s;i++)this._objects[i].setCoords();return this},setZoom:function(e){return this.zoomToPoint(new fabric.Point(0,0),e),this},absolutePan:function(e){this.viewportTransform[4]=-e.x,this.viewportTransform[5]=-e.y,this.renderAll();for(var t=0,n=this._objects.length;t<n;t++)this._objects[t].setCoords();return this},relativePan:function(e){return this.absolutePan(new fabric.Point(-e.x-this.viewportTransform[4],-e.y-this.viewportTransform[5]))},getElement:function(){return this.lowerCanvasEl},getActiveObject:function(){return null},getActiveGroup:function(){return null},_draw:function(e,t){if(!t)return;e.save();var n=this.viewportTransform;e.transform(n[0],n[1],n[2],n[3],n[4],n[5]),this._shouldRenderObject(t)&&t.render(e),e.restore(),this.controlsAboveOverlay||t._renderControls(e)},_shouldRenderObject:function(e){return e?e!==this.getActiveGroup()||!this.preserveObjectStacking:!1},_onObjectAdded:function(e){this.stateful&&e.setupState(),e.canvas=this,e.setCoords(),this.fire("object:added",{target:e}),e.fire("added")},_onObjectRemoved:function(e){this.getActiveObject()===e&&(this.fire("before:selection:cleared",{target:e}),this._discardActiveObject(),this.fire("selection:cleared")),this.fire("object:removed",{target:e}),e.fire("removed")},clearContext:function(e){return e.clearRect(0,0,this.width,this.height),this},getContext:function(){return this.contextContainer},clear:function(){return this._objects.length=0,this.discardActiveGroup&&this.discardActiveGroup(),this.discardActiveObject&&this.discardActiveObject(),this.clearContext(this.contextContainer),this.contextTop&&this.clearContext(this.contextTop),this.fire("canvas:cleared"),this.renderAll(),this},renderAll:function(e){var t=this[e===!0&&this.interactive?"contextTop":"contextContainer"],n=this.getActiveGroup();return this.contextTop&&this.selection&&!this._groupSelector&&this.clearContext(this.contextTop),e||this.clearContext(t),this.fire("before:render"),this.clipTo&&fabric.util.clipContext(this,t),this._renderBackground(t),this._renderObjects(t,n),this._renderActiveGroup(t,n),this.clipTo&&t.restore(),this._renderOverlay(t),this.controlsAboveOverlay&&this.interactive&&this.drawControls(t),this.fire("after:render"),this},_renderObjects:function(e,t){var n,r;if(!t||this.preserveObjectStacking)for(n=0,r=this._objects.length;n<r;++n)this._draw(e,this._objects[n]);else for(n=0,r=this._objects.length;n<r;++n)this._objects[n]&&!t.contains(this._objects[n])&&this._draw(e,this._objects[n])},_renderActiveGroup:function(e,t){if(t){var n=[];this.forEachObject(function(e){t.contains(e)&&n.push(e)}),t._set("objects",n),this._draw(e,t)}},_renderBackground:function(e){this.backgroundColor&&(e.fillStyle=this.backgroundColor.toLive?this.backgroundColor.toLive(e):this.backgroundColor,e.fillRect(this.backgroundColor.offsetX||0,this.backgroundColor.offsetY||0,this.width,this.height)),this.backgroundImage&&this._draw(e,this.backgroundImage)},_renderOverlay:function(e){this.overlayColor&&(e.fillStyle=this.overlayColor.toLive?this.overlayColor.toLive(e):this.overlayColor,e.fillRect(this.overlayColor.offsetX||0,this.overlayColor.offsetY||0,this.width,this.height)),this.overlayImage&&this._draw(e,this.overlayImage)},renderTop:function(){var e=this.contextTop||this.contextContainer;this.clearContext(e),this.selection&&this._groupSelector&&this._drawSelection();var t=this.getActiveGroup();return t&&t.render(e),this._renderOverlay(e),this.fire("after:render"),this},getCenter:function(){return{top:this.getHeight()/2,left:this.getWidth()/2}},centerObjectH:function(e){return this._centerObject(e,new fabric.Point(this.getCenter().left,e.getCenterPoint().y)),this.renderAll(),this},centerObjectV:function(e){return this._centerObject(e,new fabric.Point(e.getCenterPoint().x,this.getCenter().top)),this.renderAll(),this},centerObject:function(e){var t=this.getCenter();return this._centerObject(e,new fabric.Point(t.left,t.top)),this.renderAll(),this},_centerObject:function(e,t){return e.setPositionByOrigin(t,"center","center"),this},toDatalessJSON:function(e){return this.toDatalessObject(e)},toObject:function(e){return this._toObjectMethod("toObject",e)},toDatalessObject:function(e){return this._toObjectMethod("toDatalessObject",e)},_toObjectMethod:function(t,n){var r=this.getActiveGroup();r&&this.discardActiveGroup();var i={objects:this._toObjects(t,n)};return e(i,this.__serializeBgOverlay()),fabric.util.populateWithProperties(this,i,n),r&&(this.setActiveGroup(new fabric.Group(r.getObjects(),{originX:"center",originY:"center"})),r.forEachObject(function(e){e.set("active",!0)}),this._currentTransform&&(this._currentTransform.target=this.getActiveGroup())),i},_toObjects:function(e,t){return this.getObjects().map(function(n){return this._toObject(n,e,t)},this)},_toObject:function(e,t,n){var r;this.includeDefaultValues||(r=e.includeDefaultValues,e.includeDefaultValues=!1);var i=e[t](n);return this.includeDefaultValues||(e.includeDefaultValues=r),i},__serializeBgOverlay:function(){var e={background:this.backgroundColor&&this.backgroundColor.toObject?this.backgroundColor.toObject():this.backgroundColor};return this.overlayColor&&(e.overlay=this.overlayColor.toObject?this.overlayColor.toObject():this.overlayColor),this.backgroundImage&&(e.backgroundImage=this.backgroundImage.toObject()),this.overlayImage&&(e.overlayImage=this.overlayImage.toObject()),e},svgViewportTransformation:!0,toSVG:function(e,t){e||(e={});var n=[];return this._setSVGPreamble(n,e),this._setSVGHeader(n,e),this._setSVGBgOverlayColor(n,"backgroundColor"),this._setSVGBgOverlayImage(n,"backgroundImage"),this._setSVGObjects(n,t),this._setSVGBgOverlayColor(n,"overlayColor"),this._setSVGBgOverlayImage(n,"overlayImage"),n.push("</svg>"),n.join("")},_setSVGPreamble:function(e,t){t.suppressPreamble||e.push('<?xml version="1.0" encoding="',t.encoding||"UTF-8",'" standalone="no" ?>','<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" ','"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')},_setSVGHeader:function(e,t){var n,r,i;t.viewBox?(n=t.viewBox.width,r=t.viewBox.height):(n=this.width,r=this.height,this.svgViewportTransformation||(i=this.viewportTransform,n/=i[0],r/=i[3])),e.push("<svg ",'xmlns="http://www.w3.org/2000/svg" ','xmlns:xlink="http://www.w3.org/1999/xlink" ','version="1.1" ','width="',n,'" ','height="',r,'" ',this.backgroundColor&&!this.backgroundColor.toLive?'style="background-color: '+this.backgroundColor+'" ':null,t.viewBox?'viewBox="'+t.viewBox.x+" "+t.viewBox.y+" "+t.viewBox.width+" "+t.viewBox.height+'" ':null,'xml:space="preserve">',"<desc>Created with Fabric.js ",fabric.version,"</desc>","<defs>",fabric.createSVGFontFacesMarkup(this.getObjects()),fabric.createSVGRefElementsMarkup(this),"</defs>")},_setSVGObjects:function(e,t){var n=this.getActiveGroup();n&&this.discardActiveGroup();for(var r=0,i=this.getObjects(),s=i.length;r<s;r++)e.push(i[r].toSVG(t));n&&(this.setActiveGroup(new fabric.Group(n.getObjects())),n.forEachObject(function(e){e.set("active",!0)}))},_setSVGBgOverlayImage:function(e,t){this[t]&&this[t].toSVG&&e.push(this[t].toSVG())},_setSVGBgOverlayColor:function(e,t){this[t]&&this[t].source?e.push('<rect x="',this[t].offsetX,'" y="',this[t].offsetY,'" ','width="',this[t].repeat==="repeat-y"||this[t].repeat==="no-repeat"?this[t].source.width:this.width,'" height="',this[t].repeat==="repeat-x"||this[t].repeat==="no-repeat"?this[t].source.height:this.height,'" fill="url(#'+t+'Pattern)"',"></rect>"):this[t]&&t==="overlayColor"&&e.push('<rect x="0" y="0" ','width="',this.width,'" height="',this.height,'" fill="',this[t],'"',"></rect>")},sendToBack:function(e){return n(this._objects,e),this._objects.unshift(e),this.renderAll&&this.renderAll()},bringToFront:function(e){return n(this._objects,e),this._objects.push(e),this.renderAll&&this.renderAll()},sendBackwards:function(e,t){var r=this._objects.indexOf(e);if(r!==0){var i=this._findNewLowerIndex(e,r,t);n(this._objects,e),this._objects.splice(i,0,e),this.renderAll&&this.renderAll()}return this},_findNewLowerIndex:function(e,t,n){var r;if(n){r=t;for(var i=t-1;i>=0;--i){var s=e.intersectsWithObject(this._objects[i])||e.isContainedWithinObject
+(this._objects[i])||this._objects[i].isContainedWithinObject(e);if(s){r=i;break}}}else r=t-1;return r},bringForward:function(e,t){var r=this._objects.indexOf(e);if(r!==this._objects.length-1){var i=this._findNewUpperIndex(e,r,t);n(this._objects,e),this._objects.splice(i,0,e),this.renderAll&&this.renderAll()}return this},_findNewUpperIndex:function(e,t,n){var r;if(n){r=t;for(var i=t+1;i<this._objects.length;++i){var s=e.intersectsWithObject(this._objects[i])||e.isContainedWithinObject(this._objects[i])||this._objects[i].isContainedWithinObject(e);if(s){r=i;break}}}else r=t+1;return r},moveTo:function(e,t){return n(this._objects,e),this._objects.splice(t,0,e),this.renderAll&&this.renderAll()},dispose:function(){return this.clear(),this.interactive&&this.removeListeners(),this},toString:function(){return"#<fabric.Canvas ("+this.complexity()+"): "+"{ objects: "+this.getObjects().length+" }>"}}),e(fabric.StaticCanvas.prototype,fabric.Observable),e(fabric.StaticCanvas.prototype,fabric.Collection),e(fabric.StaticCanvas.prototype,fabric.DataURLExporter),e(fabric.StaticCanvas,{EMPTY_JSON:'{"objects": [], "background": "white"}',supports:function(e){var t=fabric.util.createCanvasElement();if(!t||!t.getContext)return null;var n=t.getContext("2d");if(!n)return null;switch(e){case"getImageData":return typeof n.getImageData!="undefined";case"setLineDash":return typeof n.setLineDash!="undefined";case"toDataURL":return typeof t.toDataURL!="undefined";case"toDataURLWithQuality":try{return t.toDataURL("image/jpeg",0),!0}catch(r){}return!1;default:return null}}}),fabric.StaticCanvas.prototype.toJSON=fabric.StaticCanvas.prototype.toObject}(),fabric.BaseBrush=fabric.util.createClass({color:"rgb(0, 0, 0)",width:1,shadow:null,strokeLineCap:"round",strokeLineJoin:"round",setShadow:function(e){return this.shadow=new fabric.Shadow(e),this},_setBrushStyles:function(){var e=this.canvas.contextTop;e.strokeStyle=this.color,e.lineWidth=this.width,e.lineCap=this.strokeLineCap,e.lineJoin=this.strokeLineJoin},_setShadow:function(){if(!this.shadow)return;var e=this.canvas.contextTop;e.shadowColor=this.shadow.color,e.shadowBlur=this.shadow.blur,e.shadowOffsetX=this.shadow.offsetX,e.shadowOffsetY=this.shadow.offsetY},_resetShadow:function(){var e=this.canvas.contextTop;e.shadowColor="",e.shadowBlur=e.shadowOffsetX=e.shadowOffsetY=0}}),function(){fabric.PencilBrush=fabric.util.createClass(fabric.BaseBrush,{initialize:function(e){this.canvas=e,this._points=[]},onMouseDown:function(e){this._prepareForDrawing(e),this._captureDrawingPath(e),this._render()},onMouseMove:function(e){this._captureDrawingPath(e),this.canvas.clearContext(this.canvas.contextTop),this._render()},onMouseUp:function(){this._finalizeAndAddPath()},_prepareForDrawing:function(e){var t=new fabric.Point(e.x,e.y);this._reset(),this._addPoint(t),this.canvas.contextTop.moveTo(t.x,t.y)},_addPoint:function(e){this._points.push(e)},_reset:function(){this._points.length=0,this._setBrushStyles(),this._setShadow()},_captureDrawingPath:function(e){var t=new fabric.Point(e.x,e.y);this._addPoint(t)},_render:function(){var e=this.canvas.contextTop,t=this.canvas.viewportTransform,n=this._points[0],r=this._points[1];e.save(),e.transform(t[0],t[1],t[2],t[3],t[4],t[5]),e.beginPath(),this._points.length===2&&n.x===r.x&&n.y===r.y&&(n.x-=.5,r.x+=.5),e.moveTo(n.x,n.y);for(var i=1,s=this._points.length;i<s;i++){var o=n.midPointFrom(r);e.quadraticCurveTo(n.x,n.y,o.x,o.y),n=this._points[i],r=this._points[i+1]}e.lineTo(n.x,n.y),e.stroke(),e.restore()},convertPointsToSVGPath:function(e){var t=[],n=new fabric.Point(e[0].x,e[0].y),r=new fabric.Point(e[1].x,e[1].y);t.push("M ",e[0].x," ",e[0].y," ");for(var i=1,s=e.length;i<s;i++){var o=n.midPointFrom(r);t.push("Q ",n.x," ",n.y," ",o.x," ",o.y," "),n=new fabric.Point(e[i].x,e[i].y),i+1<e.length&&(r=new fabric.Point(e[i+1].x,e[i+1].y))}return t.push("L ",n.x," ",n.y," "),t},createPath:function(e){var t=new fabric.Path(e,{fill:null,stroke:this.color,strokeWidth:this.width,strokeLineCap:this.strokeLineCap,strokeLineJoin:this.strokeLineJoin,originX:"center",originY:"center"});return this.shadow&&(this.shadow.affectStroke=!0,t.setShadow(this.shadow)),t},_finalizeAndAddPath:function(){var e=this.canvas.contextTop;e.closePath();var t=this.convertPointsToSVGPath(this._points).join("");if(t==="M 0 0 Q 0 0 0 0 L 0 0"){this.canvas.renderAll();return}var n=this.createPath(t);this.canvas.add(n),n.setCoords(),this.canvas.clearContext(this.canvas.contextTop),this._resetShadow(),this.canvas.renderAll(),this.canvas.fire("path:created",{path:n})}})}(),fabric.CircleBrush=fabric.util.createClass(fabric.BaseBrush,{width:10,initialize:function(e){this.canvas=e,this.points=[]},drawDot:function(e){var t=this.addPoint(e),n=this.canvas.contextTop,r=this.canvas.viewportTransform;n.save(),n.transform(r[0],r[1],r[2],r[3],r[4],r[5]),n.fillStyle=t.fill,n.beginPath(),n.arc(t.x,t.y,t.radius,0,Math.PI*2,!1),n.closePath(),n.fill(),n.restore()},onMouseDown:function(e){this.points.length=0,this.canvas.clearContext(this.canvas.contextTop),this._setShadow(),this.drawDot(e)},onMouseMove:function(e){this.drawDot(e)},onMouseUp:function(){var e=this.canvas.renderOnAddRemove;this.canvas.renderOnAddRemove=!1;var t=[];for(var n=0,r=this.points.length;n<r;n++){var i=this.points[n],s=new fabric.Circle({radius:i.radius,left:i.x,top:i.y,originX:"center",originY:"center",fill:i.fill});this.shadow&&s.setShadow(this.shadow),t.push(s)}var o=new fabric.Group(t,{originX:"center",originY:"center"});o.canvas=this.canvas,this.canvas.add(o),this.canvas.fire("path:created",{path:o}),this.canvas.clearContext(this.canvas.contextTop),this._resetShadow(),this.canvas.renderOnAddRemove=e,this.canvas.renderAll()},addPoint:function(e){var t=new fabric.Point(e.x,e.y),n=fabric.util.getRandomInt(Math.max(0,this.width-20),this.width+20)/2,r=(new fabric.Color(this.color)).setAlpha(fabric.util.getRandomInt(0,100)/100).toRgba();return t.radius=n,t.fill=r,this.points.push(t),t}}),fabric.SprayBrush=fabric.util.createClass(fabric.BaseBrush,{width:10,density:20,dotWidth:1,dotWidthVariance:1,randomOpacity:!1,optimizeOverlapping:!0,initialize:function(e){this.canvas=e,this.sprayChunks=[]},onMouseDown:function(e){this.sprayChunks.length=0,this.canvas.clearContext(this.canvas.contextTop),this._setShadow(),this.addSprayChunk(e),this.render()},onMouseMove:function(e){this.addSprayChunk(e),this.render()},onMouseUp:function(){var e=this.canvas.renderOnAddRemove;this.canvas.renderOnAddRemove=!1;var t=[];for(var n=0,r=this.sprayChunks.length;n<r;n++){var i=this.sprayChunks[n];for(var s=0,o=i.length;s<o;s++){var u=new fabric.Rect({width:i[s].width,height:i[s].width,left:i[s].x+1,top:i[s].y+1,originX:"center",originY:"center",fill:this.color});this.shadow&&u.setShadow(this.shadow),t.push(u)}}this.optimizeOverlapping&&(t=this._getOptimizedRects(t));var a=new fabric.Group(t,{originX:"center",originY:"center"});a.canvas=this.canvas,this.canvas.add(a),this.canvas.fire("path:created",{path:a}),this.canvas.clearContext(this.canvas.contextTop),this._resetShadow(),this.canvas.renderOnAddRemove=e,this.canvas.renderAll()},_getOptimizedRects:function(e){var t={},n;for(var r=0,i=e.length;r<i;r++)n=e[r].left+""+e[r].top,t[n]||(t[n]=e[r]);var s=[];for(n in t)s.push(t[n]);return s},render:function(){var e=this.canvas.contextTop;e.fillStyle=this.color;var t=this.canvas.viewportTransform;e.save(),e.transform(t[0],t[1],t[2],t[3],t[4],t[5]);for(var n=0,r=this.sprayChunkPoints.length;n<r;n++){var i=this.sprayChunkPoints[n];typeof i.opacity!="undefined"&&(e.globalAlpha=i.opacity),e.fillRect(i.x,i.y,i.width,i.width)}e.restore()},addSprayChunk:function(e){this.sprayChunkPoints=[];var t,n,r,i=this.width/2;for(var s=0;s<this.density;s++){t=fabric.util.getRandomInt(e.x-i,e.x+i),n=fabric.util.getRandomInt(e.y-i,e.y+i),this.dotWidthVariance?r=fabric.util.getRandomInt(Math.max(1,this.dotWidth-this.dotWidthVariance),this.dotWidth+this.dotWidthVariance):r=this.dotWidth;var o=new fabric.Point(t,n);o.width=r,this.randomOpacity&&(o.opacity=fabric.util.getRandomInt(0,100)/100),this.sprayChunkPoints.push(o)}this.sprayChunks.push(this.sprayChunkPoints)}}),fabric.PatternBrush=fabric.util.createClass(fabric.PencilBrush,{getPatternSrc:function(){var e=20,t=5,n=fabric.document.createElement("canvas"),r=n.getContext("2d");return n.width=n.height=e+t,r.fillStyle=this.color,r.beginPath(),r.arc(e/2,e/2,e/2,0,Math.PI*2,!1),r.closePath(),r.fill(),n},getPatternSrcFunction:function(){return String(this.getPatternSrc).replace("this.color",'"'+this.color+'"')},getPattern:function(){return this.canvas.contextTop.createPattern(this.source||this.getPatternSrc(),"repeat")},_setBrushStyles:function(){this.callSuper("_setBrushStyles"),this.canvas.contextTop.strokeStyle=this.getPattern()},createPath:function(e){var t=this.callSuper("createPath",e);return t.stroke=new fabric.Pattern({source:this.source||this.getPatternSrcFunction()}),t}}),function(){var e=fabric.util.getPointer,t=fabric.util.degreesToRadians,n=fabric.util.radiansToDegrees,r=Math.atan2,i=Math.abs,s=.5;fabric.Canvas=fabric.util.createClass(fabric.StaticCanvas,{initialize:function(e,t){t||(t={}),this._initStatic(e,t),this._initInteractive(),this._createCacheCanvas(),fabric.Canvas.activeInstance=this},uniScaleTransform:!1,centeredScaling:!1,centeredRotation:!1,interactive:!0,selection:!0,selectionColor:"rgba(100, 100, 255, 0.3)",selectionDashArray:[],selectionBorderColor:"rgba(255, 255, 255, 0.3)",selectionLineWidth:1,hoverCursor:"move",moveCursor:"move",defaultCursor:"default",freeDrawingCursor:"crosshair",rotationCursor:"crosshair",containerClass:"canvas-container",perPixelTargetFind:!1,targetFindTolerance:0,skipTargetFind:!1,_initInteractive:function(){this._currentTransform=null,this._groupSelector=null,this._initWrapperElement(),this._createUpperCanvas(),this._initEventListeners(),this.freeDrawingBrush=fabric.PencilBrush&&new fabric.PencilBrush(this),this.calcOffset()},_resetCurrentTransform:function(e){var t=this._currentTransform;t.target.set({scaleX:t.original.scaleX,scaleY:t.original.scaleY,left:t.original.left,top:t.original.top}),this._shouldCenterTransform(e,t.target)?t.action==="rotate"?this._setOriginToCenter(t.target):(t.originX!=="center"&&(t.originX==="right"?t.mouseXSign=-1:t.mouseXSign=1),t.originY!=="center"&&(t.originY==="bottom"?t.mouseYSign=-1:t.mouseYSign=1),t.originX="center",t.originY="center"):(t.originX=t.original.originX,t.originY=t.original.originY)},containsPoint:function(e,t){var n=this.getPointer(e,!0),r=this._normalizePointer(t,n);return t.containsPoint(r)||t._findTargetCorner(n)},_normalizePointer:function(e,t){var n=this.getActiveGroup(),r=t.x,i=t.y,s=n&&e.type!=="group"&&n.contains(e),o;return s&&(o=new fabric.Point(n.left,n.top),o=fabric.util.transformPoint(o,this.viewportTransform,!0),r-=o.x,i-=o.y),{x:r,y:i}},isTargetTransparent:function(e,t,n){var r=e.hasBorders,i=e.transparentCorners;e.hasBorders=e.transparentCorners=!1,this._draw(this.contextCache,e),e.hasBorders=r,e.transparentCorners=i;var s=fabric.util.isTransparent(this.contextCache,t,n,this.targetFindTolerance);return this.clearContext(this.contextCache),s},_shouldClearSelection:function(e,t){var n=this.getActiveGroup(),r=this.getActiveObject();return!t||t&&n&&!n.contains(t)&&n!==t&&!e.shiftKey||t&&!t.evented||t&&!t.selectable&&r&&r!==t},_shouldCenterTransform:function(e,t){if(!t)return;var n=this._currentTransform,r;return n.action==="scale"||n.action==="scaleX"||n.action==="scaleY"?r=this.centeredScaling||t.centeredScaling:n.action==="rotate"&&(r=this.centeredRotation||t.centeredRotation),r?!e.altKey:e.altKey},_getOriginFromCorner:function(e,t){var n={x:e.originX,y:e.originY};if(t==="ml"||t==="tl"||t==="bl")n.x="right";else if(t==="mr"||t==="tr"||t==="br")n.x="left";if(t==="tl"||t==="mt"||t==="tr")n.y="bottom";else if(t==="bl"||t==="mb"||t==="br")n.y="top";return n},_getActionFromCorner:function(e,t){var n="drag";return t&&(n=t==="ml"||t==="mr"?"scaleX":t==="mt"||t==="mb"?"scaleY":t==="mtr"?"rotate":"scale"),n},_setupCurrentTransform:function(e,n){if(!n)return;var r=this.getPointer(e),i=n._findTargetCorner(this.getPointer(e,!0)),s=this._getActionFromCorner(n,i),o=this._getOriginFromCorner(n,i);this._currentTransform={target:n,action:s,scaleX:n.scaleX,scaleY:n.scaleY,offsetX:r.x-n.left,offsetY:r.y-n.top,originX:o.x,originY:o.y,ex:r.x,ey:r.y,left:n.left,top:n.top,theta:t(n.angle),width:n.width*n.scaleX,mouseXSign:1,mouseYSign:1},this._currentTransform.original={left:n.left,top:n.top,scaleX:n.scaleX,scaleY:n.scaleY,originX:o.x,originY:o.y},this._resetCurrentTransform(e)},_translateObject:function(e,t){var n=this._currentTransform.target;n.get("lockMovementX")||n.set("left",e-this._currentTransform.offsetX),n.get("lockMovementY")||n.set("top",t-this._currentTransform.offsetY)},_scaleObject:function(e,t,n){var r=this._currentTransform,i=r.target,s=i.get("lockScalingX"),o=i.get("lockScalingY"),u=i.get("lockScalingFlip");if(s&&o)return;var a=i.translateToOriginPoint(i.getCenterPoint(),r.originX,r.originY),f=i.toLocalPoint(new fabric.Point(e,t),r.originX,r.originY);this._setLocalMouse(f,r),this._setObjectScale(f,r,s,o,n,u),i.setPositionByOrigin(a,r.originX,r.originY)},_setObjectScale:function(e,t,n,r,i,s){var o=t.target,u=!1,a=!1,f=o.stroke?o.strokeWidth:0;t.newScaleX=e.x/(o.width+f/2),t.newScaleY=e.y/(o.height+f/2),s&&t.newScaleX<=0&&t.newScaleX<o.scaleX&&(u=!0),s&&t.newScaleY<=0&&t.newScaleY<o.scaleY&&(a=!0),i==="equally"&&!n&&!r?u||a||this._scaleObjectEqually(e,o,t):i?i==="x"&&!o.get("lockUniScaling")?u||n||o.set("scaleX",t.newScaleX):i==="y"&&!o.get("lockUniScaling")&&(a||r||o.set("scaleY",t.newScaleY)):(u||n||o.set("scaleX",t.newScaleX),a||r||o.set("scaleY",t.newScaleY)),u||a||this._flipObject(t,i)},_scaleObjectEqually:function(e,t,n){var r=e.y+e.x,i=t.stroke?t.strokeWidth:0,s=(t.height+i/2)*n.original.scaleY+(t.width+i/2)*n.original.scaleX;n.newScaleX=n.original.scaleX*r/s,n.newScaleY=n.original.scaleY*r/s,t.set("scaleX",n.newScaleX),t.set("scaleY",n.newScaleY)},_flipObject:function(e,t){e.newScaleX<0&&t!=="y"&&(e.originX==="left"?e.originX="right":e.originX==="right"&&(e.originX="left")),e.newScaleY<0&&t!=="x"&&(e.originY==="top"?e.originY="bottom":e.originY==="bottom"&&(e.originY="top"))},_setLocalMouse:function(e,t){var n=t.target;t.originX==="right"?e.x*=-1:t.originX==="center"&&(e.x*=t.mouseXSign*2,e.x<0&&(t.mouseXSign=-t.mouseXSign)),t.originY==="bottom"?e.y*=-1:t.originY==="center"&&(e.y*=t.mouseYSign*2,e.y<0&&(t.mouseYSign=-t.mouseYSign)),i(e.x)>n.padding?e.x<0?e.x+=n.padding:e.x-=n.padding:e.x=0,i(e.y)>n.padding?e.y<0?e.y+=n.padding:e.y-=n.padding:e.y=0},_rotateObject:function(e,t){var i=this._currentTransform;if(i.target.get("lockRotation"))return;var s=r(i.ey-i.top,i.ex-i.left),o=r(t-i.top,e-i.left),u=n(o-s+i.theta);u<0&&(u=360+u),i.target.angle=u%360},setCursor:function(e){this.upperCanvasEl.style.cursor=e},_resetObjectTransform:function(e){e.scaleX=1,e.scaleY=1,e.setAngle(0)},_drawSelection:function(){var e=this.contextTop,t=this._groupSelector,n=t.left,r=t.top,o=i(n),u=i(r);e.fillStyle=this.selectionColor,e.fillRect(t.ex-(n>0?0:-n),t.ey-(r>0?0:-r),o,u),e.lineWidth=this.selectionLineWidth,e.strokeStyle=this.selectionBorderColor;if(this.selectionDashArray.length>1){var a=t.ex+s-(n>0?0:o),f=t.ey+s-(r>0?0:u);e.beginPath(),fabric.util.drawDashedLine(e,a,f,a+o,f,this.selectionDashArray),fabric.util.drawDashedLine(e,a,f+u-1,a+o,f+u-1,this.selectionDashArray),fabric.util.drawDashedLine(e,a,f,a,f+u,this.selectionDashArray),fabric.util.drawDashedLine(e,a+o-1,f,a+o-1,f+u,this.selectionDashArray),e.closePath(),e.stroke()}else e.strokeRect(t.ex+s-(n>0?0:o),t.ey+s-(r>0?0:u),o,u)},_isLastRenderedObject:function(e){return this.controlsAboveOverlay&&this.lastRenderedObjectWithControlsAboveOverlay&&this.lastRenderedObjectWithControlsAboveOverlay.visible&&this.containsPoint(e,this.lastRenderedObjectWithControlsAboveOverlay)&&this.lastRenderedObjectWithControlsAboveOverlay._findTargetCorner(this.getPointer(e,!0))},findTarget:function(e,t){if(this.skipTargetFind)return;if(this._isLastRenderedObject(e))return this.lastRenderedObjectWithControlsAboveOverlay;var n=this.getActiveGroup();if(n&&!t&&this.containsPoint(e,n))return n;var r=this._searchPossibleTargets(e);return this._fireOverOutEvents(r),r},_fireOverOutEvents:function(e){e?this._hoveredTarget!==e&&(this.fire("mouse:over",{target:e}),e.fire("mouseover"),this._hoveredTarget&&(this.fire("mouse:out",{target:this._hoveredTarget}),this._hoveredTarget.fire("mouseout")),this._hoveredTarget=e):this._hoveredTarget&&(this.fire("mouse:out",{target:this._hoveredTarget}),this._hoveredTarget.fire("mouseout"),this._hoveredTarget=null)},_checkTarget:function(e,t,n){if(t&&t.visible&&t.evented&&this.containsPoint(e,t)){if(!this.perPixelTargetFind&&!t.perPixelTargetFind||!!t.isEditing)return!0;var r=this.isTargetTransparent(t,n.x,n.y);if(!r)return!0}},_searchPossibleTargets:function(e){var t,n=this.getPointer(e,!0),r=this._objects.length;while(r--)if(this._checkTarget(e,this._objects[r],n)){this.relatedTarget=this._objects[r],t=this._objects[r];break}return t},getPointer:function(t,n,r){r||(r=this.upperCanvasEl);var i=e(t,r),s=r.getBoundingClientRect(),o=s.width||0,u=s.height||0,a;if(!o||!u)"top"in s&&"bottom"in s&&(u=Math.abs(s.top-s.bottom)),"right"in s&&"left"in s&&(o=Math.abs(s.right-s.left));return this.calcOffset(),i.x=i.x-this._offset.left,i.y=i.y-this._offset.top,n||(i=fabric.util.transformPoint(i,fabric.util.invertTransform(this.viewportTransform))),o===0||u===0?a={width:1,height:1}:a={width:r.width/o,height:r.height/u},{x:i.x*a.width,y:i.y*a.height}},_createUpperCanvas:function(){var e=this.lowerCanvasEl.className.replace(/\s*lower-canvas\s*/,"");this.upperCanvasEl=this._createCanvasElement(),fabric.util.addClass(this.upperCanvasEl,"upper-canvas "+e),this.wrapperEl.appendChild(this.upperCanvasEl),this._copyCanvasStyle(this.lowerCanvasEl,this.upperCanvasEl),this._applyCanvasStyle(this.upperCanvasEl),this.contextTop=this.upperCanvasEl.getContext("2d")},_createCacheCanvas:function(){this.cacheCanvasEl=this._createCanvasElement(),this.cacheCanvasEl.setAttribute("width",this.width),this.cacheCanvasEl.setAttribute("height",this.height),this.contextCache=this.cacheCanvasEl.getContext("2d")},_initWrapperElement:function(){this.wrapperEl=fabric.util.wrapElement(this.lowerCanvasEl,"div",{"class":this.containerClass}),fabric.util.setStyle(this.wrapperEl,{width:this.getWidth()+"px",height:this.getHeight()+"px",position:"relative"}),fabric.util.makeElementUnselectable(this.wrapperEl)},_applyCanvasStyle:function(e){var t=this.getWidth()||e.width,n=this.getHeight()||e.height;fabric.util.setStyle(e,{position:"absolute",width:t+"px",height:n+"px",left:0,top:0}),e.width=t,e.height=n,fabric.util.makeElementUnselectable(e)},_copyCanvasStyle:function(e,t){t.style.cssText=e.style.cssText},getSelectionContext:function(){return this.contextTop},getSelectionElement:function(){return this.upperCanvasEl},_setActiveObject:function(e){this._activeObject&&this._activeObject.set("active",!1),this._activeObject=e,e.set("active",!0)},setActiveObject:function(e,t){return this._setActiveObject(e),this.renderAll(),this.fire("object:selected",{target:e,e:t}),e.fire("selected",{e:t}),this},getActiveObject:function(){return this._activeObject},_discardActiveObject:function(){this._activeObject&&this._activeObject.set("active",!1),this._activeObject=null},discardActiveObject:function(e){return this._discardActiveObject(),this.renderAll(),this.fire("selection:cleared",{e:e}),this},_setActiveGroup:function(e){this._activeGroup=e,e&&e.set("active",!0)},setActiveGroup:function(e,t){return this._setActiveGroup(e),e&&(this.fire("object:selected",{target:e,e:t}),e.fire("selected",{e:t})),this},getActiveGroup:function(){return this._activeGroup},_discardActiveGroup:function(){var e=this.getActiveGroup();e&&e.destroy(),this.setActiveGroup(null)},discardActiveGroup:function(e){return this._discardActiveGroup(),this.fire("selection:cleared",{e:e}),this},deactivateAll:function(){var e=this.getObjects(),t=0,n=e.length;for(;t<n;t++)e[t].set("active",!1);return this._discardActiveGroup(),this._discardActiveObject(),this},deactivateAllWithDispatch:function(e){var t=this.getActiveGroup()||this.getActiveObject();return t&&this.fire("before:selection:cleared",{target:t,e:e}),this.deactivateAll(),t&&this.fire("selection:cleared",{e:e}),this},drawControls:function(e){var t=this.getActiveGroup();t?this._drawGroupControls(e,t):this._drawObjectsControls(e)},_drawGroupControls:function(e,t){t._renderControls(e)},_drawObjectsControls:function(e){for(var t=0,n=this._objects.length;t<n;++t){if(!this._objects[t]||!this._objects[t].active)continue;this._objects[t]._renderControls(e),this.lastRenderedObjectWithControlsAboveOverlay=this._objects[t]}}});for(var o in fabric.StaticCanvas)o!=="prototype"&&(fabric.Canvas[o]=fabric.StaticCanvas[o]);fabric.isTouchSupported&&(fabric.Canvas.prototype._setCursorFromEvent=function(){}),fabric.Element=fabric.Canvas}(),function(){var e={mt:0,tr:1,mr:2,br:3,mb:4,bl:5,ml:6,tl:7},t=fabric.util.addListener,n=fabric.util.removeListener;fabric.util.object.extend(fabric.Canvas.prototype,{cursorMap:["n-resize","ne-resize","e-resize","se-resize","s-resize","sw-resize","w-resize","nw-resize"],_initEventListeners:function(){this._bindEvents(),t(fabric.window,"resize",this._onResize),t(this.upperCanvasEl,"mousedown",this._onMouseDown),t(this.upperCanvasEl,"mousemove",this._onMouseMove),t(this.upperCanvasEl,"mousewheel",this._onMouseWheel),t(this.upperCanvasEl,"touchstart",this._onMouseDown),t(this.upperCanvasEl,"touchmove",this._onMouseMove),typeof Event!="undefined"&&"add"in Event&&(Event.add(this.upperCanvasEl,"gesture",this._onGesture),Event.add(this.upperCanvasEl,"drag",this._onDrag),Event.add(this.upperCanvasEl,"orientation",this._onOrientationChange),Event.add(this.upperCanvasEl,"shake",this._onShake),Event.add(this.upperCanvasEl,"longpress",this._onLongPress))},_bindEvents:function(){this._onMouseDown=this._onMouseDown.bind(this),this._onMouseMove=this._onMouseMove.bind(this),this._onMouseUp=this._onMouseUp.bind(this),this._onResize=this._onResize.bind(this),this._onGesture=this._onGesture.bind(this),this._onDrag=this._onDrag.bind(this),this._onShake=this._onShake.bind(this),this._onLongPress=this._onLongPress.bind(this),this._onOrientationChange=this._onOrientationChange.bind(this),this._onMouseWheel=this._onMouseWheel.bind(this)},removeListeners:function(){n(fabric.window,"resize",this._onResize),n(this.upperCanvasEl,"mousedown",this._onMouseDown),n(this.upperCanvasEl,"mousemove",this._onMouseMove),n(this.upperCanvasEl,"mousewheel",this._onMouseWheel),n(this.upperCanvasEl,"touchstart",this._onMouseDown),n(this.upperCanvasEl,"touchmove",this._onMouseMove),typeof Event!="undefined"&&"remove"in Event&&(Event.remove(this.upperCanvasEl,"gesture",this._onGesture),Event.remove(this.upperCanvasEl,"drag",this._onDrag),Event.remove(this.upperCanvasEl,"orientation",this._onOrientationChange),Event.remove(this.upperCanvasEl,"shake",this._onShake),Event.remove(this.upperCanvasEl,"longpress",this._onLongPress))},_onGesture:function(e,t){this.__onTransformGesture&&this.__onTransformGesture(e,t)},_onDrag:function(e,t){this.__onDrag&&this.__onDrag(e,t)},_onMouseWheel:function(e,t){this.__onMouseWheel&&this.__onMouseWheel(e,t)},_onOrientationChange:function(e,t){this.__onOrientationChange&&this.__onOrientationChange(e,t)},_onShake:function(e,t){this.__onShake&&this.__onShake(e,t)},_onLongPress:function(e,t){this.__onLongPress&&this.__onLongPress(e,t)},_onMouseDown:function(e){this.__onMouseDown(e),t(fabric.document,"touchend",this._onMouseUp),t(fabric.document,"touchmove",this._onMouseMove),n(this.upperCanvasEl,"mousemove",this._onMouseMove),n(this.upperCanvasEl,"touchmove",this._onMouseMove),e.type==="touchstart"?n(this.upperCanvasEl,"mousedown",this._onMouseDown):(t(fabric.document,"mouseup",this._onMouseUp),t(fabric.document,"mousemove",this._onMouseMove))},_onMouseUp:function(e){this.__onMouseUp(e),n(fabric.document,"mouseup",this._onMouseUp),n(fabric.document,"touchend",this._onMouseUp),n(fabric.document,"mousemove",this._onMouseMove),n(fabric.document,"touchmove",this._onMouseMove),t(this.upperCanvasEl,"mousemove",this._onMouseMove),t(this.upperCanvasEl,"touchmove",this._onMouseMove);if(e.type==="touchend"){var r=this;setTimeout(function(){t(r.upperCanvasEl,"mousedown",r._onMouseDown)},400)}},_onMouseMove:function(e){!this.allowTouchScrolling&&e.preventDefault&&e.preventDefault(),this.__onMouseMove(e)},_onResize:function(){this.calcOffset()},_shouldRender:function(e,t){var n=this.getActiveGroup()||this.getActiveObject();return!!(e&&(e.isMoving||e!==n)||!e&&!!n||!e&&!n&&!this._groupSelector||t&&this._previousPointer&&this.selection&&(t.x!==this._previousPointer.x||t.y!==this._previousPointer.y))},__onMouseUp:function(e){var t;if(this.isDrawingMode&&this._isCurrentlyDrawing){this._onMouseUpInDrawingMode(e);return}this._currentTransform?(this._finalizeCurrentTransform(),t=this._currentTransform.target):t=this.findTarget(e,!0);var n=this._shouldRender(t,this.getPointer(e));this._maybeGroupObjects(e),t&&(t.isMoving=!1),n&&this.renderAll(),this._handleCursorAndEvent(e,t)},_handleCursorAndEvent:function(e,t){this._setCursorFromEvent(e,t);var n=this;setTimeout(function(){n._setCursorFromEvent(e,t)},50),this.fire("mouse:up",{target:t,e:e}),t&&t.fire("mouseup",{e:e})},_finalizeCurrentTransform:function(){var e=this._currentTransform,t=e.target;t._scaling&&(t._scaling=!1),t.setCoords(),this.stateful&&t.hasStateChanged()&&(this.fire("object:modified",{target:t}),t.fire("modified")),this._restoreOriginXY(t)},_restoreOriginXY:function(e){if(this._previousOriginX&&this._previousOriginY){var t=e.translateToOriginPoint(e.getCenterPoint(),this._previousOriginX,this._previousOriginY);e.originX=this._previousOriginX,e.originY=this._previousOriginY,e.left=t.x,e.top=t.y,this._previousOriginX=null,this._previousOriginY=null}},_onMouseDownInDrawingMode:function(e){this._isCurrentlyDrawing=!0,this.discardActiveObject(e).renderAll(),this.clipTo&&fabric.util.clipContext(this,this.contextTop);var t=fabric.util.invertTransform(this.viewportTransform),n=fabric.util.transformPoint(this.getPointer(e,!0),t);this.freeDrawingBrush.onMouseDown(n),this.fire("mouse:down",{e:e});var r=this.findTarget(e);typeof r!="undefined"&&r.fire("mousedown",{e:e,target:r})},_onMouseMoveInDrawingMode:function(e){if(this._isCurrentlyDrawing){var t=fabric.util.invertTransform(this.viewportTransform),n=fabric.util.transformPoint(this.getPointer(e,!0),t);this.freeDrawingBrush.onMouseMove(n)}this.setCursor(this.freeDrawingCursor),this.fire("mouse:move",{e:e});var r=this.findTarget(e);typeof r!="undefined"&&r.fire("mousemove",{e:e,target:r})},_onMouseUpInDrawingMode:function(e){this._isCurrentlyDrawing=!1,this.clipTo&&this.contextTop.restore(),this.freeDrawingBrush.onMouseUp(),this.fire("mouse:up",{e:e});var t=this.findTarget(e);typeof t!="undefined"&&t.fire("mouseup",{e:e,target:t})},__onMouseDown:function(e){var t="which"in e?e.which===1:e.button===1;if(!t&&!fabric.isTouchSupported)return;if(this.isDrawingMode){this._onMouseDownInDrawingMode(e);return}if(this._currentTransform)return;var n=this.findTarget(e),r=this.getPointer(e,!0);this._previousPointer=r;var i=this._shouldRender(n,r),s=this._shouldGroup(e,n);this._shouldClearSelection(e,n)?this._clearSelection(e,n,r):s&&(this._handleGrouping(e,n),n=this.getActiveGroup()),n&&n.selectable&&!s&&(this._beforeTransform(e,n),this._setupCurrentTransform(e,n)),i&&this.renderAll(),this.fire("mouse:down",{target:n,e:e}),n&&n.fire("mousedown",{e:e})},_beforeTransform:function(e,t){var n;this.stateful&&t.saveState(),(n=t._findTargetCorner(this.getPointer(e)))&&this.onBeforeScaleRotate(t),t!==this.getActiveGroup()&&t!==this.getActiveObject()&&(this.deactivateAll(),this.setActiveObject(t,e))},_clearSelection:function(e,t,n){this.deactivateAllWithDispatch(e),t&&t.selectable?this.setActiveObject(t,e):this.selection&&(this._groupSelector={ex:n.x,ey:n.y,top:0,left:0})},_setOriginToCenter:function(e){this._previousOriginX=this._currentTransform.target.originX,this._previousOriginY=this._currentTransform.target.originY;var t=e.getCenterPoint();e.originX="center",e.originY="center",e.left=t.x,e.top=t.y,this._currentTransform.left=e.left,this._currentTransform.top=e.top},_setCenterToOrigin:function(e){var t=e.translateToOriginPoint(e.getCenterPoint(),this._previousOriginX,this._previousOriginY);e.originX=this._previousOriginX,e.originY=this._previousOriginY,e.left=t.x,e.top=t.y,this._previousOriginX=null,this._previousOriginY=null},__onMouseMove:function(e){var t,n;if(this.isDrawingMode){this._onMouseMoveInDrawingMode(e);return}if(typeof e.touches!="undefined"&&e.touches.length>1)return;var r=this._groupSelector;r?(n=this.getPointer(e,!0),r.left=n.x-r.ex,r.top=n.y-r.ey,this.renderTop()):this._currentTransform?this._transformObject(e):(t=this.findTarget(e),!t||t&&!t.selectable?this.setCursor(this.defaultCursor):this._setCursorFromEvent(e,t)),this.fire("mouse:move",{target:t,e:e}),t&&t.fire("mousemove",{e:e})},_transformObject:function(e){var t=this.getPointer(e),n=this._currentTransform;n.reset=!1,n.target.isMoving=!0,this._beforeScaleTransform(e,n),this._performTransformAction(e,n,t),this.renderAll()},_performTransformAction:function(e,t,n){var r=n.x,i=n.y,s=t.target,o=t.action;o==="rotate"?(this._rotateObject(r,i),this._fire("rotating",s,e)):o==="scale"?(this._onScale(e,t,r,i),this._fire("scaling",s,e)):o==="scaleX"?(this._scaleObject(r,i,"x"),this._fire("scaling",s,e)):o==="scaleY"?(this._scaleObject(r,i,"y"),this._fire("scaling",s,e)):(this._translateObject(r,i),this._fire("moving",s,e),this.setCursor(this.moveCursor))},_fire:function(e,t,n){this.fire("object:"+e,{target:t,e:n}),t.fire(e,{e:n})},_beforeScaleTransform:function(e,t){if(t.action==="scale"||t.action==="scaleX"||t.action==="scaleY"){var n=this._shouldCenterTransform(e,t.target);if(n&&(t.originX!=="center"||t.originY!=="center")||!n&&t.originX==="center"&&t.originY==="center")this._resetCurrentTransform(e),t.reset=!0}},_onScale:function(e,t,n,r){(e.shiftKey||this.uniScaleTransform)&&!t.target.get("lockUniScaling")?(t.currentAction="scale",this._scaleObject(n,r)):(!t.reset&&t.currentAction==="scale"&&this._resetCurrentTransform(e,t.target),t.currentAction="scaleEqually",this._scaleObject(n,r,"equally"))},_setCursorFromEvent:function(e,t){if(!t||!t.selectable)return this.setCursor(this.defaultCursor),!1;var n=this.getActiveGroup(),r=t._findTargetCorner&&(!n||!n.contains(t))&&t._findTargetCorner(this.getPointer(e,!0));return r?this._setCornerCursor(r,t):this.setCursor(t.hoverCursor||this.hoverCursor),!0},_setCornerCursor:function(t,n){if(t in e)this.setCursor(this._getRotatedCornerCursor(t,n));else{if(t!=="mtr"||!n.hasRotatingPoint)return this.setCursor(this.defaultCursor),!1;this.setCursor(this.rotationCursor)}},_getRotatedCornerCursor:function(t,n){var r=Math.round(n.getAngle()%360/45);return r<0&&(r+=8),r+=e[t],r%=8,this.cursorMap[r]}})}(),function(){var e=Math.min,t=Math.max;fabric.util.object.extend(fabric.Canvas.prototype,{_shouldGroup:function(e,t){var n=this.getActiveObject();return e.shiftKey&&(this.getActiveGroup()||n&&n!==t)&&this.selection},_handleGrouping:function(e,t){if(t===this.getActiveGroup()){t=this.findTarget(e,!0);if(!t||t.isType("group"))return}this.getActiveGroup()?this._updateActiveGroup(t,e):this._createActiveGroup(t,e),this._activeGroup&&this._activeGroup.saveCoords()},_updateActiveGroup:function(e,t){var n=this.getActiveGroup();if(n.contains(e)){n.removeWithUpdate(e),this._resetObjectTransform(n),e.set("active",!1);if(n.size()===1){this.discardActiveGroup(t),this.setActiveObject(n.item(0));return}}else n.addWithUpdate(e),this._resetObjectTransform(n);this.fire("selection:created",{target:n,e:t}),n.set("active",!0)},_createActiveGroup:function(e,t){if(this._activeObject&&e!==this._activeObject){var n=this._createGroup(e);n.addWithUpdate(),this.setActiveGroup(n),this._activeObject=null,this.fire("selection:created",{target:n,e:t})}e.set("active",!0)},_createGroup:function(e){var t=this.getObjects(),n=t.indexOf(this._activeObject)<t.indexOf(e),r=n?[this._activeObject,e]:[e,this._activeObject];return new fabric.Group(r,{canvas:this})},_groupSelectedObjects:function(e){var t=this._collectObjects();t.length===1?this.setActiveObject(t[0],e):t.length>1&&(t=new fabric.Group(t.reverse(),{canvas:this}),t.addWithUpdate(),this.setActiveGroup(t,e),t.saveCoords(),this.fire("selection:created",{target:t}),this.renderAll())},_collectObjects:function(){var n=[],r,i=this._groupSelector.ex,s=this._groupSelector.ey,o=i+this._groupSelector.left,u=s+this._groupSelector.top,a=new fabric.Point(e(i,o),e(s,u)),f=new fabric.Point(t(i,o),t(s,u)),
+l=i===o&&s===u;for(var c=this._objects.length;c--;){r=this._objects[c];if(!r||!r.selectable||!r.visible)continue;if(r.intersectsWithRect(a,f)||r.isContainedWithinRect(a,f)||r.containsPoint(a)||r.containsPoint(f)){r.set("active",!0),n.push(r);if(l)break}}return n},_maybeGroupObjects:function(e){this.selection&&this._groupSelector&&this._groupSelectedObjects(e);var t=this.getActiveGroup();t&&(t.setObjectsCoords().setCoords(),t.isMoving=!1,this.setCursor(this.defaultCursor)),this._groupSelector=null,this._currentTransform=null}})}(),fabric.util.object.extend(fabric.StaticCanvas.prototype,{toDataURL:function(e){e||(e={});var t=e.format||"png",n=e.quality||1,r=e.multiplier||1,i={left:e.left,top:e.top,width:e.width,height:e.height};return r!==1?this.__toDataURLWithMultiplier(t,n,i,r):this.__toDataURL(t,n,i)},__toDataURL:function(e,t,n){this.renderAll(!0);var r=this.upperCanvasEl||this.lowerCanvasEl,i=this.__getCroppedCanvas(r,n);e==="jpg"&&(e="jpeg");var s=fabric.StaticCanvas.supports("toDataURLWithQuality")?(i||r).toDataURL("image/"+e,t):(i||r).toDataURL("image/"+e);return this.contextTop&&this.clearContext(this.contextTop),this.renderAll(),i&&(i=null),s},__getCroppedCanvas:function(e,t){var n,r,i="left"in t||"top"in t||"width"in t||"height"in t;return i&&(n=fabric.util.createCanvasElement(),r=n.getContext("2d"),n.width=t.width||this.width,n.height=t.height||this.height,r.drawImage(e,-t.left||0,-t.top||0)),n},__toDataURLWithMultiplier:function(e,t,n,r){var i=this.getWidth(),s=this.getHeight(),o=i*r,u=s*r,a=this.getActiveObject(),f=this.getActiveGroup(),l=this.contextTop||this.contextContainer;r>1&&this.setWidth(o).setHeight(u),l.scale(r,r),n.left&&(n.left*=r),n.top&&(n.top*=r),n.width?n.width*=r:r<1&&(n.width=o),n.height?n.height*=r:r<1&&(n.height=u),f?this._tempRemoveBordersControlsFromGroup(f):a&&this.deactivateAll&&this.deactivateAll(),this.renderAll(!0);var c=this.__toDataURL(e,t,n);return this.width=i,this.height=s,l.scale(1/r,1/r),this.setWidth(i).setHeight(s),f?this._restoreBordersControlsOnGroup(f):a&&this.setActiveObject&&this.setActiveObject(a),this.contextTop&&this.clearContext(this.contextTop),this.renderAll(),c},toDataURLWithMultiplier:function(e,t,n){return this.toDataURL({format:e,multiplier:t,quality:n})},_tempRemoveBordersControlsFromGroup:function(e){e.origHasControls=e.hasControls,e.origBorderColor=e.borderColor,e.hasControls=!0,e.borderColor="rgba(0,0,0,0)",e.forEachObject(function(e){e.origBorderColor=e.borderColor,e.borderColor="rgba(0,0,0,0)"})},_restoreBordersControlsOnGroup:function(e){e.hideControls=e.origHideControls,e.borderColor=e.origBorderColor,e.forEachObject(function(e){e.borderColor=e.origBorderColor,delete e.origBorderColor})}}),fabric.util.object.extend(fabric.StaticCanvas.prototype,{loadFromDatalessJSON:function(e,t,n){return this.loadFromJSON(e,t,n)},loadFromJSON:function(e,t,n){if(!e)return;var r=typeof e=="string"?JSON.parse(e):e;this.clear();var i=this;return this._enlivenObjects(r.objects,function(){i._setBgOverlay(r,t)},n),this},_setBgOverlay:function(e,t){var n=this,r={backgroundColor:!1,overlayColor:!1,backgroundImage:!1,overlayImage:!1};if(!e.backgroundImage&&!e.overlayImage&&!e.background&&!e.overlay){t&&t();return}var i=function(){r.backgroundImage&&r.overlayImage&&r.backgroundColor&&r.overlayColor&&(n.renderAll(),t&&t())};this.__setBgOverlay("backgroundImage",e.backgroundImage,r,i),this.__setBgOverlay("overlayImage",e.overlayImage,r,i),this.__setBgOverlay("backgroundColor",e.background,r,i),this.__setBgOverlay("overlayColor",e.overlay,r,i),i()},__setBgOverlay:function(e,t,n,r){var i=this;if(!t){n[e]=!0;return}e==="backgroundImage"||e==="overlayImage"?fabric.Image.fromObject(t,function(t){i[e]=t,n[e]=!0,r&&r()}):this["set"+fabric.util.string.capitalize(e,!0)](t,function(){n[e]=!0,r&&r()})},_enlivenObjects:function(e,t,n){var r=this;if(!e||e.length===0){t&&t();return}var i=this.renderOnAddRemove;this.renderOnAddRemove=!1,fabric.util.enlivenObjects(e,function(e){e.forEach(function(e,t){r.insertAt(e,t,!0)}),r.renderOnAddRemove=i,t&&t()},null,n)},_toDataURL:function(e,t){this.clone(function(n){t(n.toDataURL(e))})},_toDataURLWithMultiplier:function(e,t,n){this.clone(function(r){n(r.toDataURLWithMultiplier(e,t))})},clone:function(e,t){var n=JSON.stringify(this.toJSON(t));this.cloneWithoutData(function(t){t.loadFromJSON(n,function(){e&&e(t)})})},cloneWithoutData:function(e){var t=fabric.document.createElement("canvas");t.width=this.getWidth(),t.height=this.getHeight();var n=new fabric.Canvas(t);n.clipTo=this.clipTo,this.backgroundImage?(n.setBackgroundImage(this.backgroundImage.src,function(){n.renderAll(),e&&e(n)}),n.backgroundImageOpacity=this.backgroundImageOpacity,n.backgroundImageStretch=this.backgroundImageStretch):e&&e(n)}}),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend,r=t.util.toFixed,i=t.util.string.capitalize,s=t.util.degreesToRadians,o=t.StaticCanvas.supports("setLineDash");if(t.Object)return;t.Object=t.util.createClass({type:"object",originX:"left",originY:"top",top:0,left:0,width:0,height:0,scaleX:1,scaleY:1,flipX:!1,flipY:!1,opacity:1,angle:0,cornerSize:12,transparentCorners:!0,hoverCursor:null,padding:0,borderColor:"rgba(102,153,255,0.75)",cornerColor:"rgba(102,153,255,0.5)",centeredScaling:!1,centeredRotation:!0,fill:"rgb(0,0,0)",fillRule:"nonzero",globalCompositeOperation:"source-over",backgroundColor:"",stroke:null,strokeWidth:1,strokeDashArray:null,strokeLineCap:"butt",strokeLineJoin:"miter",strokeMiterLimit:10,shadow:null,borderOpacityWhenMoving:.4,borderScaleFactor:1,transformMatrix:null,minScaleLimit:.01,selectable:!0,evented:!0,visible:!0,hasControls:!0,hasBorders:!0,hasRotatingPoint:!0,rotatingPointOffset:40,perPixelTargetFind:!1,includeDefaultValues:!0,clipTo:null,lockMovementX:!1,lockMovementY:!1,lockRotation:!1,lockScalingX:!1,lockScalingY:!1,lockUniScaling:!1,lockScalingFlip:!1,stateProperties:"top left width height scaleX scaleY flipX flipY originX originY transformMatrix stroke strokeWidth strokeDashArray strokeLineCap strokeLineJoin strokeMiterLimit angle opacity fill fillRule globalCompositeOperation shadow clipTo visible backgroundColor".split(" "),initialize:function(e){e&&this.setOptions(e)},_initGradient:function(e){e.fill&&e.fill.colorStops&&!(e.fill instanceof t.Gradient)&&this.set("fill",new t.Gradient(e.fill))},_initPattern:function(e){e.fill&&e.fill.source&&!(e.fill instanceof t.Pattern)&&this.set("fill",new t.Pattern(e.fill)),e.stroke&&e.stroke.source&&!(e.stroke instanceof t.Pattern)&&this.set("stroke",new t.Pattern(e.stroke))},_initClipping:function(e){if(!e.clipTo||typeof e.clipTo!="string")return;var n=t.util.getFunctionBody(e.clipTo);typeof n!="undefined"&&(this.clipTo=new Function("ctx",n))},setOptions:function(e){for(var t in e)this.set(t,e[t]);this._initGradient(e),this._initPattern(e),this._initClipping(e)},transform:function(e,t){this.group&&this.group.transform(e,t);var n=t?this._getLeftTopCoords():this.getCenterPoint();e.translate(n.x,n.y),e.rotate(s(this.angle)),e.scale(this.scaleX*(this.flipX?-1:1),this.scaleY*(this.flipY?-1:1))},toObject:function(e){var n=t.Object.NUM_FRACTION_DIGITS,i={type:this.type,originX:this.originX,originY:this.originY,left:r(this.left,n),top:r(this.top,n),width:r(this.width,n),height:r(this.height,n),fill:this.fill&&this.fill.toObject?this.fill.toObject():this.fill,stroke:this.stroke&&this.stroke.toObject?this.stroke.toObject():this.stroke,strokeWidth:r(this.strokeWidth,n),strokeDashArray:this.strokeDashArray,strokeLineCap:this.strokeLineCap,strokeLineJoin:this.strokeLineJoin,strokeMiterLimit:r(this.strokeMiterLimit,n),scaleX:r(this.scaleX,n),scaleY:r(this.scaleY,n),angle:r(this.getAngle(),n),flipX:this.flipX,flipY:this.flipY,opacity:r(this.opacity,n),shadow:this.shadow&&this.shadow.toObject?this.shadow.toObject():this.shadow,visible:this.visible,clipTo:this.clipTo&&String(this.clipTo),backgroundColor:this.backgroundColor,fillRule:this.fillRule,globalCompositeOperation:this.globalCompositeOperation};return this.includeDefaultValues||(i=this._removeDefaultValues(i)),t.util.populateWithProperties(this,i,e),i},toDatalessObject:function(e){return this.toObject(e)},_removeDefaultValues:function(e){var n=t.util.getKlass(e.type).prototype,r=n.stateProperties;return r.forEach(function(t){e[t]===n[t]&&delete e[t]}),e},toString:function(){return"#<fabric."+i(this.type)+">"},get:function(e){return this[e]},_setObject:function(e){for(var t in e)this._set(t,e[t])},set:function(e,t){return typeof e=="object"?this._setObject(e):typeof t=="function"&&e!=="clipTo"?this._set(e,t(this.get(e))):this._set(e,t),this},_set:function(e,n){var i=e==="scaleX"||e==="scaleY";return i&&(n=this._constrainScale(n)),e==="scaleX"&&n<0?(this.flipX=!this.flipX,n*=-1):e==="scaleY"&&n<0?(this.flipY=!this.flipY,n*=-1):e==="width"||e==="height"?this.minScaleLimit=r(Math.min(.1,1/Math.max(this.width,this.height)),2):e==="shadow"&&n&&!(n instanceof t.Shadow)&&(n=new t.Shadow(n)),this[e]=n,this},toggle:function(e){var t=this.get(e);return typeof t=="boolean"&&this.set(e,!t),this},setSourcePath:function(e){return this.sourcePath=e,this},getViewportTransform:function(){return this.canvas&&this.canvas.viewportTransform?this.canvas.viewportTransform:[1,0,0,1,0,0]},render:function(e,n){if(this.width===0&&this.height===0||!this.visible)return;e.save(),this._setupCompositeOperation(e),n||this.transform(e),this._setStrokeStyles(e),this._setFillStyles(e),this.group&&this.group.type==="path-group"&&e.translate(-this.group.width/2,-this.group.height/2),this.transformMatrix&&e.transform.apply(e,this.transformMatrix),this._setOpacity(e),this._setShadow(e),this.clipTo&&t.util.clipContext(this,e),this._render(e,n),this.clipTo&&e.restore(),this._removeShadow(e),this._restoreCompositeOperation(e),e.restore()},_setOpacity:function(e){this.group&&this.group._setOpacity(e),e.globalAlpha*=this.opacity},_setStrokeStyles:function(e){this.stroke&&(e.lineWidth=this.strokeWidth,e.lineCap=this.strokeLineCap,e.lineJoin=this.strokeLineJoin,e.miterLimit=this.strokeMiterLimit,e.strokeStyle=this.stroke.toLive?this.stroke.toLive(e,this):this.stroke)},_setFillStyles:function(e){this.fill&&(e.fillStyle=this.fill.toLive?this.fill.toLive(e,this):this.fill)},_renderControls:function(e,n){var r=this.getViewportTransform();e.save();if(this.active&&!n){var i;this.group&&(i=t.util.transformPoint(this.group.getCenterPoint(),r),e.translate(i.x,i.y),e.rotate(s(this.group.angle))),i=t.util.transformPoint(this.getCenterPoint(),r,null!=this.group),this.group&&(i.x*=this.group.scaleX,i.y*=this.group.scaleY),e.translate(i.x,i.y),e.rotate(s(this.angle)),this.drawBorders(e),this.drawControls(e)}e.restore()},_setShadow:function(e){if(!this.shadow)return;var t=this.canvas&&this.canvas._currentMultiplier||1;e.shadowColor=this.shadow.color,e.shadowBlur=this.shadow.blur*t*(this.scaleX+this.scaleY)/2,e.shadowOffsetX=this.shadow.offsetX*t*this.scaleX,e.shadowOffsetY=this.shadow.offsetY*t*this.scaleY},_removeShadow:function(e){if(!this.shadow)return;e.shadowColor="",e.shadowBlur=e.shadowOffsetX=e.shadowOffsetY=0},_renderFill:function(e){if(!this.fill)return;e.save();if(this.fill.gradientTransform){var t=this.fill.gradientTransform;e.transform.apply(e,t)}this.fill.toLive&&e.translate(-this.width/2+this.fill.offsetX||0,-this.height/2+this.fill.offsetY||0),this.fillRule==="evenodd"?e.fill("evenodd"):e.fill(),e.restore(),this.shadow&&!this.shadow.affectStroke&&this._removeShadow(e)},_renderStroke:function(e){if(!this.stroke||this.strokeWidth===0)return;e.save();if(this.strokeDashArray)1&this.strokeDashArray.length&&this.strokeDashArray.push.apply(this.strokeDashArray,this.strokeDashArray),o?(e.setLineDash(this.strokeDashArray),this._stroke&&this._stroke(e)):this._renderDashedStroke&&this._renderDashedStroke(e),e.stroke();else{if(this.stroke.gradientTransform){var t=this.stroke.gradientTransform;e.transform.apply(e,t)}this._stroke?this._stroke(e):e.stroke()}this._removeShadow(e),e.restore()},clone:function(e,n){return this.constructor.fromObject?this.constructor.fromObject(this.toObject(n),e):new t.Object(this.toObject(n))},cloneAsImage:function(e){var n=this.toDataURL();return t.util.loadImage(n,function(n){e&&e(new t.Image(n))}),this},toDataURL:function(e){e||(e={});var n=t.util.createCanvasElement(),r=this.getBoundingRect();n.width=r.width,n.height=r.height,t.util.wrapElement(n,"div");var i=new t.Canvas(n);e.format==="jpg"&&(e.format="jpeg"),e.format==="jpeg"&&(i.backgroundColor="#fff");var s={active:this.get("active"),left:this.getLeft(),top:this.getTop()};this.set("active",!1),this.setPositionByOrigin(new t.Point(n.width/2,n.height/2),"center","center");var o=this.canvas;i.add(this);var u=i.toDataURL(e);return this.set(s).setCoords(),this.canvas=o,i.dispose(),i=null,u},isType:function(e){return this.type===e},complexity:function(){return 0},toJSON:function(e){return this.toObject(e)},setGradient:function(e,n){n||(n={});var r={colorStops:[]};r.type=n.type||(n.r1||n.r2?"radial":"linear"),r.coords={x1:n.x1,y1:n.y1,x2:n.x2,y2:n.y2};if(n.r1||n.r2)r.coords.r1=n.r1,r.coords.r2=n.r2;for(var i in n.colorStops){var s=new t.Color(n.colorStops[i]);r.colorStops.push({offset:i,color:s.toRgb(),opacity:s.getAlpha()})}return this.set(e,t.Gradient.forObject(this,r))},setPatternFill:function(e){return this.set("fill",new t.Pattern(e))},setShadow:function(e){return this.set("shadow",e?new t.Shadow(e):null)},setColor:function(e){return this.set("fill",e),this},setAngle:function(e){var t=(this.originX!=="center"||this.originY!=="center")&&this.centeredRotation;return t&&this._setOriginToCenter(),this.set("angle",e),t&&this._resetOrigin(),this},centerH:function(){return this.canvas.centerObjectH(this),this},centerV:function(){return this.canvas.centerObjectV(this),this},center:function(){return this.canvas.centerObject(this),this},remove:function(){return this.canvas.remove(this),this},getLocalPointer:function(e,t){t=t||this.canvas.getPointer(e);var n=this.translateToOriginPoint(this.getCenterPoint(),"left","top");return{x:t.x-n.x,y:t.y-n.y}},_setupCompositeOperation:function(e){this.globalCompositeOperation&&(this._prevGlobalCompositeOperation=e.globalCompositeOperation,e.globalCompositeOperation=this.globalCompositeOperation)},_restoreCompositeOperation:function(e){this.globalCompositeOperation&&this._prevGlobalCompositeOperation&&(e.globalCompositeOperation=this._prevGlobalCompositeOperation)}}),t.util.createAccessors(t.Object),t.Object.prototype.rotate=t.Object.prototype.setAngle,n(t.Object.prototype,t.Observable),t.Object.NUM_FRACTION_DIGITS=2,t.Object.__uid=0}(typeof exports!="undefined"?exports:this),function(){var e=fabric.util.degreesToRadians;fabric.util.object.extend(fabric.Object.prototype,{translateToCenterPoint:function(t,n,r){var i=t.x,s=t.y,o=this.stroke?this.strokeWidth:0;return n==="left"?i=t.x+(this.getWidth()+o*this.scaleX)/2:n==="right"&&(i=t.x-(this.getWidth()+o*this.scaleX)/2),r==="top"?s=t.y+(this.getHeight()+o*this.scaleY)/2:r==="bottom"&&(s=t.y-(this.getHeight()+o*this.scaleY)/2),fabric.util.rotatePoint(new fabric.Point(i,s),t,e(this.angle))},translateToOriginPoint:function(t,n,r){var i=t.x,s=t.y,o=this.stroke?this.strokeWidth:0;return n==="left"?i=t.x-(this.getWidth()+o*this.scaleX)/2:n==="right"&&(i=t.x+(this.getWidth()+o*this.scaleX)/2),r==="top"?s=t.y-(this.getHeight()+o*this.scaleY)/2:r==="bottom"&&(s=t.y+(this.getHeight()+o*this.scaleY)/2),fabric.util.rotatePoint(new fabric.Point(i,s),t,e(this.angle))},getCenterPoint:function(){var e=new fabric.Point(this.left,this.top);return this.translateToCenterPoint(e,this.originX,this.originY)},getPointByOrigin:function(e,t){var n=this.getCenterPoint();return this.translateToOriginPoint(n,e,t)},toLocalPoint:function(t,n,r){var i=this.getCenterPoint(),s=this.stroke?this.strokeWidth:0,o,u;return n&&r?(n==="left"?o=i.x-(this.getWidth()+s*this.scaleX)/2:n==="right"?o=i.x+(this.getWidth()+s*this.scaleX)/2:o=i.x,r==="top"?u=i.y-(this.getHeight()+s*this.scaleY)/2:r==="bottom"?u=i.y+(this.getHeight()+s*this.scaleY)/2:u=i.y):(o=this.left,u=this.top),fabric.util.rotatePoint(new fabric.Point(t.x,t.y),i,-e(this.angle)).subtractEquals(new fabric.Point(o,u))},setPositionByOrigin:function(e,t,n){var r=this.translateToCenterPoint(e,t,n),i=this.translateToOriginPoint(r,this.originX,this.originY);this.set("left",i.x),this.set("top",i.y)},adjustPosition:function(t){var n=e(this.angle),r=this.getWidth()/2,i=Math.cos(n)*r,s=Math.sin(n)*r,o=this.getWidth(),u=Math.cos(n)*o,a=Math.sin(n)*o;this.originX==="center"&&t==="left"||this.originX==="right"&&t==="center"?(this.left-=i,this.top-=s):this.originX==="left"&&t==="center"||this.originX==="center"&&t==="right"?(this.left+=i,this.top+=s):this.originX==="left"&&t==="right"?(this.left+=u,this.top+=a):this.originX==="right"&&t==="left"&&(this.left-=u,this.top-=a),this.setCoords(),this.originX=t},_setOriginToCenter:function(){this._originalOriginX=this.originX,this._originalOriginY=this.originY;var e=this.getCenterPoint();this.originX="center",this.originY="center",this.left=e.x,this.top=e.y},_resetOrigin:function(){var e=this.translateToOriginPoint(this.getCenterPoint(),this._originalOriginX,this._originalOriginY);this.originX=this._originalOriginX,this.originY=this._originalOriginY,this.left=e.x,this.top=e.y,this._originalOriginX=null,this._originalOriginY=null},_getLeftTopCoords:function(){return this.translateToOriginPoint(this.getCenterPoint(),"left","center")}})}(),function(){var e=fabric.util.degreesToRadians;fabric.util.object.extend(fabric.Object.prototype,{oCoords:null,intersectsWithRect:function(e,t){var n=this.oCoords,r=new fabric.Point(n.tl.x,n.tl.y),i=new fabric.Point(n.tr.x,n.tr.y),s=new fabric.Point(n.bl.x,n.bl.y),o=new fabric.Point(n.br.x,n.br.y),u=fabric.Intersection.intersectPolygonRectangle([r,i,o,s],e,t);return u.status==="Intersection"},intersectsWithObject:function(e){function t(e){return{tl:new fabric.Point(e.tl.x,e.tl.y),tr:new fabric.Point(e.tr.x,e.tr.y),bl:new fabric.Point(e.bl.x,e.bl.y),br:new fabric.Point(e.br.x,e.br.y)}}var n=t(this.oCoords),r=t(e.oCoords),i=fabric.Intersection.intersectPolygonPolygon([n.tl,n.tr,n.br,n.bl],[r.tl,r.tr,r.br,r.bl]);return i.status==="Intersection"},isContainedWithinObject:function(e){var t=e.getBoundingRect(),n=new fabric.Point(t.left,t.top),r=new fabric.Point(t.left+t.width,t.top+t.height);return this.isContainedWithinRect(n,r)},isContainedWithinRect:function(e,t){var n=this.getBoundingRect();return n.left>=e.x&&n.left+n.width<=t.x&&n.top>=e.y&&n.top+n.height<=t.y},containsPoint:function(e){var t=this._getImageLines(this.oCoords),n=this._findCrossPoints(e,t);return n!==0&&n%2===1},_getImageLines:function(e){return{topline:{o:e.tl,d:e.tr},rightline:{o:e.tr,d:e.br},bottomline:{o:e.br,d:e.bl},leftline:{o:e.bl,d:e.tl}}},_findCrossPoints:function(e,t){var n,r,i,s,o,u,a=0,f;for(var l in t){f=t[l];if(f.o.y<e.y&&f.d.y<e.y)continue;if(f.o.y>=e.y&&f.d.y>=e.y)continue;f.o.x===f.d.x&&f.o.x>=e.x?(o=f.o.x,u=e.y):(n=0,r=(f.d.y-f.o.y)/(f.d.x-f.o.x),i=e.y-n*e.x,s=f.o.y-r*f.o.x,o=-(i-s)/(n-r),u=i+n*o),o>=e.x&&(a+=1);if(a===2)break}return a},getBoundingRectWidth:function(){return this.getBoundingRect().width},getBoundingRectHeight:function(){return this.getBoundingRect().height},getBoundingRect:function(){this.oCoords||this.setCoords();var e=[this.oCoords.tl.x,this.oCoords.tr.x,this.oCoords.br.x,this.oCoords.bl.x],t=fabric.util.array.min(e),n=fabric.util.array.max(e),r=Math.abs(t-n),i=[this.oCoords.tl.y,this.oCoords.tr.y,this.oCoords.br.y,this.oCoords.bl.y],s=fabric.util.array.min(i),o=fabric.util.array.max(i),u=Math.abs(s-o);return{left:t,top:s,width:r,height:u}},getWidth:function(){return this.width*this.scaleX},getHeight:function(){return this.height*this.scaleY},_constrainScale:function(e){return Math.abs(e)<this.minScaleLimit?e<0?-this.minScaleLimit:this.minScaleLimit:e},scale:function(e){return e=this._constrainScale(e),e<0&&(this.flipX=!this.flipX,this.flipY=!this.flipY,e*=-1),this.scaleX=e,this.scaleY=e,this.setCoords(),this},scaleToWidth:function(e){var t=this.getBoundingRectWidth()/this.getWidth();return this.scale(e/this.width/t)},scaleToHeight:function(e){var t=this.getBoundingRectHeight()/this.getHeight();return this.scale(e/this.height/t)},setCoords:function(){var t=this.strokeWidth,n=e(this.angle),r=this.getViewportTransform(),i=function(e){return fabric.util.transformPoint(e,r)},s=this.width,o,u=this.height,a,f=this.strokeLineCap==="round"||this.strokeLineCap==="square",l=this.type==="line"&&this.width===0,c=this.type==="line"&&this.height===0,h=l||c,p=f&&c||!h,d=f&&l||!h;l?s=t:c&&(u=t),p&&(s+=s>0?t:-t),d&&(u+=u>0?t:-t),o=s*this.scaleX+2*this.padding,a=u*this.scaleY+2*this.padding,o<0&&(o=Math.abs(o));var v=Math.sqrt(Math.pow(o/2,2)+Math.pow(a/2,2)),m=Math.atan(isFinite(a/o)?a/o:0),g=Math.cos(m+n)*v,y=Math.sin(m+n)*v,b=Math.sin(n),w=Math.cos(n),E=this.getCenterPoint(),S=new fabric.Point(o,a),x=new fabric.Point(E.x-g,E.y-y),T=new fabric.Point(x.x+S.x*w,x.y+S.x*b),N=new fabric.Point(x.x-S.y*b,x.y+S.y*w),C=new fabric.Point(x.x+S.x/2*w,x.y+S.x/2*b),k=i(x),L=i(T),A=i(new fabric.Point(T.x-S.y*b,T.y+S.y*w)),O=i(N),M=i(new fabric.Point(x.x-S.y/2*b,x.y+S.y/2*w)),_=i(C),D=i(new fabric.Point(T.x-S.y/2*b,T.y+S.y/2*w)),P=i(new fabric.Point(N.x+S.x/2*w,N.y+S.x/2*b)),H=i(new fabric.Point(C.x,C.y));return this.oCoords={tl:k,tr:L,br:A,bl:O,ml:M,mt:_,mr:D,mb:P,mtr:H},this._setCornerCoords&&this._setCornerCoords(),this}})}(),fabric.util.object.extend(fabric.Object.prototype,{sendToBack:function(){return this.group?fabric.StaticCanvas.prototype.sendToBack.call(this.group,this):this.canvas.sendToBack(this),this},bringToFront:function(){return this.group?fabric.StaticCanvas.prototype.bringToFront.call(this.group,this):this.canvas.bringToFront(this),this},sendBackwards:function(e){return this.group?fabric.StaticCanvas.prototype.sendBackwards.call(this.group,this,e):this.canvas.sendBackwards(this,e),this},bringForward:function(e){return this.group?fabric.StaticCanvas.prototype.bringForward.call(this.group,this,e):this.canvas.bringForward(this,e),this},moveTo:function(e){return this.group?fabric.StaticCanvas.prototype.moveTo.call(this.group,this,e):this.canvas.moveTo(this,e),this}}),fabric.util.object.extend(fabric.Object.prototype,{getSvgStyles:function(){var e=this.fill?this.fill.toLive?"url(#SVGID_"+this.fill.id+")":this.fill:"none",t=this.fillRule,n=this.stroke?this.stroke.toLive?"url(#SVGID_"+this.stroke.id+")":this.stroke:"none",r=this.strokeWidth?this.strokeWidth:"0",i=this.strokeDashArray?this.strokeDashArray.join(" "):"",s=this.strokeLineCap?this.strokeLineCap:"butt",o=this.strokeLineJoin?this.strokeLineJoin:"miter",u=this.strokeMiterLimit?this.strokeMiterLimit:"4",a=typeof this.opacity!="undefined"?this.opacity:"1",f=this.visible?"":" visibility: hidden;",l=this.shadow&&this.type!=="text"?"filter: url(#SVGID_"+this.shadow.id+");":"";return["stroke: ",n,"; ","stroke-width: ",r,"; ","stroke-dasharray: ",i,"; ","stroke-linecap: ",s,"; ","stroke-linejoin: ",o,"; ","stroke-miterlimit: ",u,"; ","fill: ",e,"; ","fill-rule: ",t,"; ","opacity: ",a,";",l,f].join("")},getSvgTransform:function(){if(this.group&&this.group.type==="path-group")return"";var e=fabric.util.toFixed,t=this.getAngle(),n=!this.canvas||this.canvas.svgViewportTransformation?this.getViewportTransform():[1,0,0,1,0,0],r=fabric.util.transformPoint(this.getCenterPoint(),n),i=fabric.Object.NUM_FRACTION_DIGITS,s=this.type==="path-group"?"":"translate("+e(r.x,i)+" "+e(r.y,i)+")",o=t!==0?" rotate("+e(t,i)+")":"",u=this.scaleX===1&&this.scaleY===1&&n[0]===1&&n[3]===1?"":" scale("+e(this.scaleX*n[0],i)+" "+e(this.scaleY*n[3],i)+")",a=this.type==="path-group"?this.width*n[0]:0,f=this.flipX?" matrix(-1 0 0 1 "+a+" 0) ":"",l=this.type==="path-group"?this.height*n[3]:0,c=this.flipY?" matrix(1 0 0 -1 0 "+l+")":"";return[s,o,u,f,c].join("")},getSvgTransformMatrix:function(){return this.transformMatrix?" matrix("+this.transformMatrix.join(" ")+")":""},_createBaseSVGMarkup:function(){var e=[];return this.fill&&this.fill.toLive&&e.push(this.fill.toSVG(this,!1)),this.stroke&&this.stroke.toLive&&e.push(this.stroke.toSVG(this,!1)),this.shadow&&e.push(this.shadow.toSVG(this)),e}}),fabric.util.object.extend(fabric.Object.prototype,{hasStateChanged:function(){return this.stateProperties.some(function(e){return this.get(e)!==this.originalState[e]},this)},saveState:function(e){return this.stateProperties.forEach(function(e){this.originalState[e]=this.get(e)},this),e&&e.stateProperties&&e.stateProperties.forEach(function(e){this.originalState[e]=this.get(e)},this),this},setupState:function(){return this.originalState={},this.saveState(),this}}),function(){var e=fabric.util.degreesToRadians,t=function(){return typeof G_vmlCanvasManager!="undefined"};fabric.util.object.extend(fabric.Object.prototype,{_controlsVisibility:null,_findTargetCorner:function(e){if(!this.hasControls||!this.active)return!1;var t=e.x,n=e.y,r,i;for(var s in this.oCoords){if(!this.isControlVisible(s))continue;if(s==="mtr"&&!this.hasRotatingPoint)continue;if(!(!this.get("lockUniScaling")||s!=="mt"&&s!=="mr"&&s!=="mb"&&s!=="ml"))continue;i=this._getImageLines(this.oCoords[s].corner),r=this._findCrossPoints({x:t,y:n},i);if(r!==0&&r%2===1)return this.__corner=s,s}return!1},_setCornerCoords:function(){var t=this.oCoords,n=e(this.angle),r=e(45-this.angle),i=Math.sqrt(2*Math.pow(this.cornerSize,2))/2,s=i*Math.cos(r),o=i*Math.sin(r),u=Math.sin(n),a=Math.cos(n);t.tl.corner={tl:{x:t.tl.x-o,y:t.tl.y-s},tr:{x:t.tl.x+s,y:t.tl.y-o},bl:{x:t.tl.x-s,y:t.tl.y+o},br:{x:t.tl.x+o,y:t.tl.y+s}},t.tr.corner={tl:{x:t.tr.x-o,y:t.tr.y-s},tr:{x:t.tr.x+s,y:t.tr.y-o},br:{x:t.tr.x+o,y:t.tr.y+s},bl:{x:t.tr.x-s,y:t.tr.y+o}},t.bl.corner={tl:{x:t.bl.x-o,y:t.bl.y-s},bl:{x:t.bl.x-s,y:t.bl.y+o},br:{x:t.bl.x+o,y:t.bl.y+s},tr:{x:t.bl.x+s,y:t.bl.y-o}},t.br.corner={tr:{x:t.br.x+s,y:t.br.y-o},bl:{x:t.br.x-s,y:t.br.y+o},br:{x:t.br.x+o,y:t.br.y+s},tl:{x:t.br.x-o,y:t.br.y-s}},t.ml.corner={tl:{x:t.ml.x-o,y:t.ml.y-s},tr:{x:t.ml.x+s,y:t.ml.y-o},bl:{x:t.ml.x-s,y:t.ml.y+o},br:{x:t.ml.x+o,y:t.ml.y+s}},t.mt.corner={tl:{x:t.mt.x-o,y:t.mt.y-s},tr:{x:t.mt.x+s,y:t.mt.y-o},bl:{x:t.mt.x-s,y:t.mt.y+o},br:{x:t.mt.x+o,y:t.mt.y+s}},t.mr.corner={tl:{x:t.mr.x-o,y:t.mr.y-s},tr:{x:t.mr.x+s,y:t.mr.y-o},bl:{x:t.mr.x-s,y:t.mr.y+o},br:{x:t.mr.x+o,y:t.mr.y+s}},t.mb.corner={tl:{x:t.mb.x-o,y:t.mb.y-s},tr:{x:t.mb.x+s,y:t.mb.y-o},bl:{x:t.mb.x-s,y:t.mb.y+o},br:{x:t.mb.x+o,y:t.mb.y+s}},t.mtr.corner={tl:{x:t.mtr.x-o+u*this.rotatingPointOffset,y:t.mtr.y-s-a*this.rotatingPointOffset},tr:{x:t.mtr.x+s+u*this.rotatingPointOffset,y:t.mtr.y-o-a*this.rotatingPointOffset},bl:{x:t.mtr.x-s+u*this.rotatingPointOffset,y:t.mtr.y+o-a*this.rotatingPointOffset},br:{x:t.mtr.x+o+u*this.rotatingPointOffset,y:t.mtr.y+s-a*this.rotatingPointOffset}}},drawBorders:function(e){if(!this.hasBorders)return this;var t=this.padding,n=t*2,r=this.getViewportTransform();e.save(),e.globalAlpha=this.isMoving?this.borderOpacityWhenMoving:1,e.strokeStyle=this.borderColor;var i=1/this._constrainScale(this.scaleX),s=1/this._constrainScale(this.scaleY);e.lineWidth=1/this.borderScaleFactor;var o=this.getWidth(),u=this.getHeight(),a=this.strokeWidth,f=this.strokeLineCap==="round"||this.strokeLineCap==="square",l=this.type==="line"&&this.width===0,c=this.type==="line"&&this.height===0,h=l||c,p=f&&c||!h,d=f&&l||!h;l?o=a/i:c&&(u=a/s),p&&(o+=a/i),d&&(u+=a/s);var v=fabric.util.transformPoint(new fabric.Point(o,u),r,!0),m=v.x,g=v.y;this.group&&(m*=this.group.scaleX,g*=this.group.scaleY),e.strokeRect(~~(-(m/2)-t)-.5,~~(-(g/2)-t)-.5,~~(m+n)+1,~~(g+n)+1);if(this.hasRotatingPoint&&this.isControlVisible("mtr")&&!this.get("lockRotation")&&this.hasControls){var y=(-g-t*2)/2;e.beginPath(),e.moveTo(0,y),e.lineTo(0,y-this.rotatingPointOffset),e.closePath(),e.stroke()}return e.restore(),this},drawControls:function(e){if(!this.hasControls)return this;var t=this.cornerSize,n=t/2,r=this.getViewportTransform(),i=this.strokeWidth,s=this.width,o=this.height,u=this.strokeLineCap==="round"||this.strokeLineCap==="square",a=this.type==="line"&&this.width===0,f=this.type==="line"&&this.height===0,l=a||f,c=u&&f||!l,h=u&&a||!l;a?s=i:f&&(o=i),c&&(s+=i),h&&(o+=i),s*=this.scaleX,o*=this.scaleY;var p=fabric.util.transformPoint(new fabric.Point(s,o),r,!0),d=p.x,v=p.y,m=-(d/2),g=-(v/2),y=this.padding,b=n,w=n-t,E=this.transparentCorners?"strokeRect":"fillRect";return e.save(),e.lineWidth=1,e.globalAlpha=this.isMoving?this.borderOpacityWhenMoving:1,e.strokeStyle=e.fillStyle=this.cornerColor,this._drawControl("tl",e,E,m-b-y,g-b-y),this._drawControl("tr",e,E,m+d-b+y,g-b-y),this._drawControl("bl",e,E,m-b-y,g+v+w+y),this._drawControl("br",e,E,m+d+w+y,g+v+w+y),this.get("lockUniScaling")||(this._drawControl("mt",e,E,m+d/2-b,g-b-y),this._drawControl("mb",e,E,m+d/2-b,g+v+w+y),this._drawControl("mr",e,E,m+d+w+y,g+v/2-b),this._drawControl("ml",e,E,m-b-y,g+v/2-b)),this.hasRotatingPoint&&this._drawControl("mtr",e,E,m+d/2-b,g-this.rotatingPointOffset-this.cornerSize/2-y),e.restore(),this},_drawControl:function(e,n,r,i,s){var o=this.cornerSize;this.isControlVisible(e)&&(t()||this.transparentCorners||n.clearRect(i,s,o,o),n[r](i,s,o,o))},isControlVisible:function(e){return this._getControlsVisibility()[e]},setControlVisible:function(e,t){return this._getControlsVisibility()[e]=t,this},setControlsVisibility:function(e){e||(e={});for(var t in e)this.setControlVisible(t,e[t]);return this},_getControlsVisibility:function(){return this._controlsVisibility||(this._controlsVisibility={tl:!0,tr:!0,br:!0,bl:!0,ml:!0,mt:!0,mr:!0,mb:!0,mtr:!0}),this._controlsVisibility}})}(),fabric.util.object.extend(fabric.StaticCanvas.prototype,{FX_DURATION:500,fxCenterObjectH:function(e,t){t=t||{};var n=function(){},r=t.onComplete||n,i=t.onChange||n,s=this;return fabric.util.animate({startValue:e.get("left"),endValue:this.getCenter().left,duration:this.FX_DURATION,onChange:function(t){e.set("left",t),s.renderAll(),i()},onComplete:function(){e.setCoords(),r()}}),this},fxCenterObjectV:function(e,t){t=t||{};var n=function(){},r=t.onComplete||n,i=t.onChange||n,s=this;return fabric.util.animate({startValue:e.get("top"),endValue:this.getCenter().top,duration:this.FX_DURATION,onChange:function(t){e.set("top",t),s.renderAll(),i()},onComplete:function(){e.setCoords(),r()}}),this},fxRemove:function(e,t){t=t||{};var n=function(){},r=t.onComplete||n,i=t.onChange||n,s=this;return fabric.util.animate({startValue:e.get("opacity"),endValue:0,duration:this.FX_DURATION,onStart:function(){e.set("active",!1)},onChange:function(t){e.set("opacity",t),s.renderAll(),i()},onComplete:function(){s.remove(e),r()}}),this}}),fabric.util.object.extend(fabric.Object.prototype,{animate:function(){if(arguments[0]&&typeof arguments[0]=="object"){var e=[],t,n;for(t in arguments[0])e.push(t);for(var r=0,i=e.length;r<i;r++)t=e[r],n=r!==i-1,this._animate(t,arguments[0][t],arguments[1],n)}else this._animate.apply(this,arguments);return this},_animate:function(e,t,n,r){var i=this,s;t=t.toString(),n?n=fabric.util.object.clone(n):n={},~e.indexOf(".")&&(s=e.split("."));var o=s?this.get(s[0])[s[1]]:this.get(e);"from"in n||(n.from=o),~t.indexOf("=")?t=o+parseFloat(t.replace("=","")):t=parseFloat(t),fabric.util.animate({startValue:n.from,endValue:t,byValue:n.by,easing:n.easing,duration:n.duration,abort:n.abort&&function(){return n.abort.call(i)},onChange:function(t){s?i[s[0]][s[1]]=t:i.set(e,t);if(r)return;n.onChange&&n.onChange()},onComplete:function(){if(r)return;i.setCoords(),n.onComplete&&n.onComplete()}})}}),function(e){"use strict";function s(e,t){var n=e.origin,r=e.axis1,i=e.axis2,s=e.dimension,o=t.nearest,u=t.center,a=t.farthest;return function(){switch(this.get(n)){case o:return Math.min(this.get(r),this.get(i));case u:return Math.min(this.get(r),this.get(i))+.5*this.get(s);case a:return Math.max(this.get(r),this.get(i))}}}var t=e.fabric||(e.fabric={}),n=t.util.object.extend,r={x1:1,x2:1,y1:1,y2:1},i=t.StaticCanvas.supports("setLineDash");if(t.Line){t.warn("fabric.Line is already defined");return}t.Line=t.util.createClass(t.Object,{type:"line",x1:0,y1:0,x2:0,y2:0,initialize:function(e,t){t=t||{},e||(e=[0,0,0,0]),this.callSuper("initialize",t),this.set("x1",e[0]),this.set("y1",e[1]),this.set("x2",e[2]),this.set("y2",e[3]),this._setWidthHeight(t)},_setWidthHeight:function(e){e||(e={}),this.width=Math.abs(this.x2-this.x1),this.height=Math.abs(this.y2-this.y1),this.left="left"in e?e.left:this._getLeftToOriginX(),this.top="top"in e?e.top:this._getTopToOriginY()},_set:function(e,t){return this.callSuper("_set",e,t),typeof r[e]!="undefined"&&this._setWidthHeight(),this},_getLeftToOriginX:s({origin:"originX",axis1:"x1",axis2:"x2",dimension:"width"},{nearest:"left",center:"center",farthest:"right"}),_getTopToOriginY:s({origin:"originY",axis1:"y1",axis2:"y2",dimension:"height"},{nearest:"top",center:"center",farthest
+:"bottom"}),_render:function(e,t){e.beginPath();if(t){var n=this.getCenterPoint();e.translate(n.x-this.strokeWidth/2,n.y-this.strokeWidth/2)}if(!this.strokeDashArray||this.strokeDashArray&&i){var r=this.calcLinePoints();e.moveTo(r.x1,r.y1),e.lineTo(r.x2,r.y2)}e.lineWidth=this.strokeWidth;var s=e.strokeStyle;e.strokeStyle=this.stroke||e.fillStyle,this.stroke&&this._renderStroke(e),e.strokeStyle=s},_renderDashedStroke:function(e){var n=this.calcLinePoints();e.beginPath(),t.util.drawDashedLine(e,n.x1,n.y1,n.x2,n.y2,this.strokeDashArray),e.closePath()},toObject:function(e){return n(this.callSuper("toObject",e),this.calcLinePoints())},calcLinePoints:function(){var e=this.x1<=this.x2?-1:1,t=this.y1<=this.y2?-1:1,n=e*this.width*.5,r=t*this.height*.5,i=e*this.width*-0.5,s=t*this.height*-0.5;return{x1:n,x2:i,y1:r,y2:s}},toSVG:function(e){var t=this._createBaseSVGMarkup(),n={x1:this.x1,x2:this.x2,y1:this.y1,y2:this.y2};if(!this.group||this.group.type!=="path-group")n=this.calcLinePoints();return t.push("<line ",'x1="',n.x1,'" y1="',n.y1,'" x2="',n.x2,'" y2="',n.y2,'" style="',this.getSvgStyles(),'" transform="',this.getSvgTransform(),this.getSvgTransformMatrix(),'"/>\n'),e?e(t.join("")):t.join("")},complexity:function(){return 1}}),t.Line.ATTRIBUTE_NAMES=t.SHARED_ATTRIBUTES.concat("x1 y1 x2 y2".split(" ")),t.Line.fromElement=function(e,r){var i=t.parseAttributes(e,t.Line.ATTRIBUTE_NAMES),s=[i.x1||0,i.y1||0,i.x2||0,i.y2||0];return new t.Line(s,n(i,r))},t.Line.fromObject=function(e){var n=[e.x1,e.y1,e.x2,e.y2];return new t.Line(n,e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";function i(e){return"radius"in e&&e.radius>0}var t=e.fabric||(e.fabric={}),n=Math.PI,r=t.util.object.extend;if(t.Circle){t.warn("fabric.Circle is already defined.");return}t.Circle=t.util.createClass(t.Object,{type:"circle",radius:0,startAngle:0,endAngle:n*2,initialize:function(e){e=e||{},this.callSuper("initialize",e),this.set("radius",e.radius||0),this.startAngle=e.startAngle||this.startAngle,this.endAngle=e.endAngle||this.endAngle},_set:function(e,t){return this.callSuper("_set",e,t),e==="radius"&&this.setRadius(t),this},toObject:function(e){return r(this.callSuper("toObject",e),{radius:this.get("radius"),startAngle:this.startAngle,endAngle:this.endAngle})},toSVG:function(e){var t=this._createBaseSVGMarkup(),r=0,i=0,s=(this.endAngle-this.startAngle)%(2*n);if(s===0)this.group&&this.group.type==="path-group"&&(r=this.left+this.radius,i=this.top+this.radius),t.push("<circle ",'cx="'+r+'" cy="'+i+'" ','r="',this.radius,'" style="',this.getSvgStyles(),'" transform="',this.getSvgTransform()," ",this.getSvgTransformMatrix(),'"/>\n');else{var o=Math.cos(this.startAngle)*this.radius,u=Math.sin(this.startAngle)*this.radius,a=Math.cos(this.endAngle)*this.radius,f=Math.sin(this.endAngle)*this.radius,l=s>n?"1":"0";t.push('<path d="M '+o+" "+u," A "+this.radius+" "+this.radius," 0 ",+l+" 1"," "+a+" "+f,'" style="',this.getSvgStyles(),'" transform="',this.getSvgTransform()," ",this.getSvgTransformMatrix(),'"/>\n')}return e?e(t.join("")):t.join("")},_render:function(e,t){e.beginPath(),e.arc(t?this.left+this.radius:0,t?this.top+this.radius:0,this.radius,this.startAngle,this.endAngle,!1),this._renderFill(e),this._renderStroke(e)},getRadiusX:function(){return this.get("radius")*this.get("scaleX")},getRadiusY:function(){return this.get("radius")*this.get("scaleY")},setRadius:function(e){this.radius=e,this.set("width",e*2).set("height",e*2)},complexity:function(){return 1}}),t.Circle.ATTRIBUTE_NAMES=t.SHARED_ATTRIBUTES.concat("cx cy r".split(" ")),t.Circle.fromElement=function(e,n){n||(n={});var s=t.parseAttributes(e,t.Circle.ATTRIBUTE_NAMES);if(!i(s))throw new Error("value of `r` attribute is required and can not be negative");s.left=s.left||0,s.top=s.top||0;var o=new t.Circle(r(s,n));return o.left-=o.radius,o.top-=o.radius,o},t.Circle.fromObject=function(e){return new t.Circle(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={});if(t.Triangle){t.warn("fabric.Triangle is already defined");return}t.Triangle=t.util.createClass(t.Object,{type:"triangle",initialize:function(e){e=e||{},this.callSuper("initialize",e),this.set("width",e.width||100).set("height",e.height||100)},_render:function(e){var t=this.width/2,n=this.height/2;e.beginPath(),e.moveTo(-t,n),e.lineTo(0,-n),e.lineTo(t,n),e.closePath(),this._renderFill(e),this._renderStroke(e)},_renderDashedStroke:function(e){var n=this.width/2,r=this.height/2;e.beginPath(),t.util.drawDashedLine(e,-n,r,0,-r,this.strokeDashArray),t.util.drawDashedLine(e,0,-r,n,r,this.strokeDashArray),t.util.drawDashedLine(e,n,r,-n,r,this.strokeDashArray),e.closePath()},toSVG:function(e){var t=this._createBaseSVGMarkup(),n=this.width/2,r=this.height/2,i=[-n+" "+r,"0 "+ -r,n+" "+r].join(",");return t.push("<polygon ",'points="',i,'" style="',this.getSvgStyles(),'" transform="',this.getSvgTransform(),'"/>'),e?e(t.join("")):t.join("")},complexity:function(){return 1}}),t.Triangle.fromObject=function(e){return new t.Triangle(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=Math.PI*2,r=t.util.object.extend;if(t.Ellipse){t.warn("fabric.Ellipse is already defined.");return}t.Ellipse=t.util.createClass(t.Object,{type:"ellipse",rx:0,ry:0,initialize:function(e){e=e||{},this.callSuper("initialize",e),this.set("rx",e.rx||0),this.set("ry",e.ry||0)},_set:function(e,t){this.callSuper("_set",e,t);switch(e){case"rx":this.rx=t,this.set("width",t*2);break;case"ry":this.ry=t,this.set("height",t*2)}return this},getRx:function(){return this.get("rx")*this.get("scaleX")},getRy:function(){return this.get("ry")*this.get("scaleY")},toObject:function(e){return r(this.callSuper("toObject",e),{rx:this.get("rx"),ry:this.get("ry")})},toSVG:function(e){var t=this._createBaseSVGMarkup(),n=0,r=0;return this.group&&this.group.type==="path-group"&&(n=this.left+this.rx,r=this.top+this.ry),t.push("<ellipse ",'cx="',n,'" cy="',r,'" ','rx="',this.rx,'" ry="',this.ry,'" style="',this.getSvgStyles(),'" transform="',this.getSvgTransform(),this.getSvgTransformMatrix(),'"/>\n'),e?e(t.join("")):t.join("")},_render:function(e,t){e.beginPath(),e.save(),e.transform(1,0,0,this.ry/this.rx,0,0),e.arc(t?this.left+this.rx:0,t?(this.top+this.ry)*this.rx/this.ry:0,this.rx,0,n,!1),e.restore(),this._renderFill(e),this._renderStroke(e)},complexity:function(){return 1}}),t.Ellipse.ATTRIBUTE_NAMES=t.SHARED_ATTRIBUTES.concat("cx cy rx ry".split(" ")),t.Ellipse.fromElement=function(e,n){n||(n={});var i=t.parseAttributes(e,t.Ellipse.ATTRIBUTE_NAMES);i.left=i.left||0,i.top=i.top||0;var s=new t.Ellipse(r(i,n));return s.top-=s.ry,s.left-=s.rx,s},t.Ellipse.fromObject=function(e){return new t.Ellipse(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend;if(t.Rect){console.warn("fabric.Rect is already defined");return}var r=t.Object.prototype.stateProperties.concat();r.push("rx","ry","x","y"),t.Rect=t.util.createClass(t.Object,{stateProperties:r,type:"rect",rx:0,ry:0,strokeDashArray:null,initialize:function(e){e=e||{},this.callSuper("initialize",e),this._initRxRy()},_initRxRy:function(){this.rx&&!this.ry?this.ry=this.rx:this.ry&&!this.rx&&(this.rx=this.ry)},_render:function(e,t){if(this.width===1&&this.height===1){e.fillRect(0,0,1,1);return}var n=this.rx?Math.min(this.rx,this.width/2):0,r=this.ry?Math.min(this.ry,this.height/2):0,i=this.width,s=this.height,o=t?this.left:-this.width/2,u=t?this.top:-this.height/2,a=n!==0||r!==0,f=.4477152502;e.beginPath(),e.moveTo(o+n,u),e.lineTo(o+i-n,u),a&&e.bezierCurveTo(o+i-f*n,u,o+i,u+f*r,o+i,u+r),e.lineTo(o+i,u+s-r),a&&e.bezierCurveTo(o+i,u+s-f*r,o+i-f*n,u+s,o+i-n,u+s),e.lineTo(o+n,u+s),a&&e.bezierCurveTo(o+f*n,u+s,o,u+s-f*r,o,u+s-r),e.lineTo(o,u+r),a&&e.bezierCurveTo(o,u+f*r,o+f*n,u,o+n,u),e.closePath(),this._renderFill(e),this._renderStroke(e)},_renderDashedStroke:function(e){var n=-this.width/2,r=-this.height/2,i=this.width,s=this.height;e.beginPath(),t.util.drawDashedLine(e,n,r,n+i,r,this.strokeDashArray),t.util.drawDashedLine(e,n+i,r,n+i,r+s,this.strokeDashArray),t.util.drawDashedLine(e,n+i,r+s,n,r+s,this.strokeDashArray),t.util.drawDashedLine(e,n,r+s,n,r,this.strokeDashArray),e.closePath()},toObject:function(e){var t=n(this.callSuper("toObject",e),{rx:this.get("rx")||0,ry:this.get("ry")||0});return this.includeDefaultValues||this._removeDefaultValues(t),t},toSVG:function(e){var t=this._createBaseSVGMarkup(),n=this.left,r=this.top;if(!this.group||this.group.type!=="path-group")n=-this.width/2,r=-this.height/2;return t.push("<rect ",'x="',n,'" y="',r,'" rx="',this.get("rx"),'" ry="',this.get("ry"),'" width="',this.width,'" height="',this.height,'" style="',this.getSvgStyles(),'" transform="',this.getSvgTransform(),this.getSvgTransformMatrix(),'"/>\n'),e?e(t.join("")):t.join("")},complexity:function(){return 1}}),t.Rect.ATTRIBUTE_NAMES=t.SHARED_ATTRIBUTES.concat("x y rx ry width height".split(" ")),t.Rect.fromElement=function(e,r){if(!e)return null;r=r||{};var i=t.parseAttributes(e,t.Rect.ATTRIBUTE_NAMES);return i.left=i.left||0,i.top=i.top||0,new t.Rect(n(r?t.util.object.clone(r):{},i))},t.Rect.fromObject=function(e){return new t.Rect(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={});if(t.Polyline){t.warn("fabric.Polyline is already defined");return}t.Polyline=t.util.createClass(t.Object,{type:"polyline",points:null,minX:0,minY:0,initialize:function(e,n){return t.Polygon.prototype.initialize.call(this,e,n)},_calcDimensions:function(){return t.Polygon.prototype._calcDimensions.call(this)},_applyPointOffset:function(){return t.Polygon.prototype._applyPointOffset.call(this)},toObject:function(e){return t.Polygon.prototype.toObject.call(this,e)},toSVG:function(e){return t.Polygon.prototype.toSVG.call(this,e)},_render:function(e){t.Polygon.prototype.commonRender.call(this,e),this._renderFill(e),this._renderStroke(e)},_renderDashedStroke:function(e){var n,r;e.beginPath();for(var i=0,s=this.points.length;i<s;i++)n=this.points[i],r=this.points[i+1]||n,t.util.drawDashedLine(e,n.x,n.y,r.x,r.y,this.strokeDashArray)},complexity:function(){return this.get("points").length}}),t.Polyline.ATTRIBUTE_NAMES=t.SHARED_ATTRIBUTES.concat(),t.Polyline.fromElement=function(e,n){if(!e)return null;n||(n={});var r=t.parsePointsAttribute(e.getAttribute("points")),i=t.parseAttributes(e,t.Polyline.ATTRIBUTE_NAMES);return r===null?null:new t.Polyline(r,t.util.object.extend(i,n))},t.Polyline.fromObject=function(e){var n=e.points;return new t.Polyline(n,e,!0)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend,r=t.util.array.min,i=t.util.array.max,s=t.util.toFixed;if(t.Polygon){t.warn("fabric.Polygon is already defined");return}t.Polygon=t.util.createClass(t.Object,{type:"polygon",points:null,minX:0,minY:0,initialize:function(e,t){t=t||{},this.points=e,this.callSuper("initialize",t),this._calcDimensions(),"top"in t||(this.top=this.minY),"left"in t||(this.left=this.minX)},_calcDimensions:function(){var e=this.points,t=r(e,"x"),n=r(e,"y"),s=i(e,"x"),o=i(e,"y");this.width=s-t||1,this.height=o-n||1,this.minX=t,this.minY=n},_applyPointOffset:function(){this.points.forEach(function(e){e.x-=this.minX+this.width/2,e.y-=this.minY+this.height/2},this)},toObject:function(e){return n(this.callSuper("toObject",e),{points:this.points.concat()})},toSVG:function(e){var t=[],n=this._createBaseSVGMarkup();for(var r=0,i=this.points.length;r<i;r++)t.push(s(this.points[r].x,2),",",s(this.points[r].y,2)," ");return n.push("<",this.type," ",'points="',t.join(""),'" style="',this.getSvgStyles(),'" transform="',this.getSvgTransform()," ",this.getSvgTransformMatrix(),'"/>\n'),e?e(n.join("")):n.join("")},_render:function(e){this.commonRender(e),this._renderFill(e);if(this.stroke||this.strokeDashArray)e.closePath(),this._renderStroke(e)},commonRender:function(e){var t;e.beginPath(),this._applyPointOffset&&((!this.group||this.group.type!=="path-group")&&this._applyPointOffset(),this._applyPointOffset=null),e.moveTo(this.points[0].x,this.points[0].y);for(var n=0,r=this.points.length;n<r;n++)t=this.points[n],e.lineTo(t.x,t.y)},_renderDashedStroke:function(e){t.Polyline.prototype._renderDashedStroke.call(this,e),e.closePath()},complexity:function(){return this.points.length}}),t.Polygon.ATTRIBUTE_NAMES=t.SHARED_ATTRIBUTES.concat(),t.Polygon.fromElement=function(e,r){if(!e)return null;r||(r={});var i=t.parsePointsAttribute(e.getAttribute("points")),s=t.parseAttributes(e,t.Polygon.ATTRIBUTE_NAMES);return i===null?null:new t.Polygon(i,n(s,r))},t.Polygon.fromObject=function(e){return new t.Polygon(e.points,e,!0)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.array.min,r=t.util.array.max,i=t.util.object.extend,s=Object.prototype.toString,o=t.util.drawArc,u={m:2,l:2,h:1,v:1,c:6,s:4,q:4,t:2,a:7},a={m:"l",M:"L"};if(t.Path){t.warn("fabric.Path is already defined");return}t.Path=t.util.createClass(t.Object,{type:"path",path:null,minX:0,minY:0,initialize:function(e,t){t=t||{},this.setOptions(t);if(!e)throw new Error("`path` argument is required");var n=s.call(e)==="[object Array]";this.path=n?e:e.match&&e.match(/[mzlhvcsqta][^mzlhvcsqta]*/gi);if(!this.path)return;n||(this.path=this._parsePath());var r=this._parseDimensions();this.minX=r.left,this.minY=r.top,this.width=r.width,this.height=r.height,r.left+=this.originX==="center"?this.width/2:this.originX==="right"?this.width:0,r.top+=this.originY==="center"?this.height/2:this.originY==="bottom"?this.height:0,this.top=this.top||r.top,this.left=this.left||r.left,this.pathOffset=this.pathOffset||{x:this.minX+this.width/2,y:this.minY+this.height/2},t.sourcePath&&this.setSourcePath(t.sourcePath)},_render:function(e){var t,n=null,r=0,i=0,s=0,u=0,a=0,f=0,l,c,h,p,d=-this.pathOffset.x,v=-this.pathOffset.y;this.group&&this.group.type==="path-group"&&(d=0,v=0),e.beginPath();for(var m=0,g=this.path.length;m<g;++m){t=this.path[m];switch(t[0]){case"l":s+=t[1],u+=t[2],e.lineTo(s+d,u+v);break;case"L":s=t[1],u=t[2],e.lineTo(s+d,u+v);break;case"h":s+=t[1],e.lineTo(s+d,u+v);break;case"H":s=t[1],e.lineTo(s+d,u+v);break;case"v":u+=t[1],e.lineTo(s+d,u+v);break;case"V":u=t[1],e.lineTo(s+d,u+v);break;case"m":s+=t[1],u+=t[2],r=s,i=u,e.moveTo(s+d,u+v);break;case"M":s=t[1],u=t[2],r=s,i=u,e.moveTo(s+d,u+v);break;case"c":l=s+t[5],c=u+t[6],a=s+t[3],f=u+t[4],e.bezierCurveTo(s+t[1]+d,u+t[2]+v,a+d,f+v,l+d,c+v),s=l,u=c;break;case"C":s=t[5],u=t[6],a=t[3],f=t[4],e.bezierCurveTo(t[1]+d,t[2]+v,a+d,f+v,s+d,u+v);break;case"s":l=s+t[3],c=u+t[4],a=a?2*s-a:s,f=f?2*u-f:u,e.bezierCurveTo(a+d,f+v,s+t[1]+d,u+t[2]+v,l+d,c+v),a=s+t[1],f=u+t[2],s=l,u=c;break;case"S":l=t[3],c=t[4],a=2*s-a,f=2*u-f,e.bezierCurveTo(a+d,f+v,t[1]+d,t[2]+v,l+d,c+v),s=l,u=c,a=t[1],f=t[2];break;case"q":l=s+t[3],c=u+t[4],a=s+t[1],f=u+t[2],e.quadraticCurveTo(a+d,f+v,l+d,c+v),s=l,u=c;break;case"Q":l=t[3],c=t[4],e.quadraticCurveTo(t[1]+d,t[2]+v,l+d,c+v),s=l,u=c,a=t[1],f=t[2];break;case"t":l=s+t[1],c=u+t[2],n[0].match(/[QqTt]/)===null?(a=s,f=u):n[0]==="t"?(a=2*s-h,f=2*u-p):n[0]==="q"&&(a=2*s-a,f=2*u-f),h=a,p=f,e.quadraticCurveTo(a+d,f+v,l+d,c+v),s=l,u=c,a=s+t[1],f=u+t[2];break;case"T":l=t[1],c=t[2],a=2*s-a,f=2*u-f,e.quadraticCurveTo(a+d,f+v,l+d,c+v),s=l,u=c;break;case"a":o(e,s+d,u+v,[t[1],t[2],t[3],t[4],t[5],t[6]+s+d,t[7]+u+v]),s+=t[6],u+=t[7];break;case"A":o(e,s+d,u+v,[t[1],t[2],t[3],t[4],t[5],t[6]+d,t[7]+v]),s=t[6],u=t[7];break;case"z":case"Z":s=r,u=i,e.closePath()}n=t}this._renderFill(e),this._renderStroke(e)},render:function(e,n){if(!this.visible)return;e.save(),this._setupCompositeOperation(e),n||this.transform(e),this._setStrokeStyles(e),this._setFillStyles(e),this.group&&this.group.type==="path-group"&&e.translate(-this.group.width/2,-this.group.height/2),this.transformMatrix&&e.transform.apply(e,this.transformMatrix),this._setOpacity(e),this._setShadow(e),this.clipTo&&t.util.clipContext(this,e),this._render(e,n),this.clipTo&&e.restore(),this._removeShadow(e),this._restoreCompositeOperation(e),e.restore()},toString:function(){return"#<fabric.Path ("+this.complexity()+'): { "top": '+this.top+', "left": '+this.left+" }>"},toObject:function(e){var t=i(this.callSuper("toObject",e),{path:this.path.map(function(e){return e.slice()}),pathOffset:this.pathOffset});return this.sourcePath&&(t.sourcePath=this.sourcePath),this.transformMatrix&&(t.transformMatrix=this.transformMatrix),t},toDatalessObject:function(e){var t=this.toObject(e);return this.sourcePath&&(t.path=this.sourcePath),delete t.sourcePath,t},toSVG:function(e){var t=[],n=this._createBaseSVGMarkup(),r="";for(var i=0,s=this.path.length;i<s;i++)t.push(this.path[i].join(" "));var o=t.join(" ");if(!this.group||this.group.type!=="path-group")r="translate("+ -this.pathOffset.x+", "+ -this.pathOffset.y+")";return n.push("<path ",'d="',o,'" style="',this.getSvgStyles(),'" transform="',this.getSvgTransform(),r,this.getSvgTransformMatrix(),'" stroke-linecap="round" ',"/>\n"),e?e(n.join("")):n.join("")},complexity:function(){return this.path.length},_parsePath:function(){var e=[],t=[],n,r,i=/([-+]?((\d+\.\d+)|((\d+)|(\.\d+)))(?:e[-+]?\d+)?)/ig,s,o;for(var f=0,l,c=this.path.length;f<c;f++){n=this.path[f],o=n.slice(1).trim(),t.length=0;while(s=i.exec(o))t.push(s[0]);l=[n.charAt(0)];for(var h=0,p=t.length;h<p;h++)r=parseFloat(t[h]),isNaN(r)||l.push(r);var d=l[0],v=u[d.toLowerCase()],m=a[d]||d;if(l.length-1>v)for(var g=1,y=l.length;g<y;g+=v)e.push([d].concat(l.slice(g,g+v))),d=m;else e.push(l)}return e},_parseDimensions:function(){var e=[],i=[],s,o=null,u=0,a=0,f=0,l=0,c=0,h=0,p,d,v,m,g;for(var y=0,b=this.path.length;y<b;++y){s=this.path[y];switch(s[0]){case"l":f+=s[1],l+=s[2],g=[];break;case"L":f=s[1],l=s[2],g=[];break;case"h":f+=s[1],g=[];break;case"H":f=s[1],g=[];break;case"v":l+=s[1],g=[];break;case"V":l=s[1],g=[];break;case"m":f+=s[1],l+=s[2],u=f,a=l,g=[];break;case"M":f=s[1],l=s[2],u=f,a=l,g=[];break;case"c":p=f+s[5],d=l+s[6],c=f+s[3],h=l+s[4],g=t.util.getBoundsOfCurve(f,l,f+s[1],l+s[2],c,h,p,d),f=p,l=d;break;case"C":f=s[5],l=s[6],c=s[3],h=s[4],g=t.util.getBoundsOfCurve(f,l,s[1],s[2],c,h,f,l);break;case"s":p=f+s[3],d=l+s[4],c=c?2*f-c:f,h=h?2*l-h:l,g=t.util.getBoundsOfCurve(f,l,c,h,f+s[1],l+s[2],p,d),c=f+s[1],h=l+s[2],f=p,l=d;break;case"S":p=s[3],d=s[4],c=2*f-c,h=2*l-h,g=t.util.getBoundsOfCurve(f,l,c,h,s[1],s[2],p,d),f=p,l=d,c=s[1],h=s[2];break;case"q":p=f+s[3],d=l+s[4],c=f+s[1],h=l+s[2],g=t.util.getBoundsOfCurve(f,l,c,h,c,h,p,d),f=p,l=d;break;case"Q":c=s[1],h=s[2],g=t.util.getBoundsOfCurve(f,l,c,h,c,h,s[3],s[4]),f=s[3],l=s[4];break;case"t":p=f+s[1],d=l+s[2],o[0].match(/[QqTt]/)===null?(c=f,h=l):o[0]==="t"?(c=2*f-v,h=2*l-m):o[0]==="q"&&(c=2*f-c,h=2*l-h),v=c,m=h,g=t.util.getBoundsOfCurve(f,l,c,h,c,h,p,d),f=p,l=d,c=f+s[1],h=l+s[2];break;case"T":p=s[1],d=s[2],c=2*f-c,h=2*l-h,g=t.util.getBoundsOfCurve(f,l,c,h,c,h,p,d),f=p,l=d;break;case"a":g=t.util.getBoundsOfArc(f,l,s[1],s[2],s[3],s[4],s[5],s[6]+f,s[7]+l),f+=s[6],l+=s[7];break;case"A":g=t.util.getBoundsOfArc(f,l,s[1],s[2],s[3],s[4],s[5],s[6],s[7]),f=s[6],l=s[7];break;case"z":case"Z":f=u,l=a}o=s,g.forEach(function(t){e.push(t.x),i.push(t.y)}),e.push(f),i.push(l)}var w=n(e),E=n(i),S=r(e),x=r(i),T=S-w,N=x-E,C={left:w,top:E,width:T,height:N};return C}}),t.Path.fromObject=function(e,n){typeof e.path=="string"?t.loadSVGFromURL(e.path,function(r){var i=r[0],s=e.path;delete e.path,t.util.object.extend(i,e),i.setSourcePath(s),n(i)}):n(new t.Path(e.path,e))},t.Path.ATTRIBUTE_NAMES=t.SHARED_ATTRIBUTES.concat(["d"]),t.Path.fromElement=function(e,n,r){var s=t.parseAttributes(e,t.Path.ATTRIBUTE_NAMES);n&&n(new t.Path(s.d,i(s,r)))},t.Path.async=!0}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend,r=t.util.array.invoke,i=t.Object.prototype.toObject;if(t.PathGroup){t.warn("fabric.PathGroup is already defined");return}t.PathGroup=t.util.createClass(t.Path,{type:"path-group",fill:"",initialize:function(e,t){t=t||{},this.paths=e||[];for(var n=this.paths.length;n--;)this.paths[n].group=this;this.setOptions(t),t.widthAttr&&(this.scaleX=t.widthAttr/t.width),t.heightAttr&&(this.scaleY=t.heightAttr/t.height),this.setCoords(),t.sourcePath&&this.setSourcePath(t.sourcePath)},render:function(e){if(!this.visible)return;e.save();var n=this.transformMatrix;n&&e.transform(n[0],n[1],n[2],n[3],n[4],n[5]),this.transform(e),this._setShadow(e),this.clipTo&&t.util.clipContext(this,e);for(var r=0,i=this.paths.length;r<i;++r)this.paths[r].render(e,!0);this.clipTo&&e.restore(),this._removeShadow(e),e.restore()},_set:function(e,t){if(e==="fill"&&t&&this.isSameColor()){var n=this.paths.length;while(n--)this.paths[n]._set(e,t)}return this.callSuper("_set",e,t)},toObject:function(e){var t=n(i.call(this,e),{paths:r(this.getObjects(),"toObject",e)});return this.sourcePath&&(t.sourcePath=this.sourcePath),t},toDatalessObject:function(e){var t=this.toObject(e);return this.sourcePath&&(t.paths=this.sourcePath),t},toSVG:function(e){var t=this.getObjects(),n="translate("+this.left+" "+this.top+")",r=["<g ",'style="',this.getSvgStyles(),'" ','transform="',n,this.getSvgTransform(),'" ',">\n"];for(var i=0,s=t.length;i<s;i++)r.push(t[i].toSVG(e));return r.push("</g>\n"),e?e(r.join("")):r.join("")},toString:function(){return"#<fabric.PathGroup ("+this.complexity()+"): { top: "+this.top+", left: "+this.left+" }>"},isSameColor:function(){var e=(this.getObjects()[0].get("fill")||"").toLowerCase();return this.getObjects().every(function(t){return(t.get("fill")||"").toLowerCase()===e})},complexity:function(){return this.paths.reduce(function(e,t){return e+(t&&t.complexity?t.complexity():0)},0)},getObjects:function(){return this.paths}}),t.PathGroup.fromObject=function(e,n){typeof e.paths=="string"?t.loadSVGFromURL(e.paths,function(r){var i=e.paths;delete e.paths;var s=t.util.groupSVGElements(r,e,i);n(s)}):t.util.enlivenObjects(e.paths,function(r){delete e.paths,n(new t.PathGroup(r,e))})},t.PathGroup.async=!0}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend,r=t.util.array.min,i=t.util.array.max,s=t.util.array.invoke;if(t.Group)return;var o={lockMovementX:!0,lockMovementY:!0,lockRotation:!0,lockScalingX:!0,lockScalingY:!0,lockUniScaling:!0};t.Group=t.util.createClass(t.Object,t.Collection,{type:"group",initialize:function(e,t){t=t||{},this._objects=e||[];for(var n=this._objects.length;n--;)this._objects[n].group=this;this.originalState={},this.callSuper("initialize"),t.originX&&(this.originX=t.originX),t.originY&&(this.originY=t.originY),this._calcBounds(),this._updateObjectsCoords(),this.callSuper("initialize",t),this.setCoords(),this.saveCoords()},_updateObjectsCoords:function(){this.forEachObject(this._updateObjectCoords,this)},_updateObjectCoords:function(e){var t=e.getLeft(),n=e.getTop(),r=this.getCenterPoint();e.set({originalLeft:t,originalTop:n,left:t-r.x,top:n-r.y}),e.setCoords(),e.__origHasControls=e.hasControls,e.hasControls=!1},toString:function(){return"#<fabric.Group: ("+this.complexity()+")>"},addWithUpdate:function(e){return this._restoreObjectsState(),e&&(this._objects.push(e),e.group=this),this.forEachObject(this._setObjectActive,this),this._calcBounds(),this._updateObjectsCoords(),this},_setObjectActive:function(e){e.set("active",!0),e.group=this},removeWithUpdate:function(e){return this._moveFlippedObject(e),this._restoreObjectsState(),this.forEachObject(this._setObjectActive,this),this.remove(e),this._calcBounds(),this._updateObjectsCoords(),this},_onObjectAdded:function(e){e.group=this},_onObjectRemoved:function(e){delete e.group,e.set("active",!1)},delegatedProperties:{fill:!0,opacity:!0,fontFamily:!0,fontWeight:!0,fontSize:!0,fontStyle:!0,lineHeight:!0,textDecoration:!0,textAlign:!0,backgroundColor:!0},_set:function(e,t){if(e in this.delegatedProperties){var n=this._objects.length;while(n--)this._objects[n].set(e,t)}this.callSuper("_set",e,t)},toObject:function(e){return n(this.callSuper("toObject",e),{objects:s(this._objects,"toObject",e)})},render:function(e){if(!this.visible)return;e.save(),this.clipTo&&t.util.clipContext(this,e);for(var n=0,r=this._objects.length;n<r;n++)this._renderObject(this._objects[n],e);this.clipTo&&e.restore(),e.restore()},_renderControls:function(e,t){this.callSuper("_renderControls",e,t);for(var n=0,r=this._objects.length;n<r;n++)this._objects[n]._renderControls(e)},_renderObject:function(e,t){var n=e.hasRotatingPoint;if(!e.visible)return;e.hasRotatingPoint=!1,e.render(t),e.hasRotatingPoint=n},_restoreObjectsState:function(){return this._objects.forEach(this._restoreObjectState,this),this},_moveFlippedObject:function(e){var t=e.get("originX"),n=e.get("originY"),r=e.getCenterPoint();e.set({originX:"center",originY:"center",left:r.x,top:r.y}),this._toggleFlipping(e);var i=e.getPointByOrigin(t,n);return e.set({originX:t,originY:n,left:i.x,top:i.y}),this},_toggleFlipping:function(e){this.flipX&&(e.toggle("flipX"),e.set("left",-e.get("left")),e.setAngle(-e.getAngle())),this.flipY&&(e.toggle("flipY"),e.set("top",-e.get("top")),e.setAngle(-e.getAngle()))},_restoreObjectState:function(e){return this._setObjectPosition(e),e.setCoords(),e.hasControls=e.__origHasControls,delete e.__origHasControls,e.set("active",!1),e.setCoords(),delete e.group,this},_setObjectPosition:function(e){var t=this.getCenterPoint(),n=this._getRotatedLeftTop(e);e.set({angle:e.getAngle()+this.getAngle(),left:t.x+n.left,top:t.y+n.top,scaleX:e.get("scaleX")*this.get("scaleX"),scaleY:e.get("scaleY")*this.get("scaleY")})},_getRotatedLeftTop:function(e){var t=this.getAngle()*(Math.PI/180);return{left:-Math.sin(t)*e.getTop()*this.get("scaleY")+Math.cos(t)*e.getLeft()*this.get("scaleX"),top:Math.cos(t)*e.getTop()*this.get("scaleY")+Math.sin(t)*e.getLeft()*this.get("scaleX")}},destroy:function(){return this._objects.forEach(this._moveFlippedObject,this),this._restoreObjectsState()},saveCoords:function(){return this._originalLeft=this.get("left"),this._originalTop=this.get("top"),this},hasMoved:function(){return this._originalLeft!==this.get("left")||this._originalTop!==this.get("top")},setObjectsCoords:function(){return this.forEachObject(function(e){e.setCoords()}),this},_calcBounds:function(e){var t=[],n=[],r;for(var i=0,s=this._objects.length;i<s;++i){r=this._objects[i],r.setCoords();for(var o in r.oCoords)t.push(r.oCoords[o].x),n.push(r.oCoords[o].y)}this.set(this._getBounds(t,n,e))},_getBounds:function(e,n,s){var o=t.util.invertTransform(this.getViewportTransform()),u=t.util.transformPoint(new t.Point(r(e),r(n)),o),a=t.util.transformPoint(new t.Point(i(e),i(n)),o),f={width:a.x-u.x||0,height:a.y-u.y||0};return s||(f.left=u.x||0,f.top=u.y||0,this.originX==="center"&&(f.left+=f.width/2),this.originX==="right"&&(f.left+=f.width),this.originY==="center"&&(f.top+=f.height/2),this.originY==="bottom"&&(f.top+=f.height)),f},toSVG:function(e){var t=["<g ",'transform="',this.getSvgTransform(),'">\n'];for(var n=0,r=this._objects.length;n<r;n++)t.push(this._objects[n].toSVG(e));return t.push("</g>\n"),e?e(t.join("")):t.join("")},get:function(e){if(e in o){if(this[e])return this[e];for(var t=0,n=this._objects.length;t<n;t++)if(this._objects[t][e])return!0;return!1}return e in this.delegatedProperties?this._objects[0]&&this._objects[0].get(e):this[e]}}),t.Group.fromObject=function(e,n){t.util.enlivenObjects(e.objects,function(r){delete e.objects,n&&n(new t.Group(r,e))})},t.Group.async=!0}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=fabric.util.object.extend;e.fabric||(e.fabric={});if(e.fabric.Image){fabric.warn("fabric.Image is already defined.");return}fabric.Image=fabric.util.createClass(fabric.Object,{type:"image",crossOrigin:"",alignX:"none",alignY:"none",meetOrSlice:"meet",_lastScaleX:1,_lastScaleY:1,initialize:function(e,t){t||(t={}),this.filters=[],this.resizeFilters=[],this.callSuper("initialize",t),this._initElement(e,t),this._initConfig(t),t.filters&&(this.filters=t.filters,this.applyFilters())},getElement:function(){return this._element},setElement:function(e,t,n){return this._element=e,this._originalElement=e,this._initConfig(n),this.filters.length!==0?this.applyFilters(t):t&&t(),this},setCrossOrigin:function(e){return this.crossOrigin=e,this._element.crossOrigin=e,this},getOriginalSize:function(){var e=this.getElement();return{width:e.width,height:e.height}},_stroke:function(e){e.save(),this._setStrokeStyles(e),e.beginPath(),e.strokeRect(-this.width/2,-this.height/2,this.width,this.height),e.closePath(),e.restore()},_renderDashedStroke:function(e){var t=-this.width/2,n=-this.height/2,r=this.width,i=this.height;e.save(),this._setStrokeStyles(e),e.beginPath(),fabric.util.drawDashedLine(e,t,n,t+r,n,this.strokeDashArray),fabric.util.drawDashedLine(e,t+r,n,t+r,n+i,this.strokeDashArray),fabric.util.drawDashedLine(e,t+r,n+i,t,n+i,this.strokeDashArray),fabric.util.drawDashedLine(e,t,n+i,t,n,this.strokeDashArray),e.closePath(),e.restore()},toObject:function(e){return t(this.callSuper("toObject",e),{src:this._originalElement.src||this._originalElement._src,filters:this.filters.map(function(e){return e&&e.toObject()}),crossOrigin:this.crossOrigin,alignX:this.alignX,alignY:this.alignY,meetOrSlice:this.meetOrSlice})},toSVG:function(e){var t=[],n=-this.width/2,r=-this.height/2,i="none";this.group&&this.group.type==="path-group"&&(n=this.left,r=this.top),this.alignX!=="none"&&this.alignY!=="none"&&(i="x"+this.alignX+"Y"+this.alignY+" "+this.meetOrSlice),t.push('<g transform="',this.getSvgTransform(),this.getSvgTransformMatrix(),'">\n','<image xlink:href="',this.getSvgSrc(),'" x="',n,'" y="',r,'" style="',this.getSvgStyles(),'" width="',this.width,'" height="',this.height,'" preserveAspectRatio="',i,'"',"></image>\n");if(this.stroke||this.strokeDashArray){var s=this.fill;this.fill=null,t.push("<rect ",'x="',n,'" y="',r,'" width="',this.width,'" height="',this.height,'" style="',this.getSvgStyles(),'"/>\n'),this.fill=s}return t.push("</g>\n"),e?e(t.join("")):t.join("")},getSrc:function(){if(this.getElement())return this.getElement().src||this.getElement()._src},setSrc:function(e,t,n){fabric.util.loadImage(e,function(e){return this.setElement(e,t,n)},this,n&&n.crossOrigin)},toString:function(){return'#<fabric.Image: { src: "'+this.getSrc()+'" }>'},clone:function(e,t){this.constructor.fromObject(this.toObject(t),e)},applyFilters:function(e,t,n,r){t=t||this.filters,n=n||this._originalElement;if(!n)return;var i=n,s=fabric.util.createCanvasElement(),o=fabric.util.createImage(),u=this;return s.width=i.width,s.height=i.height,s.getContext("2d").drawImage(i,0,0,i.width,i.height),t.length===0?(this._element=n,e&&e(),s):(t.forEach(function(e){e&&e.applyTo(s,e.scaleX||u.scaleX,e.scaleY||u.scaleY),!r&&e.type==="Resize"&&(u.width*=e.scaleX,u.height*=e.scaleY)}),o.width=s.width,o.height=s.height,fabric.isLikelyNode?(o.src=s.toBuffer(undefined,fabric.Image.pngCompression),u._element=o,!r&&(u._filteredEl=o),e&&e()):(o.onload=function(){u._element=o,!r&&(u._filteredEl=o),e&&e(),o.onload=s=i=null},o.src=s.toDataURL("image/png")),s)},_render:function(e,t){var n,r,i=this._findMargins(),s;n=t?this.left:-this.width/2,r=t?this.top:-this.height/2,this.meetOrSlice==="slice"&&(e.beginPath(),e.rect(n,r,this.width,this.height),e.clip()),this.isMoving===!1&&this.resizeFilters.length&&this._needsResize()?(this._lastScaleX=this.scaleX,this._lastScaleY=this.scaleY,s=this.applyFilters(null,this.resizeFilters,this._filteredEl||this._originalElement,!0)):s=this._element,s&&e.drawImage(s,n+i.marginX,r+i.marginY,i.width,i.height),this._renderStroke(e)},_needsResize:function(){return this.scaleX!==this._lastScaleX||this.scaleY!==this._lastScaleY},_findMargins:function(){var e=this.width,t=this.height,n,r,i=0,s=0;if(this.alignX!=="none"||this.alignY!=="none")n=[this.width/this._element.width,this.height/this._element.height],r=this.meetOrSlice==="meet"?Math.min.apply(null,n):Math.max.apply(null,n),e=this._element.width*r,t=this._element.height*r,this.alignX==="Mid"&&(i=(this.width-e)/2),this.alignX==="Max"&&(i=this.width-e),this.alignY==="Mid"&&(s=(this.height-t)/2),this.alignY==="Max"&&(s=this.height-t);return{width:e,height:t,marginX:i,marginY:s}},_resetWidthHeight:function(){var e=this.getElement();this.set("width",e.width),this.set("height",e.height)},_initElement:function(e){this.setElement(fabric.util.getById(e)),fabric.util.addClass(this.getElement(),fabric.Image.CSS_CANVAS)},_initConfig:function(e){e||(e={}),this.setOptions(e),this._setWidthHeight(e),this._element&&this.crossOrigin&&(this._element.crossOrigin=this.crossOrigin)},_initFilters:function(e,t){e.filters&&e.filters.length?fabric.util.enlivenObjects(e.filters,function(e){t&&t(e)},"fabric.Image.filters"
+):t&&t()},_setWidthHeight:function(e){this.width="width"in e?e.width:this.getElement()?this.getElement().width||0:0,this.height="height"in e?e.height:this.getElement()?this.getElement().height||0:0},complexity:function(){return 1}}),fabric.Image.CSS_CANVAS="canvas-img",fabric.Image.prototype.getSvgSrc=fabric.Image.prototype.getSrc,fabric.Image.fromObject=function(e,t){fabric.util.loadImage(e.src,function(n){fabric.Image.prototype._initFilters.call(e,e,function(r){e.filters=r||[];var i=new fabric.Image(n,e);t&&t(i)})},null,e.crossOrigin)},fabric.Image.fromURL=function(e,t,n){fabric.util.loadImage(e,function(e){t(new fabric.Image(e,n))},null,n&&n.crossOrigin)},fabric.Image.ATTRIBUTE_NAMES=fabric.SHARED_ATTRIBUTES.concat("x y width height preserveAspectRatio xlink:href".split(" ")),fabric.Image.fromElement=function(e,n,r){var i=fabric.parseAttributes(e,fabric.Image.ATTRIBUTE_NAMES),s="xMidYMid",o="meet",u,a,f;i.preserveAspectRatio&&(f=i.preserveAspectRatio.split(" ")),f&&f.length&&(o=f.pop(),o!=="meet"&&o!=="slice"?(s=o,o="meet"):f.length&&(s=f.pop())),u=s!=="none"?s.slice(1,4):"none",a=s!=="none"?s.slice(5,8):"none",i.alignX=u,i.alignY=a,i.meetOrSlice=o,fabric.Image.fromURL(i["xlink:href"],n,t(r?fabric.util.object.clone(r):{},i))},fabric.Image.async=!0,fabric.Image.pngCompression=1}(typeof exports!="undefined"?exports:this),fabric.util.object.extend(fabric.Object.prototype,{_getAngleValueForStraighten:function(){var e=this.getAngle()%360;return e>0?Math.round((e-1)/90)*90:Math.round(e/90)*90},straighten:function(){return this.setAngle(this._getAngleValueForStraighten()),this},fxStraighten:function(e){e=e||{};var t=function(){},n=e.onComplete||t,r=e.onChange||t,i=this;return fabric.util.animate({startValue:this.get("angle"),endValue:this._getAngleValueForStraighten(),duration:this.FX_DURATION,onChange:function(e){i.setAngle(e),r()},onComplete:function(){i.setCoords(),n()},onStart:function(){i.set("active",!1)}}),this}}),fabric.util.object.extend(fabric.StaticCanvas.prototype,{straightenObject:function(e){return e.straighten(),this.renderAll(),this},fxStraightenObject:function(e){return e.fxStraighten({onChange:this.renderAll.bind(this)}),this}}),fabric.Image.filters=fabric.Image.filters||{},fabric.Image.filters.BaseFilter=fabric.util.createClass({type:"BaseFilter",initialize:function(e){e&&this.setOptions(e)},setOptions:function(e){for(var t in e)this[t]=e[t]},toObject:function(){return{type:this.type}},toJSON:function(){return this.toObject()}}),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend;t.Image.filters.Brightness=t.util.createClass(t.Image.filters.BaseFilter,{type:"Brightness",initialize:function(e){e=e||{},this.brightness=e.brightness||0},applyTo:function(e){var t=e.getContext("2d"),n=t.getImageData(0,0,e.width,e.height),r=n.data,i=this.brightness;for(var s=0,o=r.length;s<o;s+=4)r[s]+=i,r[s+1]+=i,r[s+2]+=i;t.putImageData(n,0,0)},toObject:function(){return n(this.callSuper("toObject"),{brightness:this.brightness})}}),t.Image.filters.Brightness.fromObject=function(e){return new t.Image.filters.Brightness(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend;t.Image.filters.Convolute=t.util.createClass(t.Image.filters.BaseFilter,{type:"Convolute",initialize:function(e){e=e||{},this.opaque=e.opaque,this.matrix=e.matrix||[0,0,0,0,1,0,0,0,0];var n=t.util.createCanvasElement();this.tmpCtx=n.getContext("2d")},_createImageData:function(e,t){return this.tmpCtx.createImageData(e,t)},applyTo:function(e){var t=this.matrix,n=e.getContext("2d"),r=n.getImageData(0,0,e.width,e.height),i=Math.round(Math.sqrt(t.length)),s=Math.floor(i/2),o=r.data,u=r.width,a=r.height,f=u,l=a,c=this._createImageData(f,l),h=c.data,p=this.opaque?1:0;for(var d=0;d<l;d++)for(var v=0;v<f;v++){var m=d,g=v,y=(d*f+v)*4,b=0,w=0,E=0,S=0;for(var x=0;x<i;x++)for(var T=0;T<i;T++){var N=m+x-s,C=g+T-s;if(N<0||N>a||C<0||C>u)continue;var k=(N*u+C)*4,L=t[x*i+T];b+=o[k]*L,w+=o[k+1]*L,E+=o[k+2]*L,S+=o[k+3]*L}h[y]=b,h[y+1]=w,h[y+2]=E,h[y+3]=S+p*(255-S)}n.putImageData(c,0,0)},toObject:function(){return n(this.callSuper("toObject"),{opaque:this.opaque,matrix:this.matrix})}}),t.Image.filters.Convolute.fromObject=function(e){return new t.Image.filters.Convolute(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend;t.Image.filters.GradientTransparency=t.util.createClass(t.Image.filters.BaseFilter,{type:"GradientTransparency",initialize:function(e){e=e||{},this.threshold=e.threshold||100},applyTo:function(e){var t=e.getContext("2d"),n=t.getImageData(0,0,e.width,e.height),r=n.data,i=this.threshold,s=r.length;for(var o=0,u=r.length;o<u;o+=4)r[o+3]=i+255*(s-o)/s;t.putImageData(n,0,0)},toObject:function(){return n(this.callSuper("toObject"),{threshold:this.threshold})}}),t.Image.filters.GradientTransparency.fromObject=function(e){return new t.Image.filters.GradientTransparency(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={});t.Image.filters.Grayscale=t.util.createClass(t.Image.filters.BaseFilter,{type:"Grayscale",applyTo:function(e){var t=e.getContext("2d"),n=t.getImageData(0,0,e.width,e.height),r=n.data,i=n.width*n.height*4,s=0,o;while(s<i)o=(r[s]+r[s+1]+r[s+2])/3,r[s]=o,r[s+1]=o,r[s+2]=o,s+=4;t.putImageData(n,0,0)}}),t.Image.filters.Grayscale.fromObject=function(){return new t.Image.filters.Grayscale}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={});t.Image.filters.Invert=t.util.createClass(t.Image.filters.BaseFilter,{type:"Invert",applyTo:function(e){var t=e.getContext("2d"),n=t.getImageData(0,0,e.width,e.height),r=n.data,i=r.length,s;for(s=0;s<i;s+=4)r[s]=255-r[s],r[s+1]=255-r[s+1],r[s+2]=255-r[s+2];t.putImageData(n,0,0)}}),t.Image.filters.Invert.fromObject=function(){return new t.Image.filters.Invert}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend;t.Image.filters.Mask=t.util.createClass(t.Image.filters.BaseFilter,{type:"Mask",initialize:function(e){e=e||{},this.mask=e.mask,this.channel=[0,1,2,3].indexOf(e.channel)>-1?e.channel:0},applyTo:function(e){if(!this.mask)return;var n=e.getContext("2d"),r=n.getImageData(0,0,e.width,e.height),i=r.data,s=this.mask.getElement(),o=t.util.createCanvasElement(),u=this.channel,a,f=r.width*r.height*4;o.width=s.width,o.height=s.height,o.getContext("2d").drawImage(s,0,0,s.width,s.height);var l=o.getContext("2d").getImageData(0,0,s.width,s.height),c=l.data;for(a=0;a<f;a+=4)i[a+3]=c[a+u];n.putImageData(r,0,0)},toObject:function(){return n(this.callSuper("toObject"),{mask:this.mask.toObject(),channel:this.channel})}}),t.Image.filters.Mask.fromObject=function(e,n){t.util.loadImage(e.mask.src,function(r){e.mask=new t.Image(r,e.mask),n&&n(new t.Image.filters.Mask(e))})},t.Image.filters.Mask.async=!0}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend;t.Image.filters.Noise=t.util.createClass(t.Image.filters.BaseFilter,{type:"Noise",initialize:function(e){e=e||{},this.noise=e.noise||0},applyTo:function(e){var t=e.getContext("2d"),n=t.getImageData(0,0,e.width,e.height),r=n.data,i=this.noise,s;for(var o=0,u=r.length;o<u;o+=4)s=(.5-Math.random())*i,r[o]+=s,r[o+1]+=s,r[o+2]+=s;t.putImageData(n,0,0)},toObject:function(){return n(this.callSuper("toObject"),{noise:this.noise})}}),t.Image.filters.Noise.fromObject=function(e){return new t.Image.filters.Noise(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend;t.Image.filters.Pixelate=t.util.createClass(t.Image.filters.BaseFilter,{type:"Pixelate",initialize:function(e){e=e||{},this.blocksize=e.blocksize||4},applyTo:function(e){var t=e.getContext("2d"),n=t.getImageData(0,0,e.width,e.height),r=n.data,i=n.height,s=n.width,o,u,a,f,l,c,h;for(u=0;u<i;u+=this.blocksize)for(a=0;a<s;a+=this.blocksize){o=u*4*s+a*4,f=r[o],l=r[o+1],c=r[o+2],h=r[o+3];for(var p=u,d=u+this.blocksize;p<d;p++)for(var v=a,m=a+this.blocksize;v<m;v++)o=p*4*s+v*4,r[o]=f,r[o+1]=l,r[o+2]=c,r[o+3]=h}t.putImageData(n,0,0)},toObject:function(){return n(this.callSuper("toObject"),{blocksize:this.blocksize})}}),t.Image.filters.Pixelate.fromObject=function(e){return new t.Image.filters.Pixelate(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend;t.Image.filters.RemoveWhite=t.util.createClass(t.Image.filters.BaseFilter,{type:"RemoveWhite",initialize:function(e){e=e||{},this.threshold=e.threshold||30,this.distance=e.distance||20},applyTo:function(e){var t=e.getContext("2d"),n=t.getImageData(0,0,e.width,e.height),r=n.data,i=this.threshold,s=this.distance,o=255-i,u=Math.abs,a,f,l;for(var c=0,h=r.length;c<h;c+=4)a=r[c],f=r[c+1],l=r[c+2],a>o&&f>o&&l>o&&u(a-f)<s&&u(a-l)<s&&u(f-l)<s&&(r[c+3]=1);t.putImageData(n,0,0)},toObject:function(){return n(this.callSuper("toObject"),{threshold:this.threshold,distance:this.distance})}}),t.Image.filters.RemoveWhite.fromObject=function(e){return new t.Image.filters.RemoveWhite(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={});t.Image.filters.Sepia=t.util.createClass(t.Image.filters.BaseFilter,{type:"Sepia",applyTo:function(e){var t=e.getContext("2d"),n=t.getImageData(0,0,e.width,e.height),r=n.data,i=r.length,s,o;for(s=0;s<i;s+=4)o=.3*r[s]+.59*r[s+1]+.11*r[s+2],r[s]=o+100,r[s+1]=o+50,r[s+2]=o+255;t.putImageData(n,0,0)}}),t.Image.filters.Sepia.fromObject=function(){return new t.Image.filters.Sepia}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={});t.Image.filters.Sepia2=t.util.createClass(t.Image.filters.BaseFilter,{type:"Sepia2",applyTo:function(e){var t=e.getContext("2d"),n=t.getImageData(0,0,e.width,e.height),r=n.data,i=r.length,s,o,u,a;for(s=0;s<i;s+=4)o=r[s],u=r[s+1],a=r[s+2],r[s]=(o*.393+u*.769+a*.189)/1.351,r[s+1]=(o*.349+u*.686+a*.168)/1.203,r[s+2]=(o*.272+u*.534+a*.131)/2.14;t.putImageData(n,0,0)}}),t.Image.filters.Sepia2.fromObject=function(){return new t.Image.filters.Sepia2}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend;t.Image.filters.Tint=t.util.createClass(t.Image.filters.BaseFilter,{type:"Tint",initialize:function(e){e=e||{},this.color=e.color||"#000000",this.opacity=typeof e.opacity!="undefined"?e.opacity:(new t.Color(this.color)).getAlpha()},applyTo:function(e){var n=e.getContext("2d"),r=n.getImageData(0,0,e.width,e.height),i=r.data,s=i.length,o,u,a,f,l,c,h,p,d;d=(new t.Color(this.color)).getSource(),u=d[0]*this.opacity,a=d[1]*this.opacity,f=d[2]*this.opacity,p=1-this.opacity;for(o=0;o<s;o+=4)l=i[o],c=i[o+1],h=i[o+2],i[o]=u+l*p,i[o+1]=a+c*p,i[o+2]=f+h*p;n.putImageData(r,0,0)},toObject:function(){return n(this.callSuper("toObject"),{color:this.color,opacity:this.opacity})}}),t.Image.filters.Tint.fromObject=function(e){return new t.Image.filters.Tint(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend;t.Image.filters.Multiply=t.util.createClass(t.Image.filters.BaseFilter,{type:"Multiply",initialize:function(e){e=e||{},this.color=e.color||"#000000"},applyTo:function(e){var n=e.getContext("2d"),r=n.getImageData(0,0,e.width,e.height),i=r.data,s=i.length,o,u;u=(new t.Color(this.color)).getSource();for(o=0;o<s;o+=4)i[o]*=u[0]/255,i[o+1]*=u[1]/255,i[o+2]*=u[2]/255;n.putImageData(r,0,0)},toObject:function(){return n(this.callSuper("toObject"),{color:this.color})}}),t.Image.filters.Multiply.fromObject=function(e){return new t.Image.filters.Multiply(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric;t.Image.filters.Blend=t.util.createClass({type:"Blend",initialize:function(e){e=e||{},this.color=e.color||"#000",this.image=e.image||!1,this.mode=e.mode||"multiply",this.alpha=e.alpha||1},applyTo:function(e){var n=e.getContext("2d"),r=n.getImageData(0,0,e.width,e.height),i=r.data,s,o,u,a,f,l,c,h=!1;if(this.image){h=!0;var p=t.util.createCanvasElement();p.width=this.image.width,p.height=this.image.height;var d=new t.StaticCanvas(p);d.add(this.image);var v=d.getContext("2d");c=v.getImageData(0,0,d.width,d.height).data}else c=(new t.Color(this.color)).getSource(),s=c[0]*this.alpha,o=c[1]*this.alpha,u=c[2]*this.alpha;for(var m=0,g=i.length;m<g;m+=4){a=i[m],f=i[m+1],l=i[m+2],h&&(s=c[m]*this.alpha,o=c[m+1]*this.alpha,u=c[m+2]*this.alpha);switch(this.mode){case"multiply":i[m]=a*s/255,i[m+1]=f*o/255,i[m+2]=l*u/255;break;case"screen":i[m]=1-(1-a)*(1-s),i[m+1]=1-(1-f)*(1-o),i[m+2]=1-(1-l)*(1-u);break;case"add":i[m]=Math.min(255,a+s),i[m+1]=Math.min(255,f+o),i[m+2]=Math.min(255,l+u);break;case"diff":case"difference":i[m]=Math.abs(a-s),i[m+1]=Math.abs(f-o),i[m+2]=Math.abs(l-u);break;case"subtract":var y=a-s,b=f-o,w=l-u;i[m]=y<0?0:y,i[m+1]=b<0?0:b,i[m+2]=w<0?0:w;break;case"darken":i[m]=Math.min(a,s),i[m+1]=Math.min(f,o),i[m+2]=Math.min(l,u);break;case"lighten":i[m]=Math.max(a,s),i[m+1]=Math.max(f,o),i[m+2]=Math.max(l,u)}}n.putImageData(r,0,0)},toObject:function(){return{color:this.color,image:this.image,mode:this.mode,alpha:this.alpha}}}),t.Image.filters.Blend.fromObject=function(e){return new t.Image.filters.Blend(e)}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=Math.pow,r=Math.floor,i=Math.sqrt,s=Math.abs,o=Math.max,u=Math.round,a=Math.sin,f=Math.ceil;t.Image.filters.Resize=t.util.createClass(t.Image.filters.BaseFilter,{type:"Resize",resizeType:"hermite",scaleX:0,scaleY:0,lanczosLobes:3,applyTo:function(e,t,n){this.rcpScaleX=1/t,this.rcpScaleY=1/n;var r=e.width,i=e.height,s=u(r*t),o=u(i*n),a;this.resizeType==="sliceHack"&&(a=this.sliceByTwo(e,r,i,s,o)),this.resizeType==="hermite"&&(a=this.hermiteFastResize(e,r,i,s,o)),this.resizeType==="bilinear"&&(a=this.bilinearFiltering(e,r,i,s,o)),this.resizeType==="lanczos"&&(a=this.lanczosResize(e,r,i,s,o)),e.width=s,e.height=o,e.getContext("2d").putImageData(a,0,0)},sliceByTwo:function(e,n,i,s,u){var a=e.getContext("2d"),f,l=.5,c=.5,h=1,p=1,d=!1,v=!1,m=n,g=i,y=t.util.createCanvasElement(),b=y.getContext("2d");s=r(s),u=r(u),y.width=o(s,n),y.height=o(u,i),s>n&&(l=2,h=-1),u>i&&(c=2,p=-1),f=a.getImageData(0,0,n,i),e.width=o(s,n),e.height=o(u,i),a.putImageData(f,0,0);while(!d||!v)n=m,i=g,s*h<r(m*l*h)?m=r(m*l):(m=s,d=!0),u*p<r(g*c*p)?g=r(g*c):(g=u,v=!0),f=a.getImageData(0,0,n,i),b.putImageData(f,0,0),a.clearRect(0,0,m,g),a.drawImage(y,0,0,n,i,0,0,m,g);return a.getImageData(0,0,s,u)},lanczosResize:function(e,t,o,u,l){function c(e){return function(t){if(t>e)return 0;t*=Math.PI;if(s(t)<1e-16)return 1;var n=t/e;return a(t)*a(n)/t/n}}function h(e){var a,f,c,p,d,L,A,O,M,_,D;C.x=(e+.5)*b,k.x=r(C.x);for(a=0;a<l;a++){C.y=(a+.5)*w,k.y=r(C.y),d=0,L=0,A=0,O=0,M=0;for(f=k.x-x;f<=k.x+x;f++){if(f<0||f>=t)continue;_=r(1e3*s(f-C.x)),N[_]||(N[_]={});for(var P=k.y-T;P<=k.y+T;P++){if(P<0||P>=o)continue;D=r(1e3*s(P-C.y)),N[_][D]||(N[_][D]=y(i(n(_*E,2)+n(D*S,2))/1e3)),c=N[_][D],c>0&&(p=(P*t+f)*4,d+=c,L+=c*m[p],A+=c*m[p+1],O+=c*m[p+2],M+=c*m[p+3])}}p=(a*u+e)*4,g[p]=L/d,g[p+1]=A/d,g[p+2]=O/d,g[p+3]=M/d}return++e<u?h(e):v}var p=e.getContext("2d"),d=p.getImageData(0,0,t,o),v=p.getImageData(0,0,u,l),m=d.data,g=v.data,y=c(this.lanczosLobes),b=this.rcpScaleX,w=this.rcpScaleY,E=2/this.rcpScaleX,S=2/this.rcpScaleY,x=f(b*this.lanczosLobes/2),T=f(w*this.lanczosLobes/2),N={},C={},k={};return h(0)},bilinearFiltering:function(e,t,n,i,s){var o,u,a,f,l,c,h,p,d,v,m,g,y=0,b,w=this.rcpScaleX,E=this.rcpScaleY,S=e.getContext("2d"),x=4*(t-1),T=S.getImageData(0,0,t,n),N=T.data,C=S.getImageData(0,0,i,s),k=C.data;for(h=0;h<s;h++)for(p=0;p<i;p++){l=r(w*p),c=r(E*h),d=w*p-l,v=E*h-c,b=4*(c*t+l);for(m=0;m<4;m++)o=N[b+m],u=N[b+4+m],a=N[b+x+m],f=N[b+x+4+m],g=o*(1-d)*(1-v)+u*d*(1-v)+a*v*(1-d)+f*d*v,k[y++]=g}return C},hermiteFastResize:function(e,t,n,o,u){var a=this.rcpScaleX,l=this.rcpScaleY,c=f(a/2),h=f(l/2),p=e.getContext("2d"),d=p.getImageData(0,0,t,n),v=d.data,m=p.getImageData(0,0,o,u),g=m.data;for(var y=0;y<u;y++)for(var b=0;b<o;b++){var w=(b+y*o)*4,E=0,S=0,x=0,T=0,N=0,C=0,k=0,L=(y+.5)*l;for(var A=r(y*l);A<(y+1)*l;A++){var O=s(L-(A+.5))/h,M=(b+.5)*a,_=O*O;for(var D=r(b*a);D<(b+1)*a;D++){var P=s(M-(D+.5))/c,H=i(_+P*P);if(H>1&&H<-1)continue;E=2*H*H*H-3*H*H+1,E>0&&(P=4*(D+A*t),k+=E*v[P+3],x+=E,v[P+3]<255&&(E=E*v[P+3]/250),T+=E*v[P],N+=E*v[P+1],C+=E*v[P+2],S+=E)}}g[w]=T/S,g[w+1]=N/S,g[w+2]=C/S,g[w+3]=k/x}return m}}),t.Image.filters.Resize.fromObject=function(){return new t.Image.filters.Resize}}(typeof exports!="undefined"?exports:this),function(e){"use strict";var t=e.fabric||(e.fabric={}),n=t.util.object.extend,r=t.util.object.clone,i=t.util.toFixed,s=t.StaticCanvas.supports("setLineDash");if(t.Text){t.warn("fabric.Text is already defined");return}var o=t.Object.prototype.stateProperties.concat();o.push("fontFamily","fontWeight","fontSize","text","textDecoration","textAlign","fontStyle","lineHeight","textBackgroundColor","useNative","path"),t.Text=t.util.createClass(t.Object,{_dimensionAffectingProps:{fontSize:!0,fontWeight:!0,fontFamily:!0,textDecoration:!0,fontStyle:!0,lineHeight:!0,stroke:!0,strokeWidth:!0,text:!0},_reNewline:/\r?\n/,type:"text",fontSize:40,fontWeight:"normal",fontFamily:"Times New Roman",textDecoration:"",textAlign:"left",fontStyle:"",lineHeight:1.3,textBackgroundColor:"",path:null,useNative:!0,stateProperties:o,stroke:null,shadow:null,initialize:function(e,t){t=t||{},this.text=e,this.__skipDimension=!0,this.setOptions(t),this.__skipDimension=!1,this._initDimensions()},_initDimensions:function(){if(this.__skipDimension)return;var e=t.util.createCanvasElement();this._render(e.getContext("2d"))},toString:function(){return"#<fabric.Text ("+this.complexity()+'): { "text": "'+this.text+'", "fontFamily": "'+this.fontFamily+'" }>'},_render:function(e){typeof Cufon=="undefined"||this.useNative===!0?this._renderViaNative(e):this._renderViaCufon(e)},_renderViaNative:function(e){var n=this.text.split(this._reNewline);this._setTextStyles(e),this.width=this._getTextWidth(e,n),this.height=this._getTextHeight(e,n),this.clipTo&&t.util.clipContext(this,e),this._renderTextBackground(e,n),this._translateForTextAlign(e),this._renderText(e,n),this.textAlign!=="left"&&this.textAlign!=="justify"&&e.restore(),this._renderTextDecoration(e,n),this.clipTo&&e.restore(),this._setBoundaries(e,n),this._totalLineHeight=0},_renderText:function(e,t){e.save(),this._setOpacity(e),this._setShadow(e),this._setupCompositeOperation(e),this._renderTextFill(e,t),this._renderTextStroke(e,t),this._restoreCompositeOperation(e),this._removeShadow(e),e.restore()},_translateForTextAlign:function(e){this.textAlign!=="left"&&this.textAlign!=="justify"&&(e.save(),e.translate(this.textAlign==="center"?this.width/2:this.width,0))},_setBoundaries:function(e,t){this._boundaries=[];for(var n=0,r=t.length;n<r;n++){var i=this._getLineWidth(e,t[n]),s=this._getLineLeftOffset(i);this._boundaries.push({height:this.fontSize*this.lineHeight,width:i,left:s})}},_setTextStyles:function(e){this._setFillStyles(e),this._setStrokeStyles(e),e.textBaseline="alphabetic",this.skipTextAlign||(e.textAlign=this.textAlign),e.font=this._getFontDeclaration()},_getTextHeight:function(e,t){return this.fontSize*t.length*this.lineHeight},_getTextWidth:function(e,t){var n=e.measureText(t[0]||"|").width;for(var r=1,i=t.length;r<i;r++){var s=e.measureText(t[r]).width;s>n&&(n=s)}return n},_renderChars:function(e,t,n,r,i){t[e](n,r,i)},_renderTextLine:function(e,t,n,r,i,s){i-=this.fontSize/4;if(this.textAlign!=="justify"){this._renderChars(e,t,n,r,i,s);return}var o=t.measureText(n).width,u=this.width;if(u>o){var a=n.split(/\s+/),f=t.measureText(n.replace(/\s+/g,"")).width,l=u-f,c=a.length-1,h=l/c,p=0;for(var d=0,v=a.length;d<v;d++)this._renderChars(e,t,a[d],r+p,i,s),p+=t.measureText(a[d]).width+h}else this._renderChars(e,t,n,r,i,s)},_getLeftOffset:function(){return-this.width/2},_getTopOffset:function(){return-this.height/2},_renderTextFill:function(e,t){if(!this.fill&&!this._skipFillStrokeCheck)return;this._boundaries=[];var n=0;for(var r=0,i=t.length;r<i;r++){var s=this._getHeightOfLine(e,r,t);n+=s,this._renderTextLine("fillText",e,t[r],this._getLeftOffset(),this._getTopOffset()+n,r)}this.shadow&&!this.shadow.affectStroke&&this._removeShadow(e)},_renderTextStroke:function(e,t){if((!this.stroke||this.strokeWidth===0)&&!this._skipFillStrokeCheck)return;var n=0;e.save(),this.strokeDashArray&&(1&this.strokeDashArray.length&&this.strokeDashArray.push.apply(this.strokeDashArray,this.strokeDashArray),s&&e.setLineDash(this.strokeDashArray)),e.beginPath();for(var r=0,i=t.length;r<i;r++){var o=this._getHeightOfLine(e,r,t);n+=o,this._renderTextLine("strokeText",e,t[r],this._getLeftOffset(),this._getTopOffset()+n,r)}e.closePath(),e.restore()},_getHeightOfLine:function(){return this.fontSize*this.lineHeight},_renderTextBackground:function(e,t){this._renderTextBoxBackground(e),this._renderTextLinesBackground(e,t)},_renderTextBoxBackground:function(e){if(!this.backgroundColor)return;e.save(),e.fillStyle=this.backgroundColor,e.fillRect(this._getLeftOffset(),this._getTopOffset(),this.width,this.height),e.restore()},_renderTextLinesBackground:function(e,t){if(!this.textBackgroundColor)return;e.save(),e.fillStyle=this.textBackgroundColor;for(var n=0,r=t.length;n<r;n++)if(t[n]!==""){var i=this._getLineWidth(e,t[n]),s=this._getLineLeftOffset(i);e.fillRect(this._getLeftOffset()+s,this._getTopOffset()+n*this.fontSize*this.lineHeight,i,this.fontSize*this.lineHeight)}e.restore()},_getLineLeftOffset:function(e){return this.textAlign==="center"?(this.width-e)/2:this.textAlign==="right"?this.width-e:0},_getLineWidth:function(e,t){return this.textAlign==="justify"?this.width:e.measureText(t).width},_renderTextDecoration:function(e,t){function i(i){for(var s=0,o=t.length;s<o;s++){var u=r._getLineWidth(e,t[s]),a=r._getLineLeftOffset(u);e.fillRect(r._getLeftOffset()+a,~~(i+s*r._getHeightOfLine(e,s,t)-n),u,1)}}if(!this.textDecoration)return;var n=this._getTextHeight(e,t)/2,r=this;this.textDecoration.indexOf("underline")>-1&&i(this.fontSize*this.lineHeight),this.textDecoration.indexOf("line-through")>-1&&i(this.fontSize*this.lineHeight-this.fontSize/2),this.textDecoration.indexOf("overline")>-1&&i(this.fontSize*this.lineHeight-this.fontSize)},_getFontDeclaration:function(){return[t.isLikelyNode?this.fontWeight:this.fontStyle,t.isLikelyNode?this.fontStyle:this.fontWeight,this.fontSize+"px",t.isLikelyNode?'"'+this.fontFamily+'"':this.fontFamily].join(" ")},render:function(e,t){if(!this.visible)return;e.save(),t||this.transform(e);var n=this.group&&this.group.type==="path-group";n&&e.translate(-this.group.width/2,-this.group.height/2),this.transformMatrix&&e.transform.apply(e,this.transformMatrix),n&&e.translate(this.left,this.top),this._render(e),e.restore()},toObject:function(e){var t=n(this.callSuper("toObject",e),{text:this.text,fontSize:this.fontSize,fontWeight:this.fontWeight,fontFamily:this.fontFamily,fontStyle:this.fontStyle,lineHeight:this.lineHeight,textDecoration:this.textDecoration,textAlign:this.textAlign,path:this.path,textBackgroundColor:this.textBackgroundColor,useNative:this.useNative});return this.includeDefaultValues||this._removeDefaultValues(t),t},toSVG:function(e){var t=[],n=this.text.split(this._reNewline),r=this._getSVGLeftTopOffsets(n),i=this._getSVGTextAndBg(r.lineTop,r.textLeft,n),s=this._getSVGShadows(r.lineTop,n);return r.textTop+=this._fontAscent?this._fontAscent/5*this.lineHeight:0,this._wrapSVGTextAndBg(t,i,s,r),e?e(t.join("")):t.join("")},_getSVGLeftTopOffsets:function(e){var t=this.useNative?this.fontSize*this.lineHeight:-this._fontAscent-this._fontAscent/5*this.lineHeight,n=-(this.width/2),r=this.useNative?this.fontSize*this.lineHeight-.25*this.fontSize:this.height/2-e.length*this.fontSize-this._totalLineHeight;return{textLeft:n+(this.group&&this.group.type==="path-group"?this.left:0),textTop:r+(this.group&&this.group.type==="path-group"?this.top:0),lineTop:t}},_wrapSVGTextAndBg:function(e,t,n,r){e.push('<g transform="',this.getSvgTransform(),this.getSvgTransformMatrix(),'">\n',t.textBgRects.join(""),"<text ",this.fontFamily?'font-family="'+this.fontFamily.replace(/"/g,"'")+'" ':"",this.fontSize?'font-size="'+this.fontSize+'" ':"",this.fontStyle?'font-style="'+this.fontStyle+'" ':"",this.fontWeight?'font-weight="'+this.fontWeight+'" ':"",this.textDecoration?'text-decoration="'+this.textDecoration+'" ':"",'style="',this.getSvgStyles(),'" ','transform="translate(',i(r.textLeft,2)," ",i(r.textTop,2),')">',n.join(""),t.textSpans.join(""),"</text>\n","</g>\n")},_getSVGShadows:function(e,n){var r=[],s,o,u=1;if(!this.shadow||!this._boundaries)return r;for(s=0,o=n.length;s<o;s++)if(n[s]!==""){var a=this._boundaries&&this._boundaries[s]?this._boundaries[s].left:0;r.push('<tspan x="',i(a+u+this.shadow.offsetX,2),s===0||this.useNative?'" y':'" dy','="',i(this.useNative?e*s-this.height/2+this.shadow.offsetY:e+(s===0?this.shadow.offsetY:0),2),'" ',this._getFillAttributes(this.shadow.color),">",t.util.string.escapeXml(n[s]),"</tspan>"),u=1}else u++;return r},_getSVGTextAndBg:function(e,t,n){var r=[],i=[],s=1;this._setSVGBg(i);for(var o=0,u=n.length;o<u;o++){n[o]!==""?(this._setSVGTextLineText(n[o],o,r,e,s,i),s=1):s++;if(!this.textBackgroundColor||!this._boundaries)continue;this._setSVGTextLineBg(i,o,t,e)}return{textSpans:r,textBgRects:i}},_setSVGTextLineText:function(e,n,r,s,o){var u=this._boundaries&&this._boundaries[n]?i(this._boundaries[n].left,2):0;r.push('<tspan x="',u,'" ',n===0||this.useNative?"y":"dy",'="',i(this.useNative?s*n-this.height/2:s*o,2),'" ',this._getFillAttributes(this.fill),">",t.util.string.escapeXml(e),"</tspan>")},_setSVGTextLineBg:function(e,t,n,r){e.push("<rect ",this._getFillAttributes(this.textBackgroundColor),' x="',i(n+this._boundaries[t].left,2),'" y="',i(r*t-this.height/2,2),'" width="',i(this._boundaries[t].width,2),'" height="',i(this._boundaries[t].height,2),'"></rect>\n')},_setSVGBg:function(e){this.backgroundColor&&this._boundaries&&e.push("<rect ",this._getFillAttributes(this.backgroundColor),' x="',i(-this.width/2,2),'" y="',i(-this.height/2,2),'" width="',i(this.width,2),'" height="',i(this.height,2),'"></rect>')},_getFillAttributes:function(e){var n=e&&typeof e=="string"?new t.Color(e):"";return!n||!n.getSource()||n.getAlpha()===1?'fill="'+e+'"':'opacity="'+n.getAlpha()+'" fill="'+n.setAlpha(1).toRgb()+'"'},_set:function(e,t){e==="fontFamily"&&this.path&&(this.path=this.path.replace(/(.*?)([^\/]*)(\.font\.js)/,"$1"+t+"$3")),this.callSuper("_set",e,t),e in this._dimensionAffectingProps&&(this._initDimensions(),this.setCoords())},complexity:function(){return 1}}),t.Text.ATTRIBUTE_NAMES=t.SHARED_ATTRIBUTES.concat("x y dx dy font-family font-style font-weight font-size text-decoration text-anchor".split(" ")),t.Text.DEFAULT_SVG_FONT_SIZE=16,t.Text.fromElement=function(e,n){if(!e)return null;var r=t.parseAttributes(e,t.Text.ATTRIBUTE_NAMES);n=t.util.object.extend(n?t.util.object.clone(n):{},r),n.top=n.top||0,n.left=n.left||0,"dx"in r&&(n.left+=r.dx),"dy"in r&&(n.top+=r.dy),"fontSize"in n||(n.fontSize=t.Text.DEFAULT_SVG_FONT_SIZE),n.originX||(n.originX="left"),n.top+=n.fontSize/4;var i=new t.Text(e.textContent,n),s=0;return i.originX==="left"&&(s=i.getWidth()/2),i.originX==="right"&&(s=-i.getWidth()/2),i.set({left:i.getLeft()+s,top:i.getTop()-i.getHeight()/2}),i},t.Text.fromObject=function(e){return new t.Text(e.text,r(e))},t.util.createAccessors(t.Text)}(typeof exports!="undefined"?exports:this),function(){var e=fabric.util.object.clone;fabric.IText=fabric.util.createClass(fabric.Text,fabric.Observable,{type:"i-text",selectionStart:0,selectionEnd:0,selectionColor:"rgba(17,119,255,0.3)",isEditing:!1,editable:!0,editingBorderColor:"rgba(102,153,255,0.25)",cursorWidth:2,cursorColor:"#333",cursorDelay:1e3,cursorDuration:600,styles:null,caching:!0,_skipFillStrokeCheck:!0,_reSpace:/\s|\n/,_fontSizeFraction:4,_currentCursorOpacity:0,_selectionDirection:null,_abortCursorAnimation:!1,_charWidthsCache:{},initialize:function(e,t){this.styles=t?t.styles||{}:{},this.callSuper("initialize",e,t),this.initBehavior(),fabric.IText.instances.push(this),this.__lineWidths={},this.__lineHeights={},this.__lineOffsets={}},isEmptyStyles:function(){if(!this.styles)return!0;var e=this.styles;for(var t in e)for(var n in e[t])for(var r in e[t][n])return!1;return!0},setSelectionStart:function(e){this.selectionStart!==e&&(this.fire("selection:changed"),this.canvas&&this.canvas.fire("text:selection:changed",{target:this})),this.selectionStart=e,this.hiddenTextarea&&(this.hiddenTextarea.selectionStart=e)},setSelectionEnd:function(e){this.selectionEnd!==e&&(this.fire("selection:changed"),this.canvas&&this.canvas.fire("text:selection:changed",{target:this})),this.selectionEnd=e,this.hiddenTextarea&&(this.hiddenTextarea.selectionEnd=e)},getSelectionStyles:function(e,t){if(arguments.length===2){var n=[];for(var r=e;r<t;r++)n.push(this.getSelectionStyles(r));return n}var i=this.get2DCursorLocation(e);return this.styles[i.lineIndex]?this.styles[i.lineIndex][i.charIndex]||{}:{}},setSelectionStyles:function(e){if(this.selectionStart===this.selectionEnd)this._extendStyles(this.selectionStart,e);else for(var t=this.selectionStart;t<this.selectionEnd;t++)this._extendStyles(t,e);return this},_extendStyles:function(e,t){var n=this.get2DCursorLocation(e);this.styles[n.lineIndex]||(this.styles[n.lineIndex]={}),this.styles[n.lineIndex][n.charIndex]||(this.styles[n.lineIndex][n.charIndex]={}),fabric.util.object.extend(this.styles[n.lineIndex][n.charIndex],t)},_render:function(e){this.callSuper("_render",e),this.ctx=e,this.isEditing&&this.renderCursorOrSelection()},renderCursorOrSelection:function(){if(!this.active)return;var e=this.text.split(""),t;this.selectionStart===this.selectionEnd?(t=this._getCursorBoundaries(e,"cursor"),this.renderCursor(t)):(t=this._getCursorBoundaries(e,"selection"),this.renderSelection(e,t))},get2DCursorLocation:function(e){typeof e=="undefined"&&(e=this.selectionStart);var t=this.text.slice(0,e),n=t.split(this._reNewline);return{lineIndex:n.length-1,charIndex:n[n.length-1].length}},getCurrentCharStyle:function(e,t){var n=this.styles[e]&&this.styles[e][t===0?0:t-1];return{fontSize:n&&n.fontSize||this.fontSize,fill:n&&n.fill||this.fill,textBackgroundColor:n&&n.textBackgroundColor||this.textBackgroundColor,textDecoration:n&&n.textDecoration||this.textDecoration,fontFamily:n&&n.fontFamily||this.fontFamily,fontWeight:n&&n.fontWeight||this.fontWeight,fontStyle:n&&n.fontStyle||this.fontStyle,stroke:n&&n.stroke||this.stroke,strokeWidth:n&&n.strokeWidth||this.strokeWidth}},getCurrentCharFontSize:function(e,t){return this.styles[e]&&this.styles[e][t===0?0:t-1]&&this.styles[e][t===0?0:t-1].fontSize||this.fontSize},getCurrentCharColor:function(e,t){return this.styles[e]&&this.styles[e][t===0?0:t-1]&&this.styles[e][t===0?0:t-1].fill||this.cursorColor},_getCursorBoundaries:function(e,t){var n=this.get2DCursorLocation(),r=this.text.split(this._reNewline),i=Math.round(this._getLeftOffset()),s=this._getTopOffset(),o=this._getCursorBoundariesOffsets(e,t,n,r);return{left:i,top:s,leftOffset:o.left+o.lineLeft,topOffset:o.top}},_getCursorBoundariesOffsets:function(e,t,n,r){var i=0,s=0,o=0,u=0,a=t==="cursor"?this._getHeightOfLine(this.ctx,0)-this.getCurrentCharFontSize(n.lineIndex,n.charIndex):0;for(var f=0;f<this.selectionStart;f++){if(e[f]==="\n"){u=0;var l=s+(t==="cursor"?1:0);a+=this._getCachedLineHeight(l),s++,o=0}else u+=this._getWidthOfChar(this.ctx,e[f],s,o),o++;i=this._getCachedLineOffset(s,r)}return this._clearCache(),{top:a,left:u,lineLeft:i}},_clearCache:function(){this.__lineWidths={},this.__lineHeights={},this.__lineOffsets={}},_getCachedLineHeight:function(e){return this.__lineHeights[e]||(this.__lineHeights[e]=this._getHeightOfLine(this.ctx,e))},_getCachedLineWidth:function(e,t){return this.__lineWidths[e]||(this.__lineWidths[e]=this._getWidthOfLine(this.ctx,e,t))},_getCachedLineOffset:function(e,t){var n=this._getCachedLineWidth(e,t);return this.__lineOffsets[e]||(this.__lineOffsets[e]=this._getLineLeftOffset(n))},renderCursor:function(e){var t=this.ctx;t.save();var n=this.get2DCursorLocation(),r=n.lineIndex,i=n.charIndex,s=this.getCurrentCharFontSize(r,i),o=r===0&&i===0?this._getCachedLineOffset(r,this.text.split(this._reNewline)):e.leftOffset;t.fillStyle=this.getCurrentCharColor(r,i),t.globalAlpha=this.__isMousedown?1:this._currentCursorOpacity,t.fillRect(e.left+o,e.top+e.topOffset,this.cursorWidth/this.scaleX,s),t.restore()},renderSelection:function(e,t){var n=this.ctx;n.save(),n.fillStyle=this.selectionColor;var r=this.get2DCursorLocation(this.selectionStart
+),i=this.get2DCursorLocation(this.selectionEnd),s=r.lineIndex,o=i.lineIndex,u=this.text.split(this._reNewline);for(var a=s;a<=o;a++){var f=this._getCachedLineOffset(a,u)||0,l=this._getCachedLineHeight(a),c=0;if(a===s)for(var h=0,p=u[a].length;h<p;h++)h>=r.charIndex&&(a!==o||h<i.charIndex)&&(c+=this._getWidthOfChar(n,u[a][h],a,h)),h<r.charIndex&&(f+=this._getWidthOfChar(n,u[a][h],a,h));else if(a>s&&a<o)c+=this._getCachedLineWidth(a,u)||5;else if(a===o)for(var d=0,v=i.charIndex;d<v;d++)c+=this._getWidthOfChar(n,u[a][d],a,d);n.fillRect(t.left+f,t.top+t.topOffset,c,l),t.topOffset+=l}n.restore()},_renderChars:function(e,t,n,r,i,s){if(this.isEmptyStyles())return this._renderCharsFast(e,t,n,r,i);this.skipTextAlign=!0,r-=this.textAlign==="center"?this.width/2:this.textAlign==="right"?this.width:0;var o=this.text.split(this._reNewline),u=this._getWidthOfLine(t,s,o),a=this._getHeightOfLine(t,s,o),f=this._getLineLeftOffset(u),l=n.split(""),c,h="";r+=f||0,t.save();for(var p=0,d=l.length;p<=d;p++){c=c||this.getCurrentCharStyle(s,p);var v=this.getCurrentCharStyle(s,p+1);if(this._hasStyleChanged(c,v)||p===d)this._renderChar(e,t,s,p-1,h,r,i,a),h="",c=v;h+=l[p]}t.restore()},_renderCharsFast:function(e,t,n,r,i){this.skipTextAlign=!1,e==="fillText"&&this.fill&&this.callSuper("_renderChars",e,t,n,r,i),e==="strokeText"&&this.stroke&&this.callSuper("_renderChars",e,t,n,r,i)},_renderChar:function(e,t,n,r,i,s,o,u){var a,f,l;if(this.styles&&this.styles[n]&&(a=this.styles[n][r])){var c=a.stroke||this.stroke,h=a.fill||this.fill;t.save(),f=this._applyCharStylesGetWidth(t,i,n,r,a),l=this._getHeightOfChar(t,i,n,r),h&&t.fillText(i,s,o),c&&t.strokeText(i,s,o),this._renderCharDecoration(t,a,s,o,f,u,l),t.restore(),t.translate(f,0)}else e==="strokeText"&&this.stroke&&t[e](i,s,o),e==="fillText"&&this.fill&&t[e](i,s,o),f=this._applyCharStylesGetWidth(t,i,n,r),this._renderCharDecoration(t,null,s,o,f,u),t.translate(t.measureText(i).width,0)},_hasStyleChanged:function(e,t){return e.fill!==t.fill||e.fontSize!==t.fontSize||e.textBackgroundColor!==t.textBackgroundColor||e.textDecoration!==t.textDecoration||e.fontFamily!==t.fontFamily||e.fontWeight!==t.fontWeight||e.fontStyle!==t.fontStyle||e.stroke!==t.stroke||e.strokeWidth!==t.strokeWidth},_renderCharDecoration:function(e,t,n,r,i,s,o){var u=t?t.textDecoration||this.textDecoration:this.textDecoration,a=(t?t.fontSize:null)||this.fontSize;if(!u)return;u.indexOf("underline")>-1&&this._renderCharDecorationAtOffset(e,n,r+this.fontSize/this._fontSizeFraction,i,0,this.fontSize/20),u.indexOf("line-through")>-1&&this._renderCharDecorationAtOffset(e,n,r+this.fontSize/this._fontSizeFraction,i,o/2,a/20),u.indexOf("overline")>-1&&this._renderCharDecorationAtOffset(e,n,r,i,s-this.fontSize/this._fontSizeFraction,this.fontSize/20)},_renderCharDecorationAtOffset:function(e,t,n,r,i,s){e.fillRect(t,n-i,r,s)},_renderTextLine:function(e,t,n,r,i,s){i+=this.fontSize/4,this.callSuper("_renderTextLine",e,t,n,r,i,s)},_renderTextDecoration:function(e,t){if(this.isEmptyStyles())return this.callSuper("_renderTextDecoration",e,t)},_renderTextLinesBackground:function(e,t){if(!this.textBackgroundColor&&!this.styles)return;e.save(),this.textBackgroundColor&&(e.fillStyle=this.textBackgroundColor);var n=0,r=this.fontSize/this._fontSizeFraction;for(var i=0,s=t.length;i<s;i++){var o=this._getHeightOfLine(e,i,t);if(t[i]===""){n+=o;continue}var u=this._getWidthOfLine(e,i,t),a=this._getLineLeftOffset(u);this.textBackgroundColor&&(e.fillStyle=this.textBackgroundColor,e.fillRect(this._getLeftOffset()+a,this._getTopOffset()+n+r,u,o));if(this.styles[i])for(var f=0,l=t[i].length;f<l;f++)if(this.styles[i]&&this.styles[i][f]&&this.styles[i][f].textBackgroundColor){var c=t[i][f];e.fillStyle=this.styles[i][f].textBackgroundColor,e.fillRect(this._getLeftOffset()+a+this._getWidthOfCharsAt(e,i,f,t),this._getTopOffset()+n+r,this._getWidthOfChar(e,c,i,f,t)+1,o)}n+=o}e.restore()},_getCacheProp:function(e,t){return e+t.fontFamily+t.fontSize+t.fontWeight+t.fontStyle+t.shadow},_applyCharStylesGetWidth:function(t,n,r,i,s){var o=s||this.styles[r]&&this.styles[r][i];o?o=e(o):o={},this._applyFontStyles(o);var u=this._getCacheProp(n,o);if(this.isEmptyStyles()&&this._charWidthsCache[u]&&this.caching)return this._charWidthsCache[u];typeof o.shadow=="string"&&(o.shadow=new fabric.Shadow(o.shadow));var a=o.fill||this.fill;return t.fillStyle=a.toLive?a.toLive(t):a,o.stroke&&(t.strokeStyle=o.stroke&&o.stroke.toLive?o.stroke.toLive(t):o.stroke),t.lineWidth=o.strokeWidth||this.strokeWidth,t.font=this._getFontDeclaration.call(o),this._setShadow.call(o,t),this.caching?(this._charWidthsCache[u]||(this._charWidthsCache[u]=t.measureText(n).width),this._charWidthsCache[u]):t.measureText(n).width},_applyFontStyles:function(e){e.fontFamily||(e.fontFamily=this.fontFamily),e.fontSize||(e.fontSize=this.fontSize),e.fontWeight||(e.fontWeight=this.fontWeight),e.fontStyle||(e.fontStyle=this.fontStyle)},_getStyleDeclaration:function(t,n){return this.styles[t]&&this.styles[t][n]?e(this.styles[t][n]):{}},_getWidthOfChar:function(e,t,n,r){if(this.textAlign==="justify"&&/\s/.test(t))return this._getWidthOfSpace(e,n);var i=this._getStyleDeclaration(n,r);this._applyFontStyles(i);var s=this._getCacheProp(t,i);if(this._charWidthsCache[s]&&this.caching)return this._charWidthsCache[s];if(e){e.save();var o=this._applyCharStylesGetWidth(e,t,n,r);return e.restore(),o}},_getHeightOfChar:function(e,t,n,r){return this.styles[n]&&this.styles[n][r]?this.styles[n][r].fontSize||this.fontSize:this.fontSize},_getWidthOfCharAt:function(e,t,n,r){r=r||this.text.split(this._reNewline);var i=r[t].split("")[n];return this._getWidthOfChar(e,i,t,n)},_getHeightOfCharAt:function(e,t,n,r){r=r||this.text.split(this._reNewline);var i=r[t].split("")[n];return this._getHeightOfChar(e,i,t,n)},_getWidthOfCharsAt:function(e,t,n,r){var i=0;for(var s=0;s<n;s++)i+=this._getWidthOfCharAt(e,t,s,r);return i},_getWidthOfLine:function(e,t,n){return this._getWidthOfCharsAt(e,t,n[t].length,n)},_getWidthOfSpace:function(e,t){var n=this.text.split(this._reNewline),r=n[t],i=r.split(/\s+/),s=this._getWidthOfWords(e,r,t),o=this.width-s,u=i.length-1,a=o/u;return a},_getWidthOfWords:function(e,t,n){var r=0;for(var i=0;i<t.length;i++){var s=t[i];s.match(/\s/)||(r+=this._getWidthOfChar(e,s,n,i))}return r},_getTextWidth:function(e,t){if(this.isEmptyStyles())return this.callSuper("_getTextWidth",e,t);var n=this._getWidthOfLine(e,0,t);for(var r=1,i=t.length;r<i;r++){var s=this._getWidthOfLine(e,r,t);s>n&&(n=s)}return n},_getHeightOfLine:function(e,t,n){n=n||this.text.split(this._reNewline);var r=this._getHeightOfChar(e,n[t][0],t,0),i=n[t],s=i.split("");for(var o=1,u=s.length;o<u;o++){var a=this._getHeightOfChar(e,s[o],t,o);a>r&&(r=a)}return r*this.lineHeight},_getTextHeight:function(e,t){var n=0;for(var r=0,i=t.length;r<i;r++)n+=this._getHeightOfLine(e,r,t);return n},_getTopOffset:function(){var e=fabric.Text.prototype._getTopOffset.call(this);return e-this.fontSize/this._fontSizeFraction},_renderTextBoxBackground:function(e){if(!this.backgroundColor)return;e.save(),e.fillStyle=this.backgroundColor,e.fillRect(this._getLeftOffset(),this._getTopOffset()+this.fontSize/this._fontSizeFraction,this.width,this.height),e.restore()},toObject:function(t){return fabric.util.object.extend(this.callSuper("toObject",t),{styles:e(this.styles)})}}),fabric.IText.fromObject=function(t){return new fabric.IText(t.text,e(t))},fabric.IText.instances=[]}(),function(){var e=fabric.util.object.clone;fabric.util.object.extend(fabric.IText.prototype,{initBehavior:function(){this.initAddedHandler(),this.initCursorSelectionHandlers(),this.initDoubleClickSimulation()},initSelectedHandler:function(){this.on("selected",function(){var e=this;setTimeout(function(){e.selected=!0},100)})},initAddedHandler:function(){this.on("added",function(){this.canvas&&!this.canvas._hasITextHandlers&&(this.canvas._hasITextHandlers=!0,this._initCanvasHandlers())})},_initCanvasHandlers:function(){this.canvas.on("selection:cleared",function(){fabric.IText.prototype.exitEditingOnOthers.call()}),this.canvas.on("mouse:up",function(){fabric.IText.instances.forEach(function(e){e.__isMousedown=!1})}),this.canvas.on("object:selected",function(e){fabric.IText.prototype.exitEditingOnOthers.call(e.target)})},_tick:function(){if(this._abortCursorAnimation)return;var e=this;this.animate("_currentCursorOpacity",1,{duration:this.cursorDuration,onComplete:function(){e._onTickComplete()},onChange:function(){e.canvas&&e.canvas.renderAll()},abort:function(){return e._abortCursorAnimation}})},_onTickComplete:function(){if(this._abortCursorAnimation)return;var e=this;this._cursorTimeout1&&clearTimeout(this._cursorTimeout1),this._cursorTimeout1=setTimeout(function(){e.animate("_currentCursorOpacity",0,{duration:this.cursorDuration/2,onComplete:function(){e._tick()},onChange:function(){e.canvas&&e.canvas.renderAll()},abort:function(){return e._abortCursorAnimation}})},100)},initDelayedCursor:function(e){var t=this,n=e?0:this.cursorDelay;e&&(this._abortCursorAnimation=!0,clearTimeout(this._cursorTimeout1),this._currentCursorOpacity=1,this.canvas&&this.canvas.renderAll()),this._cursorTimeout2&&clearTimeout(this._cursorTimeout2),this._cursorTimeout2=setTimeout(function(){t._abortCursorAnimation=!1,t._tick()},n)},abortCursorAnimation:function(){this._abortCursorAnimation=!0,clearTimeout(this._cursorTimeout1),clearTimeout(this._cursorTimeout2),this._currentCursorOpacity=0,this.canvas&&this.canvas.renderAll();var e=this;setTimeout(function(){e._abortCursorAnimation=!1},10)},selectAll:function(){this.selectionStart=0,this.selectionEnd=this.text.length,this.fire("selection:changed"),this.canvas&&this.canvas.fire("text:selection:changed",{target:this})},getSelectedText:function(){return this.text.slice(this.selectionStart,this.selectionEnd)},findWordBoundaryLeft:function(e){var t=0,n=e-1;if(this._reSpace.test(this.text.charAt(n)))while(this._reSpace.test(this.text.charAt(n)))t++,n--;while(/\S/.test(this.text.charAt(n))&&n>-1)t++,n--;return e-t},findWordBoundaryRight:function(e){var t=0,n=e;if(this._reSpace.test(this.text.charAt(n)))while(this._reSpace.test(this.text.charAt(n)))t++,n++;while(/\S/.test(this.text.charAt(n))&&n<this.text.length)t++,n++;return e+t},findLineBoundaryLeft:function(e){var t=0,n=e-1;while(!/\n/.test(this.text.charAt(n))&&n>-1)t++,n--;return e-t},findLineBoundaryRight:function(e){var t=0,n=e;while(!/\n/.test(this.text.charAt(n))&&n<this.text.length)t++,n++;return e+t},getNumNewLinesInSelectedText:function(){var e=this.getSelectedText(),t=0;for(var n=0,r=e.split(""),i=r.length;n<i;n++)r[n]==="\n"&&t++;return t},searchWordBoundary:function(e,t){var n=this._reSpace.test(this.text.charAt(e))?e-1:e,r=this.text.charAt(n),i=/[ \n\.,;!\?\-]/;while(!i.test(r)&&n>0&&n<this.text.length)n+=t,r=this.text.charAt(n);return i.test(r)&&r!=="\n"&&(n+=t===1?0:1),n},selectWord:function(e){var t=this.searchWordBoundary(e,-1),n=this.searchWordBoundary(e,1);this.setSelectionStart(t),this.setSelectionEnd(n),this.initDelayedCursor(!0)},selectLine:function(e){var t=this.findLineBoundaryLeft(e),n=this.findLineBoundaryRight(e);this.setSelectionStart(t),this.setSelectionEnd(n),this.initDelayedCursor(!0)},enterEditing:function(){if(this.isEditing||!this.editable)return;return this.exitEditingOnOthers(),this.isEditing=!0,this.initHiddenTextarea(),this._updateTextarea(),this._saveEditingProps(),this._setEditingProps(),this._tick(),this.canvas&&this.canvas.renderAll(),this.fire("editing:entered"),this.canvas&&this.canvas.fire("text:editing:entered",{target:this}),this},exitEditingOnOthers:function(){fabric.IText.instances.forEach(function(e){e.selected=!1,e.isEditing&&e.exitEditing()},this)},_setEditingProps:function(){this.hoverCursor="text",this.canvas&&(this.canvas.defaultCursor=this.canvas.moveCursor="text"),this.borderColor=this.editingBorderColor,this.hasControls=this.selectable=!1,this.lockMovementX=this.lockMovementY=!0},_updateTextarea:function(){if(!this.hiddenTextarea)return;this.hiddenTextarea.value=this.text,this.hiddenTextarea.selectionStart=this.selectionStart},_saveEditingProps:function(){this._savedProps={hasControls:this.hasControls,borderColor:this.borderColor,lockMovementX:this.lockMovementX,lockMovementY:this.lockMovementY,hoverCursor:this.hoverCursor,defaultCursor:this.canvas&&this.canvas.defaultCursor,moveCursor:this.canvas&&this.canvas.moveCursor}},_restoreEditingProps:function(){if(!this._savedProps)return;this.hoverCursor=this._savedProps.overCursor,this.hasControls=this._savedProps.hasControls,this.borderColor=this._savedProps.borderColor,this.lockMovementX=this._savedProps.lockMovementX,this.lockMovementY=this._savedProps.lockMovementY,this.canvas&&(this.canvas.defaultCursor=this._savedProps.defaultCursor,this.canvas.moveCursor=this._savedProps.moveCursor)},exitEditing:function(){return this.selected=!1,this.isEditing=!1,this.selectable=!0,this.selectionEnd=this.selectionStart,this.hiddenTextarea&&this.canvas&&this.hiddenTextarea.parentNode.removeChild(this.hiddenTextarea),this.hiddenTextarea=null,this.abortCursorAnimation(),this._restoreEditingProps(),this._currentCursorOpacity=0,this.fire("editing:exited"),this.canvas&&this.canvas.fire("text:editing:exited",{target:this}),this},_removeExtraneousStyles:function(){var e=this.text.split(this._reNewline);for(var t in this.styles)e[t]||delete this.styles[t]},_removeCharsFromTo:function(e,t){var n=t;while(n!==e){var r=this.get2DCursorLocation(n).charIndex;n--;var i=this.get2DCursorLocation(n).charIndex,s=i>r;s?this.removeStyleObject(s,n+1):this.removeStyleObject(this.get2DCursorLocation(n).charIndex===0,n)}this.text=this.text.slice(0,e)+this.text.slice(t)},insertChars:function(e){var t=this.text.slice(this.selectionStart,this.selectionStart+1)==="\n";this.text=this.text.slice(0,this.selectionStart)+e+this.text.slice(this.selectionEnd),this.selectionStart===this.selectionEnd&&this.insertStyleObjects(e,t,this.copiedStyles),this.selectionStart+=e.length,this.selectionEnd=this.selectionStart,this.canvas&&this.canvas.renderAll().renderAll(),this.setCoords(),this.fire("changed"),this.canvas&&this.canvas.fire("text:changed",{target:this})},insertNewlineStyleObject:function(t,n,r){this.shiftLineStyles(t,1),this.styles[t+1]||(this.styles[t+1]={});var i=this.styles[t][n-1],s={};if(r)s[0]=e(i),this.styles[t+1]=s;else{for(var o in this.styles[t])parseInt(o,10)>=n&&(s[parseInt(o,10)-n]=this.styles[t][o],delete this.styles[t][o]);this.styles[t+1]=s}},insertCharStyleObject:function(t,n,r){var i=this.styles[t],s=e(i);n===0&&!r&&(n=1);for(var o in s){var u=parseInt(o,10);u>=n&&(i[u+1]=s[u])}this.styles[t][n]=r||e(i[n-1])},insertStyleObjects:function(e,t,n){var r=this.get2DCursorLocation(),i=r.lineIndex,s=r.charIndex;this.styles[i]||(this.styles[i]={}),e==="\n"?this.insertNewlineStyleObject(i,s,t):n?this._insertStyles(n):this.insertCharStyleObject(i,s)},_insertStyles:function(e){for(var t=0,n=e.length;t<n;t++){var r=this.get2DCursorLocation(this.selectionStart+t),i=r.lineIndex,s=r.charIndex;this.insertCharStyleObject(i,s,e[t])}},shiftLineStyles:function(t,n){var r=e(this.styles);for(var i in this.styles){var s=parseInt(i,10);s>t&&(this.styles[s+n]=r[s])}},removeStyleObject:function(t,n){var r=this.get2DCursorLocation(n),i=r.lineIndex,s=r.charIndex;if(t){var o=this.text.split(this._reNewline),u=o[i-1],a=u?u.length:0;this.styles[i-1]||(this.styles[i-1]={});for(s in this.styles[i])this.styles[i-1][parseInt(s,10)+a]=this.styles[i][s];this.shiftLineStyles(i,-1)}else{var f=this.styles[i];if(f){var l=this.selectionStart===this.selectionEnd?-1:0;delete f[s+l]}var c=e(f);for(var h in c){var p=parseInt(h,10);p>=s&&p!==0&&(f[p-1]=c[p],delete f[p])}}},insertNewline:function(){this.insertChars("\n")}})}(),fabric.util.object.extend(fabric.IText.prototype,{initDoubleClickSimulation:function(){this.__lastClickTime=+(new Date),this.__lastLastClickTime=+(new Date),this.__lastPointer={},this.on("mousedown",this.onMouseDown.bind(this))},onMouseDown:function(e){this.__newClickTime=+(new Date);var t=this.canvas.getPointer(e.e);this.isTripleClick(t)?(this.fire("tripleclick",e),this._stopEvent(e.e)):this.isDoubleClick(t)&&(this.fire("dblclick",e),this._stopEvent(e.e)),this.__lastLastClickTime=this.__lastClickTime,this.__lastClickTime=this.__newClickTime,this.__lastPointer=t,this.__lastIsEditing=this.isEditing,this.__lastSelected=this.selected},isDoubleClick:function(e){return this.__newClickTime-this.__lastClickTime<500&&this.__lastPointer.x===e.x&&this.__lastPointer.y===e.y&&this.__lastIsEditing},isTripleClick:function(e){return this.__newClickTime-this.__lastClickTime<500&&this.__lastClickTime-this.__lastLastClickTime<500&&this.__lastPointer.x===e.x&&this.__lastPointer.y===e.y},_stopEvent:function(e){e.preventDefault&&e.preventDefault(),e.stopPropagation&&e.stopPropagation()},initCursorSelectionHandlers:function(){this.initSelectedHandler(),this.initMousedownHandler(),this.initMousemoveHandler(),this.initMouseupHandler(),this.initClicks()},initClicks:function(){this.on("dblclick",function(e){this.selectWord(this.getSelectionStartFromPointer(e.e))}),this.on("tripleclick",function(e){this.selectLine(this.getSelectionStartFromPointer(e.e))})},initMousedownHandler:function(){this.on("mousedown",function(e){var t=this.canvas.getPointer(e.e);this.__mousedownX=t.x,this.__mousedownY=t.y,this.__isMousedown=!0,this.hiddenTextarea&&this.canvas&&this.canvas.wrapperEl.appendChild(this.hiddenTextarea),this.selected&&this.setCursorByClick(e.e),this.isEditing&&(this.__selectionStartOnMouseDown=this.selectionStart,this.initDelayedCursor(!0))})},initMousemoveHandler:function(){this.on("mousemove",function(e){if(!this.__isMousedown||!this.isEditing)return;var t=this.getSelectionStartFromPointer(e.e);t>=this.__selectionStartOnMouseDown?(this.setSelectionStart(this.__selectionStartOnMouseDown),this.setSelectionEnd(t)):(this.setSelectionStart(t),this.setSelectionEnd(this.__selectionStartOnMouseDown))})},_isObjectMoved:function(e){var t=this.canvas.getPointer(e);return this.__mousedownX!==t.x||this.__mousedownY!==t.y},initMouseupHandler:function(){this.on("mouseup",function(e){this.__isMousedown=!1;if(this._isObjectMoved(e.e))return;this.__lastSelected&&(this.enterEditing(),this.initDelayedCursor(!0)),this.selected=!0})},setCursorByClick:function(e){var t=this.getSelectionStartFromPointer(e);e.shiftKey?t<this.selectionStart?(this.setSelectionEnd(this.selectionStart),this.setSelectionStart(t)):this.setSelectionEnd(t):(this.setSelectionStart(t),this.setSelectionEnd(t))},_getLocalRotatedPointer:function(e){var t=this.canvas.getPointer(e),n=new fabric.Point(t.x,t.y),r=new fabric.Point(this.left,this.top),i=fabric.util.rotatePoint(n,r,fabric.util.degreesToRadians(-this.angle));return this.getLocalPointer(e,i)},getSelectionStartFromPointer:function(e){var t=this._getLocalRotatedPointer(e),n=this.text.split(this._reNewline),r=0,i=0,s=0,o=0,u;for(var a=0,f=n.length;a<f;a++){s+=this._getHeightOfLine(this.ctx,a)*this.scaleY;var l=this._getWidthOfLine(this.ctx,a,n),c=this._getLineLeftOffset(l);i=c*this.scaleX,this.flipX&&(n[a]=n[a].split("").reverse().join(""));for(var h=0,p=n[a].length;h<p;h++){var d=n[a][h];r=i,i+=this._getWidthOfChar(this.ctx,d,a,this.flipX?p-h:h)*this.scaleX;if(s<=t.y||i<=t.x){o++;continue}return this._getNewSelectionStartFromOffset(t,r,i,o+a,p)}if(t.y<s)return this._getNewSelectionStartFromOffset(t,r,i,o+a,p)}if(typeof u=="undefined")return this.text.length},_getNewSelectionStartFromOffset:function(e,t,n,r,i){var s=e.x-t,o=n-e.x,u=o>s?0:1,a=r+u;return this.flipX&&(a=i-a),a>this.text.length&&(a=this.text.length),a}}),fabric.util.object.extend(fabric.IText.prototype,{initHiddenTextarea:function(){this.hiddenTextarea=fabric.document.createElement("textarea"),this.hiddenTextarea.setAttribute("autocapitalize","off"),this.hiddenTextarea.style.cssText="position: fixed; bottom: 20px; left: 0px; opacity: 0; width: 0px; height: 0px; z-index: -999;",fabric.document.body.appendChild(this.hiddenTextarea),fabric.util.addListener(this.hiddenTextarea,"keydown",this.onKeyDown.bind(this)),fabric.util.addListener(this.hiddenTextarea,"keypress",this.onKeyPress.bind(this)),fabric.util.addListener(this.hiddenTextarea,"copy",this.copy.bind(this)),fabric.util.addListener(this.hiddenTextarea,"paste",this.paste.bind(this)),!this._clickHandlerInitialized&&this.canvas&&(fabric.util.addListener(this.canvas.upperCanvasEl,"click",this.onClick.bind(this)),this._clickHandlerInitialized=!0)},_keysMap:{8:"removeChars",9:"exitEditing",27:"exitEditing",13:"insertNewline",33:"moveCursorUp",34:"moveCursorDown",35:"moveCursorRight",36:"moveCursorLeft",37:"moveCursorLeft",38:"moveCursorUp",39:"moveCursorRight",40:"moveCursorDown",46:"forwardDelete"},_ctrlKeysMap:{65:"selectAll",88:"cut"},onClick:function(){this.hiddenTextarea&&this.hiddenTextarea.focus()},onKeyDown:function(e){if(!this.isEditing)return;if(e.keyCode in this._keysMap)this[this._keysMap[e.keyCode]](e);else{if(!(e.keyCode in this._ctrlKeysMap&&(e.ctrlKey||e.metaKey)))return;this[this._ctrlKeysMap[e.keyCode]](e)}e.stopImmediatePropagation(),e.preventDefault(),this.canvas&&this.canvas.renderAll()},forwardDelete:function(e){this.selectionStart===this.selectionEnd&&this.moveCursorRight(e),this.removeChars(e)},copy:function(e){var t=this.getSelectedText(),n=this._getClipboardData(e);n&&n.setData("text",t),this.copiedText=t,this.copiedStyles=this.getSelectionStyles(this.selectionStart,this.selectionEnd)},paste:function(e){var t=null,n=this._getClipboardData(e);n?t=n.getData("text"):t=this.copiedText,t&&this.insertChars(t)},cut:function(e){if(this.selectionStart===this.selectionEnd)return;this.copy(),this.removeChars(e)},_getClipboardData:function(e){return e&&(e.clipboardData||fabric.window.clipboardData)},onKeyPress:function(e){if(!this.isEditing||e.metaKey||e.ctrlKey)return;e.which!==0&&this.insertChars(String.fromCharCode(e.which)),e.stopPropagation()},getDownCursorOffset:function(e,t){var n=t?this.selectionEnd:this.selectionStart,r=this.text.split(this._reNewline),i,s,o=this.text.slice(0,n),u=this.text.slice(n),a=o.slice(o.lastIndexOf("\n")+1),f=u.match(/(.*)\n?/)[1],l=(u.match(/.*\n(.*)\n?/)||{})[1]||"",c=this.get2DCursorLocation(n);if(c.lineIndex===r.length-1||e.metaKey||e.keyCode===34)return this.text.length-n;var h=this._getWidthOfLine(this.ctx,c.lineIndex,r);s=this._getLineLeftOffset(h);var p=s,d=c.lineIndex;for(var v=0,m=a.length;v<m;v++)i=a[v],p+=this._getWidthOfChar(this.ctx,i,d,v);var g=this._getIndexOnNextLine(c,l,p,r);return f.length+1+g},_getIndexOnNextLine:function(e,t,n,r){var i=e.lineIndex+1,s=this._getWidthOfLine(this.ctx,i,r),o=this._getLineLeftOffset(s),u=o,a=0,f;for(var l=0,c=t.length;l<c;l++){var h=t[l],p=this._getWidthOfChar(this.ctx,h,i,l);u+=p;if(u>n){f=!0;var d=u-p,v=u,m=Math.abs(d-n),g=Math.abs(v-n);a=g<m?l+1:l;break}}return f||(a=t.length),a},moveCursorDown:function(e){this.abortCursorAnimation(),this._currentCursorOpacity=1;var t=this.getDownCursorOffset(e,this._selectionDirection==="right");e.shiftKey?this.moveCursorDownWithShift(t):this.moveCursorDownWithoutShift(t),this.initDelayedCursor()},moveCursorDownWithoutShift:function(e){this._selectionDirection="right",this.selectionStart+=e,this.selectionStart>this.text.length&&(this.selectionStart=this.text.length),this.selectionEnd=this.selectionStart,this.fire("selection:changed"),this.canvas&&this.canvas.fire("text:selection:changed",{target:this})},swapSelectionPoints:function(){var e=this.selectionEnd;this.selectionEnd=this.selectionStart,this.selectionStart=e},moveCursorDownWithShift:function(e){this.selectionEnd===this.selectionStart&&(this._selectionDirection="right");var t=this._selectionDirection==="right"?"selectionEnd":"selectionStart";this[t]+=e,this.selectionEnd<this.selectionStart&&this._selectionDirection==="left"&&(this.swapSelectionPoints(),this._selectionDirection="right"),this.selectionEnd>this.text.length&&(this.selectionEnd=this.text.length),this.fire("selection:changed"),this.canvas&&this.canvas.fire("text:selection:changed",{target:this})},getUpCursorOffset:function(e,t){var n=t?this.selectionEnd:this.selectionStart,r=this.get2DCursorLocation(n);if(r.lineIndex===0||e.metaKey||e.keyCode===33)return n;var i=this.text.slice(0,n),s=i.slice(i.lastIndexOf("\n")+1),o=(i.match(/\n?(.*)\n.*$/)||{})[1]||"",u=this.text.split(this._reNewline),a,f=this._getWidthOfLine(this.ctx,r.lineIndex,u),l=this._getLineLeftOffset(f),c=l,h=r.lineIndex;for(var p=0,d=s.length;p<d;p++)a=s[p],c+=this._getWidthOfChar(this.ctx,a,h,p);var v=this._getIndexOnPrevLine(r,o,c,u);return o.length-v+s.length},_getIndexOnPrevLine:function(e,t,n,r){var i=e.lineIndex-1,s=this._getWidthOfLine(this.ctx,i,r),o=this._getLineLeftOffset(s),u=o,a=0,f;for(var l=0,c=t.length;l<c;l++){var h=t[l],p=this._getWidthOfChar(this.ctx,h,i,l);u+=p;if(u>n){f=!0;var d=u-p,v=u,m=Math.abs(d-n),g=Math.abs(v-n);a=g<m?l:l-1;break}}return f||(a=t.length-1),a},moveCursorUp:function(e){this.abortCursorAnimation(),this._currentCursorOpacity=1;var t=this.getUpCursorOffset(e,this._selectionDirection==="right");e.shiftKey?this.moveCursorUpWithShift(t):this.moveCursorUpWithoutShift(t),this.initDelayedCursor()},moveCursorUpWithShift:function(e){this.selectionEnd===this.selectionStart&&(this._selectionDirection="left");var t=this._selectionDirection==="right"?"selectionEnd":"selectionStart";this[t]-=e,this.selectionEnd<this.selectionStart&&this._selectionDirection==="right"&&(this.swapSelectionPoints(),this._selectionDirection="left"),this.selectionStart<0&&(this.selectionStart=0),this.fire("selection:changed"),this.canvas&&this.canvas.fire("text:selection:changed",{target:this})},moveCursorUpWithoutShift:function(e){this.selectionStart===this.selectionEnd&&(this.selectionStart-=e),this.selectionStart<0&&(this.selectionStart=0),this.selectionEnd=this.selectionStart,this._selectionDirection="left",this.fire("selection:changed"),this.canvas&&this.canvas.fire("text:selection:changed",{target:this})},moveCursorLeft:function(e){if(this.selectionStart===0&&this.selectionEnd===0)return;this.abortCursorAnimation(),this._currentCursorOpacity=1,e.shiftKey?this.moveCursorLeftWithShift(e):this.moveCursorLeftWithoutShift(e),this.initDelayedCursor()},_move:function(e,t,n){e.altKey?this[t]=this["findWordBoundary"+n](this[t]):e.metaKey||e.keyCode===35||e.keyCode===36?this[t]=this["findLineBoundary"+n](this[t]):this[t]+=n==="Left"?-1:1},_moveLeft:function(e,t){this._move(e,t,"Left")},_moveRight:function(e,t){this._move(e,t,"Right")},moveCursorLeftWithoutShift:function(e){this._selectionDirection="left",this.selectionEnd===this.selectionStart&&this._moveLeft(e,"selectionStart"),this.selectionEnd=this.selectionStart,this.fire("selection:changed"),this.canvas&&this.canvas.fire("text:selection:changed",{target:this})},moveCursorLeftWithShift:function(e){this._selectionDirection==="right"&&this.selectionStart!==this.selectionEnd?this._moveLeft(e,"selectionEnd"):(this._selectionDirection="left",this._moveLeft(e,"selectionStart"),this.text.charAt(this.selectionStart)==="\n"&&this.selectionStart--,this.selectionStart<0&&(this.selectionStart=0)),this.fire("selection:changed"),this.canvas&&this.canvas.fire("text:selection:changed",{target:this})},moveCursorRight:function(e){if(this.selectionStart>=this.text.length&&this.selectionEnd>=this.text.length)return;this.abortCursorAnimation(),this._currentCursorOpacity=1,e.shiftKey?this.moveCursorRightWithShift(e):this.moveCursorRightWithoutShift(e),this.initDelayedCursor()},moveCursorRightWithShift:function(e){this._selectionDirection==="left"&&this.selectionStart!==this.selectionEnd?this._moveRight(e,"selectionStart"):(this._selectionDirection="right",this._moveRight(e,"selectionEnd"),this.text.charAt(this.selectionEnd-1)==="\n"&&this.selectionEnd++,this.selectionEnd>this.text.length&&(this.selectionEnd=this.text.length)),this.fire("selection:changed"),this.canvas&&this.canvas.fire("text:selection:changed",{target:this})},moveCursorRightWithoutShift:function(e){this._selectionDirection="right",this.selectionStart===this.selectionEnd?(this._moveRight(e,"selectionStart"),this.selectionEnd=this.selectionStart):(this.selectionEnd+=this.getNumNewLinesInSelectedText(),this.selectionEnd>this.text.length&&(this.selectionEnd=this.text.length),this.selectionStart=this.selectionEnd),this.fire("selection:changed"),this.canvas&&this.canvas.fire("text:selection:changed",{target:this})},removeChars:function(e){this.selectionStart===this.selectionEnd?this._removeCharsNearCursor(e):this._removeCharsFromTo(this.selectionStart,this.selectionEnd),this.selectionEnd=this.selectionStart,this._removeExtraneousStyles(),this.canvas&&this.canvas.renderAll().renderAll(),this.setCoords(),this.fire("changed"),this.canvas&&this.canvas.fire("text:changed",{target:this})},_removeCharsNearCursor:function(e){if(this.selectionStart!==0)if(e.metaKey){var t=this.findLineBoundaryLeft(this.selectionStart);this._removeCharsFromTo(t,this.selectionStart),this.selectionStart=t}else if(e.altKey){var n=this.findWordBoundaryLeft(this.selectionStart);this._removeCharsFromTo(n,this.selectionStart),this.selectionStart=n}else{var r=this.text.slice(this.selectionStart-1,this.selectionStart)==="\n";this.removeStyleObject(r),this.selectionStart--,this.text=this.text.slice(0,this.selectionStart)+this.text.slice(this.selectionStart+1)}}}),fabric.util.object.extend(fabric.IText.prototype,{_setSVGTextLineText:function(e,t,n,r,i,s){this.styles[t]?this._setSVGTextLineChars(e,t,n,r,i,s):this.callSuper("_setSVGTextLineText",e,t,n,r,i)},_setSVGTextLineChars:function(e,t,n,r,i,s){var o=t===0||this.useNative?"y":"dy",u=e.split(""),a=0,f=this._getSVGLineLeftOffset(t),l=this._getSVGLineTopOffset(t),c=this._getHeightOfLine(this.ctx,t);for(var h=0,p=u.length;h<p;h++){var d=this.styles[t][h]||{};n.push(this._createTextCharSpan(u[h],d,f,l,o,a));var v=this._getWidthOfChar(this.ctx,u[h],t,h);d.textBackgroundColor&&s.push(this._createTextCharBg(d,f,l,c,v,a)),a+=v}},_getSVGLineLeftOffset:function(e){return this._boundaries&&this._boundaries[e]?fabric.util.toFixed(this._boundaries[e].left,2):0},_getSVGLineTopOffset:function(e){var t=0;for(var n=0;n<=e;n++)t+=this._getHeightOfLine(this.ctx,n);return t-this.height/2},_createTextCharBg:function(e,t,n,r,i,s){return['<rect fill="',e.textBackgroundColor,'" transform="translate(',-this.width/2," ",-this.height+r,")",'" x="',t+s,'" y="',n+r,'" width="',i,'" height="',r,'"></rect>'].join("")},_createTextCharSpan:function(e,t,n,r,i,s){var o=this.getSvgStyles.call(fabric.util.object.extend({visible:!0,fill:this.fill,stroke:this.stroke,type:"text"},t));return['<tspan x="',n+s,'" ',i,'="',r,'" ',t.fontFamily?'font-family="'+t.fontFamily.replace(/"/g,"'")+'" ':"",t.fontSize?'font-size="'+t.fontSize+'" ':"",t.fontStyle?'font-style="'+t.fontStyle+'" ':"",t.fontWeight?'font-weight="'+t.fontWeight+'" ':"",t.textDecoration?'text-decoration="'+t.textDecoration+'" ':"",'style="',o,'">',fabric.util.string.escapeXml(e),"</tspan>"].join("")}}),function(){function request(e,t,n){var r=URL.parse(e);r.port||(r.port=r.protocol.indexOf("https:")===0?443:80);var i=r.port===443?HTTPS:HTTP,s=i.request({hostname:r.hostname,port:r.port,path:r.path,method:"GET"},function(e){var r="";t&&e.setEncoding(t),e.on("end",function(){n(r)}),e.on("data",function(t){e.statusCode===200&&(r+=t)})});s.on("error",function(e){e.errno===process.ECONNREFUSED?fabric.log("ECONNREFUSED: connection refused to "+r.hostname+":"+r.port):fabric.log(e.message)}),s.end()}function requestFs(e,t){var n=require("fs");n.readFile(e,function(e,n){if(e)throw fabric.log(e),e;t(n)})}if(typeof document!="undefined"&&typeof window!="undefined")return;var DOMParser=require("xmldom").DOMParser,URL=require("url"),HTTP=require("http"),HTTPS=require("https"),Canvas=require("canvas"),Image=require("canvas").Image;fabric.util.loadImage=function(e,t,n){function r(r){i.src=new Buffer(r,"binary"),i._src=e,t&&t.call(n,i)}var i=new Image;e&&(e instanceof Buffer||e.indexOf("data")===0)?(i.src=i._src=e,t&&t.call(n,i)):e&&e.indexOf("http")!==0?requestFs(e,r):e?request(e,"binary",r):t&&t.call(n,e)},fabric.loadSVGFromURL=function(e,t,n){e=e.replace(/^\n\s*/,"").replace(/\?.*$/,"").trim(),e.indexOf("http")!==0?requestFs(e,function(e){fabric.loadSVGFromString(e.toString(),t,n)}):request(e,"",function(e){fabric.loadSVGFromString(e,t,n)})},fabric.loadSVGFromString=function(e,t,n){var r=(new DOMParser).parseFromString(e);fabric.parseSVGDocument(r.documentElement,function(e,n){t&&t(e,n)},n)},fabric.util.getScript=function(url,callback){request(url,"",function(body){eval(body),callback&&callback()})},fabric.Image.fromObject=function(e,t){fabric.util.loadImage(e.src,function(n){var r=new fabric.Image(n);r._initConfig(e),r._initFilters(e,function(e){r.filters=e||[],t&&t(r)})})},fabric.createCanvasForNode=function(
+e,t,n,r){r=r||n;var i=fabric.document.createElement("canvas"),s=new Canvas(e||600,t||600,r);i.style={},i.width=s.width,i.height=s.height;var o=fabric.Canvas||fabric.StaticCanvas,u=new o(i,n);return u.contextContainer=s.getContext("2d"),u.nodeCanvas=s,u.Font=Canvas.Font,u},fabric.StaticCanvas.prototype.createPNGStream=function(){return this.nodeCanvas.createPNGStream()},fabric.StaticCanvas.prototype.createJPEGStream=function(e){return this.nodeCanvas.createJPEGStream(e)};var origSetWidth=fabric.StaticCanvas.prototype.setWidth;fabric.StaticCanvas.prototype.setWidth=function(e,t){return origSetWidth.call(this,e,t),this.nodeCanvas.width=e,this},fabric.Canvas&&(fabric.Canvas.prototype.setWidth=fabric.StaticCanvas.prototype.setWidth);var origSetHeight=fabric.StaticCanvas.prototype.setHeight;fabric.StaticCanvas.prototype.setHeight=function(e,t){return origSetHeight.call(this,e,t),this.nodeCanvas.height=e,this},fabric.Canvas&&(fabric.Canvas.prototype.setHeight=fabric.StaticCanvas.prototype.setHeight)}();
\ No newline at end of file
diff --git a/js/filedrop.field.js b/js/filedrop.field.js
index d25c0d461c8cbd56954c9484a3bdebda32f6ec57..ce0bf154496ae63cdccdbe5c5537649bfde3f65d 100644
--- a/js/filedrop.field.js
+++ b/js/filedrop.field.js
@@ -109,7 +109,7 @@
             // Upload failed. TODO: Add a button to the UI to retry on
             // HTTP 500
             return e.remove();
-          e.find('[name="'+that.options.name+'"]').val(json.id);
+          e.find('[name="'+that.options.name+'"]').val(''+json.id+','+file.name);
           e.data('fileId', json.id);
           e.find('.progress-bar')
             .width('100%')
@@ -171,7 +171,6 @@
             .hide())
           .append($('<input type="hidden"/>').attr('name', this.options.name)
             .val(file.id))
-          .append($('<div class="clear"></div>'));
       if (this.options.deletable) {
         filenode.prepend($('<span><i class="icon-trash"></i></span>')
           .addClass('trash pull-right')
@@ -197,7 +196,7 @@
         var i = this.uploads.indexOf(filenode);
         if (i !== -1)
             this.uploads.splice(i,1);
-        filenode.slideUp('fast', function() { this.remove(); });
+        filenode.slideUp('fast', function() { $(this).remove(); });
       }
     },
     cancelUpload: function(node) {
@@ -350,8 +349,8 @@
     this.on('drop', drop).on('dragstart', opts.dragStart).on('dragenter', dragEnter).on('dragover', dragOver).on('dragleave', dragLeave);
     $(document).on('drop', docDrop).on('dragenter', docEnter).on('dragover', docOver).on('dragleave', docLeave);
 
-    (opts.link || this).on('click', function(e){
-      $('#' + opts.fallback_id).trigger(e);
+    (opts.link || this).click(function(e) {
+      $('#' + opts.fallback_id).trigger('click');
       return false;
     });
 
diff --git a/js/jquery-1.11.2.min.js b/js/jquery-1.11.2.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..e6a051d0d1d32752ae08d9cf30fe5952da22a9b0
--- /dev/null
+++ b/js/jquery-1.11.2.min.js
@@ -0,0 +1,4 @@
+/*! jQuery v1.11.2 | (c) 2005, 2014 jQuery Foundation, Inc. | jquery.org/license */
+!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l="1.11.2",m=function(a,b){return new m.fn.init(a,b)},n=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,o=/^-ms-/,p=/-([\da-z])/gi,q=function(a,b){return b.toUpperCase()};m.fn=m.prototype={jquery:l,constructor:m,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=m.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return m.each(this,a,b)},map:function(a){return this.pushStack(m.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},m.extend=m.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||m.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(e=arguments[h]))for(d in e)a=g[d],c=e[d],g!==c&&(j&&c&&(m.isPlainObject(c)||(b=m.isArray(c)))?(b?(b=!1,f=a&&m.isArray(a)?a:[]):f=a&&m.isPlainObject(a)?a:{},g[d]=m.extend(j,f,c)):void 0!==c&&(g[d]=c));return g},m.extend({expando:"jQuery"+(l+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===m.type(a)},isArray:Array.isArray||function(a){return"array"===m.type(a)},isWindow:function(a){return null!=a&&a==a.window},isNumeric:function(a){return!m.isArray(a)&&a-parseFloat(a)+1>=0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},isPlainObject:function(a){var b;if(!a||"object"!==m.type(a)||a.nodeType||m.isWindow(a))return!1;try{if(a.constructor&&!j.call(a,"constructor")&&!j.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}if(k.ownLast)for(b in a)return j.call(a,b);for(b in a);return void 0===b||j.call(a,b)},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(b){b&&m.trim(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(o,"ms-").replace(p,q)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=r(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(n,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(r(Object(a))?m.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){var d;if(b){if(g)return g.call(b,a,c);for(d=b.length,c=c?0>c?Math.max(0,d+c):c:0;d>c;c++)if(c in b&&b[c]===a)return c}return-1},merge:function(a,b){var c=+b.length,d=0,e=a.length;while(c>d)a[e++]=b[d++];if(c!==c)while(void 0!==b[d])a[e++]=b[d++];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=r(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(f=a[b],b=a,a=f),m.isFunction(a)?(c=d.call(arguments,2),e=function(){return a.apply(b||this,c.concat(d.call(arguments)))},e.guid=a.guid=a.guid||m.guid++,e):void 0},now:function(){return+new Date},support:k}),m.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function r(a){var b=a.length,c=m.type(a);return"function"===c||m.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var s=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=hb(),z=hb(),A=hb(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ab=/[+~]/,bb=/'|\\/g,cb=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),db=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},eb=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fb){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function gb(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(bb,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+rb(o[l]);w=ab.test(a)&&pb(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function hb(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ib(a){return a[u]=!0,a}function jb(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function kb(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function lb(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function mb(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function nb(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function ob(a){return ib(function(b){return b=+b,ib(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pb(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=gb.support={},f=gb.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=gb.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",eb,!1):e.attachEvent&&e.attachEvent("onunload",eb)),p=!f(g),c.attributes=jb(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=jb(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=jb(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(cb,db);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(jb(function(a){o.appendChild(a).innerHTML="<a id='"+u+"'></a><select id='"+u+"-\f]' msallowcapture=''><option selected=''></option></select>",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),jb(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&jb(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return lb(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?lb(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},gb.matches=function(a,b){return gb(a,null,null,b)},gb.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return gb(b,n,null,[a]).length>0},gb.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},gb.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},gb.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},gb.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=gb.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=gb.selectors={cacheLength:50,createPseudo:ib,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(cb,db),a[3]=(a[3]||a[4]||a[5]||"").replace(cb,db),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||gb.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&gb.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(cb,db).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=gb.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||gb.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ib(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ib(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ib(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ib(function(a){return function(b){return gb(a,b).length>0}}),contains:ib(function(a){return a=a.replace(cb,db),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ib(function(a){return W.test(a||"")||gb.error("unsupported lang: "+a),a=a.replace(cb,db).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:ob(function(){return[0]}),last:ob(function(a,b){return[b-1]}),eq:ob(function(a,b,c){return[0>c?c+b:c]}),even:ob(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:ob(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:ob(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:ob(function(a,b,c){for(var d=0>c?c+b:c;++d<b;)a.push(d);return a})}},d.pseudos.nth=d.pseudos.eq;for(b in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})d.pseudos[b]=mb(b);for(b in{submit:!0,reset:!0})d.pseudos[b]=nb(b);function qb(){}qb.prototype=d.filters=d.pseudos,d.setFilters=new qb,g=gb.tokenize=function(a,b){var c,e,f,g,h,i,j,k=z[a+" "];if(k)return b?0:k.slice(0);h=a,i=[],j=d.preFilter;while(h){(!c||(e=S.exec(h)))&&(e&&(h=h.slice(e[0].length)||h),i.push(f=[])),c=!1,(e=T.exec(h))&&(c=e.shift(),f.push({value:c,type:e[0].replace(R," ")}),h=h.slice(c.length));for(g in d.filter)!(e=X[g].exec(h))||j[g]&&!(e=j[g](e))||(c=e.shift(),f.push({value:c,type:g,matches:e}),h=h.slice(c.length));if(!c)break}return b?h.length:h?gb.error(a):z(a,i).slice(0)};function rb(a){for(var b=0,c=a.length,d="";c>b;b++)d+=a[b].value;return d}function sb(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function tb(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ub(a,b,c){for(var d=0,e=b.length;e>d;d++)gb(a,b[d],c);return c}function vb(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wb(a,b,c,d,e,f){return d&&!d[u]&&(d=wb(d)),e&&!e[u]&&(e=wb(e,f)),ib(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ub(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:vb(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=vb(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=vb(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xb(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sb(function(a){return a===b},h,!0),l=sb(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sb(tb(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wb(i>1&&tb(m),i>1&&rb(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xb(a.slice(i,e)),f>e&&xb(a=a.slice(e)),f>e&&rb(a))}m.push(c)}return tb(m)}function yb(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=vb(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&gb.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ib(f):f}return h=gb.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xb(b[c]),f[u]?d.push(f):e.push(f);f=A(a,yb(e,d)),f.selector=a}return f},i=gb.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(cb,db),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(cb,db),ab.test(j[0].type)&&pb(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&rb(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,ab.test(a)&&pb(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=jb(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),jb(function(a){return a.innerHTML="<a href='#'></a>","#"===a.firstChild.getAttribute("href")})||kb("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&jb(function(a){return a.innerHTML="<input/>",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||kb("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),jb(function(a){return null==a.getAttribute("disabled")})||kb(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),gb}(a);m.find=s,m.expr=s.selectors,m.expr[":"]=m.expr.pseudos,m.unique=s.uniqueSort,m.text=s.getText,m.isXMLDoc=s.isXML,m.contains=s.contains;var t=m.expr.match.needsContext,u=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,v=/^.[^:#\[\.,]*$/;function w(a,b,c){if(m.isFunction(b))return m.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return m.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(v.test(b))return m.filter(b,a,c);b=m.filter(b,a)}return m.grep(a,function(a){return m.inArray(a,b)>=0!==c})}m.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?m.find.matchesSelector(d,a)?[d]:[]:m.find.matches(a,m.grep(b,function(a){return 1===a.nodeType}))},m.fn.extend({find:function(a){var b,c=[],d=this,e=d.length;if("string"!=typeof a)return this.pushStack(m(a).filter(function(){for(b=0;e>b;b++)if(m.contains(d[b],this))return!0}));for(b=0;e>b;b++)m.find(a,d[b],c);return c=this.pushStack(e>1?m.unique(c):c),c.selector=this.selector?this.selector+" "+a:a,c},filter:function(a){return this.pushStack(w(this,a||[],!1))},not:function(a){return this.pushStack(w(this,a||[],!0))},is:function(a){return!!w(this,"string"==typeof a&&t.test(a)?m(a):a||[],!1).length}});var x,y=a.document,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=m.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a.charAt(0)&&">"===a.charAt(a.length-1)&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||x).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof m?b[0]:b,m.merge(this,m.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:y,!0)),u.test(c[1])&&m.isPlainObject(b))for(c in b)m.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}if(d=y.getElementById(c[2]),d&&d.parentNode){if(d.id!==c[2])return x.find(a);this.length=1,this[0]=d}return this.context=y,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):m.isFunction(a)?"undefined"!=typeof x.ready?x.ready(a):a(m):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),m.makeArray(a,this))};A.prototype=m.fn,x=m(y);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};m.extend({dir:function(a,b,c){var d=[],e=a[b];while(e&&9!==e.nodeType&&(void 0===c||1!==e.nodeType||!m(e).is(c)))1===e.nodeType&&d.push(e),e=e[b];return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),m.fn.extend({has:function(a){var b,c=m(a,this),d=c.length;return this.filter(function(){for(b=0;d>b;b++)if(m.contains(this,c[b]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=t.test(a)||"string"!=typeof a?m(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&m.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?m.unique(f):f)},index:function(a){return a?"string"==typeof a?m.inArray(this[0],m(a)):m.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(m.unique(m.merge(this.get(),m(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){do a=a[b];while(a&&1!==a.nodeType);return a}m.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return m.dir(a,"parentNode")},parentsUntil:function(a,b,c){return m.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return m.dir(a,"nextSibling")},prevAll:function(a){return m.dir(a,"previousSibling")},nextUntil:function(a,b,c){return m.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return m.dir(a,"previousSibling",c)},siblings:function(a){return m.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return m.sibling(a.firstChild)},contents:function(a){return m.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:m.merge([],a.childNodes)}},function(a,b){m.fn[a]=function(c,d){var e=m.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=m.filter(d,e)),this.length>1&&(C[a]||(e=m.unique(e)),B.test(a)&&(e=e.reverse())),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return m.each(a.match(E)||[],function(a,c){b[c]=!0}),b}m.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):m.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(c=a.memory&&l,d=!0,f=g||0,g=0,e=h.length,b=!0;h&&e>f;f++)if(h[f].apply(l[0],l[1])===!1&&a.stopOnFalse){c=!1;break}b=!1,h&&(i?i.length&&j(i.shift()):c?h=[]:k.disable())},k={add:function(){if(h){var d=h.length;!function f(b){m.each(b,function(b,c){var d=m.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&f(c)})}(arguments),b?e=h.length:c&&(g=d,j(c))}return this},remove:function(){return h&&m.each(arguments,function(a,c){var d;while((d=m.inArray(c,h,d))>-1)h.splice(d,1),b&&(e>=d&&e--,f>=d&&f--)}),this},has:function(a){return a?m.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],e=0,this},disable:function(){return h=i=c=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,c||k.disable(),this},locked:function(){return!i},fireWith:function(a,c){return!h||d&&!i||(c=c||[],c=[a,c.slice?c.slice():c],b?i.push(c):j(c)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!d}};return k},m.extend({Deferred:function(a){var b=[["resolve","done",m.Callbacks("once memory"),"resolved"],["reject","fail",m.Callbacks("once memory"),"rejected"],["notify","progress",m.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return m.Deferred(function(c){m.each(b,function(b,f){var g=m.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&m.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?m.extend(a,d):d}},e={};return d.pipe=d.then,m.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&m.isFunction(a.promise)?e:0,g=1===f?a:m.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&m.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;m.fn.ready=function(a){return m.ready.promise().done(a),this},m.extend({isReady:!1,readyWait:1,holdReady:function(a){a?m.readyWait++:m.ready(!0)},ready:function(a){if(a===!0?!--m.readyWait:!m.isReady){if(!y.body)return setTimeout(m.ready);m.isReady=!0,a!==!0&&--m.readyWait>0||(H.resolveWith(y,[m]),m.fn.triggerHandler&&(m(y).triggerHandler("ready"),m(y).off("ready")))}}});function I(){y.addEventListener?(y.removeEventListener("DOMContentLoaded",J,!1),a.removeEventListener("load",J,!1)):(y.detachEvent("onreadystatechange",J),a.detachEvent("onload",J))}function J(){(y.addEventListener||"load"===event.type||"complete"===y.readyState)&&(I(),m.ready())}m.ready.promise=function(b){if(!H)if(H=m.Deferred(),"complete"===y.readyState)setTimeout(m.ready);else if(y.addEventListener)y.addEventListener("DOMContentLoaded",J,!1),a.addEventListener("load",J,!1);else{y.attachEvent("onreadystatechange",J),a.attachEvent("onload",J);var c=!1;try{c=null==a.frameElement&&y.documentElement}catch(d){}c&&c.doScroll&&!function e(){if(!m.isReady){try{c.doScroll("left")}catch(a){return setTimeout(e,50)}I(),m.ready()}}()}return H.promise(b)};var K="undefined",L;for(L in m(k))break;k.ownLast="0"!==L,k.inlineBlockNeedsLayout=!1,m(function(){var a,b,c,d;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",k.inlineBlockNeedsLayout=a=3===b.offsetWidth,a&&(c.style.zoom=1)),c.removeChild(d))}),function(){var a=y.createElement("div");if(null==k.deleteExpando){k.deleteExpando=!0;try{delete a.test}catch(b){k.deleteExpando=!1}}a=null}(),m.acceptData=function(a){var b=m.noData[(a.nodeName+" ").toLowerCase()],c=+a.nodeType||1;return 1!==c&&9!==c?!1:!b||b!==!0&&a.getAttribute("classid")===b};var M=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,N=/([A-Z])/g;function O(a,b,c){if(void 0===c&&1===a.nodeType){var d="data-"+b.replace(N,"-$1").toLowerCase();if(c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:M.test(c)?m.parseJSON(c):c}catch(e){}m.data(a,b,c)}else c=void 0}return c}function P(a){var b;for(b in a)if(("data"!==b||!m.isEmptyObject(a[b]))&&"toJSON"!==b)return!1;
+return!0}function Q(a,b,d,e){if(m.acceptData(a)){var f,g,h=m.expando,i=a.nodeType,j=i?m.cache:a,k=i?a[h]:a[h]&&h;if(k&&j[k]&&(e||j[k].data)||void 0!==d||"string"!=typeof b)return k||(k=i?a[h]=c.pop()||m.guid++:h),j[k]||(j[k]=i?{}:{toJSON:m.noop}),("object"==typeof b||"function"==typeof b)&&(e?j[k]=m.extend(j[k],b):j[k].data=m.extend(j[k].data,b)),g=j[k],e||(g.data||(g.data={}),g=g.data),void 0!==d&&(g[m.camelCase(b)]=d),"string"==typeof b?(f=g[b],null==f&&(f=g[m.camelCase(b)])):f=g,f}}function R(a,b,c){if(m.acceptData(a)){var d,e,f=a.nodeType,g=f?m.cache:a,h=f?a[m.expando]:m.expando;if(g[h]){if(b&&(d=c?g[h]:g[h].data)){m.isArray(b)?b=b.concat(m.map(b,m.camelCase)):b in d?b=[b]:(b=m.camelCase(b),b=b in d?[b]:b.split(" ")),e=b.length;while(e--)delete d[b[e]];if(c?!P(d):!m.isEmptyObject(d))return}(c||(delete g[h].data,P(g[h])))&&(f?m.cleanData([a],!0):k.deleteExpando||g!=g.window?delete g[h]:g[h]=null)}}}m.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(a){return a=a.nodeType?m.cache[a[m.expando]]:a[m.expando],!!a&&!P(a)},data:function(a,b,c){return Q(a,b,c)},removeData:function(a,b){return R(a,b)},_data:function(a,b,c){return Q(a,b,c,!0)},_removeData:function(a,b){return R(a,b,!0)}}),m.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=m.data(f),1===f.nodeType&&!m._data(f,"parsedAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=m.camelCase(d.slice(5)),O(f,d,e[d])));m._data(f,"parsedAttrs",!0)}return e}return"object"==typeof a?this.each(function(){m.data(this,a)}):arguments.length>1?this.each(function(){m.data(this,a,b)}):f?O(f,a,m.data(f,a)):void 0},removeData:function(a){return this.each(function(){m.removeData(this,a)})}}),m.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=m._data(a,b),c&&(!d||m.isArray(c)?d=m._data(a,b,m.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=m.queue(a,b),d=c.length,e=c.shift(),f=m._queueHooks(a,b),g=function(){m.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return m._data(a,c)||m._data(a,c,{empty:m.Callbacks("once memory").add(function(){m._removeData(a,b+"queue"),m._removeData(a,c)})})}}),m.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length<c?m.queue(this[0],a):void 0===b?this:this.each(function(){var c=m.queue(this,a,b);m._queueHooks(this,a),"fx"===a&&"inprogress"!==c[0]&&m.dequeue(this,a)})},dequeue:function(a){return this.each(function(){m.dequeue(this,a)})},clearQueue:function(a){return this.queue(a||"fx",[])},promise:function(a,b){var c,d=1,e=m.Deferred(),f=this,g=this.length,h=function(){--d||e.resolveWith(f,[f])};"string"!=typeof a&&(b=a,a=void 0),a=a||"fx";while(g--)c=m._data(f[g],a+"queueHooks"),c&&c.empty&&(d++,c.empty.add(h));return h(),e.promise(b)}});var S=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,T=["Top","Right","Bottom","Left"],U=function(a,b){return a=b||a,"none"===m.css(a,"display")||!m.contains(a.ownerDocument,a)},V=m.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===m.type(c)){e=!0;for(h in c)m.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,m.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(m(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f},W=/^(?:checkbox|radio)$/i;!function(){var a=y.createElement("input"),b=y.createElement("div"),c=y.createDocumentFragment();if(b.innerHTML="  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",k.leadingWhitespace=3===b.firstChild.nodeType,k.tbody=!b.getElementsByTagName("tbody").length,k.htmlSerialize=!!b.getElementsByTagName("link").length,k.html5Clone="<:nav></:nav>"!==y.createElement("nav").cloneNode(!0).outerHTML,a.type="checkbox",a.checked=!0,c.appendChild(a),k.appendChecked=a.checked,b.innerHTML="<textarea>x</textarea>",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue,c.appendChild(b),b.innerHTML="<input type='radio' checked='checked' name='t'/>",k.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,k.noCloneEvent=!0,b.attachEvent&&(b.attachEvent("onclick",function(){k.noCloneEvent=!1}),b.cloneNode(!0).click()),null==k.deleteExpando){k.deleteExpando=!0;try{delete b.test}catch(d){k.deleteExpando=!1}}}(),function(){var b,c,d=y.createElement("div");for(b in{submit:!0,change:!0,focusin:!0})c="on"+b,(k[b+"Bubbles"]=c in a)||(d.setAttribute(c,"t"),k[b+"Bubbles"]=d.attributes[c].expando===!1);d=null}();var X=/^(?:input|select|textarea)$/i,Y=/^key/,Z=/^(?:mouse|pointer|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=/^([^.]*)(?:\.(.+)|)$/;function ab(){return!0}function bb(){return!1}function cb(){try{return y.activeElement}catch(a){}}m.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m._data(a);if(r){c.handler&&(i=c,c=i.handler,e=i.selector),c.guid||(c.guid=m.guid++),(g=r.events)||(g=r.events={}),(k=r.handle)||(k=r.handle=function(a){return typeof m===K||a&&m.event.triggered===a.type?void 0:m.event.dispatch.apply(k.elem,arguments)},k.elem=a),b=(b||"").match(E)||[""],h=b.length;while(h--)f=_.exec(b[h])||[],o=q=f[1],p=(f[2]||"").split(".").sort(),o&&(j=m.event.special[o]||{},o=(e?j.delegateType:j.bindType)||o,j=m.event.special[o]||{},l=m.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&m.expr.match.needsContext.test(e),namespace:p.join(".")},i),(n=g[o])||(n=g[o]=[],n.delegateCount=0,j.setup&&j.setup.call(a,d,p,k)!==!1||(a.addEventListener?a.addEventListener(o,k,!1):a.attachEvent&&a.attachEvent("on"+o,k))),j.add&&(j.add.call(a,l),l.handler.guid||(l.handler.guid=c.guid)),e?n.splice(n.delegateCount++,0,l):n.push(l),m.event.global[o]=!0);a=null}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,n,o,p,q,r=m.hasData(a)&&m._data(a);if(r&&(k=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=_.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=m.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,n=k[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),i=f=n.length;while(f--)g=n[f],!e&&q!==g.origType||c&&c.guid!==g.guid||h&&!h.test(g.namespace)||d&&d!==g.selector&&("**"!==d||!g.selector)||(n.splice(f,1),g.selector&&n.delegateCount--,l.remove&&l.remove.call(a,g));i&&!n.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||m.removeEvent(a,o,r.handle),delete k[o])}else for(o in k)m.event.remove(a,o+b[j],c,d,!0);m.isEmptyObject(k)&&(delete r.handle,m._removeData(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,l,n,o=[d||y],p=j.call(b,"type")?b.type:b,q=j.call(b,"namespace")?b.namespace.split("."):[];if(h=l=d=d||y,3!==d.nodeType&&8!==d.nodeType&&!$.test(p+m.event.triggered)&&(p.indexOf(".")>=0&&(q=p.split("."),p=q.shift(),q.sort()),g=p.indexOf(":")<0&&"on"+p,b=b[m.expando]?b:new m.Event(p,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=q.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:m.makeArray(c,[b]),k=m.event.special[p]||{},e||!k.trigger||k.trigger.apply(d,c)!==!1)){if(!e&&!k.noBubble&&!m.isWindow(d)){for(i=k.delegateType||p,$.test(i+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),l=h;l===(d.ownerDocument||y)&&o.push(l.defaultView||l.parentWindow||a)}n=0;while((h=o[n++])&&!b.isPropagationStopped())b.type=n>1?i:k.bindType||p,f=(m._data(h,"events")||{})[b.type]&&m._data(h,"handle"),f&&f.apply(h,c),f=g&&h[g],f&&f.apply&&m.acceptData(h)&&(b.result=f.apply(h,c),b.result===!1&&b.preventDefault());if(b.type=p,!e&&!b.isDefaultPrevented()&&(!k._default||k._default.apply(o.pop(),c)===!1)&&m.acceptData(d)&&g&&d[p]&&!m.isWindow(d)){l=d[g],l&&(d[g]=null),m.event.triggered=p;try{d[p]()}catch(r){}m.event.triggered=void 0,l&&(d[g]=l)}return b.result}},dispatch:function(a){a=m.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(m._data(this,"events")||{})[a.type]||[],k=m.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=m.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,g=0;while((e=f.handlers[g++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(e.namespace))&&(a.handleObj=e,a.data=e.data,c=((m.event.special[e.origType]||{}).handle||e.handler).apply(f.elem,i),void 0!==c&&(a.result=c)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!=this;i=i.parentNode||this)if(1===i.nodeType&&(i.disabled!==!0||"click"!==a.type)){for(e=[],f=0;h>f;f++)d=b[f],c=d.selector+" ",void 0===e[c]&&(e[c]=d.needsContext?m(c,this).index(i)>=0:m.find(c,this,null,[i]).length),e[c]&&e.push(d);e.length&&g.push({elem:i,handlers:e})}return h<b.length&&g.push({elem:this,handlers:b.slice(h)}),g},fix:function(a){if(a[m.expando])return a;var b,c,d,e=a.type,f=a,g=this.fixHooks[e];g||(this.fixHooks[e]=g=Z.test(e)?this.mouseHooks:Y.test(e)?this.keyHooks:{}),d=g.props?this.props.concat(g.props):this.props,a=new m.Event(f),b=d.length;while(b--)c=d[b],a[c]=f[c];return a.target||(a.target=f.srcElement||y),3===a.target.nodeType&&(a.target=a.target.parentNode),a.metaKey=!!a.metaKey,g.filter?g.filter(a,f):a},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(a,b){return null==a.which&&(a.which=null!=b.charCode?b.charCode:b.keyCode),a}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(a,b){var c,d,e,f=b.button,g=b.fromElement;return null==a.pageX&&null!=b.clientX&&(d=a.target.ownerDocument||y,e=d.documentElement,c=d.body,a.pageX=b.clientX+(e&&e.scrollLeft||c&&c.scrollLeft||0)-(e&&e.clientLeft||c&&c.clientLeft||0),a.pageY=b.clientY+(e&&e.scrollTop||c&&c.scrollTop||0)-(e&&e.clientTop||c&&c.clientTop||0)),!a.relatedTarget&&g&&(a.relatedTarget=g===a.target?b.toElement:g),a.which||void 0===f||(a.which=1&f?1:2&f?3:4&f?2:0),a}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==cb()&&this.focus)try{return this.focus(),!1}catch(a){}},delegateType:"focusin"},blur:{trigger:function(){return this===cb()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return m.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):void 0},_default:function(a){return m.nodeName(a.target,"a")}},beforeunload:{postDispatch:function(a){void 0!==a.result&&a.originalEvent&&(a.originalEvent.returnValue=a.result)}}},simulate:function(a,b,c,d){var e=m.extend(new m.Event,c,{type:a,isSimulated:!0,originalEvent:{}});d?m.event.trigger(e,null,b):m.event.dispatch.call(b,e),e.isDefaultPrevented()&&c.preventDefault()}},m.removeEvent=y.removeEventListener?function(a,b,c){a.removeEventListener&&a.removeEventListener(b,c,!1)}:function(a,b,c){var d="on"+b;a.detachEvent&&(typeof a[d]===K&&(a[d]=null),a.detachEvent(d,c))},m.Event=function(a,b){return this instanceof m.Event?(a&&a.type?(this.originalEvent=a,this.type=a.type,this.isDefaultPrevented=a.defaultPrevented||void 0===a.defaultPrevented&&a.returnValue===!1?ab:bb):this.type=a,b&&m.extend(this,b),this.timeStamp=a&&a.timeStamp||m.now(),void(this[m.expando]=!0)):new m.Event(a,b)},m.Event.prototype={isDefaultPrevented:bb,isPropagationStopped:bb,isImmediatePropagationStopped:bb,preventDefault:function(){var a=this.originalEvent;this.isDefaultPrevented=ab,a&&(a.preventDefault?a.preventDefault():a.returnValue=!1)},stopPropagation:function(){var a=this.originalEvent;this.isPropagationStopped=ab,a&&(a.stopPropagation&&a.stopPropagation(),a.cancelBubble=!0)},stopImmediatePropagation:function(){var a=this.originalEvent;this.isImmediatePropagationStopped=ab,a&&a.stopImmediatePropagation&&a.stopImmediatePropagation(),this.stopPropagation()}},m.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(a,b){m.event.special[a]={delegateType:b,bindType:b,handle:function(a){var c,d=this,e=a.relatedTarget,f=a.handleObj;return(!e||e!==d&&!m.contains(d,e))&&(a.type=f.origType,c=f.handler.apply(this,arguments),a.type=b),c}}}),k.submitBubbles||(m.event.special.submit={setup:function(){return m.nodeName(this,"form")?!1:void m.event.add(this,"click._submit keypress._submit",function(a){var b=a.target,c=m.nodeName(b,"input")||m.nodeName(b,"button")?b.form:void 0;c&&!m._data(c,"submitBubbles")&&(m.event.add(c,"submit._submit",function(a){a._submit_bubble=!0}),m._data(c,"submitBubbles",!0))})},postDispatch:function(a){a._submit_bubble&&(delete a._submit_bubble,this.parentNode&&!a.isTrigger&&m.event.simulate("submit",this.parentNode,a,!0))},teardown:function(){return m.nodeName(this,"form")?!1:void m.event.remove(this,"._submit")}}),k.changeBubbles||(m.event.special.change={setup:function(){return X.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(m.event.add(this,"propertychange._change",function(a){"checked"===a.originalEvent.propertyName&&(this._just_changed=!0)}),m.event.add(this,"click._change",function(a){this._just_changed&&!a.isTrigger&&(this._just_changed=!1),m.event.simulate("change",this,a,!0)})),!1):void m.event.add(this,"beforeactivate._change",function(a){var b=a.target;X.test(b.nodeName)&&!m._data(b,"changeBubbles")&&(m.event.add(b,"change._change",function(a){!this.parentNode||a.isSimulated||a.isTrigger||m.event.simulate("change",this.parentNode,a,!0)}),m._data(b,"changeBubbles",!0))})},handle:function(a){var b=a.target;return this!==b||a.isSimulated||a.isTrigger||"radio"!==b.type&&"checkbox"!==b.type?a.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return m.event.remove(this,"._change"),!X.test(this.nodeName)}}),k.focusinBubbles||m.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){m.event.simulate(b,a.target,m.event.fix(a),!0)};m.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=m._data(d,b);e||d.addEventListener(a,c,!0),m._data(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=m._data(d,b)-1;e?m._data(d,b,e):(d.removeEventListener(a,c,!0),m._removeData(d,b))}}}),m.fn.extend({on:function(a,b,c,d,e){var f,g;if("object"==typeof a){"string"!=typeof b&&(c=c||b,b=void 0);for(f in a)this.on(f,b,c,a[f],e);return this}if(null==c&&null==d?(d=b,c=b=void 0):null==d&&("string"==typeof b?(d=c,c=void 0):(d=c,c=b,b=void 0)),d===!1)d=bb;else if(!d)return this;return 1===e&&(g=d,d=function(a){return m().off(a),g.apply(this,arguments)},d.guid=g.guid||(g.guid=m.guid++)),this.each(function(){m.event.add(this,a,d,c,b)})},one:function(a,b,c,d){return this.on(a,b,c,d,1)},off:function(a,b,c){var d,e;if(a&&a.preventDefault&&a.handleObj)return d=a.handleObj,m(a.delegateTarget).off(d.namespace?d.origType+"."+d.namespace:d.origType,d.selector,d.handler),this;if("object"==typeof a){for(e in a)this.off(e,b,a[e]);return this}return(b===!1||"function"==typeof b)&&(c=b,b=void 0),c===!1&&(c=bb),this.each(function(){m.event.remove(this,a,c,b)})},trigger:function(a,b){return this.each(function(){m.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];return c?m.event.trigger(a,b,c,!0):void 0}});function db(a){var b=eb.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}var eb="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",fb=/ jQuery\d+="(?:null|\d+)"/g,gb=new RegExp("<(?:"+eb+")[\\s/>]","i"),hb=/^\s+/,ib=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,jb=/<([\w:]+)/,kb=/<tbody/i,lb=/<|&#?\w+;/,mb=/<(?:script|style|link)/i,nb=/checked\s*(?:[^=]|=\s*.checked.)/i,ob=/^$|\/(?:java|ecma)script/i,pb=/^true\/(.*)/,qb=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,rb={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:k.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},sb=db(y),tb=sb.appendChild(y.createElement("div"));rb.optgroup=rb.option,rb.tbody=rb.tfoot=rb.colgroup=rb.caption=rb.thead,rb.th=rb.td;function ub(a,b){var c,d,e=0,f=typeof a.getElementsByTagName!==K?a.getElementsByTagName(b||"*"):typeof a.querySelectorAll!==K?a.querySelectorAll(b||"*"):void 0;if(!f)for(f=[],c=a.childNodes||a;null!=(d=c[e]);e++)!b||m.nodeName(d,b)?f.push(d):m.merge(f,ub(d,b));return void 0===b||b&&m.nodeName(a,b)?m.merge([a],f):f}function vb(a){W.test(a.type)&&(a.defaultChecked=a.checked)}function wb(a,b){return m.nodeName(a,"table")&&m.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function xb(a){return a.type=(null!==m.find.attr(a,"type"))+"/"+a.type,a}function yb(a){var b=pb.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function zb(a,b){for(var c,d=0;null!=(c=a[d]);d++)m._data(c,"globalEval",!b||m._data(b[d],"globalEval"))}function Ab(a,b){if(1===b.nodeType&&m.hasData(a)){var c,d,e,f=m._data(a),g=m._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;e>d;d++)m.event.add(b,c,h[c][d])}g.data&&(g.data=m.extend({},g.data))}}function Bb(a,b){var c,d,e;if(1===b.nodeType){if(c=b.nodeName.toLowerCase(),!k.noCloneEvent&&b[m.expando]){e=m._data(b);for(d in e.events)m.removeEvent(b,d,e.handle);b.removeAttribute(m.expando)}"script"===c&&b.text!==a.text?(xb(b).text=a.text,yb(b)):"object"===c?(b.parentNode&&(b.outerHTML=a.outerHTML),k.html5Clone&&a.innerHTML&&!m.trim(b.innerHTML)&&(b.innerHTML=a.innerHTML)):"input"===c&&W.test(a.type)?(b.defaultChecked=b.checked=a.checked,b.value!==a.value&&(b.value=a.value)):"option"===c?b.defaultSelected=b.selected=a.defaultSelected:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}}m.extend({clone:function(a,b,c){var d,e,f,g,h,i=m.contains(a.ownerDocument,a);if(k.html5Clone||m.isXMLDoc(a)||!gb.test("<"+a.nodeName+">")?f=a.cloneNode(!0):(tb.innerHTML=a.outerHTML,tb.removeChild(f=tb.firstChild)),!(k.noCloneEvent&&k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||m.isXMLDoc(a)))for(d=ub(f),h=ub(a),g=0;null!=(e=h[g]);++g)d[g]&&Bb(e,d[g]);if(b)if(c)for(h=h||ub(a),d=d||ub(f),g=0;null!=(e=h[g]);g++)Ab(e,d[g]);else Ab(a,f);return d=ub(f,"script"),d.length>0&&zb(d,!i&&ub(a,"script")),d=h=e=null,f},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,l,n=a.length,o=db(b),p=[],q=0;n>q;q++)if(f=a[q],f||0===f)if("object"===m.type(f))m.merge(p,f.nodeType?[f]:f);else if(lb.test(f)){h=h||o.appendChild(b.createElement("div")),i=(jb.exec(f)||["",""])[1].toLowerCase(),l=rb[i]||rb._default,h.innerHTML=l[1]+f.replace(ib,"<$1></$2>")+l[2],e=l[0];while(e--)h=h.lastChild;if(!k.leadingWhitespace&&hb.test(f)&&p.push(b.createTextNode(hb.exec(f)[0])),!k.tbody){f="table"!==i||kb.test(f)?"<table>"!==l[1]||kb.test(f)?0:h:h.firstChild,e=f&&f.childNodes.length;while(e--)m.nodeName(j=f.childNodes[e],"tbody")&&!j.childNodes.length&&f.removeChild(j)}m.merge(p,h.childNodes),h.textContent="";while(h.firstChild)h.removeChild(h.firstChild);h=o.lastChild}else p.push(b.createTextNode(f));h&&o.removeChild(h),k.appendChecked||m.grep(ub(p,"input"),vb),q=0;while(f=p[q++])if((!d||-1===m.inArray(f,d))&&(g=m.contains(f.ownerDocument,f),h=ub(o.appendChild(f),"script"),g&&zb(h),c)){e=0;while(f=h[e++])ob.test(f.type||"")&&c.push(f)}return h=null,o},cleanData:function(a,b){for(var d,e,f,g,h=0,i=m.expando,j=m.cache,l=k.deleteExpando,n=m.event.special;null!=(d=a[h]);h++)if((b||m.acceptData(d))&&(f=d[i],g=f&&j[f])){if(g.events)for(e in g.events)n[e]?m.event.remove(d,e):m.removeEvent(d,e,g.handle);j[f]&&(delete j[f],l?delete d[i]:typeof d.removeAttribute!==K?d.removeAttribute(i):d[i]=null,c.push(f))}}}),m.fn.extend({text:function(a){return V(this,function(a){return void 0===a?m.text(this):this.empty().append((this[0]&&this[0].ownerDocument||y).createTextNode(a))},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=wb(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?m.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||m.cleanData(ub(c)),c.parentNode&&(b&&m.contains(c.ownerDocument,c)&&zb(ub(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++){1===a.nodeType&&m.cleanData(ub(a,!1));while(a.firstChild)a.removeChild(a.firstChild);a.options&&m.nodeName(a,"select")&&(a.options.length=0)}return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return m.clone(this,a,b)})},html:function(a){return V(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a)return 1===b.nodeType?b.innerHTML.replace(fb,""):void 0;if(!("string"!=typeof a||mb.test(a)||!k.htmlSerialize&&gb.test(a)||!k.leadingWhitespace&&hb.test(a)||rb[(jb.exec(a)||["",""])[1].toLowerCase()])){a=a.replace(ib,"<$1></$2>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(m.cleanData(ub(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,m.cleanData(ub(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,n=this,o=l-1,p=a[0],q=m.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&nb.test(p))return this.each(function(c){var d=n.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(i=m.buildFragment(a,this[0].ownerDocument,!1,this),c=i.firstChild,1===i.childNodes.length&&(i=c),c)){for(g=m.map(ub(i,"script"),xb),f=g.length;l>j;j++)d=i,j!==o&&(d=m.clone(d,!0,!0),f&&m.merge(g,ub(d,"script"))),b.call(this[j],d,j);if(f)for(h=g[g.length-1].ownerDocument,m.map(g,yb),j=0;f>j;j++)d=g[j],ob.test(d.type||"")&&!m._data(d,"globalEval")&&m.contains(h,d)&&(d.src?m._evalUrl&&m._evalUrl(d.src):m.globalEval((d.text||d.textContent||d.innerHTML||"").replace(qb,"")));i=c=null}return this}}),m.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){m.fn[a]=function(a){for(var c,d=0,e=[],g=m(a),h=g.length-1;h>=d;d++)c=d===h?this:this.clone(!0),m(g[d])[b](c),f.apply(e,c.get());return this.pushStack(e)}});var Cb,Db={};function Eb(b,c){var d,e=m(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:m.css(e[0],"display");return e.detach(),f}function Fb(a){var b=y,c=Db[a];return c||(c=Eb(a,b),"none"!==c&&c||(Cb=(Cb||m("<iframe frameborder='0' width='0' height='0'/>")).appendTo(b.documentElement),b=(Cb[0].contentWindow||Cb[0].contentDocument).document,b.write(),b.close(),c=Eb(a,b),Cb.detach()),Db[a]=c),c}!function(){var a;k.shrinkWrapBlocks=function(){if(null!=a)return a;a=!1;var b,c,d;return c=y.getElementsByTagName("body")[0],c&&c.style?(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),typeof b.style.zoom!==K&&(b.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",b.appendChild(y.createElement("div")).style.width="5px",a=3!==b.offsetWidth),c.removeChild(d),a):void 0}}();var Gb=/^margin/,Hb=new RegExp("^("+S+")(?!px)[a-z%]+$","i"),Ib,Jb,Kb=/^(top|right|bottom|left)$/;a.getComputedStyle?(Ib=function(b){return b.ownerDocument.defaultView.opener?b.ownerDocument.defaultView.getComputedStyle(b,null):a.getComputedStyle(b,null)},Jb=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ib(a),g=c?c.getPropertyValue(b)||c[b]:void 0,c&&(""!==g||m.contains(a.ownerDocument,a)||(g=m.style(a,b)),Hb.test(g)&&Gb.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=g,g=c.width,h.width=d,h.minWidth=e,h.maxWidth=f)),void 0===g?g:g+""}):y.documentElement.currentStyle&&(Ib=function(a){return a.currentStyle},Jb=function(a,b,c){var d,e,f,g,h=a.style;return c=c||Ib(a),g=c?c[b]:void 0,null==g&&h&&h[b]&&(g=h[b]),Hb.test(g)&&!Kb.test(b)&&(d=h.left,e=a.runtimeStyle,f=e&&e.left,f&&(e.left=a.currentStyle.left),h.left="fontSize"===b?"1em":g,g=h.pixelLeft+"px",h.left=d,f&&(e.left=f)),void 0===g?g:g+""||"auto"});function Lb(a,b){return{get:function(){var c=a();if(null!=c)return c?void delete this.get:(this.get=b).apply(this,arguments)}}}!function(){var b,c,d,e,f,g,h;if(b=y.createElement("div"),b.innerHTML="  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",d=b.getElementsByTagName("a")[0],c=d&&d.style){c.cssText="float:left;opacity:.5",k.opacity="0.5"===c.opacity,k.cssFloat=!!c.cssFloat,b.style.backgroundClip="content-box",b.cloneNode(!0).style.backgroundClip="",k.clearCloneStyle="content-box"===b.style.backgroundClip,k.boxSizing=""===c.boxSizing||""===c.MozBoxSizing||""===c.WebkitBoxSizing,m.extend(k,{reliableHiddenOffsets:function(){return null==g&&i(),g},boxSizingReliable:function(){return null==f&&i(),f},pixelPosition:function(){return null==e&&i(),e},reliableMarginRight:function(){return null==h&&i(),h}});function i(){var b,c,d,i;c=y.getElementsByTagName("body")[0],c&&c.style&&(b=y.createElement("div"),d=y.createElement("div"),d.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",c.appendChild(d).appendChild(b),b.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",e=f=!1,h=!0,a.getComputedStyle&&(e="1%"!==(a.getComputedStyle(b,null)||{}).top,f="4px"===(a.getComputedStyle(b,null)||{width:"4px"}).width,i=b.appendChild(y.createElement("div")),i.style.cssText=b.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",i.style.marginRight=i.style.width="0",b.style.width="1px",h=!parseFloat((a.getComputedStyle(i,null)||{}).marginRight),b.removeChild(i)),b.innerHTML="<table><tr><td></td><td>t</td></tr></table>",i=b.getElementsByTagName("td"),i[0].style.cssText="margin:0;border:0;padding:0;display:none",g=0===i[0].offsetHeight,g&&(i[0].style.display="",i[1].style.display="none",g=0===i[0].offsetHeight),c.removeChild(d))}}}(),m.swap=function(a,b,c,d){var e,f,g={};for(f in b)g[f]=a.style[f],a.style[f]=b[f];e=c.apply(a,d||[]);for(f in b)a.style[f]=g[f];return e};var Mb=/alpha\([^)]*\)/i,Nb=/opacity\s*=\s*([^)]*)/,Ob=/^(none|table(?!-c[ea]).+)/,Pb=new RegExp("^("+S+")(.*)$","i"),Qb=new RegExp("^([+-])=("+S+")","i"),Rb={position:"absolute",visibility:"hidden",display:"block"},Sb={letterSpacing:"0",fontWeight:"400"},Tb=["Webkit","O","Moz","ms"];function Ub(a,b){if(b in a)return b;var c=b.charAt(0).toUpperCase()+b.slice(1),d=b,e=Tb.length;while(e--)if(b=Tb[e]+c,b in a)return b;return d}function Vb(a,b){for(var c,d,e,f=[],g=0,h=a.length;h>g;g++)d=a[g],d.style&&(f[g]=m._data(d,"olddisplay"),c=d.style.display,b?(f[g]||"none"!==c||(d.style.display=""),""===d.style.display&&U(d)&&(f[g]=m._data(d,"olddisplay",Fb(d.nodeName)))):(e=U(d),(c&&"none"!==c||!e)&&m._data(d,"olddisplay",e?c:m.css(d,"display"))));for(g=0;h>g;g++)d=a[g],d.style&&(b&&"none"!==d.style.display&&""!==d.style.display||(d.style.display=b?f[g]||"":"none"));return a}function Wb(a,b,c){var d=Pb.exec(b);return d?Math.max(0,d[1]-(c||0))+(d[2]||"px"):b}function Xb(a,b,c,d,e){for(var f=c===(d?"border":"content")?4:"width"===b?1:0,g=0;4>f;f+=2)"margin"===c&&(g+=m.css(a,c+T[f],!0,e)),d?("content"===c&&(g-=m.css(a,"padding"+T[f],!0,e)),"margin"!==c&&(g-=m.css(a,"border"+T[f]+"Width",!0,e))):(g+=m.css(a,"padding"+T[f],!0,e),"padding"!==c&&(g+=m.css(a,"border"+T[f]+"Width",!0,e)));return g}function Yb(a,b,c){var d=!0,e="width"===b?a.offsetWidth:a.offsetHeight,f=Ib(a),g=k.boxSizing&&"border-box"===m.css(a,"boxSizing",!1,f);if(0>=e||null==e){if(e=Jb(a,b,f),(0>e||null==e)&&(e=a.style[b]),Hb.test(e))return e;d=g&&(k.boxSizingReliable()||e===a.style[b]),e=parseFloat(e)||0}return e+Xb(a,b,c||(g?"border":"content"),d,f)+"px"}m.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=Jb(a,"opacity");return""===c?"1":c}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":k.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,c,d){if(a&&3!==a.nodeType&&8!==a.nodeType&&a.style){var e,f,g,h=m.camelCase(b),i=a.style;if(b=m.cssProps[h]||(m.cssProps[h]=Ub(i,h)),g=m.cssHooks[b]||m.cssHooks[h],void 0===c)return g&&"get"in g&&void 0!==(e=g.get(a,!1,d))?e:i[b];if(f=typeof c,"string"===f&&(e=Qb.exec(c))&&(c=(e[1]+1)*e[2]+parseFloat(m.css(a,b)),f="number"),null!=c&&c===c&&("number"!==f||m.cssNumber[h]||(c+="px"),k.clearCloneStyle||""!==c||0!==b.indexOf("background")||(i[b]="inherit"),!(g&&"set"in g&&void 0===(c=g.set(a,c,d)))))try{i[b]=c}catch(j){}}},css:function(a,b,c,d){var e,f,g,h=m.camelCase(b);return b=m.cssProps[h]||(m.cssProps[h]=Ub(a.style,h)),g=m.cssHooks[b]||m.cssHooks[h],g&&"get"in g&&(f=g.get(a,!0,c)),void 0===f&&(f=Jb(a,b,d)),"normal"===f&&b in Sb&&(f=Sb[b]),""===c||c?(e=parseFloat(f),c===!0||m.isNumeric(e)?e||0:f):f}}),m.each(["height","width"],function(a,b){m.cssHooks[b]={get:function(a,c,d){return c?Ob.test(m.css(a,"display"))&&0===a.offsetWidth?m.swap(a,Rb,function(){return Yb(a,b,d)}):Yb(a,b,d):void 0},set:function(a,c,d){var e=d&&Ib(a);return Wb(a,c,d?Xb(a,b,d,k.boxSizing&&"border-box"===m.css(a,"boxSizing",!1,e),e):0)}}}),k.opacity||(m.cssHooks.opacity={get:function(a,b){return Nb.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=m.isNumeric(b)?"alpha(opacity="+100*b+")":"",f=d&&d.filter||c.filter||"";c.zoom=1,(b>=1||""===b)&&""===m.trim(f.replace(Mb,""))&&c.removeAttribute&&(c.removeAttribute("filter"),""===b||d&&!d.filter)||(c.filter=Mb.test(f)?f.replace(Mb,e):f+" "+e)}}),m.cssHooks.marginRight=Lb(k.reliableMarginRight,function(a,b){return b?m.swap(a,{display:"inline-block"},Jb,[a,"marginRight"]):void 0}),m.each({margin:"",padding:"",border:"Width"},function(a,b){m.cssHooks[a+b]={expand:function(c){for(var d=0,e={},f="string"==typeof c?c.split(" "):[c];4>d;d++)e[a+T[d]+b]=f[d]||f[d-2]||f[0];return e}},Gb.test(a)||(m.cssHooks[a+b].set=Wb)}),m.fn.extend({css:function(a,b){return V(this,function(a,b,c){var d,e,f={},g=0;if(m.isArray(b)){for(d=Ib(a),e=b.length;e>g;g++)f[b[g]]=m.css(a,b[g],!1,d);return f}return void 0!==c?m.style(a,b,c):m.css(a,b)},a,b,arguments.length>1)},show:function(){return Vb(this,!0)},hide:function(){return Vb(this)},toggle:function(a){return"boolean"==typeof a?a?this.show():this.hide():this.each(function(){U(this)?m(this).show():m(this).hide()})}});function Zb(a,b,c,d,e){return new Zb.prototype.init(a,b,c,d,e)
+}m.Tween=Zb,Zb.prototype={constructor:Zb,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||"swing",this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(m.cssNumber[c]?"":"px")},cur:function(){var a=Zb.propHooks[this.prop];return a&&a.get?a.get(this):Zb.propHooks._default.get(this)},run:function(a){var b,c=Zb.propHooks[this.prop];return this.pos=b=this.options.duration?m.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Zb.propHooks._default.set(this),this}},Zb.prototype.init.prototype=Zb.prototype,Zb.propHooks={_default:{get:function(a){var b;return null==a.elem[a.prop]||a.elem.style&&null!=a.elem.style[a.prop]?(b=m.css(a.elem,a.prop,""),b&&"auto"!==b?b:0):a.elem[a.prop]},set:function(a){m.fx.step[a.prop]?m.fx.step[a.prop](a):a.elem.style&&(null!=a.elem.style[m.cssProps[a.prop]]||m.cssHooks[a.prop])?m.style(a.elem,a.prop,a.now+a.unit):a.elem[a.prop]=a.now}}},Zb.propHooks.scrollTop=Zb.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},m.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2}},m.fx=Zb.prototype.init,m.fx.step={};var $b,_b,ac=/^(?:toggle|show|hide)$/,bc=new RegExp("^(?:([+-])=|)("+S+")([a-z%]*)$","i"),cc=/queueHooks$/,dc=[ic],ec={"*":[function(a,b){var c=this.createTween(a,b),d=c.cur(),e=bc.exec(b),f=e&&e[3]||(m.cssNumber[a]?"":"px"),g=(m.cssNumber[a]||"px"!==f&&+d)&&bc.exec(m.css(c.elem,a)),h=1,i=20;if(g&&g[3]!==f){f=f||g[3],e=e||[],g=+d||1;do h=h||".5",g/=h,m.style(c.elem,a,g+f);while(h!==(h=c.cur()/d)&&1!==h&&--i)}return e&&(g=c.start=+g||+d||0,c.unit=f,c.end=e[1]?g+(e[1]+1)*e[2]:+e[2]),c}]};function fc(){return setTimeout(function(){$b=void 0}),$b=m.now()}function gc(a,b){var c,d={height:a},e=0;for(b=b?1:0;4>e;e+=2-b)c=T[e],d["margin"+c]=d["padding"+c]=a;return b&&(d.opacity=d.width=a),d}function hc(a,b,c){for(var d,e=(ec[b]||[]).concat(ec["*"]),f=0,g=e.length;g>f;f++)if(d=e[f].call(c,b,a))return d}function ic(a,b,c){var d,e,f,g,h,i,j,l,n=this,o={},p=a.style,q=a.nodeType&&U(a),r=m._data(a,"fxshow");c.queue||(h=m._queueHooks(a,"fx"),null==h.unqueued&&(h.unqueued=0,i=h.empty.fire,h.empty.fire=function(){h.unqueued||i()}),h.unqueued++,n.always(function(){n.always(function(){h.unqueued--,m.queue(a,"fx").length||h.empty.fire()})})),1===a.nodeType&&("height"in b||"width"in b)&&(c.overflow=[p.overflow,p.overflowX,p.overflowY],j=m.css(a,"display"),l="none"===j?m._data(a,"olddisplay")||Fb(a.nodeName):j,"inline"===l&&"none"===m.css(a,"float")&&(k.inlineBlockNeedsLayout&&"inline"!==Fb(a.nodeName)?p.zoom=1:p.display="inline-block")),c.overflow&&(p.overflow="hidden",k.shrinkWrapBlocks()||n.always(function(){p.overflow=c.overflow[0],p.overflowX=c.overflow[1],p.overflowY=c.overflow[2]}));for(d in b)if(e=b[d],ac.exec(e)){if(delete b[d],f=f||"toggle"===e,e===(q?"hide":"show")){if("show"!==e||!r||void 0===r[d])continue;q=!0}o[d]=r&&r[d]||m.style(a,d)}else j=void 0;if(m.isEmptyObject(o))"inline"===("none"===j?Fb(a.nodeName):j)&&(p.display=j);else{r?"hidden"in r&&(q=r.hidden):r=m._data(a,"fxshow",{}),f&&(r.hidden=!q),q?m(a).show():n.done(function(){m(a).hide()}),n.done(function(){var b;m._removeData(a,"fxshow");for(b in o)m.style(a,b,o[b])});for(d in o)g=hc(q?r[d]:0,d,n),d in r||(r[d]=g.start,q&&(g.end=g.start,g.start="width"===d||"height"===d?1:0))}}function jc(a,b){var c,d,e,f,g;for(c in a)if(d=m.camelCase(c),e=b[d],f=a[c],m.isArray(f)&&(e=f[1],f=a[c]=f[0]),c!==d&&(a[d]=f,delete a[c]),g=m.cssHooks[d],g&&"expand"in g){f=g.expand(f),delete a[d];for(c in f)c in a||(a[c]=f[c],b[c]=e)}else b[d]=e}function kc(a,b,c){var d,e,f=0,g=dc.length,h=m.Deferred().always(function(){delete i.elem}),i=function(){if(e)return!1;for(var b=$b||fc(),c=Math.max(0,j.startTime+j.duration-b),d=c/j.duration||0,f=1-d,g=0,i=j.tweens.length;i>g;g++)j.tweens[g].run(f);return h.notifyWith(a,[j,f,c]),1>f&&i?c:(h.resolveWith(a,[j]),!1)},j=h.promise({elem:a,props:m.extend({},b),opts:m.extend(!0,{specialEasing:{}},c),originalProperties:b,originalOptions:c,startTime:$b||fc(),duration:c.duration,tweens:[],createTween:function(b,c){var d=m.Tween(a,j.opts,b,c,j.opts.specialEasing[b]||j.opts.easing);return j.tweens.push(d),d},stop:function(b){var c=0,d=b?j.tweens.length:0;if(e)return this;for(e=!0;d>c;c++)j.tweens[c].run(1);return b?h.resolveWith(a,[j,b]):h.rejectWith(a,[j,b]),this}}),k=j.props;for(jc(k,j.opts.specialEasing);g>f;f++)if(d=dc[f].call(j,a,k,j.opts))return d;return m.map(k,hc,j),m.isFunction(j.opts.start)&&j.opts.start.call(a,j),m.fx.timer(m.extend(i,{elem:a,anim:j,queue:j.opts.queue})),j.progress(j.opts.progress).done(j.opts.done,j.opts.complete).fail(j.opts.fail).always(j.opts.always)}m.Animation=m.extend(kc,{tweener:function(a,b){m.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");for(var c,d=0,e=a.length;e>d;d++)c=a[d],ec[c]=ec[c]||[],ec[c].unshift(b)},prefilter:function(a,b){b?dc.unshift(a):dc.push(a)}}),m.speed=function(a,b,c){var d=a&&"object"==typeof a?m.extend({},a):{complete:c||!c&&b||m.isFunction(a)&&a,duration:a,easing:c&&b||b&&!m.isFunction(b)&&b};return d.duration=m.fx.off?0:"number"==typeof d.duration?d.duration:d.duration in m.fx.speeds?m.fx.speeds[d.duration]:m.fx.speeds._default,(null==d.queue||d.queue===!0)&&(d.queue="fx"),d.old=d.complete,d.complete=function(){m.isFunction(d.old)&&d.old.call(this),d.queue&&m.dequeue(this,d.queue)},d},m.fn.extend({fadeTo:function(a,b,c,d){return this.filter(U).css("opacity",0).show().end().animate({opacity:b},a,c,d)},animate:function(a,b,c,d){var e=m.isEmptyObject(a),f=m.speed(b,c,d),g=function(){var b=kc(this,m.extend({},a),f);(e||m._data(this,"finish"))&&b.stop(!0)};return g.finish=g,e||f.queue===!1?this.each(g):this.queue(f.queue,g)},stop:function(a,b,c){var d=function(a){var b=a.stop;delete a.stop,b(c)};return"string"!=typeof a&&(c=b,b=a,a=void 0),b&&a!==!1&&this.queue(a||"fx",[]),this.each(function(){var b=!0,e=null!=a&&a+"queueHooks",f=m.timers,g=m._data(this);if(e)g[e]&&g[e].stop&&d(g[e]);else for(e in g)g[e]&&g[e].stop&&cc.test(e)&&d(g[e]);for(e=f.length;e--;)f[e].elem!==this||null!=a&&f[e].queue!==a||(f[e].anim.stop(c),b=!1,f.splice(e,1));(b||!c)&&m.dequeue(this,a)})},finish:function(a){return a!==!1&&(a=a||"fx"),this.each(function(){var b,c=m._data(this),d=c[a+"queue"],e=c[a+"queueHooks"],f=m.timers,g=d?d.length:0;for(c.finish=!0,m.queue(this,a,[]),e&&e.stop&&e.stop.call(this,!0),b=f.length;b--;)f[b].elem===this&&f[b].queue===a&&(f[b].anim.stop(!0),f.splice(b,1));for(b=0;g>b;b++)d[b]&&d[b].finish&&d[b].finish.call(this);delete c.finish})}}),m.each(["toggle","show","hide"],function(a,b){var c=m.fn[b];m.fn[b]=function(a,d,e){return null==a||"boolean"==typeof a?c.apply(this,arguments):this.animate(gc(b,!0),a,d,e)}}),m.each({slideDown:gc("show"),slideUp:gc("hide"),slideToggle:gc("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){m.fn[a]=function(a,c,d){return this.animate(b,a,c,d)}}),m.timers=[],m.fx.tick=function(){var a,b=m.timers,c=0;for($b=m.now();c<b.length;c++)a=b[c],a()||b[c]!==a||b.splice(c--,1);b.length||m.fx.stop(),$b=void 0},m.fx.timer=function(a){m.timers.push(a),a()?m.fx.start():m.timers.pop()},m.fx.interval=13,m.fx.start=function(){_b||(_b=setInterval(m.fx.tick,m.fx.interval))},m.fx.stop=function(){clearInterval(_b),_b=null},m.fx.speeds={slow:600,fast:200,_default:400},m.fn.delay=function(a,b){return a=m.fx?m.fx.speeds[a]||a:a,b=b||"fx",this.queue(b,function(b,c){var d=setTimeout(b,a);c.stop=function(){clearTimeout(d)}})},function(){var a,b,c,d,e;b=y.createElement("div"),b.setAttribute("className","t"),b.innerHTML="  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",d=b.getElementsByTagName("a")[0],c=y.createElement("select"),e=c.appendChild(y.createElement("option")),a=b.getElementsByTagName("input")[0],d.style.cssText="top:1px",k.getSetAttribute="t"!==b.className,k.style=/top/.test(d.getAttribute("style")),k.hrefNormalized="/a"===d.getAttribute("href"),k.checkOn=!!a.value,k.optSelected=e.selected,k.enctype=!!y.createElement("form").enctype,c.disabled=!0,k.optDisabled=!e.disabled,a=y.createElement("input"),a.setAttribute("value",""),k.input=""===a.getAttribute("value"),a.value="t",a.setAttribute("type","radio"),k.radioValue="t"===a.value}();var lc=/\r/g;m.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=m.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,m(this).val()):a,null==e?e="":"number"==typeof e?e+="":m.isArray(e)&&(e=m.map(e,function(a){return null==a?"":a+""})),b=m.valHooks[this.type]||m.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=m.valHooks[e.type]||m.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(lc,""):null==c?"":c)}}}),m.extend({valHooks:{option:{get:function(a){var b=m.find.attr(a,"value");return null!=b?b:m.trim(m.text(a))}},select:{get:function(a){for(var b,c,d=a.options,e=a.selectedIndex,f="select-one"===a.type||0>e,g=f?null:[],h=f?e+1:d.length,i=0>e?h:f?e:0;h>i;i++)if(c=d[i],!(!c.selected&&i!==e||(k.optDisabled?c.disabled:null!==c.getAttribute("disabled"))||c.parentNode.disabled&&m.nodeName(c.parentNode,"optgroup"))){if(b=m(c).val(),f)return b;g.push(b)}return g},set:function(a,b){var c,d,e=a.options,f=m.makeArray(b),g=e.length;while(g--)if(d=e[g],m.inArray(m.valHooks.option.get(d),f)>=0)try{d.selected=c=!0}catch(h){d.scrollHeight}else d.selected=!1;return c||(a.selectedIndex=-1),e}}}}),m.each(["radio","checkbox"],function(){m.valHooks[this]={set:function(a,b){return m.isArray(b)?a.checked=m.inArray(m(a).val(),b)>=0:void 0}},k.checkOn||(m.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var mc,nc,oc=m.expr.attrHandle,pc=/^(?:checked|selected)$/i,qc=k.getSetAttribute,rc=k.input;m.fn.extend({attr:function(a,b){return V(this,m.attr,a,b,arguments.length>1)},removeAttr:function(a){return this.each(function(){m.removeAttr(this,a)})}}),m.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(a&&3!==f&&8!==f&&2!==f)return typeof a.getAttribute===K?m.prop(a,b,c):(1===f&&m.isXMLDoc(a)||(b=b.toLowerCase(),d=m.attrHooks[b]||(m.expr.match.bool.test(b)?nc:mc)),void 0===c?d&&"get"in d&&null!==(e=d.get(a,b))?e:(e=m.find.attr(a,b),null==e?void 0:e):null!==c?d&&"set"in d&&void 0!==(e=d.set(a,c,b))?e:(a.setAttribute(b,c+""),c):void m.removeAttr(a,b))},removeAttr:function(a,b){var c,d,e=0,f=b&&b.match(E);if(f&&1===a.nodeType)while(c=f[e++])d=m.propFix[c]||c,m.expr.match.bool.test(c)?rc&&qc||!pc.test(c)?a[d]=!1:a[m.camelCase("default-"+c)]=a[d]=!1:m.attr(a,c,""),a.removeAttribute(qc?c:d)},attrHooks:{type:{set:function(a,b){if(!k.radioValue&&"radio"===b&&m.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}}}),nc={set:function(a,b,c){return b===!1?m.removeAttr(a,c):rc&&qc||!pc.test(c)?a.setAttribute(!qc&&m.propFix[c]||c,c):a[m.camelCase("default-"+c)]=a[c]=!0,c}},m.each(m.expr.match.bool.source.match(/\w+/g),function(a,b){var c=oc[b]||m.find.attr;oc[b]=rc&&qc||!pc.test(b)?function(a,b,d){var e,f;return d||(f=oc[b],oc[b]=e,e=null!=c(a,b,d)?b.toLowerCase():null,oc[b]=f),e}:function(a,b,c){return c?void 0:a[m.camelCase("default-"+b)]?b.toLowerCase():null}}),rc&&qc||(m.attrHooks.value={set:function(a,b,c){return m.nodeName(a,"input")?void(a.defaultValue=b):mc&&mc.set(a,b,c)}}),qc||(mc={set:function(a,b,c){var d=a.getAttributeNode(c);return d||a.setAttributeNode(d=a.ownerDocument.createAttribute(c)),d.value=b+="","value"===c||b===a.getAttribute(c)?b:void 0}},oc.id=oc.name=oc.coords=function(a,b,c){var d;return c?void 0:(d=a.getAttributeNode(b))&&""!==d.value?d.value:null},m.valHooks.button={get:function(a,b){var c=a.getAttributeNode(b);return c&&c.specified?c.value:void 0},set:mc.set},m.attrHooks.contenteditable={set:function(a,b,c){mc.set(a,""===b?!1:b,c)}},m.each(["width","height"],function(a,b){m.attrHooks[b]={set:function(a,c){return""===c?(a.setAttribute(b,"auto"),c):void 0}}})),k.style||(m.attrHooks.style={get:function(a){return a.style.cssText||void 0},set:function(a,b){return a.style.cssText=b+""}});var sc=/^(?:input|select|textarea|button|object)$/i,tc=/^(?:a|area)$/i;m.fn.extend({prop:function(a,b){return V(this,m.prop,a,b,arguments.length>1)},removeProp:function(a){return a=m.propFix[a]||a,this.each(function(){try{this[a]=void 0,delete this[a]}catch(b){}})}}),m.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(a,b,c){var d,e,f,g=a.nodeType;if(a&&3!==g&&8!==g&&2!==g)return f=1!==g||!m.isXMLDoc(a),f&&(b=m.propFix[b]||b,e=m.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=m.find.attr(a,"tabindex");return b?parseInt(b,10):sc.test(a.nodeName)||tc.test(a.nodeName)&&a.href?0:-1}}}}),k.hrefNormalized||m.each(["href","src"],function(a,b){m.propHooks[b]={get:function(a){return a.getAttribute(b,4)}}}),k.optSelected||(m.propHooks.selected={get:function(a){var b=a.parentNode;return b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex),null}}),m.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){m.propFix[this.toLowerCase()]=this}),k.enctype||(m.propFix.enctype="encoding");var uc=/[\t\r\n\f]/g;m.fn.extend({addClass:function(a){var b,c,d,e,f,g,h=0,i=this.length,j="string"==typeof a&&a;if(m.isFunction(a))return this.each(function(b){m(this).addClass(a.call(this,b,this.className))});if(j)for(b=(a||"").match(E)||[];i>h;h++)if(c=this[h],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(uc," "):" ")){f=0;while(e=b[f++])d.indexOf(" "+e+" ")<0&&(d+=e+" ");g=m.trim(d),c.className!==g&&(c.className=g)}return this},removeClass:function(a){var b,c,d,e,f,g,h=0,i=this.length,j=0===arguments.length||"string"==typeof a&&a;if(m.isFunction(a))return this.each(function(b){m(this).removeClass(a.call(this,b,this.className))});if(j)for(b=(a||"").match(E)||[];i>h;h++)if(c=this[h],d=1===c.nodeType&&(c.className?(" "+c.className+" ").replace(uc," "):"")){f=0;while(e=b[f++])while(d.indexOf(" "+e+" ")>=0)d=d.replace(" "+e+" "," ");g=a?m.trim(d):"",c.className!==g&&(c.className=g)}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):this.each(m.isFunction(a)?function(c){m(this).toggleClass(a.call(this,c,this.className,b),b)}:function(){if("string"===c){var b,d=0,e=m(this),f=a.match(E)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else(c===K||"boolean"===c)&&(this.className&&m._data(this,"__className__",this.className),this.className=this.className||a===!1?"":m._data(this,"__className__")||"")})},hasClass:function(a){for(var b=" "+a+" ",c=0,d=this.length;d>c;c++)if(1===this[c].nodeType&&(" "+this[c].className+" ").replace(uc," ").indexOf(b)>=0)return!0;return!1}}),m.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(a,b){m.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),m.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)},bind:function(a,b,c){return this.on(a,null,b,c)},unbind:function(a,b){return this.off(a,null,b)},delegate:function(a,b,c,d){return this.on(b,a,c,d)},undelegate:function(a,b,c){return 1===arguments.length?this.off(a,"**"):this.off(b,a||"**",c)}});var vc=m.now(),wc=/\?/,xc=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;m.parseJSON=function(b){if(a.JSON&&a.JSON.parse)return a.JSON.parse(b+"");var c,d=null,e=m.trim(b+"");return e&&!m.trim(e.replace(xc,function(a,b,e,f){return c&&b&&(d=0),0===d?a:(c=e||b,d+=!f-!e,"")}))?Function("return "+e)():m.error("Invalid JSON: "+b)},m.parseXML=function(b){var c,d;if(!b||"string"!=typeof b)return null;try{a.DOMParser?(d=new DOMParser,c=d.parseFromString(b,"text/xml")):(c=new ActiveXObject("Microsoft.XMLDOM"),c.async="false",c.loadXML(b))}catch(e){c=void 0}return c&&c.documentElement&&!c.getElementsByTagName("parsererror").length||m.error("Invalid XML: "+b),c};var yc,zc,Ac=/#.*$/,Bc=/([?&])_=[^&]*/,Cc=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Dc=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Ec=/^(?:GET|HEAD)$/,Fc=/^\/\//,Gc=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,Hc={},Ic={},Jc="*/".concat("*");try{zc=location.href}catch(Kc){zc=y.createElement("a"),zc.href="",zc=zc.href}yc=Gc.exec(zc.toLowerCase())||[];function Lc(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(E)||[];if(m.isFunction(c))while(d=f[e++])"+"===d.charAt(0)?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Mc(a,b,c,d){var e={},f=a===Ic;function g(h){var i;return e[h]=!0,m.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Nc(a,b){var c,d,e=m.ajaxSettings.flatOptions||{};for(d in b)void 0!==b[d]&&((e[d]?a:c||(c={}))[d]=b[d]);return c&&m.extend(!0,a,c),a}function Oc(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===e&&(e=a.mimeType||b.getResponseHeader("Content-Type"));if(e)for(g in h)if(h[g]&&h[g].test(e)){i.unshift(g);break}if(i[0]in c)f=i[0];else{for(g in c){if(!i[0]||a.converters[g+" "+i[0]]){f=g;break}d||(d=g)}f=f||d}return f?(f!==i[0]&&i.unshift(f),c[f]):void 0}function Pc(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}m.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:zc,type:"GET",isLocal:Dc.test(yc[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Jc,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":m.parseJSON,"text xml":m.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Nc(Nc(a,m.ajaxSettings),b):Nc(m.ajaxSettings,a)},ajaxPrefilter:Lc(Hc),ajaxTransport:Lc(Ic),ajax:function(a,b){"object"==typeof a&&(b=a,a=void 0),b=b||{};var c,d,e,f,g,h,i,j,k=m.ajaxSetup({},b),l=k.context||k,n=k.context&&(l.nodeType||l.jquery)?m(l):m.event,o=m.Deferred(),p=m.Callbacks("once memory"),q=k.statusCode||{},r={},s={},t=0,u="canceled",v={readyState:0,getResponseHeader:function(a){var b;if(2===t){if(!j){j={};while(b=Cc.exec(f))j[b[1].toLowerCase()]=b[2]}b=j[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return 2===t?f:null},setRequestHeader:function(a,b){var c=a.toLowerCase();return t||(a=s[c]=s[c]||a,r[a]=b),this},overrideMimeType:function(a){return t||(k.mimeType=a),this},statusCode:function(a){var b;if(a)if(2>t)for(b in a)q[b]=[q[b],a[b]];else v.always(a[v.status]);return this},abort:function(a){var b=a||u;return i&&i.abort(b),x(0,b),this}};if(o.promise(v).complete=p.add,v.success=v.done,v.error=v.fail,k.url=((a||k.url||zc)+"").replace(Ac,"").replace(Fc,yc[1]+"//"),k.type=b.method||b.type||k.method||k.type,k.dataTypes=m.trim(k.dataType||"*").toLowerCase().match(E)||[""],null==k.crossDomain&&(c=Gc.exec(k.url.toLowerCase()),k.crossDomain=!(!c||c[1]===yc[1]&&c[2]===yc[2]&&(c[3]||("http:"===c[1]?"80":"443"))===(yc[3]||("http:"===yc[1]?"80":"443")))),k.data&&k.processData&&"string"!=typeof k.data&&(k.data=m.param(k.data,k.traditional)),Mc(Hc,k,b,v),2===t)return v;h=m.event&&k.global,h&&0===m.active++&&m.event.trigger("ajaxStart"),k.type=k.type.toUpperCase(),k.hasContent=!Ec.test(k.type),e=k.url,k.hasContent||(k.data&&(e=k.url+=(wc.test(e)?"&":"?")+k.data,delete k.data),k.cache===!1&&(k.url=Bc.test(e)?e.replace(Bc,"$1_="+vc++):e+(wc.test(e)?"&":"?")+"_="+vc++)),k.ifModified&&(m.lastModified[e]&&v.setRequestHeader("If-Modified-Since",m.lastModified[e]),m.etag[e]&&v.setRequestHeader("If-None-Match",m.etag[e])),(k.data&&k.hasContent&&k.contentType!==!1||b.contentType)&&v.setRequestHeader("Content-Type",k.contentType),v.setRequestHeader("Accept",k.dataTypes[0]&&k.accepts[k.dataTypes[0]]?k.accepts[k.dataTypes[0]]+("*"!==k.dataTypes[0]?", "+Jc+"; q=0.01":""):k.accepts["*"]);for(d in k.headers)v.setRequestHeader(d,k.headers[d]);if(k.beforeSend&&(k.beforeSend.call(l,v,k)===!1||2===t))return v.abort();u="abort";for(d in{success:1,error:1,complete:1})v[d](k[d]);if(i=Mc(Ic,k,b,v)){v.readyState=1,h&&n.trigger("ajaxSend",[v,k]),k.async&&k.timeout>0&&(g=setTimeout(function(){v.abort("timeout")},k.timeout));try{t=1,i.send(r,x)}catch(w){if(!(2>t))throw w;x(-1,w)}}else x(-1,"No Transport");function x(a,b,c,d){var j,r,s,u,w,x=b;2!==t&&(t=2,g&&clearTimeout(g),i=void 0,f=d||"",v.readyState=a>0?4:0,j=a>=200&&300>a||304===a,c&&(u=Oc(k,v,c)),u=Pc(k,u,v,j),j?(k.ifModified&&(w=v.getResponseHeader("Last-Modified"),w&&(m.lastModified[e]=w),w=v.getResponseHeader("etag"),w&&(m.etag[e]=w)),204===a||"HEAD"===k.type?x="nocontent":304===a?x="notmodified":(x=u.state,r=u.data,s=u.error,j=!s)):(s=x,(a||!x)&&(x="error",0>a&&(a=0))),v.status=a,v.statusText=(b||x)+"",j?o.resolveWith(l,[r,x,v]):o.rejectWith(l,[v,x,s]),v.statusCode(q),q=void 0,h&&n.trigger(j?"ajaxSuccess":"ajaxError",[v,k,j?r:s]),p.fireWith(l,[v,x]),h&&(n.trigger("ajaxComplete",[v,k]),--m.active||m.event.trigger("ajaxStop")))}return v},getJSON:function(a,b,c){return m.get(a,b,c,"json")},getScript:function(a,b){return m.get(a,void 0,b,"script")}}),m.each(["get","post"],function(a,b){m[b]=function(a,c,d,e){return m.isFunction(c)&&(e=e||d,d=c,c=void 0),m.ajax({url:a,type:b,dataType:e,data:c,success:d})}}),m._evalUrl=function(a){return m.ajax({url:a,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},m.fn.extend({wrapAll:function(a){if(m.isFunction(a))return this.each(function(b){m(this).wrapAll(a.call(this,b))});if(this[0]){var b=m(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&1===a.firstChild.nodeType)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return this.each(m.isFunction(a)?function(b){m(this).wrapInner(a.call(this,b))}:function(){var b=m(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=m.isFunction(a);return this.each(function(c){m(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){m.nodeName(this,"body")||m(this).replaceWith(this.childNodes)}).end()}}),m.expr.filters.hidden=function(a){return a.offsetWidth<=0&&a.offsetHeight<=0||!k.reliableHiddenOffsets()&&"none"===(a.style&&a.style.display||m.css(a,"display"))},m.expr.filters.visible=function(a){return!m.expr.filters.hidden(a)};var Qc=/%20/g,Rc=/\[\]$/,Sc=/\r?\n/g,Tc=/^(?:submit|button|image|reset|file)$/i,Uc=/^(?:input|select|textarea|keygen)/i;function Vc(a,b,c,d){var e;if(m.isArray(b))m.each(b,function(b,e){c||Rc.test(a)?d(a,e):Vc(a+"["+("object"==typeof e?b:"")+"]",e,c,d)});else if(c||"object"!==m.type(b))d(a,b);else for(e in b)Vc(a+"["+e+"]",b[e],c,d)}m.param=function(a,b){var c,d=[],e=function(a,b){b=m.isFunction(b)?b():null==b?"":b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};if(void 0===b&&(b=m.ajaxSettings&&m.ajaxSettings.traditional),m.isArray(a)||a.jquery&&!m.isPlainObject(a))m.each(a,function(){e(this.name,this.value)});else for(c in a)Vc(c,a[c],b,e);return d.join("&").replace(Qc,"+")},m.fn.extend({serialize:function(){return m.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=m.prop(this,"elements");return a?m.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!m(this).is(":disabled")&&Uc.test(this.nodeName)&&!Tc.test(a)&&(this.checked||!W.test(a))}).map(function(a,b){var c=m(this).val();return null==c?null:m.isArray(c)?m.map(c,function(a){return{name:b.name,value:a.replace(Sc,"\r\n")}}):{name:b.name,value:c.replace(Sc,"\r\n")}}).get()}}),m.ajaxSettings.xhr=void 0!==a.ActiveXObject?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&Zc()||$c()}:Zc;var Wc=0,Xc={},Yc=m.ajaxSettings.xhr();a.attachEvent&&a.attachEvent("onunload",function(){for(var a in Xc)Xc[a](void 0,!0)}),k.cors=!!Yc&&"withCredentials"in Yc,Yc=k.ajax=!!Yc,Yc&&m.ajaxTransport(function(a){if(!a.crossDomain||k.cors){var b;return{send:function(c,d){var e,f=a.xhr(),g=++Wc;if(f.open(a.type,a.url,a.async,a.username,a.password),a.xhrFields)for(e in a.xhrFields)f[e]=a.xhrFields[e];a.mimeType&&f.overrideMimeType&&f.overrideMimeType(a.mimeType),a.crossDomain||c["X-Requested-With"]||(c["X-Requested-With"]="XMLHttpRequest");for(e in c)void 0!==c[e]&&f.setRequestHeader(e,c[e]+"");f.send(a.hasContent&&a.data||null),b=function(c,e){var h,i,j;if(b&&(e||4===f.readyState))if(delete Xc[g],b=void 0,f.onreadystatechange=m.noop,e)4!==f.readyState&&f.abort();else{j={},h=f.status,"string"==typeof f.responseText&&(j.text=f.responseText);try{i=f.statusText}catch(k){i=""}h||!a.isLocal||a.crossDomain?1223===h&&(h=204):h=j.text?200:404}j&&d(h,i,j,f.getAllResponseHeaders())},a.async?4===f.readyState?setTimeout(b):f.onreadystatechange=Xc[g]=b:b()},abort:function(){b&&b(void 0,!0)}}}});function Zc(){try{return new a.XMLHttpRequest}catch(b){}}function $c(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}m.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(a){return m.globalEval(a),a}}}),m.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),m.ajaxTransport("script",function(a){if(a.crossDomain){var b,c=y.head||m("head")[0]||y.documentElement;return{send:function(d,e){b=y.createElement("script"),b.async=!0,a.scriptCharset&&(b.charset=a.scriptCharset),b.src=a.url,b.onload=b.onreadystatechange=function(a,c){(c||!b.readyState||/loaded|complete/.test(b.readyState))&&(b.onload=b.onreadystatechange=null,b.parentNode&&b.parentNode.removeChild(b),b=null,c||e(200,"success"))},c.insertBefore(b,c.firstChild)},abort:function(){b&&b.onload(void 0,!0)}}}});var _c=[],ad=/(=)\?(?=&|$)|\?\?/;m.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=_c.pop()||m.expando+"_"+vc++;return this[a]=!0,a}}),m.ajaxPrefilter("json jsonp",function(b,c,d){var e,f,g,h=b.jsonp!==!1&&(ad.test(b.url)?"url":"string"==typeof b.data&&!(b.contentType||"").indexOf("application/x-www-form-urlencoded")&&ad.test(b.data)&&"data");return h||"jsonp"===b.dataTypes[0]?(e=b.jsonpCallback=m.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,h?b[h]=b[h].replace(ad,"$1"+e):b.jsonp!==!1&&(b.url+=(wc.test(b.url)?"&":"?")+b.jsonp+"="+e),b.converters["script json"]=function(){return g||m.error(e+" was not called"),g[0]},b.dataTypes[0]="json",f=a[e],a[e]=function(){g=arguments},d.always(function(){a[e]=f,b[e]&&(b.jsonpCallback=c.jsonpCallback,_c.push(e)),g&&m.isFunction(f)&&f(g[0]),g=f=void 0}),"script"):void 0}),m.parseHTML=function(a,b,c){if(!a||"string"!=typeof a)return null;"boolean"==typeof b&&(c=b,b=!1),b=b||y;var d=u.exec(a),e=!c&&[];return d?[b.createElement(d[1])]:(d=m.buildFragment([a],b,e),e&&e.length&&m(e).remove(),m.merge([],d.childNodes))};var bd=m.fn.load;m.fn.load=function(a,b,c){if("string"!=typeof a&&bd)return bd.apply(this,arguments);var d,e,f,g=this,h=a.indexOf(" ");return h>=0&&(d=m.trim(a.slice(h,a.length)),a=a.slice(0,h)),m.isFunction(b)?(c=b,b=void 0):b&&"object"==typeof b&&(f="POST"),g.length>0&&m.ajax({url:a,type:f,dataType:"html",data:b}).done(function(a){e=arguments,g.html(d?m("<div>").append(m.parseHTML(a)).find(d):a)}).complete(c&&function(a,b){g.each(c,e||[a.responseText,b,a])}),this},m.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(a,b){m.fn[b]=function(a){return this.on(b,a)}}),m.expr.filters.animated=function(a){return m.grep(m.timers,function(b){return a===b.elem}).length};var cd=a.document.documentElement;function dd(a){return m.isWindow(a)?a:9===a.nodeType?a.defaultView||a.parentWindow:!1}m.offset={setOffset:function(a,b,c){var d,e,f,g,h,i,j,k=m.css(a,"position"),l=m(a),n={};"static"===k&&(a.style.position="relative"),h=l.offset(),f=m.css(a,"top"),i=m.css(a,"left"),j=("absolute"===k||"fixed"===k)&&m.inArray("auto",[f,i])>-1,j?(d=l.position(),g=d.top,e=d.left):(g=parseFloat(f)||0,e=parseFloat(i)||0),m.isFunction(b)&&(b=b.call(a,c,h)),null!=b.top&&(n.top=b.top-h.top+g),null!=b.left&&(n.left=b.left-h.left+e),"using"in b?b.using.call(a,n):l.css(n)}},m.fn.extend({offset:function(a){if(arguments.length)return void 0===a?this:this.each(function(b){m.offset.setOffset(this,a,b)});var b,c,d={top:0,left:0},e=this[0],f=e&&e.ownerDocument;if(f)return b=f.documentElement,m.contains(b,e)?(typeof e.getBoundingClientRect!==K&&(d=e.getBoundingClientRect()),c=dd(f),{top:d.top+(c.pageYOffset||b.scrollTop)-(b.clientTop||0),left:d.left+(c.pageXOffset||b.scrollLeft)-(b.clientLeft||0)}):d},position:function(){if(this[0]){var a,b,c={top:0,left:0},d=this[0];return"fixed"===m.css(d,"position")?b=d.getBoundingClientRect():(a=this.offsetParent(),b=this.offset(),m.nodeName(a[0],"html")||(c=a.offset()),c.top+=m.css(a[0],"borderTopWidth",!0),c.left+=m.css(a[0],"borderLeftWidth",!0)),{top:b.top-c.top-m.css(d,"marginTop",!0),left:b.left-c.left-m.css(d,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||cd;while(a&&!m.nodeName(a,"html")&&"static"===m.css(a,"position"))a=a.offsetParent;return a||cd})}}),m.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,b){var c=/Y/.test(b);m.fn[a]=function(d){return V(this,function(a,d,e){var f=dd(a);return void 0===e?f?b in f?f[b]:f.document.documentElement[d]:a[d]:void(f?f.scrollTo(c?m(f).scrollLeft():e,c?e:m(f).scrollTop()):a[d]=e)},a,d,arguments.length,null)}}),m.each(["top","left"],function(a,b){m.cssHooks[b]=Lb(k.pixelPosition,function(a,c){return c?(c=Jb(a,b),Hb.test(c)?m(a).position()[b]+"px":c):void 0})}),m.each({Height:"height",Width:"width"},function(a,b){m.each({padding:"inner"+a,content:b,"":"outer"+a},function(c,d){m.fn[d]=function(d,e){var f=arguments.length&&(c||"boolean"!=typeof d),g=c||(d===!0||e===!0?"margin":"border");return V(this,function(b,c,d){var e;return m.isWindow(b)?b.document.documentElement["client"+a]:9===b.nodeType?(e=b.documentElement,Math.max(b.body["scroll"+a],e["scroll"+a],b.body["offset"+a],e["offset"+a],e["client"+a])):void 0===d?m.css(b,c,g):m.style(b,c,d,g)},b,f?d:void 0,f,null)}})}),m.fn.size=function(){return this.length},m.fn.andSelf=m.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return m});var ed=a.jQuery,fd=a.$;return m.noConflict=function(b){return a.$===m&&(a.$=fd),b&&a.jQuery===m&&(a.jQuery=ed),m},typeof b===K&&(a.jQuery=a.$=m),m});
diff --git a/js/jquery-1.8.3.min.js b/js/jquery-1.8.3.min.js
deleted file mode 100644
index 38837795279c5eb281e98ce6017998b993026518..0000000000000000000000000000000000000000
--- a/js/jquery-1.8.3.min.js
+++ /dev/null
@@ -1,2 +0,0 @@
-/*! jQuery v1.8.3 jquery.com | jquery.org/license */
-(function(e,t){function _(e){var t=M[e]={};return v.each(e.split(y),function(e,n){t[n]=!0}),t}function H(e,n,r){if(r===t&&e.nodeType===1){var i="data-"+n.replace(P,"-$1").toLowerCase();r=e.getAttribute(i);if(typeof r=="string"){try{r=r==="true"?!0:r==="false"?!1:r==="null"?null:+r+""===r?+r:D.test(r)?v.parseJSON(r):r}catch(s){}v.data(e,n,r)}else r=t}return r}function B(e){var t;for(t in e){if(t==="data"&&v.isEmptyObject(e[t]))continue;if(t!=="toJSON")return!1}return!0}function et(){return!1}function tt(){return!0}function ut(e){return!e||!e.parentNode||e.parentNode.nodeType===11}function at(e,t){do e=e[t];while(e&&e.nodeType!==1);return e}function ft(e,t,n){t=t||0;if(v.isFunction(t))return v.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return v.grep(e,function(e,r){return e===t===n});if(typeof t=="string"){var r=v.grep(e,function(e){return e.nodeType===1});if(it.test(t))return v.filter(t,r,!n);t=v.filter(t,r)}return v.grep(e,function(e,r){return v.inArray(e,t)>=0===n})}function lt(e){var t=ct.split("|"),n=e.createDocumentFragment();if(n.createElement)while(t.length)n.createElement(t.pop());return n}function Lt(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function At(e,t){if(t.nodeType!==1||!v.hasData(e))return;var n,r,i,s=v._data(e),o=v._data(t,s),u=s.events;if(u){delete o.handle,o.events={};for(n in u)for(r=0,i=u[n].length;r<i;r++)v.event.add(t,n,u[n][r])}o.data&&(o.data=v.extend({},o.data))}function Ot(e,t){var n;if(t.nodeType!==1)return;t.clearAttributes&&t.clearAttributes(),t.mergeAttributes&&t.mergeAttributes(e),n=t.nodeName.toLowerCase(),n==="object"?(t.parentNode&&(t.outerHTML=e.outerHTML),v.support.html5Clone&&e.innerHTML&&!v.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):n==="input"&&Et.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):n==="option"?t.selected=e.defaultSelected:n==="input"||n==="textarea"?t.defaultValue=e.defaultValue:n==="script"&&t.text!==e.text&&(t.text=e.text),t.removeAttribute(v.expando)}function Mt(e){return typeof e.getElementsByTagName!="undefined"?e.getElementsByTagName("*"):typeof e.querySelectorAll!="undefined"?e.querySelectorAll("*"):[]}function _t(e){Et.test(e.type)&&(e.defaultChecked=e.checked)}function Qt(e,t){if(t in e)return t;var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Jt.length;while(i--){t=Jt[i]+n;if(t in e)return t}return r}function Gt(e,t){return e=t||e,v.css(e,"display")==="none"||!v.contains(e.ownerDocument,e)}function Yt(e,t){var n,r,i=[],s=0,o=e.length;for(;s<o;s++){n=e[s];if(!n.style)continue;i[s]=v._data(n,"olddisplay"),t?(!i[s]&&n.style.display==="none"&&(n.style.display=""),n.style.display===""&&Gt(n)&&(i[s]=v._data(n,"olddisplay",nn(n.nodeName)))):(r=Dt(n,"display"),!i[s]&&r!=="none"&&v._data(n,"olddisplay",r))}for(s=0;s<o;s++){n=e[s];if(!n.style)continue;if(!t||n.style.display==="none"||n.style.display==="")n.style.display=t?i[s]||"":"none"}return e}function Zt(e,t,n){var r=Rt.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function en(e,t,n,r){var i=n===(r?"border":"content")?4:t==="width"?1:0,s=0;for(;i<4;i+=2)n==="margin"&&(s+=v.css(e,n+$t[i],!0)),r?(n==="content"&&(s-=parseFloat(Dt(e,"padding"+$t[i]))||0),n!=="margin"&&(s-=parseFloat(Dt(e,"border"+$t[i]+"Width"))||0)):(s+=parseFloat(Dt(e,"padding"+$t[i]))||0,n!=="padding"&&(s+=parseFloat(Dt(e,"border"+$t[i]+"Width"))||0));return s}function tn(e,t,n){var r=t==="width"?e.offsetWidth:e.offsetHeight,i=!0,s=v.support.boxSizing&&v.css(e,"boxSizing")==="border-box";if(r<=0||r==null){r=Dt(e,t);if(r<0||r==null)r=e.style[t];if(Ut.test(r))return r;i=s&&(v.support.boxSizingReliable||r===e.style[t]),r=parseFloat(r)||0}return r+en(e,t,n||(s?"border":"content"),i)+"px"}function nn(e){if(Wt[e])return Wt[e];var t=v("<"+e+">").appendTo(i.body),n=t.css("display");t.remove();if(n==="none"||n===""){Pt=i.body.appendChild(Pt||v.extend(i.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!Ht||!Pt.createElement)Ht=(Pt.contentWindow||Pt.contentDocument).document,Ht.write("<!doctype html><html><body>"),Ht.close();t=Ht.body.appendChild(Ht.createElement(e)),n=Dt(t,"display"),i.body.removeChild(Pt)}return Wt[e]=n,n}function fn(e,t,n,r){var i;if(v.isArray(t))v.each(t,function(t,i){n||sn.test(e)?r(e,i):fn(e+"["+(typeof i=="object"?t:"")+"]",i,n,r)});else if(!n&&v.type(t)==="object")for(i in t)fn(e+"["+i+"]",t[i],n,r);else r(e,t)}function Cn(e){return function(t,n){typeof t!="string"&&(n=t,t="*");var r,i,s,o=t.toLowerCase().split(y),u=0,a=o.length;if(v.isFunction(n))for(;u<a;u++)r=o[u],s=/^\+/.test(r),s&&(r=r.substr(1)||"*"),i=e[r]=e[r]||[],i[s?"unshift":"push"](n)}}function kn(e,n,r,i,s,o){s=s||n.dataTypes[0],o=o||{},o[s]=!0;var u,a=e[s],f=0,l=a?a.length:0,c=e===Sn;for(;f<l&&(c||!u);f++)u=a[f](n,r,i),typeof u=="string"&&(!c||o[u]?u=t:(n.dataTypes.unshift(u),u=kn(e,n,r,i,u,o)));return(c||!u)&&!o["*"]&&(u=kn(e,n,r,i,"*",o)),u}function Ln(e,n){var r,i,s=v.ajaxSettings.flatOptions||{};for(r in n)n[r]!==t&&((s[r]?e:i||(i={}))[r]=n[r]);i&&v.extend(!0,e,i)}function An(e,n,r){var i,s,o,u,a=e.contents,f=e.dataTypes,l=e.responseFields;for(s in l)s in r&&(n[l[s]]=r[s]);while(f[0]==="*")f.shift(),i===t&&(i=e.mimeType||n.getResponseHeader("content-type"));if(i)for(s in a)if(a[s]&&a[s].test(i)){f.unshift(s);break}if(f[0]in r)o=f[0];else{for(s in r){if(!f[0]||e.converters[s+" "+f[0]]){o=s;break}u||(u=s)}o=o||u}if(o)return o!==f[0]&&f.unshift(o),r[o]}function On(e,t){var n,r,i,s,o=e.dataTypes.slice(),u=o[0],a={},f=0;e.dataFilter&&(t=e.dataFilter(t,e.dataType));if(o[1])for(n in e.converters)a[n.toLowerCase()]=e.converters[n];for(;i=o[++f];)if(i!=="*"){if(u!=="*"&&u!==i){n=a[u+" "+i]||a["* "+i];if(!n)for(r in a){s=r.split(" ");if(s[1]===i){n=a[u+" "+s[0]]||a["* "+s[0]];if(n){n===!0?n=a[r]:a[r]!==!0&&(i=s[0],o.splice(f--,0,i));break}}}if(n!==!0)if(n&&e["throws"])t=n(t);else try{t=n(t)}catch(l){return{state:"parsererror",error:n?l:"No conversion from "+u+" to "+i}}}u=i}return{state:"success",data:t}}function Fn(){try{return new e.XMLHttpRequest}catch(t){}}function In(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}function $n(){return setTimeout(function(){qn=t},0),qn=v.now()}function Jn(e,t){v.each(t,function(t,n){var r=(Vn[t]||[]).concat(Vn["*"]),i=0,s=r.length;for(;i<s;i++)if(r[i].call(e,t,n))return})}function Kn(e,t,n){var r,i=0,s=0,o=Xn.length,u=v.Deferred().always(function(){delete a.elem}),a=function(){var t=qn||$n(),n=Math.max(0,f.startTime+f.duration-t),r=n/f.duration||0,i=1-r,s=0,o=f.tweens.length;for(;s<o;s++)f.tweens[s].run(i);return u.notifyWith(e,[f,i,n]),i<1&&o?n:(u.resolveWith(e,[f]),!1)},f=u.promise({elem:e,props:v.extend({},t),opts:v.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:qn||$n(),duration:n.duration,tweens:[],createTween:function(t,n,r){var i=v.Tween(e,f.opts,t,n,f.opts.specialEasing[t]||f.opts.easing);return f.tweens.push(i),i},stop:function(t){var n=0,r=t?f.tweens.length:0;for(;n<r;n++)f.tweens[n].run(1);return t?u.resolveWith(e,[f,t]):u.rejectWith(e,[f,t]),this}}),l=f.props;Qn(l,f.opts.specialEasing);for(;i<o;i++){r=Xn[i].call(f,e,l,f.opts);if(r)return r}return Jn(f,l),v.isFunction(f.opts.start)&&f.opts.start.call(e,f),v.fx.timer(v.extend(a,{anim:f,queue:f.opts.queue,elem:e})),f.progress(f.opts.progress).done(f.opts.done,f.opts.complete).fail(f.opts.fail).always(f.opts.always)}function Qn(e,t){var n,r,i,s,o;for(n in e){r=v.camelCase(n),i=t[r],s=e[n],v.isArray(s)&&(i=s[1],s=e[n]=s[0]),n!==r&&(e[r]=s,delete e[n]),o=v.cssHooks[r];if(o&&"expand"in o){s=o.expand(s),delete e[r];for(n in s)n in e||(e[n]=s[n],t[n]=i)}else t[r]=i}}function Gn(e,t,n){var r,i,s,o,u,a,f,l,c,h=this,p=e.style,d={},m=[],g=e.nodeType&&Gt(e);n.queue||(l=v._queueHooks(e,"fx"),l.unqueued==null&&(l.unqueued=0,c=l.empty.fire,l.empty.fire=function(){l.unqueued||c()}),l.unqueued++,h.always(function(){h.always(function(){l.unqueued--,v.queue(e,"fx").length||l.empty.fire()})})),e.nodeType===1&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],v.css(e,"display")==="inline"&&v.css(e,"float")==="none"&&(!v.support.inlineBlockNeedsLayout||nn(e.nodeName)==="inline"?p.display="inline-block":p.zoom=1)),n.overflow&&(p.overflow="hidden",v.support.shrinkWrapBlocks||h.done(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t){s=t[r];if(Un.exec(s)){delete t[r],a=a||s==="toggle";if(s===(g?"hide":"show"))continue;m.push(r)}}o=m.length;if(o){u=v._data(e,"fxshow")||v._data(e,"fxshow",{}),"hidden"in u&&(g=u.hidden),a&&(u.hidden=!g),g?v(e).show():h.done(function(){v(e).hide()}),h.done(function(){var t;v.removeData(e,"fxshow",!0);for(t in d)v.style(e,t,d[t])});for(r=0;r<o;r++)i=m[r],f=h.createTween(i,g?u[i]:0),d[i]=u[i]||v.style(e,i),i in u||(u[i]=f.start,g&&(f.end=f.start,f.start=i==="width"||i==="height"?1:0))}}function Yn(e,t,n,r,i){return new Yn.prototype.init(e,t,n,r,i)}function Zn(e,t){var n,r={height:e},i=0;t=t?1:0;for(;i<4;i+=2-t)n=$t[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}function tr(e){return v.isWindow(e)?e:e.nodeType===9?e.defaultView||e.parentWindow:!1}var n,r,i=e.document,s=e.location,o=e.navigator,u=e.jQuery,a=e.$,f=Array.prototype.push,l=Array.prototype.slice,c=Array.prototype.indexOf,h=Object.prototype.toString,p=Object.prototype.hasOwnProperty,d=String.prototype.trim,v=function(e,t){return new v.fn.init(e,t,n)},m=/[\-+]?(?:\d*\.|)\d+(?:[eE][\-+]?\d+|)/.source,g=/\S/,y=/\s+/,b=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,w=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,E=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,S=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,T=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,N=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,C=/^-ms-/,k=/-([\da-z])/gi,L=function(e,t){return(t+"").toUpperCase()},A=function(){i.addEventListener?(i.removeEventListener("DOMContentLoaded",A,!1),v.ready()):i.readyState==="complete"&&(i.detachEvent("onreadystatechange",A),v.ready())},O={};v.fn=v.prototype={constructor:v,init:function(e,n,r){var s,o,u,a;if(!e)return this;if(e.nodeType)return this.context=this[0]=e,this.length=1,this;if(typeof e=="string"){e.charAt(0)==="<"&&e.charAt(e.length-1)===">"&&e.length>=3?s=[null,e,null]:s=w.exec(e);if(s&&(s[1]||!n)){if(s[1])return n=n instanceof v?n[0]:n,a=n&&n.nodeType?n.ownerDocument||n:i,e=v.parseHTML(s[1],a,!0),E.test(s[1])&&v.isPlainObject(n)&&this.attr.call(e,n,!0),v.merge(this,e);o=i.getElementById(s[2]);if(o&&o.parentNode){if(o.id!==s[2])return r.find(e);this.length=1,this[0]=o}return this.context=i,this.selector=e,this}return!n||n.jquery?(n||r).find(e):this.constructor(n).find(e)}return v.isFunction(e)?r.ready(e):(e.selector!==t&&(this.selector=e.selector,this.context=e.context),v.makeArray(e,this))},selector:"",jquery:"1.8.3",length:0,size:function(){return this.length},toArray:function(){return l.call(this)},get:function(e){return e==null?this.toArray():e<0?this[this.length+e]:this[e]},pushStack:function(e,t,n){var r=v.merge(this.constructor(),e);return r.prevObject=this,r.context=this.context,t==="find"?r.selector=this.selector+(this.selector?" ":"")+n:t&&(r.selector=this.selector+"."+t+"("+n+")"),r},each:function(e,t){return v.each(this,e,t)},ready:function(e){return v.ready.promise().done(e),this},eq:function(e){return e=+e,e===-1?this.slice(e):this.slice(e,e+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(l.apply(this,arguments),"slice",l.call(arguments).join(","))},map:function(e){return this.pushStack(v.map(this,function(t,n){return e.call(t,n,t)}))},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:[].sort,splice:[].splice},v.fn.init.prototype=v.fn,v.extend=v.fn.extend=function(){var e,n,r,i,s,o,u=arguments[0]||{},a=1,f=arguments.length,l=!1;typeof u=="boolean"&&(l=u,u=arguments[1]||{},a=2),typeof u!="object"&&!v.isFunction(u)&&(u={}),f===a&&(u=this,--a);for(;a<f;a++)if((e=arguments[a])!=null)for(n in e){r=u[n],i=e[n];if(u===i)continue;l&&i&&(v.isPlainObject(i)||(s=v.isArray(i)))?(s?(s=!1,o=r&&v.isArray(r)?r:[]):o=r&&v.isPlainObject(r)?r:{},u[n]=v.extend(l,o,i)):i!==t&&(u[n]=i)}return u},v.extend({noConflict:function(t){return e.$===v&&(e.$=a),t&&e.jQuery===v&&(e.jQuery=u),v},isReady:!1,readyWait:1,holdReady:function(e){e?v.readyWait++:v.ready(!0)},ready:function(e){if(e===!0?--v.readyWait:v.isReady)return;if(!i.body)return setTimeout(v.ready,1);v.isReady=!0;if(e!==!0&&--v.readyWait>0)return;r.resolveWith(i,[v]),v.fn.trigger&&v(i).trigger("ready").off("ready")},isFunction:function(e){return v.type(e)==="function"},isArray:Array.isArray||function(e){return v.type(e)==="array"},isWindow:function(e){return e!=null&&e==e.window},isNumeric:function(e){return!isNaN(parseFloat(e))&&isFinite(e)},type:function(e){return e==null?String(e):O[h.call(e)]||"object"},isPlainObject:function(e){if(!e||v.type(e)!=="object"||e.nodeType||v.isWindow(e))return!1;try{if(e.constructor&&!p.call(e,"constructor")&&!p.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}var r;for(r in e);return r===t||p.call(e,r)},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},error:function(e){throw new Error(e)},parseHTML:function(e,t,n){var r;return!e||typeof e!="string"?null:(typeof t=="boolean"&&(n=t,t=0),t=t||i,(r=E.exec(e))?[t.createElement(r[1])]:(r=v.buildFragment([e],t,n?null:[]),v.merge([],(r.cacheable?v.clone(r.fragment):r.fragment).childNodes)))},parseJSON:function(t){if(!t||typeof t!="string")return null;t=v.trim(t);if(e.JSON&&e.JSON.parse)return e.JSON.parse(t);if(S.test(t.replace(T,"@").replace(N,"]").replace(x,"")))return(new Function("return "+t))();v.error("Invalid JSON: "+t)},parseXML:function(n){var r,i;if(!n||typeof n!="string")return null;try{e.DOMParser?(i=new DOMParser,r=i.parseFromString(n,"text/xml")):(r=new ActiveXObject("Microsoft.XMLDOM"),r.async="false",r.loadXML(n))}catch(s){r=t}return(!r||!r.documentElement||r.getElementsByTagName("parsererror").length)&&v.error("Invalid XML: "+n),r},noop:function(){},globalEval:function(t){t&&g.test(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(C,"ms-").replace(k,L)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,n,r){var i,s=0,o=e.length,u=o===t||v.isFunction(e);if(r){if(u){for(i in e)if(n.apply(e[i],r)===!1)break}else for(;s<o;)if(n.apply(e[s++],r)===!1)break}else if(u){for(i in e)if(n.call(e[i],i,e[i])===!1)break}else for(;s<o;)if(n.call(e[s],s,e[s++])===!1)break;return e},trim:d&&!d.call("\ufeff\u00a0")?function(e){return e==null?"":d.call(e)}:function(e){return e==null?"":(e+"").replace(b,"")},makeArray:function(e,t){var n,r=t||[];return e!=null&&(n=v.type(e),e.length==null||n==="string"||n==="function"||n==="regexp"||v.isWindow(e)?f.call(r,e):v.merge(r,e)),r},inArray:function(e,t,n){var r;if(t){if(c)return c.call(t,e,n);r=t.length,n=n?n<0?Math.max(0,r+n):n:0;for(;n<r;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,n){var r=n.length,i=e.length,s=0;if(typeof r=="number")for(;s<r;s++)e[i++]=n[s];else while(n[s]!==t)e[i++]=n[s++];return e.length=i,e},grep:function(e,t,n){var r,i=[],s=0,o=e.length;n=!!n;for(;s<o;s++)r=!!t(e[s],s),n!==r&&i.push(e[s]);return i},map:function(e,n,r){var i,s,o=[],u=0,a=e.length,f=e instanceof v||a!==t&&typeof a=="number"&&(a>0&&e[0]&&e[a-1]||a===0||v.isArray(e));if(f)for(;u<a;u++)i=n(e[u],u,r),i!=null&&(o[o.length]=i);else for(s in e)i=n(e[s],s,r),i!=null&&(o[o.length]=i);return o.concat.apply([],o)},guid:1,proxy:function(e,n){var r,i,s;return typeof n=="string"&&(r=e[n],n=e,e=r),v.isFunction(e)?(i=l.call(arguments,2),s=function(){return e.apply(n,i.concat(l.call(arguments)))},s.guid=e.guid=e.guid||v.guid++,s):t},access:function(e,n,r,i,s,o,u){var a,f=r==null,l=0,c=e.length;if(r&&typeof r=="object"){for(l in r)v.access(e,n,l,r[l],1,o,i);s=1}else if(i!==t){a=u===t&&v.isFunction(i),f&&(a?(a=n,n=function(e,t,n){return a.call(v(e),n)}):(n.call(e,i),n=null));if(n)for(;l<c;l++)n(e[l],r,a?i.call(e[l],l,n(e[l],r)):i,u);s=1}return s?e:f?n.call(e):c?n(e[0],r):o},now:function(){return(new Date).getTime()}}),v.ready.promise=function(t){if(!r){r=v.Deferred();if(i.readyState==="complete")setTimeout(v.ready,1);else if(i.addEventListener)i.addEventListener("DOMContentLoaded",A,!1),e.addEventListener("load",v.ready,!1);else{i.attachEvent("onreadystatechange",A),e.attachEvent("onload",v.ready);var n=!1;try{n=e.frameElement==null&&i.documentElement}catch(s){}n&&n.doScroll&&function o(){if(!v.isReady){try{n.doScroll("left")}catch(e){return setTimeout(o,50)}v.ready()}}()}}return r.promise(t)},v.each("Boolean Number String Function Array Date RegExp Object".split(" "),function(e,t){O["[object "+t+"]"]=t.toLowerCase()}),n=v(i);var M={};v.Callbacks=function(e){e=typeof e=="string"?M[e]||_(e):v.extend({},e);var n,r,i,s,o,u,a=[],f=!e.once&&[],l=function(t){n=e.memory&&t,r=!0,u=s||0,s=0,o=a.length,i=!0;for(;a&&u<o;u++)if(a[u].apply(t[0],t[1])===!1&&e.stopOnFalse){n=!1;break}i=!1,a&&(f?f.length&&l(f.shift()):n?a=[]:c.disable())},c={add:function(){if(a){var t=a.length;(function r(t){v.each(t,function(t,n){var i=v.type(n);i==="function"?(!e.unique||!c.has(n))&&a.push(n):n&&n.length&&i!=="string"&&r(n)})})(arguments),i?o=a.length:n&&(s=t,l(n))}return this},remove:function(){return a&&v.each(arguments,function(e,t){var n;while((n=v.inArray(t,a,n))>-1)a.splice(n,1),i&&(n<=o&&o--,n<=u&&u--)}),this},has:function(e){return v.inArray(e,a)>-1},empty:function(){return a=[],this},disable:function(){return a=f=n=t,this},disabled:function(){return!a},lock:function(){return f=t,n||c.disable(),this},locked:function(){return!f},fireWith:function(e,t){return t=t||[],t=[e,t.slice?t.slice():t],a&&(!r||f)&&(i?f.push(t):l(t)),this},fire:function(){return c.fireWith(this,arguments),this},fired:function(){return!!r}};return c},v.extend({Deferred:function(e){var t=[["resolve","done",v.Callbacks("once memory"),"resolved"],["reject","fail",v.Callbacks("once memory"),"rejected"],["notify","progress",v.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return v.Deferred(function(n){v.each(t,function(t,r){var s=r[0],o=e[t];i[r[1]](v.isFunction(o)?function(){var e=o.apply(this,arguments);e&&v.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[s+"With"](this===i?n:this,[e])}:n[s])}),e=null}).promise()},promise:function(e){return e!=null?v.extend(e,r):r}},i={};return r.pipe=r.then,v.each(t,function(e,s){var o=s[2],u=s[3];r[s[1]]=o.add,u&&o.add(function(){n=u},t[e^1][2].disable,t[2][2].lock),i[s[0]]=o.fire,i[s[0]+"With"]=o.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t=0,n=l.call(arguments),r=n.length,i=r!==1||e&&v.isFunction(e.promise)?r:0,s=i===1?e:v.Deferred(),o=function(e,t,n){return function(r){t[e]=this,n[e]=arguments.length>1?l.call(arguments):r,n===u?s.notifyWith(t,n):--i||s.resolveWith(t,n)}},u,a,f;if(r>1){u=new Array(r),a=new Array(r),f=new Array(r);for(;t<r;t++)n[t]&&v.isFunction(n[t].promise)?n[t].promise().done(o(t,f,n)).fail(s.reject).progress(o(t,a,u)):--i}return i||s.resolveWith(f,n),s.promise()}}),v.support=function(){var t,n,r,s,o,u,a,f,l,c,h,p=i.createElement("div");p.setAttribute("className","t"),p.innerHTML="  <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",n=p.getElementsByTagName("*"),r=p.getElementsByTagName("a")[0];if(!n||!r||!n.length)return{};s=i.createElement("select"),o=s.appendChild(i.createElement("option")),u=p.getElementsByTagName("input")[0],r.style.cssText="top:1px;float:left;opacity:.5",t={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(r.getAttribute("style")),hrefNormalized:r.getAttribute("href")==="/a",opacity:/^0.5/.test(r.style.opacity),cssFloat:!!r.style.cssFloat,checkOn:u.value==="on",optSelected:o.selected,getSetAttribute:p.className!=="t",enctype:!!i.createElement("form").enctype,html5Clone:i.createElement("nav").cloneNode(!0).outerHTML!=="<:nav></:nav>",boxModel:i.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},u.checked=!0,t.noCloneChecked=u.cloneNode(!0).checked,s.disabled=!0,t.optDisabled=!o.disabled;try{delete p.test}catch(d){t.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",h=function(){t.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick"),p.detachEvent("onclick",h)),u=i.createElement("input"),u.value="t",u.setAttribute("type","radio"),t.radioValue=u.value==="t",u.setAttribute("checked","checked"),u.setAttribute("name","t"),p.appendChild(u),a=i.createDocumentFragment(),a.appendChild(p.lastChild),t.checkClone=a.cloneNode(!0).cloneNode(!0).lastChild.checked,t.appendChecked=u.checked,a.removeChild(u),a.appendChild(p);if(p.attachEvent)for(l in{submit:!0,change:!0,focusin:!0})f="on"+l,c=f in p,c||(p.setAttribute(f,"return;"),c=typeof p[f]=="function"),t[l+"Bubbles"]=c;return v(function(){var n,r,s,o,u="padding:0;margin:0;border:0;display:block;overflow:hidden;",a=i.getElementsByTagName("body")[0];if(!a)return;n=i.createElement("div"),n.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",a.insertBefore(n,a.firstChild),r=i.createElement("div"),n.appendChild(r),r.innerHTML="<table><tr><td></td><td>t</td></tr></table>",s=r.getElementsByTagName("td"),s[0].style.cssText="padding:0;margin:0;border:0;display:none",c=s[0].offsetHeight===0,s[0].style.display="",s[1].style.display="none",t.reliableHiddenOffsets=c&&s[0].offsetHeight===0,r.innerHTML="",r.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",t.boxSizing=r.offsetWidth===4,t.doesNotIncludeMarginInBodyOffset=a.offsetTop!==1,e.getComputedStyle&&(t.pixelPosition=(e.getComputedStyle(r,null)||{}).top!=="1%",t.boxSizingReliable=(e.getComputedStyle(r,null)||{width:"4px"}).width==="4px",o=i.createElement("div"),o.style.cssText=r.style.cssText=u,o.style.marginRight=o.style.width="0",r.style.width="1px",r.appendChild(o),t.reliableMarginRight=!parseFloat((e.getComputedStyle(o,null)||{}).marginRight)),typeof r.style.zoom!="undefined"&&(r.innerHTML="",r.style.cssText=u+"width:1px;padding:1px;display:inline;zoom:1",t.inlineBlockNeedsLayout=r.offsetWidth===3,r.style.display="block",r.style.overflow="visible",r.innerHTML="<div></div>",r.firstChild.style.width="5px",t.shrinkWrapBlocks=r.offsetWidth!==3,n.style.zoom=1),a.removeChild(n),n=r=s=o=null}),a.removeChild(p),n=r=s=o=u=a=p=null,t}();var D=/(?:\{[\s\S]*\}|\[[\s\S]*\])$/,P=/([A-Z])/g;v.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(v.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(e){return e=e.nodeType?v.cache[e[v.expando]]:e[v.expando],!!e&&!B(e)},data:function(e,n,r,i){if(!v.acceptData(e))return;var s,o,u=v.expando,a=typeof n=="string",f=e.nodeType,l=f?v.cache:e,c=f?e[u]:e[u]&&u;if((!c||!l[c]||!i&&!l[c].data)&&a&&r===t)return;c||(f?e[u]=c=v.deletedIds.pop()||v.guid++:c=u),l[c]||(l[c]={},f||(l[c].toJSON=v.noop));if(typeof n=="object"||typeof n=="function")i?l[c]=v.extend(l[c],n):l[c].data=v.extend(l[c].data,n);return s=l[c],i||(s.data||(s.data={}),s=s.data),r!==t&&(s[v.camelCase(n)]=r),a?(o=s[n],o==null&&(o=s[v.camelCase(n)])):o=s,o},removeData:function(e,t,n){if(!v.acceptData(e))return;var r,i,s,o=e.nodeType,u=o?v.cache:e,a=o?e[v.expando]:v.expando;if(!u[a])return;if(t){r=n?u[a]:u[a].data;if(r){v.isArray(t)||(t in r?t=[t]:(t=v.camelCase(t),t in r?t=[t]:t=t.split(" ")));for(i=0,s=t.length;i<s;i++)delete r[t[i]];if(!(n?B:v.isEmptyObject)(r))return}}if(!n){delete u[a].data;if(!B(u[a]))return}o?v.cleanData([e],!0):v.support.deleteExpando||u!=u.window?delete u[a]:u[a]=null},_data:function(e,t,n){return v.data(e,t,n,!0)},acceptData:function(e){var t=e.nodeName&&v.noData[e.nodeName.toLowerCase()];return!t||t!==!0&&e.getAttribute("classid")===t}}),v.fn.extend({data:function(e,n){var r,i,s,o,u,a=this[0],f=0,l=null;if(e===t){if(this.length){l=v.data(a);if(a.nodeType===1&&!v._data(a,"parsedAttrs")){s=a.attributes;for(u=s.length;f<u;f++)o=s[f].name,o.indexOf("data-")||(o=v.camelCase(o.substring(5)),H(a,o,l[o]));v._data(a,"parsedAttrs",!0)}}return l}return typeof e=="object"?this.each(function(){v.data(this,e)}):(r=e.split(".",2),r[1]=r[1]?"."+r[1]:"",i=r[1]+"!",v.access(this,function(n){if(n===t)return l=this.triggerHandler("getData"+i,[r[0]]),l===t&&a&&(l=v.data(a,e),l=H(a,e,l)),l===t&&r[1]?this.data(r[0]):l;r[1]=n,this.each(function(){var t=v(this);t.triggerHandler("setData"+i,r),v.data(this,e,n),t.triggerHandler("changeData"+i,r)})},null,n,arguments.length>1,null,!1))},removeData:function(e){return this.each(function(){v.removeData(this,e)})}}),v.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=v._data(e,t),n&&(!r||v.isArray(n)?r=v._data(e,t,v.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=v.queue(e,t),r=n.length,i=n.shift(),s=v._queueHooks(e,t),o=function(){v.dequeue(e,t)};i==="inprogress"&&(i=n.shift(),r--),i&&(t==="fx"&&n.unshift("inprogress"),delete s.stop,i.call(e,o,s)),!r&&s&&s.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return v._data(e,n)||v._data(e,n,{empty:v.Callbacks("once memory").add(function(){v.removeData(e,t+"queue",!0),v.removeData(e,n,!0)})})}}),v.fn.extend({queue:function(e,n){var r=2;return typeof e!="string"&&(n=e,e="fx",r--),arguments.length<r?v.queue(this[0],e):n===t?this:this.each(function(){var t=v.queue(this,e,n);v._queueHooks(this,e),e==="fx"&&t[0]!=="inprogress"&&v.dequeue(this,e)})},dequeue:function(e){return this.each(function(){v.dequeue(this,e)})},delay:function(e,t){return e=v.fx?v.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,n){var r,i=1,s=v.Deferred(),o=this,u=this.length,a=function(){--i||s.resolveWith(o,[o])};typeof e!="string"&&(n=e,e=t),e=e||"fx";while(u--)r=v._data(o[u],e+"queueHooks"),r&&r.empty&&(i++,r.empty.add(a));return a(),s.promise(n)}});var j,F,I,q=/[\t\r\n]/g,R=/\r/g,U=/^(?:button|input)$/i,z=/^(?:button|input|object|select|textarea)$/i,W=/^a(?:rea|)$/i,X=/^(?:autofocus|autoplay|async|checked|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped|selected)$/i,V=v.support.getSetAttribute;v.fn.extend({attr:function(e,t){return v.access(this,v.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){v.removeAttr(this,e)})},prop:function(e,t){return v.access(this,v.prop,e,t,arguments.length>1)},removeProp:function(e){return e=v.propFix[e]||e,this.each(function(){try{this[e]=t,delete this[e]}catch(n){}})},addClass:function(e){var t,n,r,i,s,o,u;if(v.isFunction(e))return this.each(function(t){v(this).addClass(e.call(this,t,this.className))});if(e&&typeof e=="string"){t=e.split(y);for(n=0,r=this.length;n<r;n++){i=this[n];if(i.nodeType===1)if(!i.className&&t.length===1)i.className=e;else{s=" "+i.className+" ";for(o=0,u=t.length;o<u;o++)s.indexOf(" "+t[o]+" ")<0&&(s+=t[o]+" ");i.className=v.trim(s)}}}return this},removeClass:function(e){var n,r,i,s,o,u,a;if(v.isFunction(e))return this.each(function(t){v(this).removeClass(e.call(this,t,this.className))});if(e&&typeof e=="string"||e===t){n=(e||"").split(y);for(u=0,a=this.length;u<a;u++){i=this[u];if(i.nodeType===1&&i.className){r=(" "+i.className+" ").replace(q," ");for(s=0,o=n.length;s<o;s++)while(r.indexOf(" "+n[s]+" ")>=0)r=r.replace(" "+n[s]+" "," ");i.className=e?v.trim(r):""}}}return this},toggleClass:function(e,t){var n=typeof e,r=typeof t=="boolean";return v.isFunction(e)?this.each(function(n){v(this).toggleClass(e.call(this,n,this.className,t),t)}):this.each(function(){if(n==="string"){var i,s=0,o=v(this),u=t,a=e.split(y);while(i=a[s++])u=r?u:!o.hasClass(i),o[u?"addClass":"removeClass"](i)}else if(n==="undefined"||n==="boolean")this.className&&v._data(this,"__className__",this.className),this.className=this.className||e===!1?"":v._data(this,"__className__")||""})},hasClass:function(e){var t=" "+e+" ",n=0,r=this.length;for(;n<r;n++)if(this[n].nodeType===1&&(" "+this[n].className+" ").replace(q," ").indexOf(t)>=0)return!0;return!1},val:function(e){var n,r,i,s=this[0];if(!arguments.length){if(s)return n=v.valHooks[s.type]||v.valHooks[s.nodeName.toLowerCase()],n&&"get"in n&&(r=n.get(s,"value"))!==t?r:(r=s.value,typeof r=="string"?r.replace(R,""):r==null?"":r);return}return i=v.isFunction(e),this.each(function(r){var s,o=v(this);if(this.nodeType!==1)return;i?s=e.call(this,r,o.val()):s=e,s==null?s="":typeof s=="number"?s+="":v.isArray(s)&&(s=v.map(s,function(e){return e==null?"":e+""})),n=v.valHooks[this.type]||v.valHooks[this.nodeName.toLowerCase()];if(!n||!("set"in n)||n.set(this,s,"value")===t)this.value=s})}}),v.extend({valHooks:{option:{get:function(e){var t=e.attributes.value;return!t||t.specified?e.value:e.text}},select:{get:function(e){var t,n,r=e.options,i=e.selectedIndex,s=e.type==="select-one"||i<0,o=s?null:[],u=s?i+1:r.length,a=i<0?u:s?i:0;for(;a<u;a++){n=r[a];if((n.selected||a===i)&&(v.support.optDisabled?!n.disabled:n.getAttribute("disabled")===null)&&(!n.parentNode.disabled||!v.nodeName(n.parentNode,"optgroup"))){t=v(n).val();if(s)return t;o.push(t)}}return o},set:function(e,t){var n=v.makeArray(t);return v(e).find("option").each(function(){this.selected=v.inArray(v(this).val(),n)>=0}),n.length||(e.selectedIndex=-1),n}}},attrFn:{},attr:function(e,n,r,i){var s,o,u,a=e.nodeType;if(!e||a===3||a===8||a===2)return;if(i&&v.isFunction(v.fn[n]))return v(e)[n](r);if(typeof e.getAttribute=="undefined")return v.prop(e,n,r);u=a!==1||!v.isXMLDoc(e),u&&(n=n.toLowerCase(),o=v.attrHooks[n]||(X.test(n)?F:j));if(r!==t){if(r===null){v.removeAttr(e,n);return}return o&&"set"in o&&u&&(s=o.set(e,r,n))!==t?s:(e.setAttribute(n,r+""),r)}return o&&"get"in o&&u&&(s=o.get(e,n))!==null?s:(s=e.getAttribute(n),s===null?t:s)},removeAttr:function(e,t){var n,r,i,s,o=0;if(t&&e.nodeType===1){r=t.split(y);for(;o<r.length;o++)i=r[o],i&&(n=v.propFix[i]||i,s=X.test(i),s||v.attr(e,i,""),e.removeAttribute(V?i:n),s&&n in e&&(e[n]=!1))}},attrHooks:{type:{set:function(e,t){if(U.test(e.nodeName)&&e.parentNode)v.error("type property can't be changed");else if(!v.support.radioValue&&t==="radio"&&v.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}},value:{get:function(e,t){return j&&v.nodeName(e,"button")?j.get(e,t):t in e?e.value:null},set:function(e,t,n){if(j&&v.nodeName(e,"button"))return j.set(e,t,n);e.value=t}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(e,n,r){var i,s,o,u=e.nodeType;if(!e||u===3||u===8||u===2)return;return o=u!==1||!v.isXMLDoc(e),o&&(n=v.propFix[n]||n,s=v.propHooks[n]),r!==t?s&&"set"in s&&(i=s.set(e,r,n))!==t?i:e[n]=r:s&&"get"in s&&(i=s.get(e,n))!==null?i:e[n]},propHooks:{tabIndex:{get:function(e){var n=e.getAttributeNode("tabindex");return n&&n.specified?parseInt(n.value,10):z.test(e.nodeName)||W.test(e.nodeName)&&e.href?0:t}}}}),F={get:function(e,n){var r,i=v.prop(e,n);return i===!0||typeof i!="boolean"&&(r=e.getAttributeNode(n))&&r.nodeValue!==!1?n.toLowerCase():t},set:function(e,t,n){var r;return t===!1?v.removeAttr(e,n):(r=v.propFix[n]||n,r in e&&(e[r]=!0),e.setAttribute(n,n.toLowerCase())),n}},V||(I={name:!0,id:!0,coords:!0},j=v.valHooks.button={get:function(e,n){var r;return r=e.getAttributeNode(n),r&&(I[n]?r.value!=="":r.specified)?r.value:t},set:function(e,t,n){var r=e.getAttributeNode(n);return r||(r=i.createAttribute(n),e.setAttributeNode(r)),r.value=t+""}},v.each(["width","height"],function(e,t){v.attrHooks[t]=v.extend(v.attrHooks[t],{set:function(e,n){if(n==="")return e.setAttribute(t,"auto"),n}})}),v.attrHooks.contenteditable={get:j.get,set:function(e,t,n){t===""&&(t="false"),j.set(e,t,n)}}),v.support.hrefNormalized||v.each(["href","src","width","height"],function(e,n){v.attrHooks[n]=v.extend(v.attrHooks[n],{get:function(e){var r=e.getAttribute(n,2);return r===null?t:r}})}),v.support.style||(v.attrHooks.style={get:function(e){return e.style.cssText.toLowerCase()||t},set:function(e,t){return e.style.cssText=t+""}}),v.support.optSelected||(v.propHooks.selected=v.extend(v.propHooks.selected,{get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}})),v.support.enctype||(v.propFix.enctype="encoding"),v.support.checkOn||v.each(["radio","checkbox"],function(){v.valHooks[this]={get:function(e){return e.getAttribute("value")===null?"on":e.value}}}),v.each(["radio","checkbox"],function(){v.valHooks[this]=v.extend(v.valHooks[this],{set:function(e,t){if(v.isArray(t))return e.checked=v.inArray(v(e).val(),t)>=0}})});var $=/^(?:textarea|input|select)$/i,J=/^([^\.]*|)(?:\.(.+)|)$/,K=/(?:^|\s)hover(\.\S+|)\b/,Q=/^key/,G=/^(?:mouse|contextmenu)|click/,Y=/^(?:focusinfocus|focusoutblur)$/,Z=function(e){return v.event.special.hover?e:e.replace(K,"mouseenter$1 mouseleave$1")};v.event={add:function(e,n,r,i,s){var o,u,a,f,l,c,h,p,d,m,g;if(e.nodeType===3||e.nodeType===8||!n||!r||!(o=v._data(e)))return;r.handler&&(d=r,r=d.handler,s=d.selector),r.guid||(r.guid=v.guid++),a=o.events,a||(o.events=a={}),u=o.handle,u||(o.handle=u=function(e){return typeof v=="undefined"||!!e&&v.event.triggered===e.type?t:v.event.dispatch.apply(u.elem,arguments)},u.elem=e),n=v.trim(Z(n)).split(" ");for(f=0;f<n.length;f++){l=J.exec(n[f])||[],c=l[1],h=(l[2]||"").split(".").sort(),g=v.event.special[c]||{},c=(s?g.delegateType:g.bindType)||c,g=v.event.special[c]||{},p=v.extend({type:c,origType:l[1],data:i,handler:r,guid:r.guid,selector:s,needsContext:s&&v.expr.match.needsContext.test(s),namespace:h.join(".")},d),m=a[c];if(!m){m=a[c]=[],m.delegateCount=0;if(!g.setup||g.setup.call(e,i,h,u)===!1)e.addEventListener?e.addEventListener(c,u,!1):e.attachEvent&&e.attachEvent("on"+c,u)}g.add&&(g.add.call(e,p),p.handler.guid||(p.handler.guid=r.guid)),s?m.splice(m.delegateCount++,0,p):m.push(p),v.event.global[c]=!0}e=null},global:{},remove:function(e,t,n,r,i){var s,o,u,a,f,l,c,h,p,d,m,g=v.hasData(e)&&v._data(e);if(!g||!(h=g.events))return;t=v.trim(Z(t||"")).split(" ");for(s=0;s<t.length;s++){o=J.exec(t[s])||[],u=a=o[1],f=o[2];if(!u){for(u in h)v.event.remove(e,u+t[s],n,r,!0);continue}p=v.event.special[u]||{},u=(r?p.delegateType:p.bindType)||u,d=h[u]||[],l=d.length,f=f?new RegExp("(^|\\.)"+f.split(".").sort().join("\\.(?:.*\\.|)")+"(\\.|$)"):null;for(c=0;c<d.length;c++)m=d[c],(i||a===m.origType)&&(!n||n.guid===m.guid)&&(!f||f.test(m.namespace))&&(!r||r===m.selector||r==="**"&&m.selector)&&(d.splice(c--,1),m.selector&&d.delegateCount--,p.remove&&p.remove.call(e,m));d.length===0&&l!==d.length&&((!p.teardown||p.teardown.call(e,f,g.handle)===!1)&&v.removeEvent(e,u,g.handle),delete h[u])}v.isEmptyObject(h)&&(delete g.handle,v.removeData(e,"events",!0))},customEvent:{getData:!0,setData:!0,changeData:!0},trigger:function(n,r,s,o){if(!s||s.nodeType!==3&&s.nodeType!==8){var u,a,f,l,c,h,p,d,m,g,y=n.type||n,b=[];if(Y.test(y+v.event.triggered))return;y.indexOf("!")>=0&&(y=y.slice(0,-1),a=!0),y.indexOf(".")>=0&&(b=y.split("."),y=b.shift(),b.sort());if((!s||v.event.customEvent[y])&&!v.event.global[y])return;n=typeof n=="object"?n[v.expando]?n:new v.Event(y,n):new v.Event(y),n.type=y,n.isTrigger=!0,n.exclusive=a,n.namespace=b.join("."),n.namespace_re=n.namespace?new RegExp("(^|\\.)"+b.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,h=y.indexOf(":")<0?"on"+y:"";if(!s){u=v.cache;for(f in u)u[f].events&&u[f].events[y]&&v.event.trigger(n,r,u[f].handle.elem,!0);return}n.result=t,n.target||(n.target=s),r=r!=null?v.makeArray(r):[],r.unshift(n),p=v.event.special[y]||{};if(p.trigger&&p.trigger.apply(s,r)===!1)return;m=[[s,p.bindType||y]];if(!o&&!p.noBubble&&!v.isWindow(s)){g=p.delegateType||y,l=Y.test(g+y)?s:s.parentNode;for(c=s;l;l=l.parentNode)m.push([l,g]),c=l;c===(s.ownerDocument||i)&&m.push([c.defaultView||c.parentWindow||e,g])}for(f=0;f<m.length&&!n.isPropagationStopped();f++)l=m[f][0],n.type=m[f][1],d=(v._data(l,"events")||{})[n.type]&&v._data(l,"handle"),d&&d.apply(l,r),d=h&&l[h],d&&v.acceptData(l)&&d.apply&&d.apply(l,r)===!1&&n.preventDefault();return n.type=y,!o&&!n.isDefaultPrevented()&&(!p._default||p._default.apply(s.ownerDocument,r)===!1)&&(y!=="click"||!v.nodeName(s,"a"))&&v.acceptData(s)&&h&&s[y]&&(y!=="focus"&&y!=="blur"||n.target.offsetWidth!==0)&&!v.isWindow(s)&&(c=s[h],c&&(s[h]=null),v.event.triggered=y,s[y](),v.event.triggered=t,c&&(s[h]=c)),n.result}return},dispatch:function(n){n=v.event.fix(n||e.event);var r,i,s,o,u,a,f,c,h,p,d=(v._data(this,"events")||{})[n.type]||[],m=d.delegateCount,g=l.call(arguments),y=!n.exclusive&&!n.namespace,b=v.event.special[n.type]||{},w=[];g[0]=n,n.delegateTarget=this;if(b.preDispatch&&b.preDispatch.call(this,n)===!1)return;if(m&&(!n.button||n.type!=="click"))for(s=n.target;s!=this;s=s.parentNode||this)if(s.disabled!==!0||n.type!=="click"){u={},f=[];for(r=0;r<m;r++)c=d[r],h=c.selector,u[h]===t&&(u[h]=c.needsContext?v(h,this).index(s)>=0:v.find(h,this,null,[s]).length),u[h]&&f.push(c);f.length&&w.push({elem:s,matches:f})}d.length>m&&w.push({elem:this,matches:d.slice(m)});for(r=0;r<w.length&&!n.isPropagationStopped();r++){a=w[r],n.currentTarget=a.elem;for(i=0;i<a.matches.length&&!n.isImmediatePropagationStopped();i++){c=a.matches[i];if(y||!n.namespace&&!c.namespace||n.namespace_re&&n.namespace_re.test(c.namespace))n.data=c.data,n.handleObj=c,o=((v.event.special[c.origType]||{}).handle||c.handler).apply(a.elem,g),o!==t&&(n.result=o,o===!1&&(n.preventDefault(),n.stopPropagation()))}}return b.postDispatch&&b.postDispatch.call(this,n),n.result},props:"attrChange attrName relatedNode srcElement altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return e.which==null&&(e.which=t.charCode!=null?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,n){var r,s,o,u=n.button,a=n.fromElement;return e.pageX==null&&n.clientX!=null&&(r=e.target.ownerDocument||i,s=r.documentElement,o=r.body,e.pageX=n.clientX+(s&&s.scrollLeft||o&&o.scrollLeft||0)-(s&&s.clientLeft||o&&o.clientLeft||0),e.pageY=n.clientY+(s&&s.scrollTop||o&&o.scrollTop||0)-(s&&s.clientTop||o&&o.clientTop||0)),!e.relatedTarget&&a&&(e.relatedTarget=a===e.target?n.toElement:a),!e.which&&u!==t&&(e.which=u&1?1:u&2?3:u&4?2:0),e}},fix:function(e){if(e[v.expando])return e;var t,n,r=e,s=v.event.fixHooks[e.type]||{},o=s.props?this.props.concat(s.props):this.props;e=v.Event(r);for(t=o.length;t;)n=o[--t],e[n]=r[n];return e.target||(e.target=r.srcElement||i),e.target.nodeType===3&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,s.filter?s.filter(e,r):e},special:{load:{noBubble:!0},focus:{delegateType:"focusin"},blur:{delegateType:"focusout"},beforeunload:{setup:function(e,t,n){v.isWindow(this)&&(this.onbeforeunload=n)},teardown:function(e,t){this.onbeforeunload===t&&(this.onbeforeunload=null)}}},simulate:function(e,t,n,r){var i=v.extend(new v.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?v.event.trigger(i,null,t):v.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},v.event.handle=v.event.dispatch,v.removeEvent=i.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]=="undefined"&&(e[r]=null),e.detachEvent(r,n))},v.Event=function(e,t){if(!(this instanceof v.Event))return new v.Event(e,t);e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||e.returnValue===!1||e.getPreventDefault&&e.getPreventDefault()?tt:et):this.type=e,t&&v.extend(this,t),this.timeStamp=e&&e.timeStamp||v.now(),this[v.expando]=!0},v.Event.prototype={preventDefault:function(){this.isDefaultPrevented=tt;var e=this.originalEvent;if(!e)return;e.preventDefault?e.preventDefault():e.returnValue=!1},stopPropagation:function(){this.isPropagationStopped=tt;var e=this.originalEvent;if(!e)return;e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0},stopImmediatePropagation:function(){this.isImmediatePropagationStopped=tt,this.stopPropagation()},isDefaultPrevented:et,isPropagationStopped:et,isImmediatePropagationStopped:et},v.each({mouseenter:"mouseover",mouseleave:"mouseout"},function(e,t){v.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,s=e.handleObj,o=s.selector;if(!i||i!==r&&!v.contains(r,i))e.type=s.origType,n=s.handler.apply(this,arguments),e.type=t;return n}}}),v.support.submitBubbles||(v.event.special.submit={setup:function(){if(v.nodeName(this,"form"))return!1;v.event.add(this,"click._submit keypress._submit",function(e){var n=e.target,r=v.nodeName(n,"input")||v.nodeName(n,"button")?n.form:t;r&&!v._data(r,"_submit_attached")&&(v.event.add(r,"submit._submit",function(e){e._submit_bubble=!0}),v._data(r,"_submit_attached",!0))})},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&v.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){if(v.nodeName(this,"form"))return!1;v.event.remove(this,"._submit")}}),v.support.changeBubbles||(v.event.special.change={setup:function(){if($.test(this.nodeName)){if(this.type==="checkbox"||this.type==="radio")v.event.add(this,"propertychange._change",function(e){e.originalEvent.propertyName==="checked"&&(this._just_changed=!0)}),v.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),v.event.simulate("change",this,e,!0)});return!1}v.event.add(this,"beforeactivate._change",function(e){var t=e.target;$.test(t.nodeName)&&!v._data(t,"_change_attached")&&(v.event.add(t,"change._change",function(e){this.parentNode&&!e.isSimulated&&!e.isTrigger&&v.event.simulate("change",this.parentNode,e,!0)}),v._data(t,"_change_attached",!0))})},handle:function(e){var t=e.target;if(this!==t||e.isSimulated||e.isTrigger||t.type!=="radio"&&t.type!=="checkbox")return e.handleObj.handler.apply(this,arguments)},teardown:function(){return v.event.remove(this,"._change"),!$.test(this.nodeName)}}),v.support.focusinBubbles||v.each({focus:"focusin",blur:"focusout"},function(e,t){var n=0,r=function(e){v.event.simulate(t,e.target,v.event.fix(e),!0)};v.event.special[t]={setup:function(){n++===0&&i.addEventListener(e,r,!0)},teardown:function(){--n===0&&i.removeEventListener(e,r,!0)}}}),v.fn.extend({on:function(e,n,r,i,s){var o,u;if(typeof e=="object"){typeof n!="string"&&(r=r||n,n=t);for(u in e)this.on(u,n,r,e[u],s);return this}r==null&&i==null?(i=n,r=n=t):i==null&&(typeof n=="string"?(i=r,r=t):(i=r,r=n,n=t));if(i===!1)i=et;else if(!i)return this;return s===1&&(o=i,i=function(e){return v().off(e),o.apply(this,arguments)},i.guid=o.guid||(o.guid=v.guid++)),this.each(function(){v.event.add(this,e,i,r,n)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,n,r){var i,s;if(e&&e.preventDefault&&e.handleObj)return i=e.handleObj,v(e.delegateTarget).off(i.namespace?i.origType+"."+i.namespace:i.origType,i.selector,i.handler),this;if(typeof e=="object"){for(s in e)this.off(s,n,e[s]);return this}if(n===!1||typeof n=="function")r=n,n=t;return r===!1&&(r=et),this.each(function(){v.event.remove(this,e,r,n)})},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},live:function(e,t,n){return v(this.context).on(e,this.selector,t,n),this},die:function(e,t){return v(this.context).off(e,this.selector||"**",t),this},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return arguments.length===1?this.off(e,"**"):this.off(t,e||"**",n)},trigger:function(e,t){return this.each(function(){v.event.trigger(e,t,this)})},triggerHandler:function(e,t){if(this[0])return v.event.trigger(e,t,this[0],!0)},toggle:function(e){var t=arguments,n=e.guid||v.guid++,r=0,i=function(n){var i=(v._data(this,"lastToggle"+e.guid)||0)%r;return v._data(this,"lastToggle"+e.guid,i+1),n.preventDefault(),t[i].apply(this,arguments)||!1};i.guid=n;while(r<t.length)t[r++].guid=n;return this.click(i)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),v.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){v.fn[t]=function(e,n){return n==null&&(n=e,e=null),arguments.length>0?this.on(t,null,e,n):this.trigger(t)},Q.test(t)&&(v.event.fixHooks[t]=v.event.keyHooks),G.test(t)&&(v.event.fixHooks[t]=v.event.mouseHooks)}),function(e,t){function nt(e,t,n,r){n=n||[],t=t||g;var i,s,a,f,l=t.nodeType;if(!e||typeof e!="string")return n;if(l!==1&&l!==9)return[];a=o(t);if(!a&&!r)if(i=R.exec(e))if(f=i[1]){if(l===9){s=t.getElementById(f);if(!s||!s.parentNode)return n;if(s.id===f)return n.push(s),n}else if(t.ownerDocument&&(s=t.ownerDocument.getElementById(f))&&u(t,s)&&s.id===f)return n.push(s),n}else{if(i[2])return S.apply(n,x.call(t.getElementsByTagName(e),0)),n;if((f=i[3])&&Z&&t.getElementsByClassName)return S.apply(n,x.call(t.getElementsByClassName(f),0)),n}return vt(e.replace(j,"$1"),t,n,r,a)}function rt(e){return function(t){var n=t.nodeName.toLowerCase();return n==="input"&&t.type===e}}function it(e){return function(t){var n=t.nodeName.toLowerCase();return(n==="input"||n==="button")&&t.type===e}}function st(e){return N(function(t){return t=+t,N(function(n,r){var i,s=e([],n.length,t),o=s.length;while(o--)n[i=s[o]]&&(n[i]=!(r[i]=n[i]))})})}function ot(e,t,n){if(e===t)return n;var r=e.nextSibling;while(r){if(r===t)return-1;r=r.nextSibling}return 1}function ut(e,t){var n,r,s,o,u,a,f,l=L[d][e+" "];if(l)return t?0:l.slice(0);u=e,a=[],f=i.preFilter;while(u){if(!n||(r=F.exec(u)))r&&(u=u.slice(r[0].length)||u),a.push(s=[]);n=!1;if(r=I.exec(u))s.push(n=new m(r.shift())),u=u.slice(n.length),n.type=r[0].replace(j," ");for(o in i.filter)(r=J[o].exec(u))&&(!f[o]||(r=f[o](r)))&&(s.push(n=new m(r.shift())),u=u.slice(n.length),n.type=o,n.matches=r);if(!n)break}return t?u.length:u?nt.error(e):L(e,a).slice(0)}function at(e,t,r){var i=t.dir,s=r&&t.dir==="parentNode",o=w++;return t.first?function(t,n,r){while(t=t[i])if(s||t.nodeType===1)return e(t,n,r)}:function(t,r,u){if(!u){var a,f=b+" "+o+" ",l=f+n;while(t=t[i])if(s||t.nodeType===1){if((a=t[d])===l)return t.sizset;if(typeof a=="string"&&a.indexOf(f)===0){if(t.sizset)return t}else{t[d]=l;if(e(t,r,u))return t.sizset=!0,t;t.sizset=!1}}}else while(t=t[i])if(s||t.nodeType===1)if(e(t,r,u))return t}}function ft(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function lt(e,t,n,r,i){var s,o=[],u=0,a=e.length,f=t!=null;for(;u<a;u++)if(s=e[u])if(!n||n(s,r,i))o.push(s),f&&t.push(u);return o}function ct(e,t,n,r,i,s){return r&&!r[d]&&(r=ct(r)),i&&!i[d]&&(i=ct(i,s)),N(function(s,o,u,a){var f,l,c,h=[],p=[],d=o.length,v=s||dt(t||"*",u.nodeType?[u]:u,[]),m=e&&(s||!t)?lt(v,h,e,u,a):v,g=n?i||(s?e:d||r)?[]:o:m;n&&n(m,g,u,a);if(r){f=lt(g,p),r(f,[],u,a),l=f.length;while(l--)if(c=f[l])g[p[l]]=!(m[p[l]]=c)}if(s){if(i||e){if(i){f=[],l=g.length;while(l--)(c=g[l])&&f.push(m[l]=c);i(null,g=[],f,a)}l=g.length;while(l--)(c=g[l])&&(f=i?T.call(s,c):h[l])>-1&&(s[f]=!(o[f]=c))}}else g=lt(g===o?g.splice(d,g.length):g),i?i(null,o,g,a):S.apply(o,g)})}function ht(e){var t,n,r,s=e.length,o=i.relative[e[0].type],u=o||i.relative[" "],a=o?1:0,f=at(function(e){return e===t},u,!0),l=at(function(e){return T.call(t,e)>-1},u,!0),h=[function(e,n,r){return!o&&(r||n!==c)||((t=n).nodeType?f(e,n,r):l(e,n,r))}];for(;a<s;a++)if(n=i.relative[e[a].type])h=[at(ft(h),n)];else{n=i.filter[e[a].type].apply(null,e[a].matches);if(n[d]){r=++a;for(;r<s;r++)if(i.relative[e[r].type])break;return ct(a>1&&ft(h),a>1&&e.slice(0,a-1).join("").replace(j,"$1"),n,a<r&&ht(e.slice(a,r)),r<s&&ht(e=e.slice(r)),r<s&&e.join(""))}h.push(n)}return ft(h)}function pt(e,t){var r=t.length>0,s=e.length>0,o=function(u,a,f,l,h){var p,d,v,m=[],y=0,w="0",x=u&&[],T=h!=null,N=c,C=u||s&&i.find.TAG("*",h&&a.parentNode||a),k=b+=N==null?1:Math.E;T&&(c=a!==g&&a,n=o.el);for(;(p=C[w])!=null;w++){if(s&&p){for(d=0;v=e[d];d++)if(v(p,a,f)){l.push(p);break}T&&(b=k,n=++o.el)}r&&((p=!v&&p)&&y--,u&&x.push(p))}y+=w;if(r&&w!==y){for(d=0;v=t[d];d++)v(x,m,a,f);if(u){if(y>0)while(w--)!x[w]&&!m[w]&&(m[w]=E.call(l));m=lt(m)}S.apply(l,m),T&&!u&&m.length>0&&y+t.length>1&&nt.uniqueSort(l)}return T&&(b=k,c=N),x};return o.el=0,r?N(o):o}function dt(e,t,n){var r=0,i=t.length;for(;r<i;r++)nt(e,t[r],n);return n}function vt(e,t,n,r,s){var o,u,f,l,c,h=ut(e),p=h.length;if(!r&&h.length===1){u=h[0]=h[0].slice(0);if(u.length>2&&(f=u[0]).type==="ID"&&t.nodeType===9&&!s&&i.relative[u[1].type]){t=i.find.ID(f.matches[0].replace($,""),t,s)[0];if(!t)return n;e=e.slice(u.shift().length)}for(o=J.POS.test(e)?-1:u.length-1;o>=0;o--){f=u[o];if(i.relative[l=f.type])break;if(c=i.find[l])if(r=c(f.matches[0].replace($,""),z.test(u[0].type)&&t.parentNode||t,s)){u.splice(o,1),e=r.length&&u.join("");if(!e)return S.apply(n,x.call(r,0)),n;break}}}return a(e,h)(r,t,s,n,z.test(e)),n}function mt(){}var n,r,i,s,o,u,a,f,l,c,h=!0,p="undefined",d=("sizcache"+Math.random()).replace(".",""),m=String,g=e.document,y=g.documentElement,b=0,w=0,E=[].pop,S=[].push,x=[].slice,T=[].indexOf||function(e){var t=0,n=this.length;for(;t<n;t++)if(this[t]===e)return t;return-1},N=function(e,t){return e[d]=t==null||t,e},C=function(){var e={},t=[];return N(function(n,r){return t.push(n)>i.cacheLength&&delete e[t.shift()],e[n+" "]=r},e)},k=C(),L=C(),A=C(),O="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",_=M.replace("w","w#"),D="([*^$|!~]?=)",P="\\["+O+"*("+M+")"+O+"*(?:"+D+O+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+_+")|)|)"+O+"*\\]",H=":("+M+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|([^()[\\]]*|(?:(?:"+P+")|[^:]|\\\\.)*|.*))\\)|)",B=":(even|odd|eq|gt|lt|nth|first|last)(?:\\("+O+"*((?:-\\d)?\\d*)"+O+"*\\)|)(?=[^-]|$)",j=new RegExp("^"+O+"+|((?:^|[^\\\\])(?:\\\\.)*)"+O+"+$","g"),F=new RegExp("^"+O+"*,"+O+"*"),I=new RegExp("^"+O+"*([\\x20\\t\\r\\n\\f>+~])"+O+"*"),q=new RegExp(H),R=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,U=/^:not/,z=/[\x20\t\r\n\f]*[+~]/,W=/:not\($/,X=/h\d/i,V=/input|select|textarea|button/i,$=/\\(?!\\)/g,J={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),NAME:new RegExp("^\\[name=['\"]?("+M+")['\"]?\\]"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+P),PSEUDO:new RegExp("^"+H),POS:new RegExp(B,"i"),CHILD:new RegExp("^:(only|nth|first|last)-child(?:\\("+O+"*(even|odd|(([+-]|)(\\d*)n|)"+O+"*(?:([+-]|)"+O+"*(\\d+)|))"+O+"*\\)|)","i"),needsContext:new RegExp("^"+O+"*[>+~]|"+B,"i")},K=function(e){var t=g.createElement("div");try{return e(t)}catch(n){return!1}finally{t=null}},Q=K(function(e){return e.appendChild(g.createComment("")),!e.getElementsByTagName("*").length}),G=K(function(e){return e.innerHTML="<a href='#'></a>",e.firstChild&&typeof e.firstChild.getAttribute!==p&&e.firstChild.getAttribute("href")==="#"}),Y=K(function(e){e.innerHTML="<select></select>";var t=typeof e.lastChild.getAttribute("multiple");return t!=="boolean"&&t!=="string"}),Z=K(function(e){return e.innerHTML="<div class='hidden e'></div><div class='hidden'></div>",!e.getElementsByClassName||!e.getElementsByClassName("e").length?!1:(e.lastChild.className="e",e.getElementsByClassName("e").length===2)}),et=K(function(e){e.id=d+0,e.innerHTML="<a name='"+d+"'></a><div name='"+d+"'></div>",y.insertBefore(e,y.firstChild);var t=g.getElementsByName&&g.getElementsByName(d).length===2+g.getElementsByName(d+0).length;return r=!g.getElementById(d),y.removeChild(e),t});try{x.call(y.childNodes,0)[0].nodeType}catch(tt){x=function(e){var t,n=[];for(;t=this[e];e++)n.push(t);return n}}nt.matches=function(e,t){return nt(e,null,null,t)},nt.matchesSelector=function(e,t){return nt(t,null,null,[e]).length>0},s=nt.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(i===1||i===9||i===11){if(typeof e.textContent=="string")return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=s(e)}else if(i===3||i===4)return e.nodeValue}else for(;t=e[r];r++)n+=s(t);return n},o=nt.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?t.nodeName!=="HTML":!1},u=nt.contains=y.contains?function(e,t){var n=e.nodeType===9?e.documentElement:e,r=t&&t.parentNode;return e===r||!!(r&&r.nodeType===1&&n.contains&&n.contains(r))}:y.compareDocumentPosition?function(e,t){return t&&!!(e.compareDocumentPosition(t)&16)}:function(e,t){while(t=t.parentNode)if(t===e)return!0;return!1},nt.attr=function(e,t){var n,r=o(e);return r||(t=t.toLowerCase()),(n=i.attrHandle[t])?n(e):r||Y?e.getAttribute(t):(n=e.getAttributeNode(t),n?typeof e[t]=="boolean"?e[t]?t:null:n.specified?n.value:null:null)},i=nt.selectors={cacheLength:50,createPseudo:N,match:J,attrHandle:G?{}:{href:function(e){return e.getAttribute("href",2)},type:function(e){return e.getAttribute("type")}},find:{ID:r?function(e,t,n){if(typeof t.getElementById!==p&&!n){var r=t.getElementById(e);return r&&r.parentNode?[r]:[]}}:function(e,n,r){if(typeof n.getElementById!==p&&!r){var i=n.getElementById(e);return i?i.id===e||typeof i.getAttributeNode!==p&&i.getAttributeNode("id").value===e?[i]:t:[]}},TAG:Q?function(e,t){if(typeof t.getElementsByTagName!==p)return t.getElementsByTagName(e)}:function(e,t){var n=t.getElementsByTagName(e);if(e==="*"){var r,i=[],s=0;for(;r=n[s];s++)r.nodeType===1&&i.push(r);return i}return n},NAME:et&&function(e,t){if(typeof t.getElementsByName!==p)return t.getElementsByName(name)},CLASS:Z&&function(e,t,n){if(typeof t.getElementsByClassName!==p&&!n)return t.getElementsByClassName(e)}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace($,""),e[3]=(e[4]||e[5]||"").replace($,""),e[2]==="~="&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),e[1]==="nth"?(e[2]||nt.error(e[0]),e[3]=+(e[3]?e[4]+(e[5]||1):2*(e[2]==="even"||e[2]==="odd")),e[4]=+(e[6]+e[7]||e[2]==="odd")):e[2]&&nt.error(e[0]),e},PSEUDO:function(e){var t,n;if(J.CHILD.test(e[0]))return null;if(e[3])e[2]=e[3];else if(t=e[4])q.test(t)&&(n=ut(t,!0))&&(n=t.indexOf(")",t.length-n)-t.length)&&(t=t.slice(0,n),e[0]=e[0].slice(0,n)),e[2]=t;return e.slice(0,3)}},filter:{ID:r?function(e){return e=e.replace($,""),function(t){return t.getAttribute("id")===e}}:function(e){return e=e.replace($,""),function(t){var n=typeof t.getAttributeNode!==p&&t.getAttributeNode("id");return n&&n.value===e}},TAG:function(e){return e==="*"?function(){return!0}:(e=e.replace($,"").toLowerCase(),function(t){return t.nodeName&&t.nodeName.toLowerCase()===e})},CLASS:function(e){var t=k[d][e+" "];return t||(t=new RegExp("(^|"+O+")"+e+"("+O+"|$)"))&&k(e,function(e){return t.test(e.className||typeof e.getAttribute!==p&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r,i){var s=nt.attr(r,e);return s==null?t==="!=":t?(s+="",t==="="?s===n:t==="!="?s!==n:t==="^="?n&&s.indexOf(n)===0:t==="*="?n&&s.indexOf(n)>-1:t==="$="?n&&s.substr(s.length-n.length)===n:t==="~="?(" "+s+" ").indexOf(n)>-1:t==="|="?s===n||s.substr(0,n.length+1)===n+"-":!1):!0}},CHILD:function(e,t,n,r){return e==="nth"?function(e){var t,i,s=e.parentNode;if(n===1&&r===0)return!0;if(s){i=0;for(t=s.firstChild;t;t=t.nextSibling)if(t.nodeType===1){i++;if(e===t)break}}return i-=r,i===n||i%n===0&&i/n>=0}:function(t){var n=t;switch(e){case"only":case"first":while(n=n.previousSibling)if(n.nodeType===1)return!1;if(e==="first")return!0;n=t;case"last":while(n=n.nextSibling)if(n.nodeType===1)return!1;return!0}}},PSEUDO:function(e,t){var n,r=i.pseudos[e]||i.setFilters[e.toLowerCase()]||nt.error("unsupported pseudo: "+e);return r[d]?r(t):r.length>1?(n=[e,e,"",t],i.setFilters.hasOwnProperty(e.toLowerCase())?N(function(e,n){var i,s=r(e,t),o=s.length;while(o--)i=T.call(e,s[o]),e[i]=!(n[i]=s[o])}):function(e){return r(e,0,n)}):r}},pseudos:{not:N(function(e){var t=[],n=[],r=a(e.replace(j,"$1"));return r[d]?N(function(e,t,n,i){var s,o=r(e,null,i,[]),u=e.length;while(u--)if(s=o[u])e[u]=!(t[u]=s)}):function(e,i,s){return t[0]=e,r(t,null,s,n),!n.pop()}}),has:N(function(e){return function(t){return nt(e,t).length>0}}),contains:N(function(e){return function(t){return(t.textContent||t.innerText||s(t)).indexOf(e)>-1}}),enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return t==="input"&&!!e.checked||t==="option"&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},parent:function(e){return!i.pseudos.empty(e)},empty:function(e){var t;e=e.firstChild;while(e){if(e.nodeName>"@"||(t=e.nodeType)===3||t===4)return!1;e=e.nextSibling}return!0},header:function(e){return X.test(e.nodeName)},text:function(e){var t,n;return e.nodeName.toLowerCase()==="input"&&(t=e.type)==="text"&&((n=e.getAttribute("type"))==null||n.toLowerCase()===t)},radio:rt("radio"),checkbox:rt("checkbox"),file:rt("file"),password:rt("password"),image:rt("image"),submit:it("submit"),reset:it("reset"),button:function(e){var t=e.nodeName.toLowerCase();return t==="input"&&e.type==="button"||t==="button"},input:function(e){return V.test(e.nodeName)},focus:function(e){var t=e.ownerDocument;return e===t.activeElement&&(!t.hasFocus||t.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},active:function(e){return e===e.ownerDocument.activeElement},first:st(function(){return[0]}),last:st(function(e,t){return[t-1]}),eq:st(function(e,t,n){return[n<0?n+t:n]}),even:st(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:st(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:st(function(e,t,n){for(var r=n<0?n+t:n;--r>=0;)e.push(r);return e}),gt:st(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}},f=y.compareDocumentPosition?function(e,t){return e===t?(l=!0,0):(!e.compareDocumentPosition||!t.compareDocumentPosition?e.compareDocumentPosition:e.compareDocumentPosition(t)&4)?-1:1}:function(e,t){if(e===t)return l=!0,0;if(e.sourceIndex&&t.sourceIndex)return e.sourceIndex-t.sourceIndex;var n,r,i=[],s=[],o=e.parentNode,u=t.parentNode,a=o;if(o===u)return ot(e,t);if(!o)return-1;if(!u)return 1;while(a)i.unshift(a),a=a.parentNode;a=u;while(a)s.unshift(a),a=a.parentNode;n=i.length,r=s.length;for(var f=0;f<n&&f<r;f++)if(i[f]!==s[f])return ot(i[f],s[f]);return f===n?ot(e,s[f],-1):ot(i[f],t,1)},[0,0].sort(f),h=!l,nt.uniqueSort=function(e){var t,n=[],r=1,i=0;l=h,e.sort(f);if(l){for(;t=e[r];r++)t===e[r-1]&&(i=n.push(r));while(i--)e.splice(n[i],1)}return e},nt.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},a=nt.compile=function(e,t){var n,r=[],i=[],s=A[d][e+" "];if(!s){t||(t=ut(e)),n=t.length;while(n--)s=ht(t[n]),s[d]?r.push(s):i.push(s);s=A(e,pt(i,r))}return s},g.querySelectorAll&&function(){var e,t=vt,n=/'|\\/g,r=/\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,i=[":focus"],s=[":active"],u=y.matchesSelector||y.mozMatchesSelector||y.webkitMatchesSelector||y.oMatchesSelector||y.msMatchesSelector;K(function(e){e.innerHTML="<select><option selected=''></option></select>",e.querySelectorAll("[selected]").length||i.push("\\["+O+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),e.querySelectorAll(":checked").length||i.push(":checked")}),K(function(e){e.innerHTML="<p test=''></p>",e.querySelectorAll("[test^='']").length&&i.push("[*^$]="+O+"*(?:\"\"|'')"),e.innerHTML="<input type='hidden'/>",e.querySelectorAll(":enabled").length||i.push(":enabled",":disabled")}),i=new RegExp(i.join("|")),vt=function(e,r,s,o,u){if(!o&&!u&&!i.test(e)){var a,f,l=!0,c=d,h=r,p=r.nodeType===9&&e;if(r.nodeType===1&&r.nodeName.toLowerCase()!=="object"){a=ut(e),(l=r.getAttribute("id"))?c=l.replace(n,"\\$&"):r.setAttribute("id",c),c="[id='"+c+"'] ",f=a.length;while(f--)a[f]=c+a[f].join("");h=z.test(e)&&r.parentNode||r,p=a.join(",")}if(p)try{return S.apply(s,x.call(h.querySelectorAll(p),0)),s}catch(v){}finally{l||r.removeAttribute("id")}}return t(e,r,s,o,u)},u&&(K(function(t){e=u.call(t,"div");try{u.call(t,"[test!='']:sizzle"),s.push("!=",H)}catch(n){}}),s=new RegExp(s.join("|")),nt.matchesSelector=function(t,n){n=n.replace(r,"='$1']");if(!o(t)&&!s.test(n)&&!i.test(n))try{var a=u.call(t,n);if(a||e||t.document&&t.document.nodeType!==11)return a}catch(f){}return nt(n,null,null,[t]).length>0})}(),i.pseudos.nth=i.pseudos.eq,i.filters=mt.prototype=i.pseudos,i.setFilters=new mt,nt.attr=v.attr,v.find=nt,v.expr=nt.selectors,v.expr[":"]=v.expr.pseudos,v.unique=nt.uniqueSort,v.text=nt.getText,v.isXMLDoc=nt.isXML,v.contains=nt.contains}(e);var nt=/Until$/,rt=/^(?:parents|prev(?:Until|All))/,it=/^.[^:#\[\.,]*$/,st=v.expr.match.needsContext,ot={children:!0,contents:!0,next:!0,prev:!0};v.fn.extend({find:function(e){var t,n,r,i,s,o,u=this;if(typeof e!="string")return v(e).filter(function(){for(t=0,n=u.length;t<n;t++)if(v.contains(u[t],this))return!0});o=this.pushStack("","find",e);for(t=0,n=this.length;t<n;t++){r=o.length,v.find(e,this[t],o);if(t>0)for(i=r;i<o.length;i++)for(s=0;s<r;s++)if(o[s]===o[i]){o.splice(i--,1);break}}return o},has:function(e){var t,n=v(e,this),r=n.length;return this.filter(function(){for(t=0;t<r;t++)if(v.contains(this,n[t]))return!0})},not:function(e){return this.pushStack(ft(this,e,!1),"not",e)},filter:function(e){return this.pushStack(ft(this,e,!0),"filter",e)},is:function(e){return!!e&&(typeof e=="string"?st.test(e)?v(e,this.context).index(this[0])>=0:v.filter(e,this).length>0:this.filter(e).length>0)},closest:function(e,t){var n,r=0,i=this.length,s=[],o=st.test(e)||typeof e!="string"?v(e,t||this.context):0;for(;r<i;r++){n=this[r];while(n&&n.ownerDocument&&n!==t&&n.nodeType!==11){if(o?o.index(n)>-1:v.find.matchesSelector(n,e)){s.push(n);break}n=n.parentNode}}return s=s.length>1?v.unique(s):s,this.pushStack(s,"closest",e)},index:function(e){return e?typeof e=="string"?v.inArray(this[0],v(e)):v.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(e,t){var n=typeof e=="string"?v(e,t):v.makeArray(e&&e.nodeType?[e]:e),r=v.merge(this.get(),n);return this.pushStack(ut(n[0])||ut(r[0])?r:v.unique(r))},addBack:function(e){return this.add(e==null?this.prevObject:this.prevObject.filter(e))}}),v.fn.andSelf=v.fn.addBack,v.each({parent:function(e){var t=e.parentNode;return t&&t.nodeType!==11?t:null},parents:function(e){return v.dir(e,"parentNode")},parentsUntil:function(e,t,n){return v.dir(e,"parentNode",n)},next:function(e){return at(e,"nextSibling")},prev:function(e){return at(e,"previousSibling")},nextAll:function(e){return v.dir(e,"nextSibling")},prevAll:function(e){return v.dir(e,"previousSibling")},nextUntil:function(e,t,n){return v.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return v.dir(e,"previousSibling",n)},siblings:function(e){return v.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return v.sibling(e.firstChild)},contents:function(e){return v.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:v.merge([],e.childNodes)}},function(e,t){v.fn[e]=function(n,r){var i=v.map(this,t,n);return nt.test(e)||(r=n),r&&typeof r=="string"&&(i=v.filter(r,i)),i=this.length>1&&!ot[e]?v.unique(i):i,this.length>1&&rt.test(e)&&(i=i.reverse()),this.pushStack(i,e,l.call(arguments).join(","))}}),v.extend({filter:function(e,t,n){return n&&(e=":not("+e+")"),t.length===1?v.find.matchesSelector(t[0],e)?[t[0]]:[]:v.find.matches(e,t)},dir:function(e,n,r){var i=[],s=e[n];while(s&&s.nodeType!==9&&(r===t||s.nodeType!==1||!v(s).is(r)))s.nodeType===1&&i.push(s),s=s[n];return i},sibling:function(e,t){var n=[];for(;e;e=e.nextSibling)e.nodeType===1&&e!==t&&n.push(e);return n}});var ct="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",ht=/ jQuery\d+="(?:null|\d+)"/g,pt=/^\s+/,dt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,vt=/<([\w:]+)/,mt=/<tbody/i,gt=/<|&#?\w+;/,yt=/<(?:script|style|link)/i,bt=/<(?:script|object|embed|option|style)/i,wt=new RegExp("<(?:"+ct+")[\\s/>]","i"),Et=/^(?:checkbox|radio)$/,St=/checked\s*(?:[^=]|=\s*.checked.)/i,xt=/\/(java|ecma)script/i,Tt=/^\s*<!(?:\[CDATA\[|\-\-)|[\]\-]{2}>\s*$/g,Nt={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],area:[1,"<map>","</map>"],_default:[0,"",""]},Ct=lt(i),kt=Ct.appendChild(i.createElement("div"));Nt.optgroup=Nt.option,Nt.tbody=Nt.tfoot=Nt.colgroup=Nt.caption=Nt.thead,Nt.th=Nt.td,v.support.htmlSerialize||(Nt._default=[1,"X<div>","</div>"]),v.fn.extend({text:function(e){return v.access(this,function(e){return e===t?v.text(this):this.empty().append((this[0]&&this[0].ownerDocument||i).createTextNode(e))},null,e,arguments.length)},wrapAll:function(e){if(v.isFunction(e))return this.each(function(t){v(this).wrapAll(e.call(this,t))});if(this[0]){var t=v(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){var e=this;while(e.firstChild&&e.firstChild.nodeType===1)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return v.isFunction(e)?this.each(function(t){v(this).wrapInner(e.call(this,t))}):this.each(function(){var t=v(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=v.isFunction(e);return this.each(function(n){v(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){v.nodeName(this,"body")||v(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(e){(this.nodeType===1||this.nodeType===11)&&this.appendChild(e)})},prepend:function(){return this.domManip(arguments,!0,function(e){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(e,this.firstChild)})},before:function(){if(!ut(this[0]))return this.domManip(arguments,!1,function(e){this.parentNode.insertBefore(e,this)});if(arguments.length){var e=v.clean(arguments);return this.pushStack(v.merge(e,this),"before",this.selector)}},after:function(){if(!ut(this[0]))return this.domManip(arguments,!1,function(e){this.parentNode.insertBefore(e,this.nextSibling)});if(arguments.length){var e=v.clean(arguments);return this.pushStack(v.merge(this,e),"after",this.selector)}},remove:function(e,t){var n,r=0;for(;(n=this[r])!=null;r++)if(!e||v.filter(e,[n]).length)!t&&n.nodeType===1&&(v.cleanData(n.getElementsByTagName("*")),v.cleanData([n])),n.parentNode&&n.parentNode.removeChild(n);return this},empty:function(){var e,t=0;for(;(e=this[t])!=null;t++){e.nodeType===1&&v.cleanData(e.getElementsByTagName("*"));while(e.firstChild)e.removeChild(e.firstChild)}return this},clone:function(e,t){return e=e==null?!1:e,t=t==null?e:t,this.map(function(){return v.clone(this,e,t)})},html:function(e){return v.access(this,function(e){var n=this[0]||{},r=0,i=this.length;if(e===t)return n.nodeType===1?n.innerHTML.replace(ht,""):t;if(typeof e=="string"&&!yt.test(e)&&(v.support.htmlSerialize||!wt.test(e))&&(v.support.leadingWhitespace||!pt.test(e))&&!Nt[(vt.exec(e)||["",""])[1].toLowerCase()]){e=e.replace(dt,"<$1></$2>");try{for(;r<i;r++)n=this[r]||{},n.nodeType===1&&(v.cleanData(n.getElementsByTagName("*")),n.innerHTML=e);n=0}catch(s){}}n&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(e){return ut(this[0])?this.length?this.pushStack(v(v.isFunction(e)?e():e),"replaceWith",e):this:v.isFunction(e)?this.each(function(t){var n=v(this),r=n.html();n.replaceWith(e.call(this,t,r))}):(typeof e!="string"&&(e=v(e).detach()),this.each(function(){var t=this.nextSibling,n=this.parentNode;v(this).remove(),t?v(t).before(e):v(n).append(e)}))},detach:function(e){return this.remove(e,!0)},domManip:function(e,n,r){e=[].concat.apply([],e);var i,s,o,u,a=0,f=e[0],l=[],c=this.length;if(!v.support.checkClone&&c>1&&typeof f=="string"&&St.test(f))return this.each(function(){v(this).domManip(e,n,r)});if(v.isFunction(f))return this.each(function(i){var s=v(this);e[0]=f.call(this,i,n?s.html():t),s.domManip(e,n,r)});if(this[0]){i=v.buildFragment(e,this,l),o=i.fragment,s=o.firstChild,o.childNodes.length===1&&(o=s);if(s){n=n&&v.nodeName(s,"tr");for(u=i.cacheable||c-1;a<c;a++)r.call(n&&v.nodeName(this[a],"table")?Lt(this[a],"tbody"):this[a],a===u?o:v.clone(o,!0,!0))}o=s=null,l.length&&v.each(l,function(e,t){t.src?v.ajax?v.ajax({url:t.src,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0}):v.error("no ajax"):v.globalEval((t.text||t.textContent||t.innerHTML||"").replace(Tt,"")),t.parentNode&&t.parentNode.removeChild(t)})}return this}}),v.buildFragment=function(e,n,r){var s,o,u,a=e[0];return n=n||i,n=!n.nodeType&&n[0]||n,n=n.ownerDocument||n,e.length===1&&typeof a=="string"&&a.length<512&&n===i&&a.charAt(0)==="<"&&!bt.test(a)&&(v.support.checkClone||!St.test(a))&&(v.support.html5Clone||!wt.test(a))&&(o=!0,s=v.fragments[a],u=s!==t),s||(s=n.createDocumentFragment(),v.clean(e,n,s,r),o&&(v.fragments[a]=u&&s)),{fragment:s,cacheable:o}},v.fragments={},v.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){v.fn[e]=function(n){var r,i=0,s=[],o=v(n),u=o.length,a=this.length===1&&this[0].parentNode;if((a==null||a&&a.nodeType===11&&a.childNodes.length===1)&&u===1)return o[t](this[0]),this;for(;i<u;i++)r=(i>0?this.clone(!0):this).get(),v(o[i])[t](r),s=s.concat(r);return this.pushStack(s,e,o.selector)}}),v.extend({clone:function(e,t,n){var r,i,s,o;v.support.html5Clone||v.isXMLDoc(e)||!wt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(kt.innerHTML=e.outerHTML,kt.removeChild(o=kt.firstChild));if((!v.support.noCloneEvent||!v.support.noCloneChecked)&&(e.nodeType===1||e.nodeType===11)&&!v.isXMLDoc(e)){Ot(e,o),r=Mt(e),i=Mt(o);for(s=0;r[s];++s)i[s]&&Ot(r[s],i[s])}if(t){At(e,o);if(n){r=Mt(e),i=Mt(o);for(s=0;r[s];++s)At(r[s],i[s])}}return r=i=null,o},clean:function(e,t,n,r){var s,o,u,a,f,l,c,h,p,d,m,g,y=t===i&&Ct,b=[];if(!t||typeof t.createDocumentFragment=="undefined")t=i;for(s=0;(u=e[s])!=null;s++){typeof u=="number"&&(u+="");if(!u)continue;if(typeof u=="string")if(!gt.test(u))u=t.createTextNode(u);else{y=y||lt(t),c=t.createElement("div"),y.appendChild(c),u=u.replace(dt,"<$1></$2>"),a=(vt.exec(u)||["",""])[1].toLowerCase(),f=Nt[a]||Nt._default,l=f[0],c.innerHTML=f[1]+u+f[2];while(l--)c=c.lastChild;if(!v.support.tbody){h=mt.test(u),p=a==="table"&&!h?c.firstChild&&c.firstChild.childNodes:f[1]==="<table>"&&!h?c.childNodes:[];for(o=p.length-1;o>=0;--o)v.nodeName(p[o],"tbody")&&!p[o].childNodes.length&&p[o].parentNode.removeChild(p[o])}!v.support.leadingWhitespace&&pt.test(u)&&c.insertBefore(t.createTextNode(pt.exec(u)[0]),c.firstChild),u=c.childNodes,c.parentNode.removeChild(c)}u.nodeType?b.push(u):v.merge(b,u)}c&&(u=c=y=null);if(!v.support.appendChecked)for(s=0;(u=b[s])!=null;s++)v.nodeName(u,"input")?_t(u):typeof u.getElementsByTagName!="undefined"&&v.grep(u.getElementsByTagName("input"),_t);if(n){m=function(e){if(!e.type||xt.test(e.type))return r?r.push(e.parentNode?e.parentNode.removeChild(e):e):n.appendChild(e)};for(s=0;(u=b[s])!=null;s++)if(!v.nodeName(u,"script")||!m(u))n.appendChild(u),typeof u.getElementsByTagName!="undefined"&&(g=v.grep(v.merge([],u.getElementsByTagName("script")),m),b.splice.apply(b,[s+1,0].concat(g)),s+=g.length)}return b},cleanData:function(e,t){var n,r,i,s,o=0,u=v.expando,a=v.cache,f=v.support.deleteExpando,l=v.event.special;for(;(i=e[o])!=null;o++)if(t||v.acceptData(i)){r=i[u],n=r&&a[r];if(n){if(n.events)for(s in n.events)l[s]?v.event.remove(i,s):v.removeEvent(i,s,n.handle);a[r]&&(delete a[r],f?delete i[u]:i.removeAttribute?i.removeAttribute(u):i[u]=null,v.deletedIds.push(r))}}}}),function(){var e,t;v.uaMatch=function(e){e=e.toLowerCase();var t=/(chrome)[ \/]([\w.]+)/.exec(e)||/(webkit)[ \/]([\w.]+)/.exec(e)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(e)||/(msie) ([\w.]+)/.exec(e)||e.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(e)||[];return{browser:t[1]||"",version:t[2]||"0"}},e=v.uaMatch(o.userAgent),t={},e.browser&&(t[e.browser]=!0,t.version=e.version),t.chrome?t.webkit=!0:t.webkit&&(t.safari=!0),v.browser=t,v.sub=function(){function e(t,n){return new e.fn.init(t,n)}v.extend(!0,e,this),e.superclass=this,e.fn=e.prototype=this(),e.fn.constructor=e,e.sub=this.sub,e.fn.init=function(r,i){return i&&i instanceof v&&!(i instanceof e)&&(i=e(i)),v.fn.init.call(this,r,i,t)},e.fn.init.prototype=e.fn;var t=e(i);return e}}();var Dt,Pt,Ht,Bt=/alpha\([^)]*\)/i,jt=/opacity=([^)]*)/,Ft=/^(top|right|bottom|left)$/,It=/^(none|table(?!-c[ea]).+)/,qt=/^margin/,Rt=new RegExp("^("+m+")(.*)$","i"),Ut=new RegExp("^("+m+")(?!px)[a-z%]+$","i"),zt=new RegExp("^([-+])=("+m+")","i"),Wt={BODY:"block"},Xt={position:"absolute",visibility:"hidden",display:"block"},Vt={letterSpacing:0,fontWeight:400},$t=["Top","Right","Bottom","Left"],Jt=["Webkit","O","Moz","ms"],Kt=v.fn.toggle;v.fn.extend({css:function(e,n){return v.access(this,function(e,n,r){return r!==t?v.style(e,n,r):v.css(e,n)},e,n,arguments.length>1)},show:function(){return Yt(this,!0)},hide:function(){return Yt(this)},toggle:function(e,t){var n=typeof e=="boolean";return v.isFunction(e)&&v.isFunction(t)?Kt.apply(this,arguments):this.each(function(){(n?e:Gt(this))?v(this).show():v(this).hide()})}}),v.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=Dt(e,"opacity");return n===""?"1":n}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":v.support.cssFloat?"cssFloat":"styleFloat"},style:function(e,n,r,i){if(!e||e.nodeType===3||e.nodeType===8||!e.style)return;var s,o,u,a=v.camelCase(n),f=e.style;n=v.cssProps[a]||(v.cssProps[a]=Qt(f,a)),u=v.cssHooks[n]||v.cssHooks[a];if(r===t)return u&&"get"in u&&(s=u.get(e,!1,i))!==t?s:f[n];o=typeof r,o==="string"&&(s=zt.exec(r))&&(r=(s[1]+1)*s[2]+parseFloat(v.css(e,n)),o="number");if(r==null||o==="number"&&isNaN(r))return;o==="number"&&!v.cssNumber[a]&&(r+="px");if(!u||!("set"in u)||(r=u.set(e,r,i))!==t)try{f[n]=r}catch(l){}},css:function(e,n,r,i){var s,o,u,a=v.camelCase(n);return n=v.cssProps[a]||(v.cssProps[a]=Qt(e.style,a)),u=v.cssHooks[n]||v.cssHooks[a],u&&"get"in u&&(s=u.get(e,!0,i)),s===t&&(s=Dt(e,n)),s==="normal"&&n in Vt&&(s=Vt[n]),r||i!==t?(o=parseFloat(s),r||v.isNumeric(o)?o||0:s):s},swap:function(e,t,n){var r,i,s={};for(i in t)s[i]=e.style[i],e.style[i]=t[i];r=n.call(e);for(i in t)e.style[i]=s[i];return r}}),e.getComputedStyle?Dt=function(t,n){var r,i,s,o,u=e.getComputedStyle(t,null),a=t.style;return u&&(r=u.getPropertyValue(n)||u[n],r===""&&!v.contains(t.ownerDocument,t)&&(r=v.style(t,n)),Ut.test(r)&&qt.test(n)&&(i=a.width,s=a.minWidth,o=a.maxWidth,a.minWidth=a.maxWidth=a.width=r,r=u.width,a.width=i,a.minWidth=s,a.maxWidth=o)),r}:i.documentElement.currentStyle&&(Dt=function(e,t){var n,r,i=e.currentStyle&&e.currentStyle[t],s=e.style;return i==null&&s&&s[t]&&(i=s[t]),Ut.test(i)&&!Ft.test(t)&&(n=s.left,r=e.runtimeStyle&&e.runtimeStyle.left,r&&(e.runtimeStyle.left=e.currentStyle.left),s.left=t==="fontSize"?"1em":i,i=s.pixelLeft+"px",s.left=n,r&&(e.runtimeStyle.left=r)),i===""?"auto":i}),v.each(["height","width"],function(e,t){v.cssHooks[t]={get:function(e,n,r){if(n)return e.offsetWidth===0&&It.test(Dt(e,"display"))?v.swap(e,Xt,function(){return tn(e,t,r)}):tn(e,t,r)},set:function(e,n,r){return Zt(e,n,r?en(e,t,r,v.support.boxSizing&&v.css(e,"boxSizing")==="border-box"):0)}}}),v.support.opacity||(v.cssHooks.opacity={get:function(e,t){return jt.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=v.isNumeric(t)?"alpha(opacity="+t*100+")":"",s=r&&r.filter||n.filter||"";n.zoom=1;if(t>=1&&v.trim(s.replace(Bt,""))===""&&n.removeAttribute){n.removeAttribute("filter");if(r&&!r.filter)return}n.filter=Bt.test(s)?s.replace(Bt,i):s+" "+i}}),v(function(){v.support.reliableMarginRight||(v.cssHooks.marginRight={get:function(e,t){return v.swap(e,{display:"inline-block"},function(){if(t)return Dt(e,"marginRight")})}}),!v.support.pixelPosition&&v.fn.position&&v.each(["top","left"],function(e,t){v.cssHooks[t]={get:function(e,n){if(n){var r=Dt(e,t);return Ut.test(r)?v(e).position()[t]+"px":r}}}})}),v.expr&&v.expr.filters&&(v.expr.filters.hidden=function(e){return e.offsetWidth===0&&e.offsetHeight===0||!v.support.reliableHiddenOffsets&&(e.style&&e.style.display||Dt(e,"display"))==="none"},v.expr.filters.visible=function(e){return!v.expr.filters.hidden(e)}),v.each({margin:"",padding:"",border:"Width"},function(e,t){v.cssHooks[e+t]={expand:function(n){var r,i=typeof n=="string"?n.split(" "):[n],s={};for(r=0;r<4;r++)s[e+$t[r]+t]=i[r]||i[r-2]||i[0];return s}},qt.test(e)||(v.cssHooks[e+t].set=Zt)});var rn=/%20/g,sn=/\[\]$/,on=/\r?\n/g,un=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,an=/^(?:select|textarea)/i;v.fn.extend({serialize:function(){return v.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?v.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||an.test(this.nodeName)||un.test(this.type))}).map(function(e,t){var n=v(this).val();return n==null?null:v.isArray(n)?v.map(n,function(e,n){return{name:t.name,value:e.replace(on,"\r\n")}}):{name:t.name,value:n.replace(on,"\r\n")}}).get()}}),v.param=function(e,n){var r,i=[],s=function(e,t){t=v.isFunction(t)?t():t==null?"":t,i[i.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};n===t&&(n=v.ajaxSettings&&v.ajaxSettings.traditional);if(v.isArray(e)||e.jquery&&!v.isPlainObject(e))v.each(e,function(){s(this.name,this.value)});else for(r in e)fn(r,e[r],n,s);return i.join("&").replace(rn,"+")};var ln,cn,hn=/#.*$/,pn=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,dn=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,vn=/^(?:GET|HEAD)$/,mn=/^\/\//,gn=/\?/,yn=/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,bn=/([?&])_=[^&]*/,wn=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,En=v.fn.load,Sn={},xn={},Tn=["*/"]+["*"];try{cn=s.href}catch(Nn){cn=i.createElement("a"),cn.href="",cn=cn.href}ln=wn.exec(cn.toLowerCase())||[],v.fn.load=function(e,n,r){if(typeof e!="string"&&En)return En.apply(this,arguments);if(!this.length)return this;var i,s,o,u=this,a=e.indexOf(" ");return a>=0&&(i=e.slice(a,e.length),e=e.slice(0,a)),v.isFunction(n)?(r=n,n=t):n&&typeof n=="object"&&(s="POST"),v.ajax({url:e,type:s,dataType:"html",data:n,complete:function(e,t){r&&u.each(r,o||[e.responseText,t,e])}}).done(function(e){o=arguments,u.html(i?v("<div>").append(e.replace(yn,"")).find(i):e)}),this},v.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(e,t){v.fn[t]=function(e){return this.on(t,e)}}),v.each(["get","post"],function(e,n){v[n]=function(e,r,i,s){return v.isFunction(r)&&(s=s||i,i=r,r=t),v.ajax({type:n,url:e,data:r,success:i,dataType:s})}}),v.extend({getScript:function(e,n){return v.get(e,t,n,"script")},getJSON:function(e,t,n){return v.get(e,t,n,"json")},ajaxSetup:function(e,t){return t?Ln(e,v.ajaxSettings):(t=e,e=v.ajaxSettings),Ln(e,t),e},ajaxSettings:{url:cn,isLocal:dn.test(ln[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":Tn},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":e.String,"text html":!0,"text json":v.parseJSON,"text xml":v.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:Cn(Sn),ajaxTransport:Cn(xn),ajax:function(e,n){function T(e,n,s,a){var l,y,b,w,S,T=n;if(E===2)return;E=2,u&&clearTimeout(u),o=t,i=a||"",x.readyState=e>0?4:0,s&&(w=An(c,x,s));if(e>=200&&e<300||e===304)c.ifModified&&(S=x.getResponseHeader("Last-Modified"),S&&(v.lastModified[r]=S),S=x.getResponseHeader("Etag"),S&&(v.etag[r]=S)),e===304?(T="notmodified",l=!0):(l=On(c,w),T=l.state,y=l.data,b=l.error,l=!b);else{b=T;if(!T||e)T="error",e<0&&(e=0)}x.status=e,x.statusText=(n||T)+"",l?d.resolveWith(h,[y,T,x]):d.rejectWith(h,[x,T,b]),x.statusCode(g),g=t,f&&p.trigger("ajax"+(l?"Success":"Error"),[x,c,l?y:b]),m.fireWith(h,[x,T]),f&&(p.trigger("ajaxComplete",[x,c]),--v.active||v.event.trigger("ajaxStop"))}typeof e=="object"&&(n=e,e=t),n=n||{};var r,i,s,o,u,a,f,l,c=v.ajaxSetup({},n),h=c.context||c,p=h!==c&&(h.nodeType||h instanceof v)?v(h):v.event,d=v.Deferred(),m=v.Callbacks("once memory"),g=c.statusCode||{},b={},w={},E=0,S="canceled",x={readyState:0,setRequestHeader:function(e,t){if(!E){var n=e.toLowerCase();e=w[n]=w[n]||e,b[e]=t}return this},getAllResponseHeaders:function(){return E===2?i:null},getResponseHeader:function(e){var n;if(E===2){if(!s){s={};while(n=pn.exec(i))s[n[1].toLowerCase()]=n[2]}n=s[e.toLowerCase()]}return n===t?null:n},overrideMimeType:function(e){return E||(c.mimeType=e),this},abort:function(e){return e=e||S,o&&o.abort(e),T(0,e),this}};d.promise(x),x.success=x.done,x.error=x.fail,x.complete=m.add,x.statusCode=function(e){if(e){var t;if(E<2)for(t in e)g[t]=[g[t],e[t]];else t=e[x.status],x.always(t)}return this},c.url=((e||c.url)+"").replace(hn,"").replace(mn,ln[1]+"//"),c.dataTypes=v.trim(c.dataType||"*").toLowerCase().split(y),c.crossDomain==null&&(a=wn.exec(c.url.toLowerCase()),c.crossDomain=!(!a||a[1]===ln[1]&&a[2]===ln[2]&&(a[3]||(a[1]==="http:"?80:443))==(ln[3]||(ln[1]==="http:"?80:443)))),c.data&&c.processData&&typeof c.data!="string"&&(c.data=v.param(c.data,c.traditional)),kn(Sn,c,n,x);if(E===2)return x;f=c.global,c.type=c.type.toUpperCase(),c.hasContent=!vn.test(c.type),f&&v.active++===0&&v.event.trigger("ajaxStart");if(!c.hasContent){c.data&&(c.url+=(gn.test(c.url)?"&":"?")+c.data,delete c.data),r=c.url;if(c.cache===!1){var N=v.now(),C=c.url.replace(bn,"$1_="+N);c.url=C+(C===c.url?(gn.test(c.url)?"&":"?")+"_="+N:"")}}(c.data&&c.hasContent&&c.contentType!==!1||n.contentType)&&x.setRequestHeader("Content-Type",c.contentType),c.ifModified&&(r=r||c.url,v.lastModified[r]&&x.setRequestHeader("If-Modified-Since",v.lastModified[r]),v.etag[r]&&x.setRequestHeader("If-None-Match",v.etag[r])),x.setRequestHeader("Accept",c.dataTypes[0]&&c.accepts[c.dataTypes[0]]?c.accepts[c.dataTypes[0]]+(c.dataTypes[0]!=="*"?", "+Tn+"; q=0.01":""):c.accepts["*"]);for(l in c.headers)x.setRequestHeader(l,c.headers[l]);if(!c.beforeSend||c.beforeSend.call(h,x,c)!==!1&&E!==2){S="abort";for(l in{success:1,error:1,complete:1})x[l](c[l]);o=kn(xn,c,n,x);if(!o)T(-1,"No Transport");else{x.readyState=1,f&&p.trigger("ajaxSend",[x,c]),c.async&&c.timeout>0&&(u=setTimeout(function(){x.abort("timeout")},c.timeout));try{E=1,o.send(b,T)}catch(k){if(!(E<2))throw k;T(-1,k)}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var Mn=[],_n=/\?/,Dn=/(=)\?(?=&|$)|\?\?/,Pn=v.now();v.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Mn.pop()||v.expando+"_"+Pn++;return this[e]=!0,e}}),v.ajaxPrefilter("json jsonp",function(n,r,i){var s,o,u,a=n.data,f=n.url,l=n.jsonp!==!1,c=l&&Dn.test(f),h=l&&!c&&typeof a=="string"&&!(n.contentType||"").indexOf("application/x-www-form-urlencoded")&&Dn.test(a);if(n.dataTypes[0]==="jsonp"||c||h)return s=n.jsonpCallback=v.isFunction(n.jsonpCallback)?n.jsonpCallback():n.jsonpCallback,o=e[s],c?n.url=f.replace(Dn,"$1"+s):h?n.data=a.replace(Dn,"$1"+s):l&&(n.url+=(_n.test(f)?"&":"?")+n.jsonp+"="+s),n.converters["script json"]=function(){return u||v.error(s+" was not called"),u[0]},n.dataTypes[0]="json",e[s]=function(){u=arguments},i.always(function(){e[s]=o,n[s]&&(n.jsonpCallback=r.jsonpCallback,Mn.push(s)),u&&v.isFunction(o)&&o(u[0]),u=o=t}),"script"}),v.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(e){return v.globalEval(e),e}}}),v.ajaxPrefilter("script",function(e){e.cache===t&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),v.ajaxTransport("script",function(e){if(e.crossDomain){var n,r=i.head||i.getElementsByTagName("head")[0]||i.documentElement;return{send:function(s,o){n=i.createElement("script"),n.async="async",e.scriptCharset&&(n.charset=e.scriptCharset),n.src=e.url,n.onload=n.onreadystatechange=function(e,i){if(i||!n.readyState||/loaded|complete/.test(n.readyState))n.onload=n.onreadystatechange=null,r&&n.parentNode&&r.removeChild(n),n=t,i||o(200,"success")},r.insertBefore(n,r.firstChild)},abort:function(){n&&n.onload(0,1)}}}});var Hn,Bn=e.ActiveXObject?function(){for(var e in Hn)Hn[e](0,1)}:!1,jn=0;v.ajaxSettings.xhr=e.ActiveXObject?function(){return!this.isLocal&&Fn()||In()}:Fn,function(e){v.extend(v.support,{ajax:!!e,cors:!!e&&"withCredentials"in e})}(v.ajaxSettings.xhr()),v.support.ajax&&v.ajaxTransport(function(n){if(!n.crossDomain||v.support.cors){var r;return{send:function(i,s){var o,u,a=n.xhr();n.username?a.open(n.type,n.url,n.async,n.username,n.password):a.open(n.type,n.url,n.async);if(n.xhrFields)for(u in n.xhrFields)a[u]=n.xhrFields[u];n.mimeType&&a.overrideMimeType&&a.overrideMimeType(n.mimeType),!n.crossDomain&&!i["X-Requested-With"]&&(i["X-Requested-With"]="XMLHttpRequest");try{for(u in i)a.setRequestHeader(u,i[u])}catch(f){}a.send(n.hasContent&&n.data||null),r=function(e,i){var u,f,l,c,h;try{if(r&&(i||a.readyState===4)){r=t,o&&(a.onreadystatechange=v.noop,Bn&&delete Hn[o]);if(i)a.readyState!==4&&a.abort();else{u=a.status,l=a.getAllResponseHeaders(),c={},h=a.responseXML,h&&h.documentElement&&(c.xml=h);try{c.text=a.responseText}catch(p){}try{f=a.statusText}catch(p){f=""}!u&&n.isLocal&&!n.crossDomain?u=c.text?200:404:u===1223&&(u=204)}}}catch(d){i||s(-1,d)}c&&s(u,f,c,l)},n.async?a.readyState===4?setTimeout(r,0):(o=++jn,Bn&&(Hn||(Hn={},v(e).unload(Bn)),Hn[o]=r),a.onreadystatechange=r):r()},abort:function(){r&&r(0,1)}}}});var qn,Rn,Un=/^(?:toggle|show|hide)$/,zn=new RegExp("^(?:([-+])=|)("+m+")([a-z%]*)$","i"),Wn=/queueHooks$/,Xn=[Gn],Vn={"*":[function(e,t){var n,r,i=this.createTween(e,t),s=zn.exec(t),o=i.cur(),u=+o||0,a=1,f=20;if(s){n=+s[2],r=s[3]||(v.cssNumber[e]?"":"px");if(r!=="px"&&u){u=v.css(i.elem,e,!0)||n||1;do a=a||".5",u/=a,v.style(i.elem,e,u+r);while(a!==(a=i.cur()/o)&&a!==1&&--f)}i.unit=r,i.start=u,i.end=s[1]?u+(s[1]+1)*n:n}return i}]};v.Animation=v.extend(Kn,{tweener:function(e,t){v.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");var n,r=0,i=e.length;for(;r<i;r++)n=e[r],Vn[n]=Vn[n]||[],Vn[n].unshift(t)},prefilter:function(e,t){t?Xn.unshift(e):Xn.push(e)}}),v.Tween=Yn,Yn.prototype={constructor:Yn,init:function(e,t,n,r,i,s){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=s||(v.cssNumber[n]?"":"px")},cur:function(){var e=Yn.propHooks[this.prop];return e&&e.get?e.get(this):Yn.propHooks._default.get(this)},run:function(e){var t,n=Yn.propHooks[this.prop];return this.options.duration?this.pos=t=v.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):Yn.propHooks._default.set(this),this}},Yn.prototype.init.prototype=Yn.prototype,Yn.propHooks={_default:{get:function(e){var t;return e.elem[e.prop]==null||!!e.elem.style&&e.elem.style[e.prop]!=null?(t=v.css(e.elem,e.prop,!1,""),!t||t==="auto"?0:t):e.elem[e.prop]},set:function(e){v.fx.step[e.prop]?v.fx.step[e.prop](e):e.elem.style&&(e.elem.style[v.cssProps[e.prop]]!=null||v.cssHooks[e.prop])?v.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},Yn.propHooks.scrollTop=Yn.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},v.each(["toggle","show","hide"],function(e,t){var n=v.fn[t];v.fn[t]=function(r,i,s){return r==null||typeof r=="boolean"||!e&&v.isFunction(r)&&v.isFunction(i)?n.apply(this,arguments):this.animate(Zn(t,!0),r,i,s)}}),v.fn.extend({fadeTo:function(e,t,n,r){return this.filter(Gt).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=v.isEmptyObject(e),s=v.speed(t,n,r),o=function(){var t=Kn(this,v.extend({},e),s);i&&t.stop(!0)};return i||s.queue===!1?this.each(o):this.queue(s.queue,o)},stop:function(e,n,r){var i=function(e){var t=e.stop;delete e.stop,t(r)};return typeof e!="string"&&(r=n,n=e,e=t),n&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,n=e!=null&&e+"queueHooks",s=v.timers,o=v._data(this);if(n)o[n]&&o[n].stop&&i(o[n]);else for(n in o)o[n]&&o[n].stop&&Wn.test(n)&&i(o[n]);for(n=s.length;n--;)s[n].elem===this&&(e==null||s[n].queue===e)&&(s[n].anim.stop(r),t=!1,s.splice(n,1));(t||!r)&&v.dequeue(this,e)})}}),v.each({slideDown:Zn("show"),slideUp:Zn("hide"),slideToggle:Zn("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){v.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),v.speed=function(e,t,n){var r=e&&typeof e=="object"?v.extend({},e):{complete:n||!n&&t||v.isFunction(e)&&e,duration:e,easing:n&&t||t&&!v.isFunction(t)&&t};r.duration=v.fx.off?0:typeof r.duration=="number"?r.duration:r.duration in v.fx.speeds?v.fx.speeds[r.duration]:v.fx.speeds._default;if(r.queue==null||r.queue===!0)r.queue="fx";return r.old=r.complete,r.complete=function(){v.isFunction(r.old)&&r.old.call(this),r.queue&&v.dequeue(this,r.queue)},r},v.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},v.timers=[],v.fx=Yn.prototype.init,v.fx.tick=function(){var e,n=v.timers,r=0;qn=v.now();for(;r<n.length;r++)e=n[r],!e()&&n[r]===e&&n.splice(r--,1);n.length||v.fx.stop(),qn=t},v.fx.timer=function(e){e()&&v.timers.push(e)&&!Rn&&(Rn=setInterval(v.fx.tick,v.fx.interval))},v.fx.interval=13,v.fx.stop=function(){clearInterval(Rn),Rn=null},v.fx.speeds={slow:600,fast:200,_default:400},v.fx.step={},v.expr&&v.expr.filters&&(v.expr.filters.animated=function(e){return v.grep(v.timers,function(t){return e===t.elem}).length});var er=/^(?:body|html)$/i;v.fn.offset=function(e){if(arguments.length)return e===t?this:this.each(function(t){v.offset.setOffset(this,e,t)});var n,r,i,s,o,u,a,f={top:0,left:0},l=this[0],c=l&&l.ownerDocument;if(!c)return;return(r=c.body)===l?v.offset.bodyOffset(l):(n=c.documentElement,v.contains(n,l)?(typeof l.getBoundingClientRect!="undefined"&&(f=l.getBoundingClientRect()),i=tr(c),s=n.clientTop||r.clientTop||0,o=n.clientLeft||r.clientLeft||0,u=i.pageYOffset||n.scrollTop,a=i.pageXOffset||n.scrollLeft,{top:f.top+u-s,left:f.left+a-o}):f)},v.offset={bodyOffset:function(e){var t=e.offsetTop,n=e.offsetLeft;return v.support.doesNotIncludeMarginInBodyOffset&&(t+=parseFloat(v.css(e,"marginTop"))||0,n+=parseFloat(v.css(e,"marginLeft"))||0),{top:t,left:n}},setOffset:function(e,t,n){var r=v.css(e,"position");r==="static"&&(e.style.position="relative");var i=v(e),s=i.offset(),o=v.css(e,"top"),u=v.css(e,"left"),a=(r==="absolute"||r==="fixed")&&v.inArray("auto",[o,u])>-1,f={},l={},c,h;a?(l=i.position(),c=l.top,h=l.left):(c=parseFloat(o)||0,h=parseFloat(u)||0),v.isFunction(t)&&(t=t.call(e,n,s)),t.top!=null&&(f.top=t.top-s.top+c),t.left!=null&&(f.left=t.left-s.left+h),"using"in t?t.using.call(e,f):i.css(f)}},v.fn.extend({position:function(){if(!this[0])return;var e=this[0],t=this.offsetParent(),n=this.offset(),r=er.test(t[0].nodeName)?{top:0,left:0}:t.offset();return n.top-=parseFloat(v.css(e,"marginTop"))||0,n.left-=parseFloat(v.css(e,"marginLeft"))||0,r.top+=parseFloat(v.css(t[0],"borderTopWidth"))||0,r.left+=parseFloat(v.css(t[0],"borderLeftWidth"))||0,{top:n.top-r.top,left:n.left-r.left}},offsetParent:function(){return this.map(function(){var e=this.offsetParent||i.body;while(e&&!er.test(e.nodeName)&&v.css(e,"position")==="static")e=e.offsetParent;return e||i.body})}}),v.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,n){var r=/Y/.test(n);v.fn[e]=function(i){return v.access(this,function(e,i,s){var o=tr(e);if(s===t)return o?n in o?o[n]:o.document.documentElement[i]:e[i];o?o.scrollTo(r?v(o).scrollLeft():s,r?s:v(o).scrollTop()):e[i]=s},e,i,arguments.length,null)}}),v.each({Height:"height",Width:"width"},function(e,n){v.each({padding:"inner"+e,content:n,"":"outer"+e},function(r,i){v.fn[i]=function(i,s){var o=arguments.length&&(r||typeof i!="boolean"),u=r||(i===!0||s===!0?"margin":"border");return v.access(this,function(n,r,i){var s;return v.isWindow(n)?n.document.documentElement["client"+e]:n.nodeType===9?(s=n.documentElement,Math.max(n.body["scroll"+e],s["scroll"+e],n.body["offset"+e],s["offset"+e],s["client"+e])):i===t?v.css(n,r,i,u):v.style(n,r,i,u)},n,o?i:t,o,null)}})}),e.jQuery=e.$=v,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return v})})(window);
\ No newline at end of file
diff --git a/js/jquery.multiselect.min.js b/js/jquery.multiselect.min.js
deleted file mode 100644
index e9243506c0555f0d53b58b40d5d185ace8035b7c..0000000000000000000000000000000000000000
--- a/js/jquery.multiselect.min.js
+++ /dev/null
@@ -1,20 +0,0 @@
-/*
- * jQuery MultiSelect UI Widget 1.13
- * Copyright (c) 2012 Eric Hynds
- *
- * http://www.erichynds.com/jquery/jquery-ui-multiselect-widget/
- *
- * Depends:
- *   - jQuery 1.4.2+
- *   - jQuery UI 1.8 widget factory
- *
- * Optional:
- *   - jQuery UI effects
- *   - jQuery UI position utility
- *
- * Dual licensed under the MIT and GPL licenses:
- *   http://www.opensource.org/licenses/mit-license.php
- *   http://www.gnu.org/licenses/gpl.html
- *
- */
-(function(d){var k=0;d.widget("ech.multiselect",{options:{header:!0,height:175,minWidth:225,classes:"",checkAllText:"Check all",uncheckAllText:"Uncheck all",noneSelectedText:"Select options",selectedText:"# selected",selectedList:0,show:null,hide:null,autoOpen:!1,multiple:!0,position:{}},_create:function(){var a=this.element.hide(),b=this.options;this.speed=d.fx.speeds._default;this._isOpen=!1;a=(this.button=d('<button type="button"><span class="ui-icon ui-icon-triangle-2-n-s"></span></button>')).addClass("ui-multiselect ui-widget ui-state-default ui-corner-all").addClass(b.classes).attr({title:a.attr("title"),"aria-haspopup":!0,tabIndex:a.attr("tabIndex")}).insertAfter(a);(this.buttonlabel=d("<span />")).html(b.noneSelectedText).appendTo(a);var a=(this.menu=d("<div />")).addClass("ui-multiselect-menu ui-widget ui-widget-content ui-corner-all").addClass(b.classes).appendTo(document.body),c=(this.header=d("<div />")).addClass("ui-widget-header ui-corner-all ui-multiselect-header ui-helper-clearfix").appendTo(a);(this.headerLinkContainer=d("<ul />")).addClass("ui-helper-reset").html(function(){return!0===b.header?'<li><a class="ui-multiselect-all" href="#"><span class="ui-icon ui-icon-check"></span><span>'+b.checkAllText+'</span></a></li><li><a class="ui-multiselect-none" href="#"><span class="ui-icon ui-icon-closethick"></span><span>'+b.uncheckAllText+"</span></a></li>":"string"===typeof b.header?"<li>"+b.header+"</li>":""}).append('<li class="ui-multiselect-close"><a href="#" class="ui-multiselect-close"><span class="ui-icon ui-icon-circle-close"></span></a></li>').appendTo(c);(this.checkboxContainer=d("<ul />")).addClass("ui-multiselect-checkboxes ui-helper-reset").appendTo(a);this._bindEvents();this.refresh(!0);b.multiple||a.addClass("ui-multiselect-single")},_init:function(){!1===this.options.header&&this.header.hide();this.options.multiple||this.headerLinkContainer.find(".ui-multiselect-all, .ui-multiselect-none").hide();this.options.autoOpen&&this.open();this.element.is(":disabled")&&this.disable()},refresh:function(a){var b=this.element,c=this.options,f=this.menu,h=this.checkboxContainer,g=[],e="",i=b.attr("id")||k++;b.find("option").each(function(b){d(this);var a=this.parentNode,f=this.innerHTML,h=this.title,k=this.value,b="ui-multiselect-"+(this.id||i+"-option-"+b),l=this.disabled,n=this.selected,m=["ui-corner-all"],o=(l?"ui-multiselect-disabled ":" ")+this.className,j;"OPTGROUP"===a.tagName&&(j=a.getAttribute("label"),-1===d.inArray(j,g)&&(e+='<li class="ui-multiselect-optgroup-label '+a.className+'"><a href="#">'+j+"</a></li>",g.push(j)));l&&m.push("ui-state-disabled");n&&!c.multiple&&m.push("ui-state-active");e+='<li class="'+o+'">';e+='<label for="'+b+'" title="'+h+'" class="'+m.join(" ")+'">';e+='<input id="'+b+'" name="multiselect_'+i+'" type="'+(c.multiple?"checkbox":"radio")+'" value="'+k+'" title="'+f+'"';n&&(e+=' checked="checked"',e+=' aria-selected="true"');l&&(e+=' disabled="disabled"',e+=' aria-disabled="true"');e+=" /><span>"+f+"</span></label></li>"});h.html(e);this.labels=f.find("label");this.inputs=this.labels.children("input");this._setButtonWidth();this._setMenuWidth();this.button[0].defaultValue=this.update();a||this._trigger("refresh")},update:function(){var a=this.options,b=this.inputs,c=b.filter(":checked"),f=c.length,a=0===f?a.noneSelectedText:d.isFunction(a.selectedText)?a.selectedText.call(this,f,b.length,c.get()):/\d/.test(a.selectedList)&&0<a.selectedList&&f<=a.selectedList?c.map(function(){return d(this).next().html()}).get().join(", "):a.selectedText.replace("#",f).replace("#",b.length);this.buttonlabel.html(a);return a},_bindEvents:function(){function a(){b[b._isOpen? "close":"open"]();return!1}var b=this,c=this.button;c.find("span").bind("click.multiselect",a);c.bind({click:a,keypress:function(a){switch(a.which){case 27:case 38:case 37:b.close();break;case 39:case 40:b.open()}},mouseenter:function(){c.hasClass("ui-state-disabled")||d(this).addClass("ui-state-hover")},mouseleave:function(){d(this).removeClass("ui-state-hover")},focus:function(){c.hasClass("ui-state-disabled")||d(this).addClass("ui-state-focus")},blur:function(){d(this).removeClass("ui-state-focus")}});this.header.delegate("a","click.multiselect",function(a){if(d(this).hasClass("ui-multiselect-close"))b.close();else b[d(this).hasClass("ui-multiselect-all")?"checkAll":"uncheckAll"]();a.preventDefault()});this.menu.delegate("li.ui-multiselect-optgroup-label a","click.multiselect",function(a){a.preventDefault();var c=d(this),g=c.parent().nextUntil("li.ui-multiselect-optgroup-label").find("input:visible:not(:disabled)"),e=g.get(),c=c.parent().text();!1!==b._trigger("beforeoptgrouptoggle",a,{inputs:e,label:c})&&(b._toggleChecked(g.filter(":checked").length!==g.length,g),b._trigger("optgrouptoggle",a,{inputs:e,label:c,checked:e[0].checked}))}).delegate("label","mouseenter.multiselect",function(){d(this).hasClass("ui-state-disabled")||(b.labels.removeClass("ui-state-hover"),d(this).addClass("ui-state-hover").find("input").focus())}).delegate("label","keydown.multiselect",function(a){a.preventDefault();switch(a.which){case 9:case 27:b.close();break;case 38:case 40:case 37:case 39:b._traverse(a.which,this);break;case 13:d(this).find("input")[0].click()}}).delegate('input[type="checkbox"], input[type="radio"]',"click.multiselect",function(a){var c=d(this),g=this.value,e=this.checked,i=b.element.find("option");this.disabled||!1===b._trigger("click",a,{value:g,text:this.title,checked:e})?a.preventDefault():(c.focus(),c.attr("aria-selected",e),i.each(function(){this.value===g?this.selected=e:b.options.multiple||(this.selected=!1)}),b.options.multiple||(b.labels.removeClass("ui-state-active"),c.closest("label").toggleClass("ui-state-active",e),b.close()),b.element.trigger("change"),setTimeout(d.proxy(b.update,b),10))});d(document).bind("mousedown.multiselect",function(a){b._isOpen&&(!d.contains(b.menu[0],a.target)&&!d.contains(b.button[0],a.target)&&a.target!==b.button[0])&&b.close()});d(this.element[0].form).bind("reset.multiselect",function(){setTimeout(d.proxy(b.refresh,b),10)})},_setButtonWidth:function(){var a=this.element.outerWidth(),b=this.options;/\d/.test(b.minWidth)&&a<b.minWidth&&(a=b.minWidth);this.button.width(a)},_setMenuWidth:function(){var a=this.menu,b=this.button.outerWidth()-parseInt(a.css("padding-left"),10)-parseInt(a.css("padding-right"),10)-parseInt(a.css("border-right-width"),10)-parseInt(a.css("border-left-width"),10);a.width(b||this.button.outerWidth())},_traverse:function(a,b){var c=d(b),f=38===a||37===a,c=c.parent()[f?"prevAll":"nextAll"]("li:not(.ui-multiselect-disabled, .ui-multiselect-optgroup-label)")[f?"last":"first"]();c.length?c.find("label").trigger("mouseover"):(c=this.menu.find("ul").last(),this.menu.find("label")[f? "last":"first"]().trigger("mouseover"),c.scrollTop(f?c.height():0))},_toggleState:function(a,b){return function(){this.disabled||(this[a]=b);b?this.setAttribute("aria-selected",!0):this.removeAttribute("aria-selected")}},_toggleChecked:function(a,b){var c=b&&b.length?b:this.inputs,f=this;c.each(this._toggleState("checked",a));c.eq(0).focus();this.update();var h=c.map(function(){return this.value}).get();this.element.find("option").each(function(){!this.disabled&&-1<d.inArray(this.value,h)&&f._toggleState("selected",a).call(this)});c.length&&this.element.trigger("change")},_toggleDisabled:function(a){this.button.attr({disabled:a,"aria-disabled":a})[a?"addClass":"removeClass"]("ui-state-disabled");var b=this.menu.find("input"),b=a?b.filter(":enabled").data("ech-multiselect-disabled",!0):b.filter(function(){return!0===d.data(this,"ech-multiselect-disabled")}).removeData("ech-multiselect-disabled");b.attr({disabled:a,"arial-disabled":a}).parent()[a?"addClass":"removeClass"]("ui-state-disabled");this.element.attr({disabled:a,"aria-disabled":a})},open:function(){var a=this.button,b=this.menu,c=this.speed,f=this.options,h=[];if(!(!1===this._trigger("beforeopen")||a.hasClass("ui-state-disabled")||this._isOpen)){var g=b.find("ul").last(),e=f.show,i=a.offset();d.isArray(f.show)&&(e=f.show[0],c=f.show[1]||this.speed);e&&(h=[e,c]);g.scrollTop(0).height(f.height);d.ui.position&&!d.isEmptyObject(f.position)?(f.position.of=f.position.of||a,b.show().position(f.position).hide()):b.css({top:i.top+a.outerHeight(),left:i.left});d.fn.show.apply(b,h);this.labels.eq(0).trigger("mouseover").trigger("mouseenter").find("input").trigger("focus");a.addClass("ui-state-active");this._isOpen=!0;this._trigger("open")}},close:function(){if(!1!==this._trigger("beforeclose")){var a=this.options,b=a.hide,c=this.speed,f=[];d.isArray(a.hide)&&(b=a.hide[0],c=a.hide[1]||this.speed);b&&(f=[b,c]);d.fn.hide.apply(this.menu,f);this.button.removeClass("ui-state-active").trigger("blur").trigger("mouseleave");this._isOpen=!1;this._trigger("close")}},enable:function(){this._toggleDisabled(!1)},disable:function(){this._toggleDisabled(!0)},checkAll:function(){this._toggleChecked(!0);this._trigger("checkAll")},uncheckAll:function(){this._toggleChecked(!1);this._trigger("uncheckAll")},getChecked:function(){return this.menu.find("input").filter(":checked")},destroy:function(){d.Widget.prototype.destroy.call(this);this.button.remove();this.menu.remove();this.element.show();return this},isOpen:function(){return this._isOpen},widget:function(){return this.menu},getButton:function(){return this.button},_setOption:function(a,b){var c=this.menu;switch(a){case "header":c.find("div.ui-multiselect-header")[b?"show":"hide"]();break;case "checkAllText":c.find("a.ui-multiselect-all span").eq(-1).text(b);break;case "uncheckAllText":c.find("a.ui-multiselect-none span").eq(-1).text(b);break;case "height":c.find("ul").last().height(parseInt(b,10));break;case "minWidth":this.options[a]=parseInt(b,10);this._setButtonWidth();this._setMenuWidth();break;case "selectedText":case "selectedList":case "noneSelectedText":this.options[a]=b;this.update();break;case "classes":c.add(this.button).removeClass(this.options.classes).addClass(b);break;case "multiple":c.toggleClass("ui-multiselect-single",!b),this.options.multiple=b,this.element[0].multiple=b,this.refresh()}d.Widget.prototype._setOption.apply(this,arguments)}})})(jQuery);
diff --git a/js/jstz.min.js b/js/jstz.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..d457648ef4b460c19b85427f6d95f74431241306
--- /dev/null
+++ b/js/jstz.min.js
@@ -0,0 +1,2 @@
+/*! jstz - v1.0.4 - 2012-12-18 */
+(function(e){var t=function(){"use strict";var e="s",n=function(e){var t=-e.getTimezoneOffset();return t!==null?t:0},r=function(e,t,n){var r=new Date;return e!==undefined&&r.setFullYear(e),r.setDate(n),r.setMonth(t),r},i=function(e){return n(r(e,0,2))},s=function(e){return n(r(e,5,2))},o=function(e){var t=e.getMonth()>7?s(e.getFullYear()):i(e.getFullYear()),r=n(e);return t-r!==0},u=function(){var t=i(),n=s(),r=i()-s();return r<0?t+",1":r>0?n+",1,"+e:t+",0"},a=function(){var e=u();return new t.TimeZone(t.olson.timezones[e])},f=function(e){var t=new Date(2010,6,15,1,0,0,0),n={"America/Denver":new Date(2011,2,13,3,0,0,0),"America/Mazatlan":new Date(2011,3,3,3,0,0,0),"America/Chicago":new Date(2011,2,13,3,0,0,0),"America/Mexico_City":new Date(2011,3,3,3,0,0,0),"America/Asuncion":new Date(2012,9,7,3,0,0,0),"America/Santiago":new Date(2012,9,3,3,0,0,0),"America/Campo_Grande":new Date(2012,9,21,5,0,0,0),"America/Montevideo":new Date(2011,9,2,3,0,0,0),"America/Sao_Paulo":new Date(2011,9,16,5,0,0,0),"America/Los_Angeles":new Date(2011,2,13,8,0,0,0),"America/Santa_Isabel":new Date(2011,3,5,8,0,0,0),"America/Havana":new Date(2012,2,10,2,0,0,0),"America/New_York":new Date(2012,2,10,7,0,0,0),"Asia/Beirut":new Date(2011,2,27,1,0,0,0),"Europe/Helsinki":new Date(2011,2,27,4,0,0,0),"Europe/Istanbul":new Date(2011,2,28,5,0,0,0),"Asia/Damascus":new Date(2011,3,1,2,0,0,0),"Asia/Jerusalem":new Date(2011,3,1,6,0,0,0),"Asia/Gaza":new Date(2009,2,28,0,30,0,0),"Africa/Cairo":new Date(2009,3,25,0,30,0,0),"Pacific/Auckland":new Date(2011,8,26,7,0,0,0),"Pacific/Fiji":new Date(2010,11,29,23,0,0,0),"America/Halifax":new Date(2011,2,13,6,0,0,0),"America/Goose_Bay":new Date(2011,2,13,2,1,0,0),"America/Miquelon":new Date(2011,2,13,5,0,0,0),"America/Godthab":new Date(2011,2,27,1,0,0,0),"Europe/Moscow":t,"Asia/Yekaterinburg":t,"Asia/Omsk":t,"Asia/Krasnoyarsk":t,"Asia/Irkutsk":t,"Asia/Yakutsk":t,"Asia/Vladivostok":t,"Asia/Kamchatka":t,"Europe/Minsk":t,"Australia/Perth":new Date(2008,10,1,1,0,0,0)};return n[e]};return{determine:a,date_is_dst:o,dst_start_for:f}}();t.TimeZone=function(e){"use strict";var n={"America/Denver":["America/Denver","America/Mazatlan"],"America/Chicago":["America/Chicago","America/Mexico_City"],"America/Santiago":["America/Santiago","America/Asuncion","America/Campo_Grande"],"America/Montevideo":["America/Montevideo","America/Sao_Paulo"],"Asia/Beirut":["Asia/Beirut","Europe/Helsinki","Europe/Istanbul","Asia/Damascus","Asia/Jerusalem","Asia/Gaza"],"Pacific/Auckland":["Pacific/Auckland","Pacific/Fiji"],"America/Los_Angeles":["America/Los_Angeles","America/Santa_Isabel"],"America/New_York":["America/Havana","America/New_York"],"America/Halifax":["America/Goose_Bay","America/Halifax"],"America/Godthab":["America/Miquelon","America/Godthab"],"Asia/Dubai":["Europe/Moscow"],"Asia/Dhaka":["Asia/Yekaterinburg"],"Asia/Jakarta":["Asia/Omsk"],"Asia/Shanghai":["Asia/Krasnoyarsk","Australia/Perth"],"Asia/Tokyo":["Asia/Irkutsk"],"Australia/Brisbane":["Asia/Yakutsk"],"Pacific/Noumea":["Asia/Vladivostok"],"Pacific/Tarawa":["Asia/Kamchatka"],"Africa/Johannesburg":["Asia/Gaza","Africa/Cairo"],"Asia/Baghdad":["Europe/Minsk"]},r=e,i=function(){var e=n[r],i=e.length,s=0,o=e[0];for(;s<i;s+=1){o=e[s];if(t.date_is_dst(t.dst_start_for(o))){r=o;return}}},s=function(){return typeof n[r]!="undefined"};return s()&&i(),{name:function(){return r}}},t.olson={},t.olson.timezones={"-720,0":"Etc/GMT+12","-660,0":"Pacific/Pago_Pago","-600,1":"America/Adak","-600,0":"Pacific/Honolulu","-570,0":"Pacific/Marquesas","-540,0":"Pacific/Gambier","-540,1":"America/Anchorage","-480,1":"America/Los_Angeles","-480,0":"Pacific/Pitcairn","-420,0":"America/Phoenix","-420,1":"America/Denver","-360,0":"America/Guatemala","-360,1":"America/Chicago","-360,1,s":"Pacific/Easter","-300,0":"America/Bogota","-300,1":"America/New_York","-270,0":"America/Caracas","-240,1":"America/Halifax","-240,0":"America/Santo_Domingo","-240,1,s":"America/Santiago","-210,1":"America/St_Johns","-180,1":"America/Godthab","-180,0":"America/Argentina/Buenos_Aires","-180,1,s":"America/Montevideo","-120,0":"Etc/GMT+2","-120,1":"Etc/GMT+2","-60,1":"Atlantic/Azores","-60,0":"Atlantic/Cape_Verde","0,0":"Etc/UTC","0,1":"Europe/London","60,1":"Europe/Berlin","60,0":"Africa/Lagos","60,1,s":"Africa/Windhoek","120,1":"Asia/Beirut","120,0":"Africa/Johannesburg","180,0":"Asia/Baghdad","180,1":"Europe/Moscow","210,1":"Asia/Tehran","240,0":"Asia/Dubai","240,1":"Asia/Baku","270,0":"Asia/Kabul","300,1":"Asia/Yekaterinburg","300,0":"Asia/Karachi","330,0":"Asia/Kolkata","345,0":"Asia/Kathmandu","360,0":"Asia/Dhaka","360,1":"Asia/Omsk","390,0":"Asia/Rangoon","420,1":"Asia/Krasnoyarsk","420,0":"Asia/Jakarta","480,0":"Asia/Shanghai","480,1":"Asia/Irkutsk","525,0":"Australia/Eucla","525,1,s":"Australia/Eucla","540,1":"Asia/Yakutsk","540,0":"Asia/Tokyo","570,0":"Australia/Darwin","570,1,s":"Australia/Adelaide","600,0":"Australia/Brisbane","600,1":"Asia/Vladivostok","600,1,s":"Australia/Sydney","630,1,s":"Australia/Lord_Howe","660,1":"Asia/Kamchatka","660,0":"Pacific/Noumea","690,0":"Pacific/Norfolk","720,1,s":"Pacific/Auckland","720,0":"Pacific/Tarawa","765,1,s":"Pacific/Chatham","780,0":"Pacific/Tongatapu","780,1,s":"Pacific/Apia","840,0":"Pacific/Kiritimati"},typeof exports!="undefined"?exports.jstz=t:e.jstz=t})(this);
\ No newline at end of file
diff --git a/js/osticket.js b/js/osticket.js
index f4aeebd1a1075541fadb34c70f4f49ce6fd70452..bd2f937e61af3b5f7cabd3b53e1d635cbb324486 100644
--- a/js/osticket.js
+++ b/js/osticket.js
@@ -21,7 +21,7 @@ $(document).ready(function(){
         left : ($(window).width() / 2 - 160)
      });
 
-    $("form :input").change(function() {
+    $(document).on('change', "form :input:not(.nowarn)", function() {
         var fObj = $(this).closest('form');
         if(!fObj.data('changed')){
             fObj.data('changed', true);
@@ -30,7 +30,7 @@ $(document).ready(function(){
                 return __("Are you sure you want to leave? Any changes or info you've entered will be discarded!");
              });
         }
-       });
+    });
 
     $("form :input[type=reset]").click(function() {
         var fObj = $(this).closest('form');
@@ -78,39 +78,6 @@ $(document).ready(function(){
 
     });
 
-    getConfig = (function() {
-        var dfd = $.Deferred(),
-            requested = false;
-        return function() {
-            if (dfd.state() != 'resolved' && !requested)
-                requested = $.ajax({
-                    url: "ajax.php/config/client",
-                    dataType: 'json',
-                    success: function (json_config) {
-                        dfd.resolve(json_config);
-                    }
-                });
-            return dfd;
-        }
-    })();
-
-    $.translate_format = function(str) {
-        var translation = {
-            'd':'dd',
-            'j':'d',
-            'z':'o',
-            'm':'mm',
-            'F':'MM',
-            'n':'m',
-            'Y':'yy'
-        };
-        // Change PHP formats to datepicker ones
-        $.each(translation, function(php, jqdp) {
-            str = str.replace(php, jqdp);
-        });
-        return str;
-    };
-
     var showNonLocalImage = function(div) {
         var $div = $(div),
             $img = $div.append($('<img>')
@@ -166,6 +133,10 @@ $(document).ready(function(){
             // TODO: Add a hover-button to show just one image
         });
     });
+
+    $('div.thread-body a').each(function() {
+        $(this).attr('target', '_blank');
+    });
 });
 
 showImagesInline = function(urls, thread_id) {
@@ -181,25 +152,41 @@ showImagesInline = function(urls, thread_id) {
             var timeout, caption = $('<div class="image-hover">')
                 .css({'float':e.css('float')});
             e.wrap(caption).parent()
-                .hover(
-                    function() {
-                        var self = this;
-                        timeout = setTimeout(
-                            function() { $(self).find('.caption').slideDown(250); },
-                            500);
-                    },
-                    function() {
-                        clearTimeout(timeout);
-                        $(this).find('.caption').slideUp(250);
-                    }
-                ).append($('<div class="caption">')
-                    .append('<span class="filename">'+info.filename+'</span>')
-                    .append('<a href="'+info.download_url+'" class="action-button pull-right"><i class="icon-download-alt"></i> ' + __('Download') + '</a>')
+                .append($('<div class="caption">')
+                    .append($('<a href="'+info.download_url+'" class="button action-button pull-right no-pjax"><i class="icon-download-alt"></i></a>')
+                      .attr('download', info.filename)
+                      .attr('title', __('Download'))
+                    )
                 );
             e.data('wrapped', true);
         }
     });
-}
+};
+
+getConfig = (function() {
+    var dfd = $.Deferred(),
+        requested = false;
+    return function() {
+        return dfd;
+    };
+})();
+
+$.translate_format = function(str) {
+    var translation = {
+        'd':'dd',
+        'j':'d',
+        'z':'o',
+        'm':'mm',
+        'F':'MM',
+        'n':'m',
+        'Y':'yy'
+    };
+    // Change PHP formats to datepicker ones
+    $.each(translation, function(php, jqdp) {
+        str = str.replace(php, jqdp);
+    });
+    return str;
+};
 
 $.sysAlert = function (title, msg, cb) {
     var $dialog =  $('.dialog#alert');
@@ -233,3 +220,11 @@ $(document).on('submit', 'form', function() {
         $e.val(year+'-'+month+'-'+day);
     });
 });
+
+$(document).on('click', '.link:not(a):not(.button)', function(event) {
+  var $e = $(event.currentTarget);
+  $('<a>').prop({href: $e.attr('href'), 'class': $e.attr('class')})
+    .hide()
+    .insertBefore($e)
+    .get(0).click(event);
+});
diff --git a/js/redactor-fonts.js b/js/redactor-fonts.js
deleted file mode 100644
index 021ca561d63b28bd5e38b8c4f1c4ef8bdc6bfe57..0000000000000000000000000000000000000000
--- a/js/redactor-fonts.js
+++ /dev/null
@@ -1,187 +0,0 @@
-if (!RedactorPlugins) var RedactorPlugins = {};
-
-RedactorPlugins.fontfamily = {
-    init: function ()
-    {
-        var fonts = [ 'Arial', 'Helvetica', 'Georgia', 'Times New Roman', __('Monospace') ];
-        var that = this;
-        var dropdown = {};
-
-        $.each(fonts, function(i, s)
-        {
-            dropdown['s' + i] = { title: '<span style="font-family:'+s+';">'+s+'</style>', callback: function() { that.setFontfamily(s); }};
-        });
-
-        dropdown['remove'] = { title: 'Remove font', callback: function() { that.resetFontfamily(); }};
-
-        this.buttonAddBefore('bold', 'fontfamily', __('Change font family'), false, dropdown);
-    },
-    setFontfamily: function (value)
-    {
-        this.inlineSetStyle('font-family', value);
-    },
-    resetFontfamily: function()
-    {
-        this.inlineRemoveStyle('font-family');
-    }
-};
-
-RedactorPlugins.fontcolor = {
-    init: function()
-    {
-        var colors = [
-            '#ffffff', '#000000', '#eeece1', '#1f497d', '#4f81bd', '#c0504d', '#9bbb59', '#8064a2', '#4bacc6', '#f79646', '#ffff00',
-            '#f2f2f2', '#7f7f7f', '#ddd9c3', '#c6d9f0', '#dbe5f1', '#f2dcdb', '#ebf1dd', '#e5e0ec', '#dbeef3', '#fdeada', '#fff2ca',
-            '#d8d8d8', '#595959', '#c4bd97', '#8db3e2', '#b8cce4', '#e5b9b7', '#d7e3bc', '#ccc1d9', '#b7dde8', '#fbd5b5', '#ffe694',
-            '#bfbfbf', '#3f3f3f', '#938953', '#548dd4', '#95b3d7', '#d99694', '#c3d69b', '#b2a2c7', '#b7dde8', '#fac08f', '#f2c314',
-            '#a5a5a5', '#262626', '#494429', '#17365d', '#366092', '#953734', '#76923c', '#5f497a', '#92cddc', '#e36c09', '#c09100',
-            '#7f7f7f', '#0c0c0c', '#1d1b10', '#0f243e', '#244061', '#632423', '#4f6128', '#3f3151', '#31859b',  '#974806', '#7f6000'
-        ];
-
-        var buttons = ['fontcolor', 'backcolor'];
-
-        for (var i = 0; i < 2; i++)
-        {
-            var name = buttons[i];
-
-            var $dropdown = $('<div class="redactor_dropdown redactor_dropdown_box_' + name + '" style="display: none; width: 265px;">');
-
-            this.pickerBuild($dropdown, name, colors);
-            $(this.$toolbar).append($dropdown);
-
-            var btn = this.buttonAddBefore('deleted', name, this.opts.curLang[name], $.proxy(function(btnName, $button, btnObject, e)
-            {
-                this.dropdownShow(e, btnName);
-
-            }, this));
-
-            btn.data('dropdown', $dropdown);
-        }
-    },
-    pickerBuild: function($dropdown, name, colors)
-    {
-        var rule = 'color';
-        if (name === 'backcolor') rule = 'background-color';
-
-        var _self = this;
-        var onSwatch = function(e)
-        {
-            e.preventDefault();
-
-            var $this = $(this);
-            _self.pickerSet($this.data('rule'), $this.attr('rel'));
-
-        };
-
-        var len = colors.length;
-        for (var z = 0; z < len; z++)
-        {
-            var color = colors[z];
-
-            var $swatch = $('<a rel="' + color + '" data-rule="' + rule +'" href="#" style="float: left; font-size: 0; border: 2px solid #fff; padding: 0; margin: 0; width: 20px; height: 20px;"></a>');
-            $swatch.css('background-color', color);
-            $dropdown.append($swatch);
-            $swatch.on('click', onSwatch);
-        }
-
-        var $elNone = $('<a href="#" style="display: block; clear: both; padding: 4px 0; font-size: 11px; line-height: 1;"></a>')
-        .html(this.opts.curLang.none)
-        .on('click', function(e)
-        {
-            e.preventDefault();
-            _self.pickerSet(rule, false);
-        });
-
-        $dropdown.append($elNone);
-    },
-    pickerSet: function(rule, type)
-    {
-        this.bufferSet();
-
-        this.$editor.focus();
-        this.inlineRemoveStyle(rule);
-        if (type !== false) this.inlineSetStyle(rule, type);
-        if (this.opts.air) this.$air.fadeOut(100);
-        this.sync();
-    }
-};
-
-RedactorPlugins.fontsize = {
-	init: function()
-	{
-		var fonts = [10, 14, 22, 32];
-		var that = this;
-		var dropdown = {};
-
-		$.each(fonts, function(i, s)
-		{
-			dropdown['s' + i] = {
-                title: '<span style="font-size:'+s+'px">'+s+'px</span>',
-                callback: function() { that.setFontsize(s); } };
-		});
-
-		dropdown['remove'] = { title: __('Remove font size'), callback: function() { that.resetFontsize(); } };
-
-		this.buttonAddAfter('formatting', 'fontsize', __('Change font size'), false, dropdown);
-	},
-	setFontsize: function(size)
-	{
-		this.inlineSetStyle('font-size', size + 'px');
-	},
-	resetFontsize: function()
-	{
-		this.inlineRemoveStyle('font-size');
-	}
-};
-
-RedactorPlugins.textdirection = {
-    init: function()
-    {
-        var that = this;
-        var dropdown = {};
-
-        dropdown.ltr = { title: __('Left to Right'), callback: this.setLtr };
-        dropdown.rtl = { title: __('Right to Left'), callback: this.setRtl };
-
-        var button = this.buttonAdd('textdirection', __('Change Text Direction'),
-            false, dropdown);
-
-        if (this.opts.direction == 'rtl')
-            this.setRtl();
-    },
-    setRtl: function()
-    {
-        var c = this.getCurrent(), s = this.getSelection();
-        this.bufferSet();
-        if (s.type == 'Range' && s.focusNode.nodeName != 'div') {
-            this.linebreakHack(s);
-        }
-        else if (!c) {
-            var repl = '<div dir="rtl">' + this.get() + '</div>';
-            this.set(repl, false);
-        }
-        $(this.getCurrent()).attr('dir', 'rtl');
-        this.sync();
-    },
-    setLtr: function()
-    {
-        var c = this.getCurrent(), s = this.getSelection();
-        this.bufferSet();
-        if (s.type == 'Range' && s.focusNode.nodeName != 'div') {
-            this.linebreakHack(s);
-        }
-        else if (!c) {
-            var repl = '<div dir="ltr">' + this.get() + '</div>';
-            this.set(repl, false);
-        }
-        $(this.getCurrent()).attr('dir', 'ltr');
-        this.sync();
-    },
-    linebreakHack: function(sel) {
-        var range = sel.getRangeAt(0);
-        var wrapper = document.createElement('div');
-        wrapper.appendChild(range.extractContents());
-        range.insertNode(wrapper);
-        this.selectionElement(wrapper);
-    }
-};
diff --git a/js/redactor-osticket.js b/js/redactor-osticket.js
index cded2e1f3ffdc7d425428155eec3cf2a0815777d..8754c637161e12905f0a9b3412e9b373e2acb233 100644
--- a/js/redactor-osticket.js
+++ b/js/redactor-osticket.js
@@ -12,98 +12,111 @@ if (typeof RedactorPlugins === 'undefined') var RedactorPlugins = {};
  * uploads. Furthermore, the id of the staff is considered for the drafts,
  * so one user will not retrieve drafts for another user.
  */
-RedactorPlugins.draft = {
+RedactorPlugins.draft = function() {
+  return {
     init: function() {
         if (!this.opts.draftNamespace)
             return;
 
-        this.opts.changeCallback = this.hideDraftSaved;
+        this.opts.changeCallback = this.draft.hideDraftSaved;
         var autosave_url = 'ajax.php/draft/' + this.opts.draftNamespace;
         if (this.opts.draftObjectId)
             autosave_url += '.' + this.opts.draftObjectId;
-        this.opts.autosave = autosave_url;
-        this.opts.autosaveInterval = 10;
-        this.opts.autosaveCallback = this.setupDraftUpdate;
-        this.opts.initCallback = this.recoverDraft;
-
-        this.$draft_saved = $('<span>')
-            .addClass("pull-right draft-saved")
-            .hide()
-            .append($('<span>')
-                .text(__('Draft Saved')));
-        // Float the [Draft Saved] box with the toolbar
-        this.$toolbar.append(this.$draft_saved);
-        if (this.opts.draftDelete) {
-            var trash = this.buttonAdd('deleteDraft', __('Delete Draft'), this.deleteDraft);
-            this.buttonAwesome('deleteDraft', 'icon-trash');
-            trash.parent().addClass('pull-right');
-            trash.addClass('delete-draft');
+        this.opts.autosave = this.opts.autoCreateUrl = autosave_url;
+        this.opts.autosaveInterval = 30;
+        this.opts.autosaveCallback = this.draft.afterUpdateDraft;
+        this.opts.autosaveErrorCallback = this.draft.autosaveFailed;
+        this.opts.imageUploadErrorCallback = this.draft.displayError;
+        if (this.opts.draftId) {
+            this.opts.autosave = 'ajax.php/draft/'+this.opts.draftId;
+            this.opts.clipboardUploadUrl =
+            this.opts.imageUpload =
+                'ajax.php/draft/'+this.opts.draftId+'/attach';
         }
-    },
-    recoverDraft: function() {
-        var self = this;
-        $.ajax(this.opts.autosave, {
-            dataType: 'json',
-            statusCode: {
-                200: function(json) {
-                    self.draft_id = json.draft_id;
-                    // Replace the current content with the draft, sync, and make
-                    // images editable
-                    self.setupDraftUpdate(json);
-                    if (!json.body) return;
-                    self.set(json.body, false);
-                    self.observeStart();
-                },
-                205: function() {
-                    // Save empty draft immediately;
-                    var ai = self.opts.autosaveInterval;
+        else if (this.$textarea.hasClass('draft')) {
+            // Just upload the file. A draft will be created automatically
+            // and will be configured locally in the afterUpateDraft()
+            this.opts.clipboardUploadUrl =
+            this.opts.imageUpload = this.opts.autoCreateUrl + '/attach';
+            this.opts.imageUploadCallback = this.afterUpdateDraft;
+        }
+
+        // FIXME: Monkey patch Redactor's autosave enable method to disable first
+        var oldAse = this.autosave.enable;
+        this.autosave.enable = function() {
+            this.autosave.disable();
+            oldAse.call(this);
+        }.bind(this);
+
+        if (autosave_url)
+            this.autosave.enable();
 
-                    // Save immediately -- capture the created autosave
-                    // interval and clear it as soon as possible. Note that
-                    // autosave()ing doesn't happen immediately. It happens
-                    // async after the autosaveInterval expires.
-                    self.opts.autosaveInterval = 0;
-                    self.autosave();
-                    var interval = self.autosaveInterval;
-                    setTimeout(function() {
-                        clearInterval(interval);
-                    }, 1);
+        if (this.$textarea.hasClass('draft')) {
+            this.$draft_saved = $('<span>')
+                .addClass("pull-right draft-saved")
+                .hide()
+                .append($('<span>')
+                    .text(__('Draft Saved')));
+            // Float the [Draft Saved] box with the toolbar
+            this.$toolbar.append(this.$draft_saved);
 
-                    // Reinstate previous autosave interval timing
-                    self.opts.autosaveInterval = ai;
-                }
+            // Add [Delete Draft] button to the toolbar
+            if (this.opts.draftDelete) {
+                var trash = this.draft.deleteButton =
+                    this.button.add('deleteDraft', __('Delete Draft'))
+                this.button.addCallback(trash, this.draft.deleteDraft);
+                this.button.setAwesome('deleteDraft', 'icon-trash');
+                trash.parent().addClass('pull-right');
+                trash.addClass('delete-draft');
+                if (!this.opts.draftId)
+                    trash.hide();
             }
-        });
+        }
+        if (this.code.get())
+            this.$box.trigger('draft:recovered');
     },
-    setupDraftUpdate: function(data) {
-        if (this.get())
+    afterUpdateDraft: function(name, data) {
+        // If the draft was created, a draft_id will be sent back — update
+        // the URL to send updates in the future
+        if (!this.opts.draftId && data.draft_id) {
+            this.opts.draftId = data.draft_id;
+            this.opts.autosave = 'ajax.php/draft/' + data.draft_id;
+            this.opts.clipboardUploadUrl =
+            this.opts.imageUpload =
+                'ajax.php/draft/'+this.opts.draftId+'/attach';
+            if (!this.code.get())
+                this.code.set(' ', false);
+        }
+        // Only show the [Draft Saved] notice if there is content in the
+        // field that has been touched
+        if (!this.draft.firstSave) {
+            this.draft.firstSave = true;
+            // No change yet — dont't show the button
+            return;
+        }
+        if (data && this.code.get()) {
             this.$draft_saved.show().delay(5000).fadeOut();
-
-        // Slight workaround. Signal the 'keyup' event normally signaled
-        // from typing in the <textarea>
-        if ($.autoLock && this.opts.draftNamespace == 'ticket.response')
-            if (this.get())
-                $.autoLock.handleEvent();
-
-        if (typeof data != 'object')
-            data = $.parseJSON(data);
-
-        if (!data || !data.draft_id)
+        }
+        // Show the button if there is a draft to delete
+        if (this.opts.draftId && this.opts.draftDelete)
+            this.draft.deleteButton.show();
+        this.$box.trigger('draft:saved');
+    },
+    autosaveFailed: function(error) {
+        if (error.code == 422)
+            // Unprocessable request (Empty message)
             return;
 
-        $('input[name=draft_id]', this.$box.closest('form'))
-            .val(data.draft_id);
-        this.draft_id = data.draft_id;
-        this.opts.clipboardUploadUrl =
-        this.opts.imageUpload =
-            'ajax.php/draft/'+data.draft_id+'/attach';
-        this.opts.imageUploadErrorCallback = this.displayError;
-        this.opts.original_autosave = this.opts.autosave;
-        this.opts.autosave = 'ajax.php/draft/'+data.draft_id;
+        this.draft.displayError(error);
+        // Cancel autosave
+        this.autosave.disable();
+        this.hideDraftSaved();
+        this.$box.trigger('draft:failed');
     },
 
     displayError: function(json) {
-        alert(json.error);
+        $.sysAlert(json.error,
+            __('Unable to save draft. Refresh the current page to restore and continue your draft.'));
     },
 
     hideDraftSaved: function() {
@@ -111,24 +124,42 @@ RedactorPlugins.draft = {
     },
 
     deleteDraft: function() {
-        if (!this.draft_id)
+        if (!this.opts.draftId)
             // Nothing to delete
             return;
         var self = this;
-        $.ajax('ajax.php/draft/'+this.draft_id, {
+        $.ajax('ajax.php/draft/'+this.opts.draftId, {
             type: 'delete',
             async: false,
             success: function() {
-                self.draft_id = undefined;
-                self.hideDraftSaved();
-                self.set('', false, false);
-                self.opts.autosave = self.opts.original_autosave;
+                self.draft_id = self.opts.draftId = undefined;
+                self.draft.hideDraftSaved();
+                self.code.set(self.opts.draftOriginal || '', false, false);
+                self.opts.autosave = self.opts.autoCreateUrl;
+                self.draft.deleteButton.hide();
+                self.draft.firstSave = false;
+                self.$box.trigger('draft:deleted');
             }
         });
     }
+  };
 };
 
-RedactorPlugins.signature = {
+RedactorPlugins.autolock = function() {
+  return {
+    init: function() {
+      var code = this.$box.closest('form').find('[name=lockCode]'),
+          self = this;
+      if (code.length)
+        this.opts.keydownCallback = function(e) {
+          self.$box.closest('[data-lock-object-id]').exclusive('acquire');
+        };
+    }
+  };
+}
+
+RedactorPlugins.signature = function() {
+  return {
     init: function() {
         var $el = $(this.$element.get(0)),
             inner = $('<div class="inner"></div>');
@@ -141,10 +172,10 @@ RedactorPlugins.signature = {
             else
                 this.$signatureBox.hide();
             $('input[name='+$el.data('signatureField')+']', $el.closest('form'))
-                .on('change', false, false, $.proxy(this.updateSignature, this))
+                .on('change', false, false, $.proxy(this.signature.updateSignature, this));
             if ($el.data('deptField'))
                 $(':input[name='+$el.data('deptField')+']', $el.closest('form'))
-                    .on('change', false, false, $.proxy(this.updateSignature, this))
+                    .on('change', false, false, $.proxy(this.signature.updateSignature, this));
             // Expand on hover
             var outer = this.$signatureBox,
                 inner = $('.inner', this.$signatureBox).get(0),
@@ -181,17 +212,21 @@ RedactorPlugins.signature = {
             url += 'dept/' + $el.data('deptId');
         else if (selected == 'dept' && $el.data('deptField')) {
             if (dept)
-                url += 'dept/' + dept
+                url += 'dept/' + dept;
             else
                 return inner.empty().parent().hide();
         }
+        else if (selected == 'theirs' && $el.data('posterId')) {
+            url += 'agent/' + $el.data('posterId');
+        }
         else if (type == 'none')
            return inner.empty().parent().hide();
         else
-            url += selected
+            url += selected;
 
         inner.load(url).parent().show();
     }
+  }
 };
 
 /* Redactor richtext init */
@@ -206,10 +241,6 @@ $(function() {
                       .attr('height',img.clientHeight);
             html = html.replace(before, img.outerHTML);
         });
-        // Drop <inline> elements if found in the text (shady mojo happening
-        // inside the Redactor editor)
-        // DELME: When this is fixed upstream in Redactor
-        html = html.replace(/<inline /, '<span ').replace(/<\/inline>/, '</span>');
         return html;
     },
     redact = $.redact = function(el, options) {
@@ -221,24 +252,38 @@ $(function() {
         });
         var options = $.extend({
                 'air': el.hasClass('no-bar'),
-                'airButtons': ['formatting', '|', 'bold', 'italic', 'underline', 'deleted', '|', 'unorderedlist', 'orderedlist', 'outdent', 'indent', '|', 'image'],
-                'buttons': ['html', '|', 'formatting', '|', 'bold',
+                'buttons': el.hasClass('no-bar')
+                  ? ['formatting', '|', 'bold', 'italic', 'underline', 'deleted', '|', 'unorderedlist', 'orderedlist', 'outdent', 'indent', '|', 'link', 'image']
+                  : ['html', '|', 'formatting', '|', 'bold',
                     'italic', 'underline', 'deleted', '|', 'unorderedlist',
                     'orderedlist', 'outdent', 'indent', '|', 'image', 'video',
                     'file', 'table', 'link', '|', 'alignment', '|',
                     'horizontalrule'],
-                'autoresize': !el.hasClass('no-bar'),
+                'buttonSource': !el.hasClass('no-bar'),
+                'autoresize': !el.hasClass('no-bar') && !el.closest('.dialog').length,
+                'maxHeight': el.closest('.dialog').length ? selectedSize : false,
                 'minHeight': selectedSize,
                 'focus': false,
-                'plugins': [],
-                'imageGetJson': 'ajax.php/draft/images/browse',
+                'plugins': el.hasClass('no-bar')
+                  ? ['imagemanager','definedlinks']
+                  : ['imagemanager','imageannotate','table','video','definedlinks','autolock'],
+                'imageUpload': el.hasClass('draft'),
+                'imageManagerJson': 'ajax.php/draft/images/browse',
                 'syncBeforeCallback': captureImageSizes,
                 'linebreaks': true,
                 'tabFocus': false,
                 'toolbarFixedBox': true,
                 'focusCallback': function() { this.$box.addClass('no-pjax'); },
+                'initCallback': function() {
+                    if (this.$element.data('width'))
+                        this.$editor.width(this.$element.data('width'));
+                    this.$editor.attr('spellcheck', 'true');
+                    var lang = this.$editor.closest('[lang]').attr('lang');
+                    if (lang)
+                        this.$editor.attr('lang', lang);
+                },
                 'linkSize': 100000,
-                'predefinedLinks': 'ajax.php/config/links'
+                'definedLinks': 'ajax.php/config/links'
             }, options||{});
         if (el.data('redactor')) return;
         var reset = $('input[type=reset]', el.closest('form'));
@@ -255,7 +300,7 @@ $(function() {
             // where Redactor does not sync properly after adding an image.
             // Therefore, the ::get() call will not include text added after
             // the image was inserted.
-            el.redactor('sync');
+            el.redactor('code.sync');
         });
         if (!$.clientPortal) {
             options['plugins'] = options['plugins'].concat(
@@ -264,15 +309,25 @@ $(function() {
         if (el.hasClass('draft')) {
             el.closest('form').append($('<input type="hidden" name="draft_id"/>'));
             options['plugins'].push('draft');
+            options['plugins'].push('imagepaste');
             options.draftDelete = el.hasClass('draft-delete');
         }
+        if (true || 'scp') { // XXX: Add this to SCP only
+            options['plugins'].push('contexttypeahead');
+        }
+        if (el.hasClass('fullscreen'))
+            options['plugins'].push('fullscreen');
+        if (el.data('translateTag'))
+            options['plugins'].push('translatable');
+        if ($('#thread-items[data-thread-id]').length)
+            options['imageManagerJson'] += '?threadId=' + $('#thread-items').data('threadId');
         getConfig().then(function(c) {
             if (c.lang && c.lang.toLowerCase() != 'en_us' &&
                     $.Redactor.opts.langs[c.short_lang])
                 options['lang'] = c.short_lang;
-            if (c.has_rtl)
-                options['plugins'].push('textdirection');
-            if ($('html.rtl').length)
+            //if (c.has_rtl)
+              //  options['plugins'].push('textdirection');
+            if (el.find('rtl').length)
                 options['direction'] = 'rtl';
             el.redactor(options);
         });
@@ -295,7 +350,7 @@ $(function() {
         $('.richtext').each(function() {
             var redactor = $(this).data('redactor');
             if (redactor)
-                redactor.destroy();
+                redactor.core.destroy();
         });
     };
     findRichtextBoxes();
@@ -304,17 +359,21 @@ $(function() {
     $(document).on('pjax:start', cleanupRedactorElements);
 });
 
+$(document).on('focusout.redactor', 'div.redactor_richtext', function (e) {
+    $(this).siblings('textarea').trigger('change');
+});
+
 $(document).ajaxError(function(event, request, settings) {
     if (settings.url.indexOf('ajax.php/draft') != -1
             && settings.type.toUpperCase() == 'POST') {
         $('.richtext').each(function() {
             var redactor = $(this).data('redactor');
             if (redactor) {
+                redactor.autosave.disable();
                 clearInterval(redactor.autosaveInterval);
             }
         });
-        $('#overlay').show();
-        alert(__('Unable to save draft. Refresh the current page to restore and continue your draft.'));
-        $('#overlay').hide();
+        $.sysAlert(__('Unable to save draft.'),
+            __('Refresh the current page to restore and continue your draft.'));
     }
 });
diff --git a/js/redactor-plugins.js b/js/redactor-plugins.js
new file mode 100644
index 0000000000000000000000000000000000000000..0a65831274553f2a10524a05df64d2237fadebca
--- /dev/null
+++ b/js/redactor-plugins.js
@@ -0,0 +1,2185 @@
+if (!RedactorPlugins) var RedactorPlugins = {};
+
+(function($)
+{
+    $.Redactor.prototype.definedlinks = function()
+    {
+        return {
+            init: function()
+            {
+                if (!this.opts.definedLinks) return;
+
+                this.modal.addCallback('link', $.proxy(this.definedlinks.load, this));
+
+            },
+            load: function()
+            {
+                var $select = $('<select id="redactor-defined-links" />');
+                $('#redactor-modal-link-insert').prepend($select);
+
+                this.definedlinks.storage = {};
+
+                $.getJSON(this.opts.definedLinks, $.proxy(function(data)
+                {
+                    $.each(data, $.proxy(function(key, val)
+                    {
+                        this.definedlinks.storage[key] = val;
+                        $select.append($('<option>').val(key).html(val.name));
+
+                    }, this));
+
+                    $select.on('change', $.proxy(this.definedlinks.select, this));
+
+                }, this));
+
+            },
+            select: function(e)
+            {
+                var key = $(e.target).val();
+                var name = '', url = '';
+                if (key !== 0)
+                {
+                    name = this.definedlinks.storage[key].name;
+                    url = this.definedlinks.storage[key].url;
+                }
+
+                $('#redactor-link-url').val(url);
+
+                var $el = $('#redactor-link-url-text');
+                if ($el.val() === '') $el.val(name);
+            }
+        };
+    };
+})(jQuery);
+
+RedactorPlugins.fontcolor = function()
+{
+	return {
+		init: function()
+		{
+			var colors = [
+				'#ffffff', '#000000', '#eeece1', '#1f497d', '#4f81bd', '#c0504d', '#9bbb59', '#8064a2', '#4bacc6', '#f79646', '#ffff00',
+				'#f2f2f2', '#7f7f7f', '#ddd9c3', '#c6d9f0', '#dbe5f1', '#f2dcdb', '#ebf1dd', '#e5e0ec', '#dbeef3', '#fdeada', '#fff2ca',
+				'#d8d8d8', '#595959', '#c4bd97', '#8db3e2', '#b8cce4', '#e5b9b7', '#d7e3bc', '#ccc1d9', '#b7dde8', '#fbd5b5', '#ffe694',
+				'#bfbfbf', '#3f3f3f', '#938953', '#548dd4', '#95b3d7', '#d99694', '#c3d69b', '#b2a2c7', '#b7dde8', '#fac08f', '#f2c314',
+				'#a5a5a5', '#262626', '#494429', '#17365d', '#366092', '#953734', '#76923c', '#5f497a', '#92cddc', '#e36c09', '#c09100',
+				'#7f7f7f', '#0c0c0c', '#1d1b10', '#0f243e', '#244061', '#632423', '#4f6128', '#3f3151', '#31859b',  '#974806', '#7f6000'
+			];
+
+			var buttons = ['fontcolor', 'backcolor'];
+
+			for (var i = 0; i < 2; i++)
+			{
+				var name = buttons[i];
+
+				var button = this.button.addBefore('deleted', name, this.lang.get(name));
+				var $dropdown = this.button.addDropdown(button);
+
+				$dropdown.width(242);
+				this.fontcolor.buildPicker($dropdown, name, colors);
+
+			}
+		},
+		buildPicker: function($dropdown, name, colors)
+		{
+			var rule = (name == 'backcolor') ? 'background-color' : 'color';
+
+			var len = colors.length;
+			var self = this;
+			var func = function(e)
+			{
+				e.preventDefault();
+                var $this = $(e.target);
+				self.fontcolor.set(rule, $this.attr('rel'));
+			};
+            $dropdown.on('click', 'a.redactor.color-swatch', func);
+
+            var template = $('<a class="redactor color-swatch" href="#"></a>');
+
+			for (var z = 0; z < len; z++)
+			{
+				var color = colors[z];
+				var $swatch = template.clone().attr('rel', color);
+				$swatch.css('background-color', color);
+				$dropdown.append($swatch);
+			}
+
+			var $elNone = $('<a href="#" style="redactor uncolor"></a>').html(this.lang.get('none'));
+			$elNone.on('click', $.proxy(function(e)
+			{
+				e.preventDefault();
+				this.fontcolor.remove(rule);
+
+			}, this));
+
+			$dropdown.append($elNone);
+		},
+		set: function(rule, type)
+		{
+			this.inline.format('span', 'style', rule + ': ' + type + ';');
+		},
+		remove: function(rule)
+		{
+			this.inline.removeStyleRule(rule);
+		}
+	};
+};
+
+RedactorPlugins.fontfamily = function()
+{
+	return {
+		init: function ()
+		{
+			var fonts = [ 'Arial', 'Helvetica', 'Georgia', 'Times New Roman', 'Monospace' ];
+			var that = this;
+			var dropdown = {};
+
+			$.each(fonts, function(i, s)
+			{
+				dropdown['s' + i] = { title: '<span style="font-family:' + s.toLowerCase() + ';">' +
+                    s + '</span>', func: function() { that.fontfamily.set(s); }};
+			});
+
+			dropdown.remove = { title: __('Remove Font Family'), func: that.fontfamily.reset };
+
+			var button = this.button.addBefore('bold', 'fontfamily', __('Change Font Family'));
+			this.button.addDropdown(button, dropdown);
+
+		},
+		set: function (value)
+		{
+			this.inline.format('span', 'style', 'font-family:' + value + ';');
+		},
+		reset: function()
+		{
+			this.inline.removeStyleRule('font-family');
+		}
+	};
+};
+
+RedactorPlugins.fullscreen = function()
+{
+	return {
+		init: function()
+		{
+			this.fullscreen.isOpen = false;
+
+			var button = this.button.add('fullscreen', 'Fullscreen');
+			this.button.addCallback(button, this.fullscreen.toggle);
+
+			if (this.opts.fullscreen) this.fullscreen.toggle();
+		},
+		enable: function()
+		{
+			this.button.changeIcon('fullscreen', 'normalscreen');
+			this.button.setActive('fullscreen');
+			this.fullscreen.isOpen = true;
+
+			if (this.opts.toolbarExternal)
+			{
+				this.fullscreen.toolcss = {};
+				this.fullscreen.boxcss = {};
+				this.fullscreen.toolcss.width = this.$toolbar.css('width');
+				this.fullscreen.toolcss.top = this.$toolbar.css('top');
+				this.fullscreen.toolcss.position = this.$toolbar.css('position');
+				this.fullscreen.boxcss.top = this.$box.css('top');
+			}
+
+			this.fullscreen.height = this.$editor.height();
+
+			if (this.opts.maxHeight) this.$editor.css('max-height', '');
+			if (this.opts.minHeight) this.$editor.css('min-height', '');
+
+			if (!this.$fullscreenPlaceholder) this.$fullscreenPlaceholder = $('<div/>');
+			this.$fullscreenPlaceholder.insertAfter(this.$box);
+
+			this.$box.appendTo(document.body);
+
+			this.$box.addClass('redactor-box-fullscreen');
+			$('body, html').css('overflow', 'hidden');
+
+			this.fullscreen.resize();
+			$(window).on('resize.redactor.fullscreen', $.proxy(this.fullscreen.resize, this));
+			$(document).scrollTop(0, 0);
+
+			this.$editor.focus();
+			this.observe.load();
+		},
+		disable: function()
+		{
+			this.button.removeIcon('fullscreen', 'normalscreen');
+			this.button.setInactive('fullscreen');
+			this.fullscreen.isOpen = false;
+
+			$(window).off('resize.redactor.fullscreen');
+			$('body, html').css('overflow', '');
+
+			this.$box.insertBefore(this.$fullscreenPlaceholder);
+			this.$fullscreenPlaceholder.remove();
+
+			this.$box.removeClass('redactor-box-fullscreen').css({ width: 'auto', height: 'auto' });
+
+			this.code.sync();
+
+			if (this.opts.toolbarExternal)
+			{
+				this.$box.css('top', this.fullscreen.boxcss.top);
+				this.$toolbar.css({
+					'width': this.fullscreen.toolcss.width,
+					'top': this.fullscreen.toolcss.top,
+					'position': this.fullscreen.toolcss.position
+				});
+			}
+
+			if (this.opts.minHeight) this.$editor.css('minHeight', this.opts.minHeight);
+			if (this.opts.maxHeight) this.$editor.css('maxHeight', this.opts.maxHeight);
+
+			this.$editor.css('height', 'auto');
+			this.$editor.focus();
+			this.observe.load();
+		},
+		toggle: function()
+		{
+			if (this.fullscreen.isOpen)
+			{
+				this.fullscreen.disable();
+			}
+			else
+			{
+				this.fullscreen.enable();
+			}
+		},
+		resize: function()
+		{
+			if (!this.fullscreen.isOpen) return;
+
+			var toolbarHeight = this.$toolbar.height();
+
+			var height = $(window).height() - toolbarHeight;
+			this.$box.width($(window).width() - 2).height(height + toolbarHeight);
+
+			if (this.opts.toolbarExternal)
+			{
+				this.$toolbar.css({
+					'top': '0px',
+					'position': 'absolute',
+					'width': '100%'
+				});
+
+				this.$box.css('top', toolbarHeight + 'px');
+			}
+
+			this.$editor.height(height - 14);
+		}
+	};
+};
+
+(function($)
+{
+    $.Redactor.prototype.imagemanager = function()
+    {
+        return {
+            init: function()
+            {
+                if (!this.opts.imageManagerJson) return;
+
+                this.modal.addCallback('image', this.imagemanager.load);
+            },
+            load: function()
+            {
+                var $modal = this.modal.getModal();
+
+                this.modal.createTabber($modal);
+                this.modal.addTab(1, 'Upload', 'active');
+                this.modal.addTab(2, 'Choose');
+
+                $('#redactor-modal-image-droparea').addClass('redactor-tab redactor-tab1');
+
+                var $box = $('<div id="redactor-image-manager-box" style="overflow: auto; height: 300px;" class="redactor-tab redactor-tab2">').hide();
+                $modal.append($box);
+
+                $.ajax({
+                  dataType: "json",
+                  cache: false,
+                  url: this.opts.imageManagerJson,
+                  success: $.proxy(function(data)
+                    {
+                        $.each(data, $.proxy(function(key, val)
+                        {
+                            // title
+                            var thumbtitle = '';
+                            if (typeof val.title !== 'undefined') thumbtitle = val.title;
+
+                            var img = $('<img src="' + val.thumb + '" rel="' + val.image + '" title="' + thumbtitle + '" style="width: 100px; height: 75px; cursor: pointer;" />');
+                            $('#redactor-image-manager-box').append(img);
+                            $(img).click($.proxy(this.imagemanager.insert, this));
+
+                        }, this));
+
+
+                    }, this)
+                });
+
+
+            },
+            insert: function(e)
+            {
+                this.image.insert('<img src="' + $(e.target).attr('rel') + '" alt="' + $(e.target).attr('title') + '">');
+            }
+        };
+    };
+})(jQuery);
+
+(function($)
+{
+    $.Redactor.prototype.table = function()
+    {
+        return {
+            getTemplate: function()
+            {
+                return String()
+                + '<section id="redactor-modal-table-insert">'
+                    + '<label>' + this.lang.get('rows') + '</label>'
+                    + '<input type="text" size="5" value="2" id="redactor-table-rows" />'
+                    + '<label>' + this.lang.get('columns') + '</label>'
+                    + '<input type="text" size="5" value="3" id="redactor-table-columns" />'
+                + '</section>';
+            },
+            init: function()
+            {
+                var dropdown = {};
+
+                dropdown.insert_table = {
+                                    title: this.lang.get('insert_table'),
+                                    func: this.table.show,
+                                    observe: {
+                                        element: 'table',
+                                        'in': {
+                                            attr: {
+                                                'class': 'redactor-dropdown-link-inactive',
+                                                'aria-disabled': true,
+                                            }
+                                        }
+                                    }
+                                };
+
+                dropdown.insert_row_above = {
+                                    title: this.lang.get('insert_row_above'),
+                                    func: this.table.addRowAbove,
+                                    observe: {
+                                        element: 'table',
+                                        out: {
+                                            attr: {
+                                                'class': 'redactor-dropdown-link-inactive',
+                                                'aria-disabled': true,
+                                            }
+                                        }
+                                    }
+                                };
+
+                dropdown.insert_row_below = {
+                                    title: this.lang.get('insert_row_below'),
+                                    func: this.table.addRowBelow,
+                                    observe: {
+                                        element: 'table',
+                                        out: {
+                                            attr: {
+                                                'class': 'redactor-dropdown-link-inactive',
+                                                'aria-disabled': true,
+                                            }
+                                        }
+                                    }
+                                };
+
+                dropdown.insert_row_below = {
+                                    title: this.lang.get('insert_row_below'),
+                                    func: this.table.addRowBelow,
+                                    observe: {
+                                        element: 'table',
+                                        out: {
+                                            attr: {
+                                                'class': 'redactor-dropdown-link-inactive',
+                                                'aria-disabled': true,
+                                            }
+                                        }
+                                    }
+                                };
+
+                dropdown.insert_column_left = {
+                                    title: this.lang.get('insert_column_left'),
+                                    func: this.table.addColumnLeft,
+                                    observe: {
+                                        element: 'table',
+                                        out: {
+                                            attr: {
+                                                'class': 'redactor-dropdown-link-inactive',
+                                                'aria-disabled': true,
+                                            }
+                                        }
+                                    }
+                                };
+
+                dropdown.insert_column_right = {
+                                    title: this.lang.get('insert_column_right'),
+                                    func: this.table.addColumnRight,
+                                    observe: {
+                                        element: 'table',
+                                        out: {
+                                            attr: {
+                                                'class': 'redactor-dropdown-link-inactive',
+                                                'aria-disabled': true,
+                                            }
+                                        }
+                                    }
+                                };
+
+                dropdown.add_head = {
+                                    title: this.lang.get('add_head'),
+                                    func: this.table.addHead,
+                                    observe: {
+                                        element: 'table',
+                                        out: {
+                                            attr: {
+                                                'class': 'redactor-dropdown-link-inactive',
+                                                'aria-disabled': true,
+                                            }
+                                        }
+                                    }
+                                };
+
+                dropdown.delete_head = {
+                                    title: this.lang.get('delete_head'),
+                                    func: this.table.deleteHead,
+                                    observe: {
+                                        element: 'table',
+                                        out: {
+                                            attr: {
+                                                'class': 'redactor-dropdown-link-inactive',
+                                                'aria-disabled': true,
+                                            }
+                                        }
+                                    }
+                                };
+
+                dropdown.delete_column = {
+                                    title: this.lang.get('delete_column'),
+                                    func: this.table.deleteColumn,
+                                    observe: {
+                                        element: 'table',
+                                        out: {
+                                            attr: {
+                                                'class': 'redactor-dropdown-link-inactive',
+                                                'aria-disabled': true,
+                                            }
+                                        }
+                                    }
+                                };
+
+                dropdown.delete_row = {
+                                    title: this.lang.get('delete_row'),
+                                    func: this.table.deleteRow,
+                                    observe: {
+                                        element: 'table',
+                                        out: {
+                                            attr: {
+                                                'class': 'redactor-dropdown-link-inactive',
+                                                'aria-disabled': true,
+                                            }
+                                        }
+                                    }
+                                };
+
+                dropdown.delete_row = {
+                                    title: this.lang.get('delete_table'),
+                                    func: this.table.deleteTable,
+                                    observe: {
+                                        element: 'table',
+                                        out: {
+                                            attr: {
+                                                'class': 'redactor-dropdown-link-inactive',
+                                                'aria-disabled': true,
+                                            }
+                                        }
+                                    }
+                                };
+
+                this.observe.addButton('td', 'table');
+                this.observe.addButton('th', 'table');
+
+                var button = this.button.addBefore('link', 'table', this.lang.get('table'));
+                this.button.addDropdown(button, dropdown);
+            },
+            show: function()
+            {
+                this.modal.addTemplate('table', this.table.getTemplate());
+
+                this.modal.load('table', this.lang.get('insert_table'), 300);
+                this.modal.createCancelButton();
+
+                var button = this.modal.createActionButton(this.lang.get('insert'));
+                button.on('click', this.table.insert);
+
+                this.selection.save();
+                this.modal.show();
+
+                $('#redactor-table-rows').focus();
+
+            },
+            insert: function()
+            {
+                this.placeholder.remove();
+
+                var rows = $('#redactor-table-rows').val(),
+                    columns = $('#redactor-table-columns').val(),
+                    $tableBox = $('<div>'),
+                    tableId = Math.floor(Math.random() * 99999),
+                    $table = $('<table id="table' + tableId + '"><tbody></tbody></table>'),
+                    i, $row, z, $column;
+
+                for (i = 0; i < rows; i++)
+                {
+                    $row = $('<tr>');
+
+                    for (z = 0; z < columns; z++)
+                    {
+                        $column = $('<td>' + this.opts.invisibleSpace + '</td>');
+
+                        // set the focus to the first td
+                        if (i === 0 && z === 0)
+                        {
+                            $column.append(this.selection.getMarker());
+                        }
+
+                        $($row).append($column);
+                    }
+
+                    $table.append($row);
+                }
+
+                $tableBox.append($table);
+                var html = $tableBox.html();
+
+                this.modal.close();
+                this.selection.restore();
+
+                if (this.table.getTable()) return;
+
+                this.buffer.set();
+
+                var current = this.selection.getBlock() || this.selection.getCurrent();
+                if (current && current.tagName != 'BODY')
+                {
+                    if (current.tagName == 'LI') current = $(current).closest('ul, ol');
+                    $(current).after(html);
+                }
+                else
+                {
+                    this.insert.html(html, false);
+                }
+
+                this.selection.restore();
+
+                var table = this.$editor.find('#table' + tableId);
+
+                var p = table.prev("p");
+
+                if (p.length > 0 && this.utils.isEmpty(p.html()))
+                {
+                    p.remove();
+                }
+
+                if (!this.opts.linebreaks && (this.utils.browser('mozilla') || this.utils.browser('msie')))
+                {
+                    var $next = table.next();
+                    if ($next.length === 0)
+                    {
+                         table.after(this.opts.emptyHtml);
+                    }
+                }
+
+                this.observe.buttons();
+
+                table.find('span.redactor-selection-marker').remove();
+                table.removeAttr('id');
+
+                this.code.sync();
+                this.core.setCallback('insertedTable', table);
+            },
+            getTable: function()
+            {
+                var $table = $(this.selection.getParent()).closest('table');
+
+                if (!this.utils.isRedactorParent($table)) return false;
+                if ($table.size() === 0) return false;
+
+                return $table;
+            },
+            restoreAfterDelete: function($table)
+            {
+                this.selection.restore();
+                $table.find('span.redactor-selection-marker').remove();
+                this.code.sync();
+            },
+            deleteTable: function()
+            {
+                var $table = this.table.getTable();
+                if (!$table) return;
+
+                this.buffer.set();
+
+
+                var $next = $table.next();
+                if (!this.opts.linebreaks && $next.length !== 0)
+                {
+                    this.caret.setStart($next);
+                }
+                else
+                {
+                    this.caret.setAfter($table);
+                }
+
+
+                $table.remove();
+
+                this.code.sync();
+            },
+            deleteRow: function()
+            {
+            var $table = this.table.getTable();
+            if (!$table) return;
+
+            var $current = $(this.selection.getCurrent());
+
+            this.buffer.set();
+
+            var $current_tr = $current.closest('tr');
+            var $focus_tr = $current_tr.prev().length ? $current_tr.prev() : $current_tr.next();
+            if ($focus_tr.length)
+            {
+                var $focus_td = $focus_tr.children('td, th').first();
+                if ($focus_td.length) $focus_td.prepend(this.selection.getMarker());
+            }
+
+            $current_tr.remove();
+            this.table.restoreAfterDelete($table);
+        },
+            deleteColumn: function()
+            {
+            var $table = this.table.getTable();
+            if (!$table) return;
+
+            this.buffer.set();
+
+            var $current = $(this.selection.getCurrent());
+            var $current_td = $current.closest('td, th');
+            var index = $current_td[0].cellIndex;
+
+            $table.find('tr').each($.proxy(function(i, elem)
+            {
+                var $elem = $(elem);
+                var focusIndex = index - 1 < 0 ? index + 1 : index - 1;
+                if (i === 0) $elem.find('td, th').eq(focusIndex).prepend(this.selection.getMarker());
+
+                $elem.find('td, th').eq(index).remove();
+
+            }, this));
+
+            this.table.restoreAfterDelete($table);
+        },
+            addHead: function()
+            {
+                var $table = this.table.getTable();
+                if (!$table) return;
+
+                this.buffer.set();
+
+                if ($table.find('thead').size() !== 0)
+                {
+                    this.table.deleteHead();
+                    return;
+                }
+
+                var tr = $table.find('tr').first().clone();
+                tr.find('td').replaceWith($.proxy(function()
+                {
+                    return $('<th>').html(this.opts.invisibleSpace);
+                }, this));
+
+                $thead = $('<thead></thead>').append(tr);
+                $table.prepend($thead);
+
+                this.code.sync();
+
+            },
+            deleteHead: function()
+            {
+                var $table = this.table.getTable();
+                if (!$table) return;
+
+                var $thead = $table.find('thead');
+                if ($thead.size() === 0) return;
+
+                this.buffer.set();
+
+                $thead.remove();
+                this.code.sync();
+            },
+            addRowAbove: function()
+            {
+                this.table.addRow('before');
+            },
+            addRowBelow: function()
+            {
+                this.table.addRow('after');
+            },
+            addColumnLeft: function()
+            {
+                this.table.addColumn('before');
+            },
+            addColumnRight: function()
+            {
+                this.table.addColumn('after');
+            },
+            addRow: function(type)
+            {
+                var $table = this.table.getTable();
+                if (!$table) return;
+
+                this.buffer.set();
+
+                var $current = $(this.selection.getCurrent());
+                var $current_tr = $current.closest('tr');
+                var new_tr = $current_tr.clone();
+
+                new_tr.find('th').replaceWith(function()
+                {
+                    var $td = $('<td>');
+                    $td[0].attributes = this.attributes;
+
+                    return $td.append($(this).contents());
+                });
+
+                new_tr.find('td').html(this.opts.invisibleSpace);
+
+                if (type == 'after')
+                {
+                    $current_tr.after(new_tr);
+                }
+                else
+                {
+                    $current_tr.before(new_tr);
+                }
+
+                this.code.sync();
+            },
+            addColumn: function (type)
+            {
+                var $table = this.table.getTable();
+                if (!$table) return;
+
+                var index = 0;
+                var current = $(this.selection.getCurrent());
+
+                this.buffer.set();
+
+                var $current_tr = current.closest('tr');
+                var $current_td = current.closest('td, th');
+
+                $current_tr.find('td, th').each($.proxy(function(i, elem)
+                {
+                    if ($(elem)[0] === $current_td[0]) index = i;
+
+                }, this));
+
+                $table.find('tr').each($.proxy(function(i, elem)
+                {
+                    var $current = $(elem).find('td, th').eq(index);
+
+                    var td = $current.clone();
+                    td.html(this.opts.invisibleSpace);
+
+                    if (type == 'after')
+                    {
+                        $current.after(td);
+                    }
+                    else
+                    {
+                        $current.before(td);
+                    }
+
+                }, this));
+
+                this.code.sync();
+            }
+        };
+    };
+})(jQuery);
+
+RedactorPlugins.textdirection = function() {
+  return {
+    init: function()
+    {
+        var that = this;
+        var dropdown = {};
+
+        dropdown.ltr = { title: __('Left to Right'), callback: this.setLtr };
+        dropdown.rtl = { title: __('Right to Left'), callback: this.setRtl };
+
+        var button = this.button.add('textdirection', __('Change Text Direction'),
+            false, dropdown);
+
+        if (this.opts.direction == 'rtl')
+            this.setRtl();
+    },
+    setRtl: function()
+    {
+        var c = this.getCurrent(), s = this.getSelection();
+        this.buffer.set();
+        if (s.type == 'Range' && s.focusNode.nodeName != 'div') {
+            this.linebreakHack(s);
+        }
+        else if (!c) {
+            var repl = '<div dir="rtl">' + this.get() + '</div>';
+            this.set(repl, false);
+        }
+        $(this.getCurrent()).attr('dir', 'rtl');
+        this.sync();
+    },
+    setLtr: function()
+    {
+        var c = this.getCurrent(), s = this.getSelection();
+        this.buffer.set();
+        if (s.type == 'Range' && s.focusNode.nodeName != 'div') {
+            this.linebreakHack(s);
+        }
+        else if (!c) {
+            var repl = '<div dir="ltr">' + this.get() + '</div>';
+            this.set(repl, false);
+        }
+        $(this.getCurrent()).attr('dir', 'ltr');
+        this.sync();
+    },
+    linebreakHack: function(sel) {
+        var range = sel.getRangeAt(0);
+        var wrapper = document.createElement('div');
+        wrapper.appendChild(range.extractContents());
+        range.insertNode(wrapper);
+        this.selectionElement(wrapper);
+    }
+  };
+};
+
+(function($)
+{
+    $.Redactor.prototype.video = function()
+    {
+        return {
+            reUrlYoutube: /https?:\/\/(?:[0-9A-Z-]+\.)?(?:youtu\.be\/|youtube\.com\S*[^\w\-\s])([\w\-]{11})(?=[^\w\-]|$)(?![?=&+%\w.-]*(?:['"][^<>]*>|<\/a>))[?=&+%\w.-]*/ig,
+            reUrlVimeo: /https?:\/\/(www\.)?vimeo.com\/(\d+)($|\/)/,
+            getTemplate: function()
+            {
+                return String()
+                + '<section id="redactor-modal-video-insert">'
+                    + '<label>' + this.lang.get('video_html_code') + '</label>'
+                    + '<textarea id="redactor-insert-video-area" style="height: 160px;"></textarea>'
+                + '</section>';
+            },
+            init: function()
+            {
+                var button = this.button.addAfter('image', 'video', this.lang.get('video'));
+                this.button.addCallback(button, this.video.show);
+            },
+            show: function()
+            {
+                this.modal.addTemplate('video', this.video.getTemplate());
+
+                this.modal.load('video', this.lang.get('video'), 700);
+                this.modal.createCancelButton();
+
+                var button = this.modal.createActionButton(this.lang.get('insert'));
+                button.on('click', this.video.insert);
+
+                this.selection.save();
+                this.modal.show();
+
+                $('#redactor-insert-video-area').focus();
+
+            },
+            insert: function()
+            {
+                var data = $('#redactor-insert-video-area').val();
+
+                if (!data.match(/<iframe|<video/gi))
+                {
+                    data = this.clean.stripTags(data);
+
+                    // parse if it is link on youtube & vimeo
+                    var iframeStart = '<iframe style="width: 500px; height: 281px;" src="',
+                        iframeEnd = '" frameborder="0" allowfullscreen></iframe>';
+
+                    if (data.match(this.video.reUrlYoutube))
+                    {
+                        data = data.replace(this.video.reUrlYoutube, iframeStart + '//www.youtube.com/embed/$1' + iframeEnd);
+                    }
+                    else if (data.match(this.video.reUrlVimeo))
+                    {
+                        data = data.replace(this.video.reUrlVimeo, iframeStart + '//player.vimeo.com/video/$2' + iframeEnd);
+                    }
+                }
+
+                this.selection.restore();
+                this.modal.close();
+
+                var current = this.selection.getBlock() || this.selection.getCurrent();
+
+                if (current) $(current).after(data);
+                else
+                {
+                    this.insert.html(data);
+                }
+
+                this.code.sync();
+            }
+
+        };
+    };
+})(jQuery);
+
+RedactorPlugins.imagepaste = function() {
+  return {
+    init: function() {
+      if (this.utils.browser('webkit') && navigator.userAgent.indexOf('Chrome') === -1)
+      {
+        var arr = this.utils.browser('version').split('.');
+        if (arr[0] < 536)
+          return true;
+      }
+
+      // paste except opera (not webkit)
+      if (this.utils.browser('opera'))
+          return true;
+
+      this.$editor.on('paste.imagepaste', $.proxy(this.imagepaste.buildEventPaste, this));
+
+      // Capture the selection position every so often as Redactor seems to
+      // drop it when attempting an image paste before `paste` browser event
+      // fires
+      var that = this,
+          plugin = this.imagepaste;
+      setInterval(function() {
+        if (plugin.inpaste)
+          return;
+        that.selection.get();
+        var coords = that.range.getClientRects();
+        if (!coords.length)
+            return;
+        coords = coords[0];
+        var proxy = {
+          clientX: (Math.max(coords.left, 0) || 0) + 10,
+          clientY: (coords.top || 0) + 10,
+        };
+        if (coords.left < 0)
+            return;
+        plugin.offset = proxy; //that.caret.getOffset() || plugin.offset;
+      }, 300);
+    },
+    offset: 0,
+    inpaste: false,
+    buildEventPaste: function(e)
+    {
+      var event = e.originalEvent || e,
+          fileUpload = false,
+          files = [],
+          i, file,
+          plugin = this.imagepaste,
+          cd = event.clipboardData,
+          self = this, node,
+          bail = function() {
+            plugin.inpaste = false;
+          };
+
+      plugin.inpaste = true;
+      if (typeof(cd) === 'undefined')
+          return bail();
+
+      if (cd.items && cd.items.length)
+      {
+        for (i = 0, k = cd.items.length; i < k; i++) {
+          if (cd.items[i].kind == 'file' && cd.items[i].type.indexOf('image/') !== -1) {
+            file = cd.items[i].getAsFile();
+            if (file !== null)
+              files.push(file);
+            }
+        }
+      }
+      else if (cd.files && cd.files.length)
+      {
+        files = cd.files;
+      }
+      else if (cd.types.length) {
+        for (i = 0, k = cd.types.length; i < k; i++) {
+          if (cd.types[i].indexOf('image/') != -1) {
+            var data = cd.getData(cd.types[i]);
+            if (data.length) {
+                files.push(new Blob([data], {type: cd.types[i]}));
+                break;
+            }
+          }
+        }
+      }
+
+      if (!files.length)
+        return bail();
+
+      e.preventDefault();
+      e.stopImmediatePropagation();
+
+      if (plugin.offset == 0) {
+        // Assume top left of editor window since no last position is known
+        var offset = self.$editor.offset();
+        plugin.offset = {
+          clientX: offset.left - $(document).scrollLeft() + 20,
+          clientY: offset.top - $(document).scrollTop() + self.$toolbar.height() + 20
+        }
+      }
+
+      // Add cool wait cursor
+      var waitCursor = $('<span class="-image-upload-placeholder icon-stack"><i class="icon-circle icon-stack-base"></i><i class="icon-picture icon-light icon-spin"></i></span>');
+      self.insert.nodeToCaretPositionFromPoint(plugin.offset, waitCursor);
+
+      var oldIUC = self.opts.imageUploadCallback;
+      self.opts.imageUploadCallback = function(image, json) {
+        if ($.contains(waitCursor.get(0), image))
+          waitCursor.replaceWith(image);
+        else
+          waitCursor.remove();
+
+        self.opts.imageUploadCallback = oldIUC;
+        // Add a zero-width space so that the caret:getOffset will find
+        // locations after pictures if only <br> tags exist otherwise. In
+        // other words, ensure there is at least one character after the
+        // image for text character counting. Additionally, Redactor will
+        // strip the zero-width space when saving
+        $(document.createTextNode("\u200b")).insertAfter($(image));
+        bail();
+      };
+
+      // Upload clipboard files
+      for (i = 0, k = files.length; i < k; i++)
+        self.upload.directUpload(files[i], plugin.offset);
+    }
+  };
+};
+
+var loadedFabric = false;
+RedactorPlugins.imageannotate = function() {
+  return {
+    annotateButton: false,
+    init: function() {
+      var redactor = this,
+          self = this.imageannotate;
+      $(document).on('click', '.redactor-box img', function() {
+        var $image = $(this),
+            image_box = $('#redactor-image-box');
+        if (!image_box.length || !redactor.image.editter)
+            return;
+
+        var edit_size = redactor.image.editter.outerWidth();
+
+        self.annotateButton = redactor.image.editter
+          .on('remove.annotate',
+            function() { self.teardownAnnotate.call(redactor, image_box); })
+          .clone()
+          .text(' '+__('Annotate'))
+          .prepend('<i class="icon-pencil"></i>')
+          .addClass('annotate-button')
+          .insertAfter(redactor.image.editter)
+          .data('image', this)
+          .on('click',
+            function() { self.startAnnotate.call(redactor, $image) });
+        var diff = (edit_size - self.annotateButton.outerWidth()) / 2;
+        self.annotateButton.css('margin-left',
+          (diff + 5) + 'px');
+        redactor.image.editter.css('margin-left',
+          (-edit_size + diff - 5) + 'px');
+      });
+    },
+    startAnnotate: function(img) {
+        canvas = this.imageannotate.initCanvas(img);
+        this.imageannotate.buildToolbar(img);
+        this.image.editter.hide();
+        this.imageannotate.annotateButton.hide();
+    },
+    teardownAnnotate: function(box) {
+        this.image.editter.off('.annotate');
+        this.opts.keydownCallback = false;
+        this.opts.keyupCallback = false;
+        box.find('.annotate-toolbar').remove();
+        box.find('.annotate-button').remove();
+        var img = box.find('img')[0],
+            $img = $(img),
+            fcanvas = $img.data('canvas'),
+            state = fcanvas.toObject();
+        // Capture current annotations
+        delete state.backgroundImage;
+        $img.attr('data-annotations', btoa(JSON.stringify(state)));
+        // Drop the canvas
+        fcanvas.dispose();
+        box.find('canvas').parent().remove();
+        $img.data('canvas', false);
+        // Deselect the image
+        this.image.hideResize();
+        // Show the original image
+        $img.removeClass('hidden');
+    },
+    buildToolbar: function(img) {
+        var box = img.parent(),
+            redactor = this,
+            plugin = this.imageannotate,
+            shapes = $('<span>')
+              .attr('data-redactor', 'verified')
+              .attr('contenteditable', 'false')
+              .css({'display': 'inline-block', 'vertical-align': 'top'}),
+            swatches = shapes.clone(),
+            actions = shapes.clone(),
+            container = $('<div></div>')
+              .addClass('annotate-toolbar')
+              .attr('data-redactor', 'verified')
+              .attr('contenteditable', 'false')
+              .css({position: 'absolute', bottom: 0, 'min-height': '28px',
+                width: '100%', 'background-color': 'rgba(0,0,0,0.5)',
+                margin: 0, 'padding-top': '4px' })
+              .appendTo(box)
+              .append(shapes)
+              .append(swatches)
+              .append(actions);
+
+        var button = $('<a></a>')
+            .attr('href', '#')
+            .attr('data-redactor', 'verified')
+            .attr('contenteditable', 'false')
+            .css({color: 'white', padding: '0 7px 1px', margin: '1px 3px',
+                'text-decoration': 'none', 'vertical-align': 'top'});
+
+        shapes
+            .append(button.clone()
+              .append($('<i class="icon-arrow-right icon-large"></i>')
+              .on('click', plugin.drawArrow.bind(redactor))
+              .attr('title', __('Add Arrow')))
+            )
+            .append(button.clone()
+              .append($('<i class="icon-check-empty icon-large"></i>')
+              .on('click', plugin.drawBox.bind(redactor))
+              .attr('title', __('Add Rectangle')))
+            )
+            .append(button.clone()
+              .append($('<i class="icon-circle-blank icon-large"></i>')
+              .on('click', plugin.drawEllipse.bind(redactor))
+              .attr('title', __('Add Ellipse')))
+            )
+            .append(button.clone()
+              .append($('<i class="icon-text-height icon-large"></i>')
+              .on('click', plugin.drawText.bind(redactor))
+              .attr('title', __('Add Text')))
+            );
+
+      var colors = [
+          '#ffffff', '#888888', '#000000', 'fuchsia', 'blue', 'red',
+          'lime', 'blueviolet', 'cyan', '#f4a63b', 'yellow']
+          len = colors.length;
+
+      swatches.append(
+        $('<span><i class="icon-ellipsis-vertical icon-large"></i></span>')
+          .css({color: 'white', padding: '0 3px 1px', margin: '1px 3px',
+            height: '21px', position: 'relative', bottom: '8px'}
+          )
+      );
+      for (var z = 0; z < len; z++) {
+        var color = colors[z];
+
+        var $swatch = $('<a rel="' + color + '" href="#" style="font-size: 0; padding: 0; margin: 2px; width: 22px; height: 22px;"></a>');
+        $swatch.css({'background-color': color, 'border': '1px dotted rgba(255,255,255,0.4)'});
+        $swatch.attr('data-redactor', 'verified');
+        $swatch.attr('contenteditable', 'false');
+        $swatch.on('click', plugin.setColor.bind(redactor));
+
+        swatches.append($swatch);
+      }
+
+        actions
+            .append(
+              $('<span><i class="icon-ellipsis-vertical icon-large"></i></span>')
+                .css({color: 'white', padding: '0 3px 1px', margin: '1px 3px',
+                  height: '21px'}
+                )
+            )
+            .append(button.clone()
+              .css('padding-left', '1px')
+              .append($('<span></span>').css('position','relative')
+                .append($('<i class="icon-font"></i>'))
+                .append($('<i class="icon-minus"></i>')
+                  .css({position: 'absolute', right: '-4px', top: '5px',
+                    'text-shadow': '0 0 2px black', 'font-size':'80%'})
+                )
+              )
+              .on('click', plugin.smallerFont.bind(redactor))
+              .attr('title', __('Decrease Font Size'))
+            )
+            .append(button.clone()
+              .css('padding-left', '1px')
+              .append($('<span></span>').css('position','relative')
+                .append($('<i class="icon-font icon-large"></i>'))
+                .append($('<i class="icon-plus"></i>')
+                  .css({position: 'absolute', right: '-8px', top: '4px',
+                    'text-shadow': '0 0 2px black'})
+                )
+              )
+              .on('click', plugin.biggerFont.bind(redactor))
+              .attr('title', __('Increase Font Size'))
+            )
+            .append(button.clone()
+              .attr('id', 'annotate-set-stroke')
+              .append($('<span></span>').css({'position': 'relative', 'top': '2px'})
+                .append($('<i class="icon-check-empty icon-large"></i>')
+                  .css('font-size', '120%')
+                ).append($('<i class="icon-tint"></i>')
+                  .css({position: 'absolute', left: '4.5px', top: 0})
+                )
+              )
+              .on('click', plugin.paintStroke.bind(redactor))
+              .attr('title', __('Set Stroke'))
+            )
+            .append(button.clone()
+              .attr('id', 'annotate-set-fill')
+              .append($('<span></span>').css('position','relative')
+                .append($('<i class="icon-sign-blank icon-large"></i>'))
+                .append($('<i class="icon-tint icon-dark"></i>')
+                  .css({position: 'absolute', left: '4px', top: '2px'})
+                )
+              )
+              .on('click', plugin.paintFill.bind(redactor))
+              .attr('title', __('Set Fill'))
+            )
+            .append(button.clone()
+              .append($('<i class="icon-eye-close icon-large"></i>'))
+              .on('click', plugin.setOpacity.bind(redactor))
+              .attr('title', __('Toggle Opacity'))
+            )
+            .append(button.clone()
+              .append($('<i class="icon-double-angle-up icon-large"></i>'))
+              .on('click', plugin.bringForward.bind(redactor))
+              .attr('title', __('Bring Forward'))
+            )
+            .append(button.clone()
+              .append($('<i class="icon-trash icon-large"></i>'))
+              .on('click', plugin.discard.bind(redactor))
+              .attr('title', __('Delete Object'))
+            );
+
+        container.append(button.clone()
+          .append($('<i class="icon-save icon-large"></i>'))
+          .on('click', plugin.commit.bind(redactor))
+          .addClass('pull-right')
+          .attr('title', __('Commit Annotations'))
+        );
+        plugin.paintStroke();
+    },
+
+    setColor: function(e) {
+      e.preventDefault();
+      var plugin = this.imageannotate,
+          redactor = this,
+          swatch = e.target,
+          image_box = $('#redactor-image-box'),
+          img = image_box.find('img')[0],
+          fcanvas = $(img).data('canvas');
+      $.each(fcanvas.getObjects(), function() {
+        if (this.get('active')) {
+          if (plugin.paintMode == 'fill')
+            this.setFill($(e.target).attr('rel'));
+          else
+            this.setStroke($(e.target).attr('rel'));
+        }
+      });
+      fcanvas.renderAll();
+    },
+
+    // Shapes
+    drawShape: function(ondown, onmove, onup, cursor) {
+      // @see http://jsfiddle.net/URWru/
+      var plugin = this.imageannotate,
+          redactor = this,
+          image_box = $('#redactor-image-box'),
+          img = image_box.find('img')[0],
+          fcanvas = $(img).data('canvas'),
+          isDown, shape,
+          mousedown = function(o) {
+            isDown = true;
+            plugin.setBuffer();
+            var pointer = fcanvas.getPointer(o.e);
+            shape = ondown(pointer, o.e);
+            fcanvas.add(shape);
+          },
+          mousemove = function(o) {
+            if (!isDown) return;
+            var pointer = fcanvas.getPointer(o.e);
+            onmove(shape, pointer, o.e);
+            fcanvas.renderAll();
+          },
+          mouseup = function(o) {
+            isDown = false;
+            if (onup) {
+              if (shape2 = onup(shape, fcanvas.getPointer(o.e))) {
+                shape.remove();
+                fcanvas.add(shape2);
+                shape = shape2;
+              }
+            }
+            shape.setCoords()
+              .set({
+                transparentCorners: false,
+                borderColor: 'rgba(102,153,255,0.9)',
+                cornerColor: 'rgba(102,153,255,0.5)',
+                cornerSize: 10
+              });
+            fcanvas.calcOffset()
+              .off('mouse:down', mousedown)
+              .off('mouse:up', mouseup)
+              .off('mouse:move', mousemove)
+              .deactivateAll()
+              .setActiveObject(shape)
+              .renderAll();
+            fcanvas.selection = true;
+            fcanvas.defaultCursor = 'default';
+          };
+
+        fcanvas.selection = false;
+        fcanvas.defaultCursor = cursor || 'crosshair';
+        // Ensure double presses of same button are squelched
+        fcanvas.off('mouse:down');
+        fcanvas.off('mouse:up');
+        fcanvas.off('mouse:move');
+        fcanvas.on('mouse:down', mousedown);
+        fcanvas.on('mouse:up', mouseup);
+        fcanvas.on('mouse:move', mousemove);
+        return false;
+    },
+
+    drawArrow: function(e) {
+      e.preventDefault();
+      var top, left;
+      return this.imageannotate.drawShape(
+        function(pointer) {
+          top = pointer.y;
+          left = pointer.x;
+          return new fabric.Group([
+            new fabric.Line([0, 5, 0, 5], {
+              strokeWidth: 5,
+              fill: 'red',
+              stroke: 'red',
+              originX: 'center',
+              originY: 'center',
+              selectable: false,
+              hasBorders: false
+            }),
+            new fabric.Polygon([
+              {x: 20, y: 0},
+              {x: 0, y: -5},
+              {x: 0, y: 5}
+              ], {
+              strokeWidth: 0,
+              fill: 'red',
+              originX: 'center',
+              originY: 'center',
+              selectable: false,
+              hasBorders: false
+            })
+          ], {
+            left: pointer.x,
+            top: pointer.y,
+            originX: 'center',
+            originY: 'center'
+          });
+        },
+        function(group, pointer) {
+          var dx = pointer.x - left,
+              dy = pointer.y - top,
+              angle = Math.atan(dy / dx),
+              d = Math.sqrt(dx * dx + dy * dy) - 10,
+              sign = dx < 0 ? -1 : 1,
+              dy2 = Math.sin(angle) * d * sign;
+              dx2 = Math.cos(angle) * d * sign,
+          group.item(0)
+            .set({ x2: dx2, y2: dy2 });
+          group.item(1)
+            .set({
+              angle: angle * 180 / Math.PI,
+              flipX: dx < 0,
+              flipY: dy < 0
+            })
+            .setPositionByOrigin(new fabric.Point(dx, dy),
+                'center', 'center');
+        },
+        function(shape, pointer) {
+          var dx = pointer.x - left,
+              dy = pointer.y - top,
+              angle = Math.atan(dy / dx),
+              d = Math.sqrt(dx * dx + dy * dy);
+          // Mess with the next two lines and you *will* be sorry!
+          shape.forEachObject(function(e) { shape.removeWithUpdate(e); });
+          return new fabric.Path(
+            'M '+left+' '+top+' l '+(d-20)+' 0 0 -3 15 3 -15 3 0 -3 z', {
+            angle: angle * 180 / Math.PI + (dx < 0 ? 180 : 0),
+            strokeWidth: 5,
+            fill: 'red',
+            stroke: 'red'
+          });
+        }
+      );
+    },
+
+    drawEllipse: function(e) {
+      e.preventDefault();
+      return this.imageannotate.drawShape(
+        function(pointer) {
+          return new fabric.Ellipse({
+            top: pointer.y,
+            left: pointer.x,
+            strokeWidth: 5,
+            fill: 'transparent',
+            stroke: 'red',
+            originX: 'left',
+            originY: 'top'
+          });
+        },
+        function(circle, pointer, event) {
+          var x = circle.get('left'), y = circle.get('top'),
+              dx = pointer.x - x, dy = pointer.y - y,
+              sw = circle.getStrokeWidth()/2;
+          // Use SHIFT to draw circles
+          if (event.shiftKey) {
+            dy = dx = Math.max(dx, dy);
+          }
+          circle.set({
+            rx: Math.max(0, Math.abs(dx/2) - sw),
+            ry: Math.max(0, Math.abs(dy/2) - sw),
+            originX: dx < 0 ? 'right' : 'left',
+            originY: dy < 0 ? 'bottom' : 'top'});
+        }
+      );
+    },
+
+    drawBox: function(e) {
+      e.preventDefault();
+      return this.imageannotate.drawShape(
+        function(pointer) {
+          return new fabric.Rect({
+            top: pointer.y,
+            left: pointer.x,
+            strokeWidth: 5,
+            fill: 'transparent',
+            stroke: 'red',
+            originX: 'left',
+            originY: 'top'
+          });
+        },
+        function(rect, pointer, event) {
+          var x = rect.get('left'), y = rect.get('top'),
+              dx = pointer.x - x, dy = pointer.y - y;
+          // Use SHIFT to draw squares
+          if (event.shiftKey) {
+            dy = dx = Math.max(dx, dy);
+          }
+          rect.set({ width: Math.abs(dx), height: Math.abs(dy),
+            originX: dx < 0 ? 'right' : 'left',
+            originY: dy < 0 ? 'bottom' : 'top'});
+        }
+      );
+    },
+
+    drawText: function(e) {
+      e.preventDefault();
+      return this.imageannotate.drawShape(
+        function(pointer) {
+          return new fabric.IText(__('Text'), {
+            top: pointer.y,
+            left: pointer.x,
+            fill: 'red',
+            originX: 'left',
+            originY: 'top',
+            fontFamily: 'sans-serif',
+            fontSize: 30
+          });
+        },
+        function(rect, pointer, event) {
+          var x = rect.get('left'), y = rect.get('top'),
+              dx = pointer.x - x, dy = pointer.y - y;
+          // Use SHIFT to draw squares
+          if (event.shiftKey) {
+            dy = dx = Math.max(dx, dy);
+          }
+          rect.set({ width: Math.abs(dx), height: Math.abs(dy),
+            originX: dx < 0 ? 'right' : 'left',
+            originY: dy < 0 ? 'bottom' : 'top'});
+        },
+        function(shape) {
+          shape.on('editing:exited', function() {
+            if (!shape.getText())
+              shape.remove();
+          });
+        },
+        'text'
+      );
+    },
+
+    // Action buttons
+    biggerFont: function(e) {
+      e.preventDefault();
+      var image_box = $('#redactor-image-box'),
+          img = image_box.find('img')[0],
+          fcanvas = $(img).data('canvas');
+      $.each(fcanvas.getObjects(), function() {
+        if (this.get('active') && this instanceof fabric.IText) {
+          if (this.getSelectedText()) {
+            this.setSelectionStyles({
+              fontSize: (this.getSelectionStyles().fontSize || this.getFontSize()) + 5
+            });
+          }
+          else {
+            this.setFontSize(this.getFontSize() + 5);
+          }
+        }
+      });
+      fcanvas.renderAll();
+      return false;
+    },
+    smallerFont: function(e) {
+      e.preventDefault();
+      var image_box = $('#redactor-image-box'),
+          img = image_box.find('img')[0],
+          fcanvas = $(img).data('canvas');
+      $.each(fcanvas.getObjects(), function() {
+        if (this.get('active') && this instanceof fabric.IText) {
+          if (this.getSelectedText()) {
+            this.setSelectionStyles({
+              fontSize: (this.getSelectionStyles().fontSize || this.getFontSize()) - 5
+            });
+          }
+          else {
+            this.setFontSize(this.getFontSize() - 5);
+          }
+        }
+      });
+      fcanvas.renderAll();
+      return false;
+    },
+
+    paintStroke: function(e) {
+      $('#annotate-set-stroke').css({'background-color': 'rgba(255,255,255,0.3)'});
+      $('#annotate-set-fill').css({'background-color': 'transparent'});
+      this.imageannotate.paintMode = 'stroke';
+      return false;
+    },
+    paintFill: function(e) {
+      $('#annotate-set-fill').css({'background-color': 'rgba(255,255,255,0.3)'});
+      $('#annotate-set-stroke').css({'background-color': 'transparent'});
+      this.imageannotate.paintMode = 'fill';
+      return false;
+    },
+
+    setOpacity: function(e) {
+      e.preventDefault();
+      var image_box = $('#redactor-image-box'),
+          img = image_box.find('img')[0],
+          fcanvas = $(img).data('canvas');
+      $.each(fcanvas.getObjects(), function() {
+        if (this.get('active')) {
+          if (this.getOpacity() != 1)
+            this.setOpacity(1);
+          else
+            this.setOpacity(0.6);
+        }
+      });
+      fcanvas.renderAll();
+      return false;
+    },
+
+    bringForward: function(e) {
+      e.preventDefault();
+      var image_box = $('#redactor-image-box'),
+          img = image_box.find('img')[0],
+          fcanvas = $(img).data('canvas');
+      $.each(fcanvas.getObjects(), function() {
+        if (this.get('active')) {
+          this.bringForward();
+        }
+      });
+    },
+
+    keydown: function(e) {
+      var image_box = $('#redactor-image-box'),
+          img = image_box.find('img')[0],
+          fcanvas = $(img).data('canvas');
+
+      if (!fcanvas)
+          return;
+
+      var active = fcanvas.getActiveObject();
+
+      // Check if editing a text element
+      if (active instanceof fabric.IText && active.get('isEditing')) {
+        // This keystroke is not for redactor
+        var ss = active.get('selectionStart'),
+            se = active.get('selectionEnd');
+        active.exitEditing();
+        active.enterEditing();
+        active.set({
+          'selectionStart': ss,
+          'selectionEnd': se
+        });
+        if (e.type == 'keydown')
+            active.onKeyDown(e);
+        else
+            active.onKeyPress(e);
+        return false;
+      }
+
+      // Check if [delete] was pressed with selected objects
+      if (e.keyCode == 8 || e.keyCode == 46)
+        return this.imageannotate.discard(e);
+      else if (e.keyCode == 90 && (e.metaKey || e.ctrlKey)) {
+        fcanvas.loadFromJSON(atob($(img).attr('data-annotations')));
+        return false;
+      }
+    },
+
+    discard: function(e) {
+      var image_box = $('#redactor-image-box', this.$editor),
+          img = image_box && image_box.find('img')[0],
+          fcanvas = img && $(img).data('canvas');
+
+      if (!fcanvas)
+        // Not annotating
+        return;
+
+      e.preventDefault();
+      this.imageannotate.setBuffer();
+      $.each(fcanvas.getObjects(), function() {
+        if (this.get('active'))
+          this.remove();
+      });
+      fcanvas.renderAll();
+      return false;
+    },
+
+    commit: function(e) {
+      e.preventDefault();
+      var redactor = this,
+          image_box = $('#redactor-image-box'),
+          img = image_box.find('img')[0],
+          $img = $(img),
+          fcanvas = $(img).data('canvas');
+      fcanvas.deactivateAll();
+
+      // Upload to server
+      redactor.buffer.set();
+      var annotated = fcanvas.toDataURL({
+            format: 'jpg', quality: 4,
+            multiplier: 1/fcanvas.getZoom()
+          }),
+          file = new Blob([annotated], {type: 'image/jpeg'});
+
+      // Fallback to the data URL — show while the image is being uploaded
+      var origSrc = $img.attr('src');
+      $img.attr('src', annotated);
+
+      var origCallback = redactor.opts.imageUploadCallback,
+          origErrorCbk = redactor.opts.imageUploadErrorCallback;
+
+      // After successful upload, replace the old image with the new one.
+      // Transfer the annotation state to the new image for replay.
+      redactor.opts.imageUploadCallback = function(image, json) {
+        redactor.opts.imageUploadCallback = origCallback;
+        redactor.opts.imageUploadErrorCallback = origErrorCbk;
+        // Transfer the annotation JSON data and drop the original image.
+        image.attr('data-annotations', $img.attr('data-annotations'));
+        // Record the image that was originally annotated. If the committed
+        // image is annotated again, it should be the original image with
+        // the annotations placed live on the original image. The image
+        // being committed here will be discarded.
+        image.attr('data-orig-annotated-image-src',
+          $img.attr('data-orig-annotated-image-src') || origSrc
+        );
+        $img.remove();
+        // Redactor will add <br> before and after the image in linebreaks
+        // mode
+        var N = image.next();
+        if (N.is('br')) N.remove();
+        var P = image.prev();
+        if (N.is('br')) P.remove();
+      };
+
+      // Handle upload issues
+      redactor.opts.imageUploadErrorCallback = function(json) {
+        redactor.opts.imageUploadCallback = origCallback;
+        redactor.opts.imageUploadErrorCallback = origErrorCbk;
+        $img.show();
+      };
+      redactor.imageannotate.teardownAnnotate(image_box);
+      $img.css({opacity: 0.5});
+      redactor.upload.directUpload(file, e);
+      return false;
+    },
+
+    // Utils
+    resizeShape: function(o) {
+      var shape = o.target;
+      if (shape instanceof fabric.Ellipse) {
+        shape.set({
+          rx: shape.get('rx') * shape.get('scaleX'),
+          ry: shape.get('ry') * shape.get('scaleY'),
+          scaleX: 1,
+          scaleY: 1
+        });
+      }
+      else if (shape instanceof fabric.Rect) {
+        shape.set({
+          width: shape.get('width') * shape.get('scaleX'),
+          height: shape.get('height') * shape.get('scaleY'),
+          scaleX: 1,
+          scaleY: 1
+        });
+      }
+    },
+    setBuffer: function() {
+      var image_box = $('#redactor-image-box'),
+          img = image_box.find('img')[0],
+          $img = $(img),
+          fcanvas = $img.data('canvas'),
+          state = fcanvas.toObject();
+      // Capture current annotations
+      delete state.backgroundImage;
+      $img.attr('data-annotations', btoa(JSON.stringify(state)));
+    },
+
+    // Startup
+
+    initCanvas: function(img) {
+      var self = this,
+          plugin = this.imageannotate,
+          $img = $(img);
+      if ($img.data('canvas'))
+        return;
+      var box = $img.parent(),
+          canvas = $('<canvas>').css({
+            position: 'absolute',
+            top: 0, bottom: 0, left: 0, right: 0,
+            width: '100%', height: '100%'
+          }).appendTo(box),
+          fcanvas = new fabric.Canvas(canvas[0], {
+            backgroundColor: 'rgba(0,0,0,0,0)',
+            containerClass: 'no-margin',
+            includeDefaultValues: false,
+          }),
+          previous = $(img).attr('data-annotations');
+
+      // Catch [delete] key and map to delete object
+      self.opts.keydownCallback = plugin.keydown.bind(self);
+      self.opts.keyupCallback = plugin.keydown.bind(self);
+
+      var I = new Image(), scale;
+      I.src = $img.attr('src');
+      // Use a maximum zoom-out of 0.7, so that very large pictures do not
+      // result in unusually small annotations (esp. stroke widths which are
+      // not adjustable).
+      scale = Math.max(0.7, $img.width() / I.width);
+      var scaleWidth = $img.width() / scale,
+          scaleHeight = $img.height() / scale;
+      fcanvas
+        .setDimensions({width: $img.width(), height: $img.height()})
+        .setZoom(scale)
+        .setBackgroundImage(
+            $img.attr('data-orig-annotated-image-src') || $img.attr('src'),
+            fcanvas.renderAll.bind(fcanvas), {
+          width: scaleWidth,
+          height: scaleHeight,
+          // Needed to position overlayImage at 0/0
+          originX: 'left',
+          originY: 'top'
+        })
+        .on('object:scaling', plugin.resizeShape.bind(self));
+      if (previous) {
+        fcanvas.loadFromJSON(atob(previous));
+        fcanvas.forEachObject(function(o) {
+          o.set({
+            transparentCorners: false,
+            borderColor: 'rgba(102,153,255,0.9)',
+            cornerColor: 'rgba(102,153,255,0.5)',
+            cornerSize: 10
+          });
+        });
+      }
+      $img.data('canvas', fcanvas).addClass('hidden');
+      return fcanvas;
+    }
+  };
+};
+
+RedactorPlugins.contexttypeahead = function() {
+  return {
+    typeahead: false,
+    context: false,
+    variables: false,
+
+    init: function() {
+      if (!this.$element.data('rootContext'))
+        return;
+
+      this.opts.keyupCallback = this.contexttypeahead.watch.bind(this);
+      this.opts.keydownCallback = this.contexttypeahead.watch.bind(this);
+      this.$editor.on('click', this.contexttypeahead.watch.bind(this));
+    },
+
+    watch: function(e) {
+      var current = this.selection.getCurrent(),
+          allText = this.$editor.text(),
+          offset = this.caret.getOffset(),
+          lhs = allText.substring(0, offset),
+          search = new RegExp(/%\{([^}]*)$/),
+          match;
+
+      if (!lhs) {
+        return !e.isDefaultPrevented();
+      }
+
+      if (e.which == 27 || !(match = search.exec(lhs)))
+        // No longer in a element — close typeahead
+        return this.contexttypeahead.destroy();
+
+      if (e.type == 'click')
+        return;
+
+      // Locate the position of the cursor and the number of characters back
+      // to the `%{` symbols
+      var sel         = this.selection.get(),
+          range       = this.sel.getRangeAt(0),
+          content     = current.textContent,
+          clientRects = range.getClientRects(),
+          position    = clientRects[0],
+          backText    = match[1],
+          parent      = this.selection.getParent() || this.$editor,
+          plugin      = this.contexttypeahead;
+
+      // Insert a hidden text input to receive the typed text and add a
+      // typeahead widget
+      if (!this.contexttypeahead.typeahead) {
+        this.contexttypeahead.typeahead = $('<input type="text">')
+          .css({position: 'absolute', visibility: 'hidden'})
+          .width(0).height(position.height - 4)
+          .appendTo(document.body)
+          .typeahead({
+            property: 'variable',
+            minLength: 0,
+            arrow: $('<span class="pull-right"><i class="icon-muted icon-chevron-right"></i></span>')
+                .css('padding', '0 0 0 6px'),
+            highlighter: function(variable, item) {
+              var base = $.fn.typeahead.Constructor.prototype.highlighter
+                    .call(this, variable),
+                  further = new RegExp(variable + '\\.'),
+                  extendable = Object.keys(plugin.variables).some(function(v) {
+                    return v.match(further);
+                  }),
+                  arrow = extendable ? this.options.arrow.clone() : '';
+
+              return $('<div/>').html(base).prepend(arrow).html()
+                + $('<span class="faded">')
+                  .text(' — ' + item.desc)
+                  .wrap('<div>').parent().html();
+            },
+            item: '<li><a href="#" style="display:block"></a></li>',
+            source: this.contexttypeahead.getContext.bind(this),
+            sorter: function(items) {
+              items.sort(
+                function(a,b) {return a.variable > b.variable ? 1 : -1;}
+              );
+              return items;
+            },
+            matcher: function(item) {
+              if (item.toLowerCase().indexOf(this.query.toLowerCase()) !== 0)
+                return false;
+
+              return (this.query.match(/\./g) || []).length == (item.match(/\./g) || []).length;
+            },
+            onselect: this.contexttypeahead.select.bind(this),
+            scroll: true,
+            items: 100
+          });
+      }
+
+      if (position) {
+        var width = plugin.textWidth(
+              backText,
+              this.selection.getParent() || $('<div class="redactor-editor">')
+            ),
+            pleft = $(parent).offset().left,
+            left = position.left - width;
+
+        if (left < pleft)
+            // This is a bug in chrome, but I'm not sure how to adjust it
+            left += pleft;
+
+        plugin.typeahead
+          .css({top: position.top + $(window).scrollTop(), left: left});
+      }
+
+      plugin.typeahead
+        .val(match[1])
+        .trigger(e);
+
+      return !e.isDefaultPrevented();
+    },
+
+    getContext: function(typeahead, query) {
+      var dfd, that=this.contexttypeahead,
+          root = this.$element.data('rootContext');
+      if (!this.contexttypeahead.context) {
+        dfd = $.Deferred();
+        $.ajax('ajax.php/content/context', {
+          data: {root: root},
+          success: function(json) {
+            var items = $.map(json, function(v,k) {
+              return {variable: k, desc: v};
+            });
+            that.variables = json;
+            dfd.resolve(items);
+          }
+        });
+        this.contexttypeahead.context = dfd;
+      }
+      // Only fetch the context once for this redactor box
+      this.contexttypeahead.context.then(function(items) {
+        typeahead.process(items);
+      });
+    },
+
+    textWidth: function(text, clone) {
+      var c = $(clone),
+          o = c.clone().text(text)
+            .css({'position': 'absolute', 'float': 'left', 'white-space': 'nowrap', 'visibility': 'hidden'})
+            .css({'font-family': c.css('font-family'), 'font-weight': c.css('font-weight'),
+              'font-size': c.css('font-size')})
+            .appendTo($('body')),
+          w = o.width();
+
+      o.remove();
+
+      return w;
+    },
+
+    destroy: function() {
+      if (this.contexttypeahead.typeahead) {
+        this.contexttypeahead.typeahead.typeahead('hide');
+        this.contexttypeahead.typeahead.remove();
+        this.contexttypeahead.typeahead = false;
+      }
+    },
+
+    select: function(item, event) {
+      // Collapse multiple textNodes together
+      (this.selection.getBlock() || this.$editor.get(0)).normalize();
+      var current = this.selection.getCurrent(),
+          sel     = this.selection.get(),
+          range   = this.sel.getRangeAt(0),
+          cursorAt = range.endOffset,
+          // TODO: Consume immediately following `}` symbols
+          plugin  = this.contexttypeahead,
+          search  = new RegExp(/%\{([^}]*)(\}?)$/);
+
+      // FIXME: ENTER will end up here, but current will be empty
+
+      if (!current)
+        return;
+
+      // Set cursor at the end of the expanded text
+      var left = current.textContent.substring(0, cursorAt),
+          right = current.textContent.substring(cursorAt),
+          autoExpand = event.target.nodeName == 'I',
+          selected = item.variable + (autoExpand ? '.' : '')
+          newLeft = left.replace(search, '%{' + selected + '}');
+
+      current.textContent = newLeft
+        // Drop the remaining part of a variable block, if any
+        + right.replace(/[^%}]*?[%}]/, '');
+
+      this.range.setStart(current, newLeft.length - 1);
+      this.range.setEnd(current, newLeft.length - 1);
+      this.selection.addRange();
+      if (!autoExpand)
+          return plugin.destroy();
+
+      plugin.typeahead.val(selected);
+      plugin.typeahead.typeahead('lookup');
+      return false;
+    }
+  };
+};
+
+RedactorPlugins.translatable = function() {
+  return {
+    langs: undefined,
+    config: undefined,
+    textareas: {},
+    current: undefined,
+    primary: undefined,
+    button: undefined,
+
+    init: function() {
+      $.ajax({
+        url: 'ajax.php/i18n/langs/all',
+        success: this.translatable.setLangs.bind(this)
+      });
+      getConfig().then(this.translatable.setConfig.bind(this));
+      this.opts.keydownCallback = this.translatable.showCommit.bind(this);
+      this.translatable.translateTag = this.$textarea.data('translateTag');
+    },
+
+    setLangs: function(langs) {
+      this.translatable.langs = langs;
+      this.translatable.buildDropdown();
+    },
+
+    setConfig: function(config) {
+      this.translatable.config = config;
+      this.translatable.buildDropdown();
+    },
+
+    buildDropdown: function() {
+      if (!this.translatable.config || !this.translatable.langs)
+        return;
+
+      var plugin = this.translatable,
+          primary = this.$textarea,
+          primary_lang = plugin.config.primary_language.replace('-','_'),
+          primary_info = plugin.langs[primary_lang],
+          dropdown = {},
+          items = {};
+
+      langs = plugin.langs;
+      plugin.textareas[primary_lang] = primary;
+      plugin.primary = plugin.current = primary_lang;
+
+      dropdown[primary_lang] = {
+        title: '<i class="flag flag-'+primary_info.flag+'"></i> '+primary_info.name,
+        func: function() { plugin.switchTo(primary_lang); }
+      }
+
+      $.each(langs, function(lang, info) {
+        if (lang == primary_lang)
+          return;
+        dropdown[lang] = {
+          title: '<i class="flag flag-'+info.flag+'"></i> '+info.name,
+          func: function() { plugin.switchTo(lang); }
+        };
+        plugin.textareas[lang] = primary.clone(false).attr({
+          lang: lang,
+          dir: info['direction'],
+          'class': '',
+        })
+        .removeAttr('name').removeAttr('data-translate-tag')
+        .text('')
+        .insertAfter(primary);
+      });
+
+      // Add the button to the toolbar
+      plugin.button = this.button.add('translate', __('Translate')),
+      this.button.setAwesome('translate', 'flag flag-' + plugin.config.primary_lang_flag);
+      plugin.button.parent().addClass('pull-right');
+      this.button.addDropdown(plugin.button, dropdown);
+
+      // Flip back to primary language before submitting
+      this.$textarea.closest('form').submit(function() {
+        plugin.switchTo(primary_lang);
+      });
+    },
+
+    switchTo: function(lang) {
+      var that = this;
+
+      if (lang == this.translatable.current)
+        return;
+
+      if (this.translatable.translations === undefined) {
+        this.translatable.fetch('ajax.php/i18n/translate/' + this.translatable.translateTag)
+        .then(function(json) {
+          that.translatable.translations = json;
+          $.each(json, function(l, text) {
+            that.translatable.textareas[l].val(text);
+          });
+          // Now switch to the language
+          that.translatable.switchTo(lang);
+        });
+        return;
+      }
+
+      var html = this.$editor.html();
+      this.$textarea.val(this.clean.onSync(html));
+      this.$textarea = this.translatable.textareas[lang];
+      this.code.set(this.$textarea.val());
+      this.translatable.current = lang;
+
+      this.button.setAwesome('translate', 'flag flag-' + this.translatable.langs[lang].flag);
+      this.$editor.attr({lang: lang, dir: this.translatable.langs[lang].direction});
+    },
+
+    showCommit: function() {
+      var plugin = this.translatable;
+
+      if (this.translatable.current == this.translatable.primary) {
+        if (this.translatable.$commit)
+          this.translatable.$commit
+          .slideUp(function() { $(this).remove(); plugin.$commit = undefined; });
+        return true;
+      }
+
+      if (this.translatable.$commit)
+        return true;
+
+      this.translatable.$commit = $('<div class="language-commit"></div>')
+      .hide()
+      .appendTo(this.$box)
+      .append($('<button type="button" class="white button commit"><i class="fa fa-save icon-save"></i> '+__('Save')+'</button>')
+        .on('click', $.proxy(this.translatable.commit, this))
+      )
+      .slideDown();
+    },
+
+    commit: function() {
+      var changes = {}, self = this,
+          plugin = this.translatable,
+          $commit = plugin.$commit;
+      $commit.find('button').empty().text(' '+__('Saving'))
+          .prop('disabled', true)
+          .prepend($('<i>').addClass('fa icon-spin icon-spinner'));
+      changes[plugin.current] = this.code.get();
+      $.ajax('ajax.php/i18n/translate/' + plugin.translateTag, {
+        type: 'post',
+        data: changes,
+        success: function() {
+          $commit.slideUp(function() { $(this).remove(); plugin.$commit = undefined; });
+        }
+      });
+    },
+
+    urlcache: {},
+    fetch: function( url, data, callback ) {
+      var urlcache = this.translatable.urlcache;
+      if ( !urlcache[ url ] ) {
+        urlcache[ url ] = $.Deferred(function( defer ) {
+          $.ajax( url, { data: data, dataType: 'json' } )
+            .then( defer.resolve, defer.reject );
+        }).promise();
+      }
+      return urlcache[ url ].done( callback );
+    },
+  };
+};
diff --git a/js/redactor.min.js b/js/redactor.min.js
index 32e661ee98ae716d7c3629e8cbfbf27d365edabc..1a97a6de53e1f2c8b24df7e33010f683dadf8f06 100644
--- a/js/redactor.min.js
+++ b/js/redactor.min.js
@@ -1,12 +1,12 @@
 /*
-	Redactor v9.2.4
-	Updated: May 15, 2014
+	Redactor 10.2.2
+	Updated: July 15, 2015
 
 	http://imperavi.com/redactor/
 
-	Copyright (c) 2009-2014, Imperavi LLC.
+	Copyright (c) 2009-2015, Imperavi LLC.
 	License: http://imperavi.com/redactor/license/
 
 	Usage: $('#content').redactor();
 */
-eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('(B($){q 7z=0;"lI lJ";q cp=B(O){c[0]=O.lO;c[1]=O.jP;c.O=O;F c};cp.5g.iv=B(){F c[0]===c[1]};q 8s=/5C?:\\/\\/(?:[0-9A-Z-]+\\.)?(?:lN\\.be\\/|bU\\.6V\\S*[^\\w\\-\\s])([\\w\\-]{11})(?=[^\\w\\-]|$)(?![?=&+%\\w.-]*(?:[\'"][^<>]*>|<\\/a>))[?=&+%\\w.-]*/ig;q 8x=/5C?:\\/\\/(bg\\.)?bN.6V\\/(\\d+)($|\\/)/;$.fn.U=B(3Q){q 1p=[];q jW=fk.5g.k0.5B(hj,1);if(1E 3Q==="8O"){c.1u(B(){q 6H=$.1a(c,"U");if(1E 6H!=="1I"&&$.72(6H[3Q])){q aA=6H[3Q].cx(6H,jW);if(aA!==1I&&aA!==6H){1p.3a(aA)}}I{F $.3V(\'m5 lL 5w "\'+3Q+\'" 3w 3G\')}})}I{c.1u(B(){if(!$.1a(c,"U")){$.1a(c,"U",3G(c,3Q))}})}if(1p.1m===0){F c}I{if(1p.1m===1){F 1p[0]}I{F 1p}}};B 3G(el,3Q){F 2a 3G.5g.7B(el,3Q)}$.3G=3G;$.3G.lK="9.2.4";$.3G.C={4a:E,1Q:E,4j:E,1f:E,1y:"en",63:"lE",4R:E,8u:E,fI:E,kq:N,gl:N,h0:N,kj:E,do:N,kl:N,bV:N,d5:E,jU:E,5L:N,2j:E,7y:E,4K:N,7R:E,ch:E,6p:{"3k+m, 4E+m":"c.22(\'kz\', E)","3k+b, 4E+b":"c.22(\'3q\', E)","3k+i, 4E+i":"c.22(\'3r\', E)","3k+h, 4E+h":"c.22(\'fh\', E)","3k+l, 4E+l":"c.22(\'fo\', E)","3k+k, 4E+k":"c.an()","3k+8E+7":"c.22(\'8f\', E)","3k+8E+8":"c.22(\'82\', E)"},ji:{"3k+3":"c.22(\'kz\', E)"},7p:E,9N:60,e6:E,7I:"9W://",jE:E,6N:50,eQ:E,8c:"9a",7k:E,fr:N,kF:N,3Z:E,ba:"26",hX:N,7N:E,dF:"26",9l:N,cJ:E,cB:["T/le","T/kP","T/kN"],5p:E,3E:E,4F:N,5F:N,eX:N,ag:E,fv:N,3d:E,g8:["6c","3q","3r","5j","5V","6u","6W","5S"],1A:N,64:E,8w:X,a3:0,b4:E,8P:E,gq:E,gA:N,42:["o","6c","3q","3r","5j","5V","6u","6W","5S","T","3C","26","1n","1s","9Y","|","8b"],bz:[],aB:["5j","3r","3q","4W","5V","6u","dv","dx","dk","dK","1n"],eF:{b:"3q",43:"3q",i:"3r",em:"3r",56:"5j",5J:"5j",2q:"5V",ol:"6u",u:"4W",3h:"1n",1g:"1n",1n:"1n"},gB:["p","2h","2r","h1","h2","h3","h4","h5","h6"],1N:E,6a:N,aP:N,6P:N,68:E,6O:E,fD:E,8e:E,5G:E,7M:["o","aK","1s","2v","4E","3s","1o","lH"],5X:"43",5Y:"em",aJ:20,3J:[],7Y:[],6z:E,5a:"<p>&#aY;</p>",2i:"&#aY;",dJ:/^(P|H[1-6]|3f|b2|bi|b6|b8|am|ao)$/i,5r:["P","kG","l9","l0","hZ","hu","hF","hN","hK","jm","8J","6i","3L","jw","dP","b2","bi","b6","b8","am","ao"],dU:["gj","2v","aK","hr","i?2Z","1s","4E","m8","1o","3s","1n","7L","3X","cg"],dM:["li","dt","dt","h[1-6]","47","3s"],jb:["2h","12","dl","ga","2s","lj","gk","ol","p","2r","3n","1g","eN","3h","2q"],ac:["P","kG","l9","l0","hZ","hu","hF","hN","hK","jm","8J","3f","3L","jw","dP","6w","b2","bi","b6","b8","am","ao","6i"],jj:{en:{o:"kc",3C:"6C iY",T:"6C iX",1n:"bA",1s:"j3",eJ:"6C 1s",fU:"jx 1s",67:"lD",6c:"lC",jf:"lB Y",jg:"lF",2o:"iZ",hT:"8q 1",hS:"8q 2",hP:"8q 3",hG:"8q 4",hw:"8q 5",3q:"lR",3r:"lS",m3:"m2 jn",m4:"lA jn",5V:"m6 jq",6u:"m1 jq",6W:"lg",5S:"lf",6I:"ll",4u:"6C",83:"ln",ih:"8o",dS:"6C bA",dX:"8L bJ lw",ea:"8L bJ lu",ec:"8L bG iR",e3:"8L bG iW",cY:"8o bG",cX:"8o bJ",cW:"8o bA",ae:"lp",ap:"lr",dD:"8L j1",d9:"8o j1",1c:"ls",io:"nB",3o:"n6",1t:"iR",4I:"iW",6D:"iB",hW:"iX n5 j3",Y:"n4",er:"n3",n7:"hC",ht:"iY n8 iZ",26:"6C nb",8r:"iz",na:"n9",dR:"n2",iF:"n1 dR",iD:"mV 26 mU",du:"iM Y iL iK 1t",ds:"iB Y",dj:"iM Y iL iK 4I",ei:"mT Y",8b:"6C mS mW",5j:"mX",n0:"mZ",dA:"mY 1s in 2a 53",4W:"nc",9Y:"nd",dI:"nu (nt)",84:"jx"}}};3G.fn=$.3G.5g={2O:{9D:8,d3:46,ev:40,b3:13,eA:27,fF:9,nr:17,nq:91,nv:37,gd:91},7B:B(el,3Q){c.85=E;c.$2g=c.$1v=$(el);c.7z=7z++;q C=$.4H(N,{},$.3G.C);c.C=$.4H({},C,c.$2g.1a(),3Q);c.2u=N;c.nx=[];c.aF=c.$1v.1f("21");c.nA=c.$1v.1f("2l");if(c.C.4j){c.C.1Q=N}if(c.C.1N){c.C.6a=E}if(c.C.6a){c.C.1N=E}if(c.C.b4){c.C.64=N}c.X=X;c.48=48;c.5M=E;c.gL=2a 2L("^<(/?"+c.C.dU.5U("|/?")+"|"+c.C.dM.5U("|")+")[ >]");c.gI=2a 2L("^<(br|/?"+c.C.dU.5U("|/?")+"|/"+c.C.dM.5U("|/")+")[ >]");c.cF=2a 2L("^</?("+c.C.jb.5U("|")+")[ >]");c.a7=2a 2L("^("+c.C.ac.5U("|")+")$","i");if(c.C.1N===E){if(c.C.5G!==E){q dh=["43","em","56"];q j8=["b","i","5J"];if($.4L("p",c.C.5G)==="-1"){c.C.5G.3a("p")}3w(i in dh){if($.4L(dh[i],c.C.5G)!="-1"){c.C.5G.3a(j8[i])}}}if(c.C.7M!==E){q 3Y=$.4L("p",c.C.7M);if(3Y!=="-1"){c.C.7M.9r(3Y,3Y)}}}if(c.1C("3t")||c.1C("8S")){c.C.42=c.lb(c.C.42,"8b")}c.C.1F=c.C.jj[c.C.1y];$.4H(c.C.6p,c.C.ji);c.gV();c.ks()},fQ:B(1y){F{o:{1c:1y.o,1H:"cA"},6c:{1c:1y.6c,1H:"2N",1P:{p:{1c:1y.jf,1H:"5i"},2h:{1c:1y.jg,1H:"bR",2U:"ny"},2r:{1c:1y.2o,1H:"5i",2U:"np"},h1:{1c:1y.hT,1H:"5i",2U:"no"},h2:{1c:1y.hS,1H:"5i",2U:"m9"},h3:{1c:1y.hP,1H:"5i",2U:"ng"},h4:{1c:1y.hG,1H:"5i",2U:"nf"},h5:{1c:1y.hw,1H:"5i",2U:"ne"}}},3q:{1c:1y.3q,2p:"3q"},3r:{1c:1y.3r,2p:"3r"},5j:{1c:1y.5j,2p:"fC"},4W:{1c:1y.4W,2p:"4W"},5V:{1c:"&ni; "+1y.5V,2p:"82"},6u:{1c:"1. "+1y.6u,2p:"8f"},6W:{1c:"< "+1y.6W,1H:"dz"},5S:{1c:"> "+1y.5S,1H:"dr"},T:{1c:1y.T,1H:"kJ"},3C:{1c:1y.3C,1H:"kC"},26:{1c:1y.26,1H:"kL"},1n:{1c:1y.1n,1H:"2N",1P:{dS:{1c:1y.dS,1H:"l4"},nj:{2n:"aj"},dX:{1c:1y.dX,1H:"lc"},ea:{1c:1y.ea,1H:"la"},ec:{1c:1y.ec,1H:"kT"},e3:{1c:1y.e3,1H:"kR"},nn:{2n:"aj"},dD:{1c:1y.dD,1H:"l8"},d9:{1c:1y.d9,1H:"d8"},nm:{2n:"aj"},cY:{1c:1y.cY,1H:"kW"},cX:{1c:1y.cX,1H:"kV"},cW:{1c:1y.cW,1H:"l6"}}},1s:{1c:1y.1s,1H:"2N",1P:{1s:{1c:1y.eJ,1H:"an"},67:{1c:1y.67,2p:"67"}}},9Y:{1c:1y.9Y,1H:"2N",1P:{dv:{1c:1y.du,1H:"dQ"},dx:{1c:1y.ds,1H:"dG"},dk:{1c:1y.dj,1H:"dT"},dK:{1c:1y.ei,1H:"dL"}}},dv:{1c:1y.du,1H:"dQ"},dx:{1c:1y.ds,1H:"dG"},dk:{1c:1y.dj,1H:"dT"},nk:{1c:1y.ei,1H:"dL"},8b:{2p:"fX",1c:1y.8b}}},1e:B(1G,6t,1a){q 1e=c.C[1G+"mQ"];if($.72(1e)){if(6t===E){F 1e.5B(c,1a)}I{F 1e.5B(c,6t,1a)}}I{F 1a}},mo:B(){gf(c.9N);$(48).3z(".U");c.$1v.3z("U-5R");c.$2g.3z(".U").mn("U");q o=c.2R();if(c.C.6z){c.$2A.2E(c.$1v);c.$2A.1w();c.$1v.1p(o).2N()}I{q $1B=c.$K;if(c.C.1Q){$1B=c.$2g}c.$2A.2E($1B);c.$2A.1w();$1B.3e("4e").3e("fP").2C("3F").o(o).2N()}if(c.C.8P){$(c.C.8P).o("")}if(c.C.3d){$("#gr"+c.7z).1w()}},mm:B(){F $.4H({},c)},ml:B(){F c.$K},mq:B(){F c.$2A},mj:B(){F(c.C.1Q)?c.$2Z:E},mc:B(){F(c.$1A)?c.$1A:E},2R:B(){F c.$1v.1p()},l1:B(){c.$K.2C("3F").2C("6m");q o=c.49(c.$2Z.1W().4k());c.$K.1i({3F:N,6m:c.C.63});F o},7x:B(o,a9,eC){o=o.3T();o=o.G(/\\$/g,"&#36;");if(c.C.4j){c.kX(o)}I{c.kS(o,a9)}if(o==""){eC=E}if(eC!==E){c.fB()}},kS:B(o,a9){if(a9!==E){o=c.bt(o);o=c.7S(o);o=c.de(o);o=c.ax(o,N);if(c.C.1N===E){o=c.db(o)}I{o=o.G(/<p(.*?)>([\\w\\W]*?)<\\/p>/gi,"$2<br>")}}o=o.G(/&ab;#36;/g,"$");o=c.e5(o);c.$K.o(o);c.7J();c.aN();c.1j()},kX:B(o){q 3y=c.bm();c.$2Z[0].3l="me:mf";o=c.de(o);o=c.ax(o);o=c.79(o);3y.b1();3y.gw(o);3y.g5();if(c.C.4j){c.$K=c.$2Z.1W().1b("2v").1i({3F:N,6m:c.C.63})}c.7J();c.aN();c.1j()},cz:B(o){c.7A=o.1S(/^<\\!l3[^>]*>/i);if(c.7A&&c.7A.1m==1){o=o.G(/^<\\!l3[^>]*>/i,"")}o=c.bt(o,N);o=c.db(o);o=c.e5(o);c.$K.o(o);c.7J();c.aN();c.1j()},k1:B(){if(c.7A&&c.7A.1m==1){q 1v=c.7A[0]+"\\n"+c.$1v.1p();c.$1v.1p(1v)}},aN:B(){q 93=c.$K.1b("V");q 6G="4o";$.1u(93,B(){q au=c.ka;q 5Q=2a 2L("<"+c.Q,"gi");q 5A=au.G(5Q,"<"+6G);5Q=2a 2L("</"+c.Q,"gi");5A=5A.G(5Q,"</"+6G);$(c).2f(5A)})},aV:B(o){o=o.G(/<V(.*?)>/,"<4o$1>");F o.G(/<\\/V>/,"</4o>")},7J:B(){c.$K.1b(".mi").1i("3F",E)},1j:B(e){q o="";c.gy();if(c.C.4j){o=c.l1()}I{o=c.$K.o()}o=c.bF(o);o=c.cb(o);q 1v=c.79(c.$1v.1p(),E);q K=c.79(o,E);if(1v==K){F E}o=o.G(/<\\/li><(2q|ol)>([\\w\\W]*?)<\\/(2q|ol)>/gi,"<$1>$2</$1></li>");if($.28(o)==="<br>"){o=""}if(c.C.jU){q jT=["br","hr","1z","1s","2S","4E"];$.1u(jT,B(i,s){o=o.G(2a 2L("<"+s+"(.*?[^/$]?)>","gi"),"<"+s+"$1 />")})}o=c.1e("mu",E,o);c.$1v.1p(o);c.k1();c.1e("mv",E,o);if(c.2u===E){if(1E e!="1I"){mK(e.6h){9O 37:8h;9O 38:8h;9O 39:8h;9O 40:8h;mJ:c.1e("5W",E,o)}}I{c.1e("5W",E,o)}}},bF:B(o){if(!c.C.4j){o=c.7S(o)}o=$.28(o);o=c.fS(o);o=o.G(/&#aY;/gi,"");o=o.G(/&#mI;/gi,"");o=o.G(/<\\/a>&3u;/gi,"</a> ");o=o.G(/\\7m/g,"");if(o=="<p></p>"||o=="<p> </p>"||o=="<p>&3u;</p>"){o=""}if(c.C.jE){o=o.G(/<a(.*?)4h="jI"(.*?)>/gi,"<a$1$2>");o=o.G(/<a(.*?)>/gi,\'<a$1 4h="jI">\')}o=o.G("<!--?4P","<?4P");o=o.G("?-->","?>");o=o.G(/<(.*?)1x="k4"(.*?) 3F="E"(.*?)>/gi,\'<$mL="k4"$2$3>\');o=o.G(/ 1a-7Z=""/gi,"");o=o.G(/<br\\s?\\/?>\\n?<\\/(P|H[1-6]|3f|b2|bi|b6|b8|am|ao)>/gi,"</$1>");o=o.G(/<V(.*?)id="U-T-2A"(.*?)>([\\w\\W]*?)<1z(.*?)><\\/V>/gi,"$3<1z$4>");o=o.G(/<V(.*?)id="U-T-da"(.*?)>(.*?)<\\/V>/gi,"");o=o.G(/<V(.*?)id="U-T-d6"(.*?)>(.*?)<\\/V>/gi,"");o=o.G(/<(2q|ol)>\\s*\\t*\\n*<\\/(2q|ol)>/gi,"");if(c.C.bV){o=o.G(/<2F(.*?)>([\\w\\W]*?)<\\/2F>/gi,"$2")}o=o.G(/<V(.*?)>([\\w\\W]*?)<\\/V>/gi,"$2");o=o.G(/<4o>/gi,"<V>");o=o.G(/<4o /gi,"<V ");o=o.G(/<\\/4o>/gi,"</V>");o=o.G(/<V(.*?)1x="57"(.*?)>([\\w\\W]*?)<\\/V>/gi,"");o=o.G(/<1z(.*?)3F="E"(.*?)>/gi,"<1z$1$2>");o=o.G(/&/gi,"&");o=o.G(/\\mM/gi,"&mP;");o=o.G(/\\mO/gi,"&mN;");o=o.G(/\\mG/gi,"&mF;");o=o.G(/\\mz/gi,"&my;");o=o.G(/\\mx/gi,"&mw;");o=c.gM(o);F o},ks:B(){c.3I="";c.$2A=$(\'<12 1x="mE" />\');if(c.$1v[0].Q==="mD"){c.C.6z=N}if(c.C.kq===E&&c.5c()){c.k9()}I{c.k6();if(c.C.1Q){c.C.4K=E;c.5n()}I{if(c.C.6z){c.kb()}I{c.kg()}}if(!c.C.1Q){c.cK();c.cm()}}},k9:B(){if(!c.C.6z){c.$K=c.$1v;c.$K.2T();c.$1v=c.b0(c.$K);c.$1v.1p(c.3I)}c.$2A.aG(c.$1v).1h(c.$1v)},k6:B(){if(c.C.6z){c.3I=$.28(c.$1v.1p())}I{c.3I=$.28(c.$1v.o())}},kb:B(){c.$K=$("<12 />");c.$2A.aG(c.$1v).1h(c.$K).1h(c.$1v);c.kd(c.$K);c.ci()},kg:B(){c.$K=c.$1v;c.$1v=c.b0(c.$K);c.$2A.aG(c.$K).1h(c.$K).1h(c.$1v);c.ci()},b0:B($1v){F $("<5R />").1i("2n",$1v.1i("id")).1f("21",c.aF)},kd:B(el){$.1u(c.$1v.2R(0).2U.4i(/\\s+/),B(i,s){el.2y("nh"+s)})},ci:B(){c.$K.2y("4e").1i({3F:N,6m:c.C.63});c.$1v.1i("6m",c.C.63).2T();c.7x(c.3I,N,E)},cK:B(){q $1v=c.$K;if(c.C.1Q){$1v=c.$2Z}if(c.C.7y){$1v.1i("7y",c.C.7y)}if(c.C.7R){$1v.1f("fO-21",c.C.7R+"px")}I{if(c.1C("3c")&&c.C.1N){c.$K.1f("fO-21","mC")}}if(c.1C("3c")&&c.C.1N){c.$K.1f("95-i0","9a")}if(c.C.ch){c.C.4K=E;c.aF=c.C.ch}if(c.C.fI){c.$K.2y("fP")}if(c.C.8u){c.$K.2y("U-K-8u")}if(!c.C.4K){$1v.1f("21",c.aF)}},cm:B(){c.2u=E;if(c.C.1A){c.C.1A=c.fQ(c.C.1F);c.g6()}c.iu();c.gW();c.cU();if(c.C.7p){c.7p()}2W($.M(c.7D,c),4);if(c.1C("3c")){bK{c.X.22("mB",E,E);c.X.22("mA",E,E)}bI(e){}}if(c.C.2j){2W($.M(c.2j,c),3P)}if(!c.C.5L){2W($.M(B(){c.C.5L=N;c.cA(E)},c),5l)}c.1e("7B")},cU:B(){c.89=0;if(c.C.fr&&(c.C.3Z!==E||c.C.5p!==E)){c.$K.on("6y.U",$.M(c.g2,c))}c.$K.on("25.U",$.M(B(){c.5O=E},c));c.$K.on("2S.U",$.M(c.1j,c));c.$K.on("aW.U",$.M(c.gc,c));c.$K.on("5y.U",$.M(c.g9,c));c.$K.on("58.U",$.M(c.fs,c));if($.72(c.C.fi)){c.$1v.on("5y.U-5R",$.M(c.C.fi,c))}if($.72(c.C.fy)){c.$K.on("2j.U",$.M(c.C.fy,c))}q aq;$(X).8U(B(e){aq=$(e.1O)});c.$K.on("bO.U",$.M(B(e){if(!$(aq).3v("bs")&&$(aq).8y(".bs").1U()==0){c.5O=E;if($.72(c.C.mH)){c.1e("bO",e)}}},c))},g2:B(e){e=e.go||e;if(48.eP===1I||!e.8T){F N}q 1m=e.8T.7d.1m;if(1m==0){F N}e.2x();q 26=e.8T.7d[0];if(c.C.cB!==E&&c.C.cB.3W(26.1G)==-1){F N}c.1Z();c.8I();if(c.C.5p===E){c.ey(c.C.3Z,26,N,e,c.C.ba)}I{c.e2(26)}},gc:B(e){q bn=E;if(c.1C("4C")&&6k.7i.3W("mg")===-1){q 2M=c.1C("9y").4i(".");if(2M[0]<mh){bn=N}}if(bn){F N}if(c.1C("8S")){F N}if(c.C.9l&&c.gv(e)){F N}if(c.C.gl){c.85=N;c.2e();if(!c.5O){if(c.C.4K===N&&c.cl!==N){c.$K.21(c.$K.21());c.aX=c.X.2v.3g}I{c.aX=c.$K.3g()}}q 4f=c.cE();2W($.M(B(){q gE=c.cE();c.$K.1h(4f);c.1Y();q o=c.hR(gE);c.kk(o);if(c.C.4K===N&&c.cl!==N){c.$K.1f("21","4d")}},c),1)}},gv:B(e){q 6t=e.go||e;c.gm=E;if(1E(6t.eL)==="1I"){F E}if(6t.eL.gz){q 26=6t.eL.gz[0].ma();if(26!==2J){c.1Z();c.gm=N;q es=2a mb();es.jt=$.M(c.jK,c);es.md(26);F N}}F E},g9:B(e){if(c.85){F E}q 1k=e.6h;q 3k=e.aw||e.8K;q L=c.2B();q 1r=c.3S();q 1l=c.2P();q 2r=E;c.1e("5y",e);if(c.1C("3c")&&"ej"in 48.29()){if((3k)&&(e.2O===37||e.2O===39)){q 1J=c.29();q ex=(e.8K?"9v":"mk");if(e.2O===37){1J.ej("4H","1t",ex);if(!e.5d){1J.he()}}if(e.2O===39){1J.ej("4H","4I",ex);if(!e.5d){1J.mr()}}e.2x()}}c.62(E);if((L&&$(L).2R(0).Q==="6w")||(1r&&$(1r).2R(0).Q==="6w")){2r=N;if(1k===c.2O.ev){c.6Q(1l)}}if(1k===c.2O.ev){if(L&&$(L)[0].Q==="3L"){c.6Q(L)}if(1r&&$(1r)[0].Q==="3L"){c.6Q(1r)}if(L&&$(L)[0].Q==="P"&&$(L).L()[0].Q=="3L"){c.6Q(L,$(L).L()[0])}if(1r&&$(1r)[0].Q==="P"&&L&&$(L)[0].Q=="3L"){c.6Q(1r,L)}}c.6p(e,1k);if(3k&&1k===90&&!e.5d&&!e.gh){e.2x();if(c.C.3J.1m){c.jN()}I{c.X.22("mt",E,E)}F}I{if(3k&&1k===90&&e.5d&&!e.gh){e.2x();if(c.C.7Y.1m!=0){c.jO()}I{c.X.22("mp",E,E)}F}}if(1k==32){c.1Z()}if(3k&&1k===65){c.1Z();c.5O=N}I{if(1k!=c.2O.gd&&!3k){c.5O=E}}if(1k==c.2O.b3&&!e.5d&&!e.aw&&!e.8K){q O=c.3m();if(O&&O.4M===E){1q=c.29();if(1q.51){O.ay()}}if(c.1C("3t")&&(L.4v==1&&(L.Q=="6i"||L.Q=="mR"))){e.2x();c.1Z();c.3A(X.4y("br"));c.1e("6E",e);F E}if(1l&&(1l.Q=="3L"||$(1l).L()[0].Q=="3L")){if(c.cR()){if(c.89==1){q 2g;q 2X;if(1l.Q=="3L"){2X="br";2g=1l}I{2X="p";2g=$(1l).L()[0]}e.2x();c.cf(2g);c.89=0;if(2X=="p"){$(1l).L().1b("p").2X().1w()}I{q 2H=$.28($(1l).o());$(1l).o(2H.G(/<br\\s?\\/?>$/i,""))}F}I{c.89++}}I{c.89++}}if(2r===N){F c.fT(e,1r)}I{if(!c.C.1N){if(1l&&1l.Q=="3f"){q 5E=c.2P();if(5E!==E||5E.Q==="3f"){q aC=$.28($(1l).Y());q gN=$.28($(5E).Y());if(aC==""&&gN==""&&$(5E).4l("li").1U()==0&&$(5E).8y("li").1U()==0){c.1Z();q $30=$(5E).2c("ol, 2q");$(5E).1w();q J=$("<p>"+c.C.2i+"</p>");$30.2E(J);c.4b(J);c.1j();c.1e("6E",e);F E}}}if(1l&&c.C.dJ.4c(1l.Q)){c.1Z();2W($.M(B(){q 55=c.2P();if(55.Q==="8J"&&!$(55).3v("4e")){q J=$("<p>"+c.C.2i+"</p>");$(55).2f(J);c.4b(J)}},c),1)}I{if(1l===E){c.1Z();q J=$("<p>"+c.C.2i+"</p>");c.3A(J[0]);c.4b(J);c.1e("6E",e);F E}}}if(c.C.1N){if(1l&&c.C.dJ.4c(1l.Q)){c.1Z();2W($.M(B(){q 55=c.2P();if((55.Q==="8J"||55.Q==="P")&&!$(55).3v("4e")){c.ko(55)}},c),1)}I{F c.ef(e)}}if(1l.Q=="3L"||1l.Q=="dP"){F c.ef(e)}}c.1e("6E",e)}I{if(1k===c.2O.b3&&(e.aw||e.5d)){c.1Z();e.2x();c.aD()}}if((1k===c.2O.fF||e.8K&&1k===gH)&&c.C.6p){F c.fx(e,2r,1k)}if(1k===c.2O.9D){c.fW(e,1r,L)}},fT:B(e,1r){e.2x();c.1Z();q o=$(1r).L().Y();c.3A(X.8d("\\n"));if(o.4G(/\\s$/)==-1){c.3A(X.8d("\\n"))}c.1j();c.1e("6E",e);F E},fx:B(e,2r,1k){if(!c.C.fv){F N}if(c.a4(c.2R())&&c.C.ag===E){F N}e.2x();if(2r===N&&!e.5d){c.1Z();c.3A(X.8d("\\t"));c.1j();F E}I{if(c.C.ag!==E){c.1Z();c.3A(X.8d(fk(c.C.ag+1).5U("\\nz")));c.1j();F E}I{if(!e.5d){c.dr()}I{c.dz()}}}F E},fW:B(e,1r,L){if(L&&1r&&L.4Q.Q=="6i"&&L.Q=="fJ"&&1r.Q=="3f"&&$(L).4k("li").1U()==1){q Y=$(1r).Y().G(/[\\7m-\\hk\\hm]/g,"");if(Y==""){q J=L.4Q;$(L).1w();c.4b(J);c.1j();F E}}if(1E 1r.Q!=="1I"&&/^(H[1-6])$/i.4c(1r.Q)){q J;if(c.C.1N===E){J=$("<p>"+c.C.2i+"</p>")}I{J=$("<br>"+c.C.2i)}$(1r).2f(J);c.4b(J);c.1j()}if(1E 1r.aZ!=="1I"&&1r.aZ!==2J){if(1r.1w&&1r.4v===3&&1r.aZ.1S(/[^\\7m]/g)==2J){$(1r).4Z().1w();c.1j()}}},ef:B(e){c.1Z();e.2x();c.aD();c.1e("6E",e);F},fs:B(e){if(c.85){F E}q 1k=e.6h;q L=c.2B();q 1r=c.3S();if(!c.C.1N&&1r.4v==3&&(L==E||L.Q=="bp")){q J=$("<p>").1h($(1r).6f());$(1r).2f(J);q 4l=$(J).4l();if(1E(4l[0])!=="1I"&&4l[0].Q=="cP"){4l.1w()}c.7e(J)}if((c.C.6P||c.C.68||c.C.6O)&&1k===c.2O.b3){c.gS()}if(1k===c.2O.d3||1k===c.2O.9D){F c.gX(e)}c.1e("58",e);c.1j(e)},gS:B(){c.c6(c.C.7I,c.C.6P,c.C.68,c.C.6O,c.C.6N);2W($.M(B(){if(c.C.68){c.4F()}if(c.C.5F){c.5F()}},c),5)},gW:B(){if(!c.C.e6){F}$.1u(c.C.e6,$.M(B(i,s){if(df[s]){$.4H(c,df[s]);if($.72(df[s].7B)){c.7B()}}},c))},5n:B(){c.gu();if(c.C.6z){c.fg(c.$1v)}I{c.$eZ=c.$1v.2T();c.$1v=c.b0(c.$eZ);c.fg(c.$eZ)}},fg:B(el){c.$1v.1i("6m",c.C.63).2T();c.$2A.aG(el).1h(c.$2Z).1h(c.$1v)},gu:B(){c.$2Z=$(\'<1Q 1o="2l: 3P%;" c2="0" />\').8Z("iJ",$.M(B(){if(c.C.4j){c.bm();if(c.3I===""){c.3I=c.C.2i}c.$2Z.1W()[0].gw(c.3I);c.$2Z.1W()[0].g5();q gG=hc($.M(B(){if(c.$2Z.1W().1b("2v").o()){gf(gG);c.cG()}},c),0)}I{c.cG()}},c))},bM:B(){F c.$2Z[0].dp.X},bm:B(){q 3y=c.bM();if(3y.9L){3y.lM(3y.9L)}F 3y},cI:B(1f){1f=1f||c.C.1f;if(c.iE(1f)){c.$2Z.1W().1b("aK").1h(\'<1s 4h="ly" 1R="\'+1f+\'" />\')}if($.lP(1f)){$.1u(1f,$.M(B(i,1L){c.cI(1L)},c))}},cG:B(){c.$K=c.$2Z.1W().1b("2v").1i({3F:N,6m:c.C.63});if(c.$K[0]){c.X=c.$K[0].lh;c.48=c.X.lq||48}c.cI();if(c.C.4j){c.cz(c.$1v.1p())}I{c.7x(c.3I,N,E)}c.cK();c.cm()},gV:B(){if(c.C.4R!==E){c.bQ=c.C.4R;c.C.4R=N}I{if(1E c.$2g.1i("4R")=="1I"||c.$2g.1i("4R")==""){c.C.4R=E}I{c.bQ=c.$2g.1i("4R");c.C.4R=N}}},gO:B(o){if(c.C.4R===E){F E}if(c.a4(o)){c.C.2j=E;c.c3();c.c1();F c.c4()}I{c.c1()}F E},c3:B(){c.$K.on("2j.57",$.M(c.fp,c))},c1:B(){c.$K.on("bO.57",$.M(c.fM,c))},c4:B(){q ph=$(\'<V 1x="57">\').1a("U","8R").1i("3F",E).Y(c.bQ);if(c.C.1N===E){F $("<p>").1h(ph)}I{F ph}},fM:B(){q o=c.2R();if(c.a4(o)){c.c3();c.$K.o(c.c4())}},fp:B(){c.$K.1b("V.57").1w();q o="";if(c.C.1N===E){o=c.C.5a}c.$K.3z("2j.57");c.$K.o(o);if(c.C.1N===E){c.4b(c.$K.4k()[0])}I{c.2j()}c.1j()},fB:B(){c.$K.1b("V.57").1w();c.$K.3z("2j.57")},fS:B(o){F o.G(/<V 1x="57"(.*?)>(.*?)<\\/V>/i,"")},6p:B(e,1k){if(!c.C.6p){if((e.aw||e.8K)&&(1k===66||1k===73)){e.2x()}F E}$.1u(c.C.6p,$.M(B(4D,g1){q 52=4D.4i(",");3w(q i in 52){if(1E 52[i]==="8O"){c.fj(e,$.28(52[i]),$.M(B(){lQ(g1)},c))}}},c))},fj:B(e,52,hg){q gK={8:"lG",9:"53",10:"F",13:"F",16:"8E",17:"3k",18:"87",19:"m7",20:"m0",27:"lV",32:"6s",33:"lU",34:"lT",35:"3p",36:"lW",37:"1t",38:"lX",39:"4I",40:"lZ",45:"4u",46:"56",59:";",61:"=",96:"0",97:"1",98:"2",99:"3",3P:"4",lY:"5",lx:"6",lk:"7",lm:"8",lz:"9",lv:"*",lo:"+",ms:"-",oz:".",qG:"/",qF:"f1",qH:"f2",qI:"f3",qJ:"f4",qE:"f5",qD:"f6",qy:"f7",qx:"f8",qz:"f9",qA:"qC",qB:"qK",qL:"qV",qU:"qW",qX:"gn",qY:"-",qT:";",qS:"=",qN:",",qM:"-",qO:".",qP:"/",qR:"`",gH:"[",qQ:"\\\\",qw:"]",qv:"\'"};q cr={"`":"~","1":"!","2":"@","3":"#","4":"$","5":"%","6":"^","7":"&","8":"*","9":"(","0":")","-":"r0","=":"+",";":": ","\'":\'"\',",":"<",".":">","/":"?","\\\\":"|"};52=52.3N().4i(" ");q 8X=gK[e.2O],8C=4S.qb(e.6h).3N(),7w="",74={};$.1u(["87","3k","4E","8E"],B(2I,8V){if(e[8V+"qd"]&&8X!==8V){7w+=8V+"+"}});if(8X){74[7w+8X]=N}if(8C){74[7w+8C]=N;74[7w+cr[8C]]=N;if(7w==="8E+"){74[cr[8C]]=N}}3w(q i=0,l=52.1m;i<l;i++){if(74[52[i]]){e.2x();F hg.cx(c,hj)}}},2j:B(){if(!c.1C("8S")){c.48.2W($.M(c.9E,c,N),1)}I{c.$K.2j()}},4m:B(){if(c.1C("3t")){q 1T=c.X.9L.3g}c.$K.2j();if(c.1C("3t")){c.X.9L.3g=1T}},bX:B(){if(!c.1C("3c")){c.9E()}I{if(c.C.1N===E){q 2X=c.$K.4k().2X();c.$K.2j();c.7e(2X)}I{c.9E()}}},9E:B(5I,2g){c.$K.2j();if(1E 2g=="1I"){2g=c.$K[0]}q O=c.3m();O.9K(2g);O.5I(5I||E);q 1q=c.29();1q.4O();1q.5e(O)},cA:B(9Q){if(c.C.5L){c.h8(9Q)}I{c.hl()}},hl:B(){q o=c.$1v.2T().1p();if(1E c.6n!=="1I"){q 6n=c.6n.G(/\\n/g,"");q 7f=o.G(/\\n/g,"");7f=c.79(7f,E);c.6n=c.79(6n,E)!==7f}if(c.6n){if(c.C.4j&&o===""){c.cz(o)}I{c.7x(o);if(c.C.4j){c.cU()}}c.1e("5W",E,o)}if(c.C.1Q){c.$2Z.2N()}I{c.$K.2N()}if(c.C.4j){c.$K.1i("3F",N)}c.$1v.3z("5y.U-5R-h9");c.$K.2j();c.1Y();c.7D();c.gT();c.dW("o");c.C.5L=N},h8:B(9Q){if(9Q!==E){c.2e()}q 21=2J;if(c.C.1Q){21=c.$2Z.21();if(c.C.4j){c.$K.2C("3F")}c.$2Z.2T()}I{21=c.$K.gZ();c.$K.2T()}q o=c.$1v.1p();if(o!==""&&c.C.h0){c.$1v.1p(c.gF(o))}c.6n=o;c.$1v.21(21).2N().2j();c.$1v.on("5y.U-5R-h9",c.ha);c.hn();c.6M("o");c.C.5L=E},ha:B(e){if(e.2O===9){q $el=$(c);q 2u=$el.2R(0).4b;$el.1p($el.1p().a8(0,2u)+"\\t"+$el.1p().a8($el.2R(0).7e));$el.2R(0).4b=$el.2R(0).7e=2u+1;F E}},7p:B(){q bE=E;c.9N=hc($.M(B(){q o=c.2R();if(bE!==o){q 2n=c.$1v.1i("2n");$.j2({1L:c.C.7p,1G:"7X",1a:"2n="+2n+"&"+2n+"="+qe(qf(o)),4n:$.M(B(1a){q 1K=$.8G(1a);if(1E 1K.3V=="1I"){c.1e("7p",E,1K)}I{c.1e("qa",E,1K)}bE=o},c)})}},c),c.C.9N*q9)},g6:B(){if(c.5c()&&c.C.bz.1m>0){$.1u(c.C.bz,$.M(B(i,s){q 2I=c.C.42.3W(s);c.C.42.9r(2I,1)},c))}if(c.C.3d){c.C.42=c.C.g8}I{if(!c.C.gA){q 2I=c.C.42.3W("o");c.C.42.9r(2I,1)}}if(c.C.1A){$.1u(c.C.1A.6c.1P,$.M(B(i,s){if($.4L(i,c.C.gB)=="-1"){d2 c.C.1A.6c.1P[i]}},c))}if(c.C.42.1m===0){F E}c.gb();c.$1A=$("<2q>").2y("bs").1i("id","q4"+c.7z);if(c.C.8u){c.$1A.2y("U-1A-8u")}if(c.C.gq&&c.5c()){c.$1A.2y("U-1A-af")}if(c.C.3d){c.$3d=$(\'<12 1x="gg">\').1i("id","gr"+c.7z).2T();c.$3d.1h(c.$1A);$("2v").1h(c.$3d)}I{if(c.C.8P){c.$1A.2y("U-1A-q3");$(c.C.8P).o(c.$1A)}I{c.$2A.6v(c.$1A)}}$.1u(c.C.42,$.M(B(i,2z){if(c.C.1A[2z]){q 2d=c.C.1A[2z];if(c.C.7N===E&&2z==="26"){F N}c.$1A.1h($("<li>").1h(c.6X(2z,2d)))}},c));c.$1A.1b("a").1i("7y","-1");if(c.C.64){c.cV();$(c.C.8w).on("gn.U",$.M(c.cV,c))}if(c.C.aB){c.$K.on("9x.U 58.U",$.M(c.7K,c))}},cV:B(){q 3g=$(c.C.8w).3g();q 8v=0;q 1t=0;q 3p=0;if(c.C.8w===X){8v=c.$2A.2V().1T}I{8v=1}3p=8v+c.$2A.21()+40;if(3g>8v){q 2l="3P%";if(c.C.b4){1t=c.$2A.2V().1t;2l=c.$2A.hd();c.$1A.2y("g7")}c.64=N;if(c.C.8w===X){c.$1A.1f({3j:"9w",2l:2l,8Q:gp,1T:c.C.a3+"px",1t:1t})}I{c.$1A.1f({3j:"8B",2l:2l,8Q:gp,1T:(c.C.a3+3g)+"px",1t:0})}if(3g<3p){c.$1A.1f("gx","j7")}I{c.$1A.1f("gx","8F")}}I{c.64=E;c.$1A.1f({3j:"ia",2l:"4d",1T:0,1t:1t});if(c.C.b4){c.$1A.3e("g7")}}},gb:B(){if(!c.C.3d){F}c.$K.on("9x.U 58.U",c,$.M(B(e){q Y=c.9V();if(e.1G==="9x"&&Y!=""){c.eV(e)}if(e.1G==="58"&&e.5d&&Y!=""){q $fc=$(c.7U(c.29().q5)),2V=$fc.2V();2V.21=$fc.21();c.eV(2V,N)}},c))},eV:B(e,hb){if(!c.C.3d){F}q 1t,1T;$(".gg").2T();if(hb){1t=e.1t;1T=e.1T+e.21+14;if(c.C.1Q){1T+=c.$2A.3j().1T-$(c.X).3g();1t+=c.$2A.3j().1t}}I{q 2l=c.$3d.hd();1t=e.k5;if($(c.X).2l()<(1t+2l)){1t-=2l}1T=e.k7+14;if(c.C.1Q){1T+=c.$2A.3j().1T;1t+=c.$2A.3j().1t}I{1T+=$(c.X).3g()}}c.$3d.1f({1t:1t+"px",1T:1T+"px"}).2N();c.h7()},h7:B(){if(!c.C.3d){F}q 7r=$.M(B(3y){$(3y).on("8U.U",$.M(B(e){if($(e.1O).2c(c.$1A).1m===0){c.$3d.6J(3P);c.cn();$(3y).3z(e)}},c)).on("5y.U",$.M(B(e){if(e.6h===c.2O.eA){c.29().he()}c.$3d.6J(3P);$(3y).3z(e)},c))},c);7r(X);if(c.C.1Q){7r(c.X)}},9h:B(){if(!c.C.3d){F}q 7r=$.M(B(3y){$(3y).on("jB.U",$.M(B(e){if($(e.1O).2c(c.$1A).1m===0){c.$3d.6J(3P);$(3y).3z(e)}},c))},c);7r(X);if(c.C.1Q){7r(c.X)}},gU:B($1P,hf){$.1u(hf,$.M(B(2z,2d){if(!2d.2U){2d.2U=""}q $76;if(2d.2n==="aj"){$76=$(\'<a 1x="q6">\')}I{$76=$(\'<a 1R="#" 1x="\'+2d.2U+" q8"+2z+\'">\'+2d.1c+"</a>");$76.on("25",$.M(B(e){if(e.2x){e.2x()}if(c.1C("3t")){e.gP=E}if(2d.1e){2d.1e.5B(c,2z,$76,2d,e)}if(2d.2p){c.22(2d.2p,2z)}if(2d.1H){c[2d.1H](2z)}c.7K();if(c.C.3d){c.$3d.6J(3P)}},c))}$1P.1h($76)},c))},e8:B(e,1k){if(!c.C.5L){e.2x();F E}q $1D=c.4s(1k);q $1P=$1D.1a("1P").8D(X.2v);if($1D.3v("7u")){c.9t()}I{c.9t();c.1e("e8",{1P:$1P,1k:1k,1D:$1D});c.6M(1k);$1D.2y("7u");q 8H=$1D.2V();q dq=$1P.2l();if((8H.1t+dq)>$(X).2l()){8H.1t-=dq}q 1t=8H.1t+"px";q dc=$1D.gZ();q 3j="8B";q 1T=(dc+c.C.a3)+"px";if(c.C.64&&c.64){3j="9w"}I{1T=8H.1T+dc+"px"}$1P.1f({3j:3j,1t:1t,1T:1T}).2N();c.1e("q7",{1P:$1P,1k:1k,1D:$1D})}q 8W=$.M(B(e){c.e1(e,$1P)},c);$(X).8Z("25",8W);c.$K.8Z("25",8W);c.$K.8Z("qg",8W);e.qh();c.4m()},9t:B(){c.$1A.1b("a.7u").3e("8t").3e("7u");$(".gQ").2T();c.1e("e1")},e1:B(e,$1P){if(!$(e.1O).3v("7u")){$1P.3e("7u");c.9t()}},6X:B(2z,2d,gJ){q $1D=$(\'<a 1R="9I:;" 1c="\'+2d.1c+\'" 7y="-1" 1x="re-b5 re-\'+2z+\'"></a>\');if(1E gJ!="1I"){$1D.2y("U-2b-T")}$1D.on("25",$.M(B(e){if(e.2x){e.2x()}if(c.1C("3t")){e.gP=E}if($1D.3v("dm")){F E}if(c.7H()===E&&!2d.2p){c.4m()}if(2d.2p){c.4m();c.22(2d.2p,2z);c.9h()}I{if(2d.1H&&2d.1H!=="2N"){c[2d.1H](2z);c.9h()}I{if(2d.1e){2d.1e.5B(c,2z,$1D,2d,e);c.9h()}I{if(2d.1P){c.e8(e,2z)}}}}c.7K(E,2z)},c));if(2d.1P){q $1P=$(\'<12 1x="gQ qr\'+2z+\'" 1o="3i: 3o;">\');$1D.1a("1P",$1P);c.gU($1P,2d.1P)}F $1D},4s:B(1k){if(!c.C.1A){F E}F $(c.$1A.1b("a.re-"+1k))},qq:B(dV,Q){c.C.aB.3a(dV);c.C.eF[Q]=dV},fV:B(1k){q 2b=c.4s(1k);if(2b.3v("8t")){c.dW(1k)}I{c.6M(1k)}},6M:B(1k){q 2b=c.4s(1k);2b.2y("8t")},dW:B(1k){q 2b=c.4s(1k);2b.3e("8t")},fK:B(2z){c.$1A.1b("a.re-b5").ar(".re-"+2z).3e("8t")},gT:B(){c.$1A.1b("a.re-b5").ar("a.re-o").3e("dm")},hn:B(){c.$1A.1b("a.re-b5").ar("a.re-o").2y("dm")},qs:B(1k,b7){c.4s(1k).2y("re-"+b7)},qt:B(1k,b7){c.4s(1k).3e("re-"+b7)},qu:B(1k,2n){q 1D=c.4s(1k);1D.3e("U-2b-T");1D.2y("fa-U-2b");1D.o(\'<i 1x="fa \'+2n+\'"></i>\')},qp:B(1k,1c,1e,1P){if(!c.C.1A){F}q 2b=c.6X(1k,{1c:1c,1e:1e,1P:1P},N);c.$1A.1h($("<li>").1h(2b));F 2b},qo:B(1k,1c,1e,1P){if(!c.C.1A){F}q 2b=c.6X(1k,{1c:1c,1e:1e,1P:1P},N);c.$1A.6v($("<li>").1h(2b))},qj:B(g0,1k,1c,1e,1P){if(!c.C.1A){F}q 2b=c.6X(1k,{1c:1c,1e:1e,1P:1P},N);q $2b=c.4s(g0);if($2b.1U()!==0){$2b.L().2E($("<li>").1h(2b))}I{c.$1A.1h($("<li>").1h(2b))}F 2b},qi:B(fl,1k,1c,1e,1P){if(!c.C.1A){F}q 2b=c.6X(1k,{1c:1c,1e:1e,1P:1P},N);q $2b=c.4s(fl);if($2b.1U()!==0){$2b.L().3M($("<li>").1h(2b))}I{c.$1A.1h($("<li>").1h(2b))}F 2b},qk:B(1k){q $2b=c.4s(1k);$2b.1w()},7K:B(e,2z){q L=c.2B();c.fK(2z);if(e===E&&2z!=="o"){if($.4L(2z,c.C.aB)!=-1){c.fV(2z)}F}if(L&&L.Q==="A"){c.$1A.1b("a.fq").Y(c.C.1F.fU)}I{c.$1A.1b("a.fq").Y(c.C.1F.eJ)}$.1u(c.C.eF,$.M(B(1k,2t){if($(L).2c(1k,c.$K.2R()[0]).1m!=0){c.6M(2t)}},c));q $L=$(L).2c(c.C.5r.3T().3N(),c.$K[0]);if($L.1m){q 54=$L.1f("Y-54");if(54==""){54="1t"}c.6M("54"+54)}},bb:B(o){q 1q=c.29();if(1q.41&&1q.51){q O=c.3m();O.ay();q el=c.X.4y("12");el.3O=o;q 4f=c.X.cO(),J,5N;3x((J=el.8z)){5N=4f.7c(J)}q ql=4f.8z;O.3A(4f);if(5N){O=O.9p();O.9d(5N);O.5I(N)}1q.4O();1q.5e(O)}},2p:B(23,2K,1j){if(23==="c0"&&c.1C("3t")){2K="<"+2K+">"}if(23==="5b"&&c.1C("3t")){if(!c.8N()){c.4m();c.X.1J.5z().cd(2K)}I{c.bb(2K)}}I{c.X.22(23,E,2K)}if(1j!==E){c.1j()}c.1e("22",23,2K)},22:B(23,2K,1j){if(!c.C.5L){c.$1v.2j();F E}if(23==="3q"||23==="3r"||23==="4W"||23==="fC"){c.1Z()}if(23==="fh"||23==="fo"){q L=c.2B();if(L.Q==="qn"||L.Q==="qm"){c.9P(L)}}if(23==="5b"){c.9m(2K,1j);c.1e("22",23,2K);F}if(c.7s("6w")&&!c.C.fD){F E}if(23==="82"||23==="8f"){F c.fE(23,2K)}if(23==="67"){F c.fG(23,2K)}c.2p(23,2K,1j);if(23==="fX"){c.$K.1b("hr").2C("id")}},fG:B(23,2K){c.1Z();q 1s=c.7s("A");if(1s){$(1s).2f($(1s).Y());c.1j();c.1e("22",23,2K);F}},fE:B(23,2K){c.1Z();q L=c.2B();q $30=$(L).2c("ol, 2q");if(!c.4g($30)&&$30.1U()!=0){$30=E}q 1w=E;if($30&&$30.1m){1w=N;q 5q=$30[0].Q;if((23==="82"&&5q==="qZ")||(23==="8f"&&5q==="fJ")){1w=E}}c.2e();if(1w){q 1V=c.7b();q 4p=c.3H(1V);if(1E 1V[0]!="1I"&&1V.1m>1&&1V[0].4v==3){4p.k2(c.2P())}q 1a="",5v="";$.1u(4p,$.M(B(i,s){if(s.Q=="3f"){q $s=$(s);q 7a=$s.6f();7a.1b("2q","ol").1w();if(c.C.1N===E){1a+=c.49($("<p>").1h(7a.1W()))}I{q fR=7a.o().G(/<br\\s?\\/?>$/i,"");1a+=fR+"<br>"}if(i==0){$s.2y("U-5v").6d();5v=c.49($s)}I{$s.1w()}}},c));o=c.$K.o().G(5v,"</"+5q+">"+1a+"<"+5q+">");c.$K.o(o);c.$K.1b(5q+":6d").1w()}I{q fz=$(c.2B()).2c("1g");if(c.1C("3t")&&!c.8N()&&c.C.1N){q 3K=c.cs("12");q az=$(3K).o();q 6R=$("<2q>");if(23=="8f"){6R=$("<ol>")}q 8n=$("<li>");if($.28(az)==""){8n.1h(az+\'<V id="1J-1M-1">\'+c.C.2i+"</V>");6R.1h(8n);c.$K.1b("#1J-1M-1").2f(6R)}I{8n.1h(az);6R.1h(8n);$(3K).2f(6R)}}I{c.X.22(23)}q L=c.2B();q $30=$(L).2c("ol, 2q");if(c.C.1N===E){q aC=$.28($30.Y());if(aC==""){$30.4k("li").1b("br").1w();$30.4k("li").1h(\'<V id="1J-1M-1">\'+c.C.2i+"</V>")}}if(fz.1U()!=0){$30.gR("<1g>")}if($30.1m){q $6F=$30.L();if(c.4g($6F)&&$6F[0].Q!="3f"&&c.7n($6F[0])){$6F.2f($6F.1W())}}if(c.1C("3c")){c.$K.2j()}}c.1Y();c.$K.1b("#1J-1M-1").2C("id");c.1j();c.1e("22",23,2K);F},dr:B(){c.e4("5S")},dz:B(){c.e4("6W")},e4:B(23){c.1Z();if(23==="5S"){q 1l=c.2P();c.2e();if(1l&&1l.Q=="3f"){q L=c.2B();q $30=$(L).2c("ol, 2q");q 5q=$30[0].Q;q 4p=c.3H();$.1u(4p,B(i,s){if(s.Q=="3f"){q $4Z=$(s).4Z();if($4Z.1U()!=0&&$4Z[0].Q=="3f"){q $eb=$4Z.4k("2q, ol");if($eb.1U()==0){$4Z.1h($("<"+5q+">").1h(s))}I{$eb.1h(s)}}}})}I{if(1l===E&&c.C.1N===N){c.2p("6B","2h");q 7v=c.2P();q 1l=$(\'<12 1a-7Z="">\').o($(7v).o());$(7v).2f(1l);q 1t=c.9g($(1l).1f("2G-1t"))+c.C.aJ;$(1l).1f("2G-1t",1t+"px")}I{q 7G=c.3H();$.1u(7G,$.M(B(i,1B){q $el=E;if(1B.Q==="6i"){F}if($.4L(1B.Q,c.C.5r)!==-1){$el=$(1B)}I{$el=$(1B).2c(c.C.5r.3T().3N(),c.$K[0])}q 1t=c.9g($el.1f("2G-1t"))+c.C.aJ;$el.1f("2G-1t",1t+"px")},c))}}c.1Y()}I{c.2e();q 1l=c.2P();if(1l&&1l.Q=="3f"){q 4p=c.3H();q 2I=0;c.dH(1l,2I,4p)}I{q 7G=c.3H();$.1u(7G,$.M(B(i,1B){q $el=E;if($.4L(1B.Q,c.C.5r)!==-1){$el=$(1B)}I{$el=$(1B).2c(c.C.5r.3T().3N(),c.$K[0])}q 1t=c.9g($el.1f("2G-1t"))-c.C.aJ;if(1t<=0){if(c.C.1N===N&&1E($el.1a("7Z"))!=="1I"){$el.2f($el.o()+"<br>")}I{$el.1f("2G-1t","");c.4V($el,"1o")}}I{$el.1f("2G-1t",1t+"px")}},c))}c.1Y()}c.1j()},dH:B(li,2I,4p){if(li&&li.Q=="3f"){q $L=$(li).L().L();if($L.1U()!=0&&$L[0].Q=="3f"){$L.2E(li)}I{if(1E 4p[2I]!="1I"){li=4p[2I];2I++;c.dH(li,2I,4p)}I{c.22("82")}}}},dQ:B(){c.88("","rA")},dT:B(){c.88("4I","ry")},dG:B(){c.88("6D","rs")},dL:B(){c.88("dK","rt")},88:B(1G,23){c.1Z();if(c.cv()){c.X.22(23,E,E);F N}c.2e();q 1l=c.2P();if(!1l&&c.C.1N){c.2p("c0","12");q 7v=c.2P();q 1l=$(\'<12 1a-7Z="">\').o($(7v).o());$(7v).2f(1l);$(1l).1f("Y-54",1G);c.4V(1l,"1o");if(1G==""&&1E($(1l).1a("7Z"))!=="1I"){$(1l).2f($(1l).o())}}I{q 7G=c.3H();$.1u(7G,$.M(B(i,1B){q $el=E;if($.4L(1B.Q,c.C.5r)!==-1){$el=$(1B)}I{$el=$(1B).2c(c.C.5r.3T().3N(),c.$K[0])}if($el){$el.1f("Y-54",1G);c.4V($el,"1o")}},c))}c.1Y();c.1j()},e5:B(o){q ph=c.gO(o);if(ph!==E){F ph}if(c.C.1N===E){if(o===""){o=c.C.5a}I{if(o.4G(/^<hr\\s?\\/?>$/gi)!==-1){o="<hr>"+c.C.5a}}}F o},db:B(o){if(c.C.aP&&!c.C.ru){o=o.G(/<12(.*?)>([\\w\\W]*?)<\\/12>/gi,"<p$1>$2</p>")}if(c.C.6a){o=c.bj(o)}F o},de:B(o){if(c.C.d5){o=o.G(/\\{\\{(.*?)\\}\\}/gi,"<!-- a2 gY $1 -->");o=o.G(/\\{(.*?)\\}/gi,"<!-- a2 $1 -->")}o=o.G(/<3s(.*?)>([\\w\\W]*?)<\\/3s>/gi,\'<1c 1G="Y/9I" 1o="3i: 3o;" 1x="U-3s-1d"$1>$2</1c>\');o=o.G(/<1o(.*?)>([\\w\\W]*?)<\\/1o>/gi,\'<2m$1 1o="3i: 3o;" 4h="U-1o-1d">$2</2m>\');o=o.G(/<2s(.*?)>([\\w\\W]*?)<\\/2s>/gi,\'<2m$1 4h="U-2s-1d">$2</2m>\');if(c.C.8e){o=o.G(/<\\?4P([\\w\\W]*?)\\?>/gi,\'<2m 1o="3i: 3o;" 4h="U-4P-1d">$1</2m>\')}I{o=o.G(/<\\?4P([\\w\\W]*?)\\?>/gi,"")}F o},gM:B(o){if(c.C.d5){o=o.G(/<!-- a2 gY (.*?) -->/gi,"{{$1}}");o=o.G(/<!-- a2 (.*?) -->/gi,"{$1}")}o=o.G(/<1c 1G="Y\\/9I" 1o="3i: 3o;" 1x="U-3s-1d"(.*?)>([\\w\\W]*?)<\\/1c>/gi,\'<3s$1 1G="Y/9I">$2<\\/3s>\');o=o.G(/<2m(.*?) 1o="3i: 3o;" 4h="U-1o-1d">([\\w\\W]*?)<\\/2m>/gi,"<1o$1>$2</1o>");o=o.G(/<2m(.*?)4h="U-2s-1d"(.*?)>([\\w\\W]*?)<\\/2m>/gi,"<2s$1$2>$3</2s>");if(c.C.8e){o=o.G(/<2m 1o="3i: 3o;" 4h="U-4P-1d">([\\w\\W]*?)<\\/2m>/gi,"<?4P\\r\\n$1\\r\\n?>")}F o},79:B(o,3J){if(3J!==E){q 3J=[];q 2w=o.1S(/<(2r|1o|3s|1c)(.*?)>([\\w\\W]*?)<\\/(2r|1o|3s|1c)>/gi);if(2w===2J){2w=[]}if(c.C.8e){q 7l=o.1S(/<\\?4P([\\w\\W]*?)\\?>/gi);if(7l){2w=$.eW(2w,7l)}}if(2w){$.1u(2w,B(i,s){o=o.G(s,"hh"+i);3J.3a(s)})}}o=o.G(/\\n/g," ");o=o.G(/[\\t]*/g,"");o=o.G(/\\n\\s*\\n/g,"\\n");o=o.G(/^[\\s\\n]*/g," ");o=o.G(/[\\s\\n]*$/g," ");o=o.G(/>\\s{2,}</g,"> <");o=c.hi(o,3J);o=o.G(/\\n\\n/g,"\\n");F o},hi:B(o,3J){if(3J===E){F o}$.1u(3J,B(i,s){o=o.G("hh"+i,s)});F o},cb:B(o){o=o.G(/[\\7m-\\hk\\hm]/g,"");q di=["<b>\\\\s*</b>","<b>&3u;</b>","<em>\\\\s*</em>"];q 75=["<2r></2r>","<2h>\\\\s*</2h>","<dd></dd>","<dt></dt>","<2q></2q>","<ol></ol>","<li></li>","<1n></1n>","<3h></3h>","<V>\\\\s*<V>","<V>&3u;<V>","<p>\\\\s*</p>","<p></p>","<p>&3u;</p>","<p>\\\\s*<br>\\\\s*</p>","<12>\\\\s*</12>","<12>\\\\s*<br>\\\\s*</12>"];if(c.C.do){75=75.rq(di)}I{75=di}q 4T=75.1m;3w(q i=0;i<4T;++i){o=o.G(2a 2L(75[i],"gi"),"")}F o},bj:B(o){o=$.28(o);if(c.C.1N===N){F o}if(o===""||o==="<p></p>"){F c.C.5a}o=o+"\\n";if(c.C.do===E){F o}q eG=[];q 2w=o.1S(/<(1n|12|2r|3R)(.*?)>([\\w\\W]*?)<\\/(1n|12|2r|3R)>/gi);if(!2w){2w=[]}q ff=o.1S(/<!--([\\w\\W]*?)-->/gi);if(ff){2w=$.eW(2w,ff)}if(c.C.8e){q 7l=o.1S(/<2m(.*?)4h="U-4P-1d">([\\w\\W]*?)<\\/2m>/gi);if(7l){2w=$.eW(2w,7l)}}if(2w){$.1u(2w,B(i,s){eG[i]=s;o=o.G(s,"{G"+i+"}\\n")})}o=o.G(/<br \\/>\\s*<br \\/>/gi,"\\n\\n");o=o.G(/<br><br>/gi,"\\n\\n");B R(4D,ge,r){F o.G(2a 2L(4D,ge),r)}q 3D="(rw|o|2v|aK|1c|4E|1o|3s|1s|1Q|1n|3X|cg|rp|rr|rx|7L|3h|1g|eN|12|dl|dd|dt|2q|ol|li|2r|3n|47|2s|gk|gj|2h|c5|rz|1o|p|h[1-6]|hr|ga|rn|2m|kf|ke|r7|aI|44|r8|r9|r5|r1|r2|ro)";o=R("(<"+3D+"[^>]*>)","gi","\\n$1");o=R("(</"+3D+">)","gi","$1\\n\\n");o=R("\\r\\n","g","\\n");o=R("\\r","g","\\n");o=R("/\\n\\n+/","g","\\n\\n");q 4B=o.4i(2a 2L("\\ns*\\n","g"),-1);o="";3w(q i in 4B){if(4B.r3(i)){if(4B[i].4G("{G")==-1){4B[i]=4B[i].G(/<p>\\n\\t?<\\/p>/gi,"");4B[i]=4B[i].G(/<p><\\/p>/gi,"");if(4B[i]!=""){o+="<p>"+4B[i].G(/^\\n+|\\n+$/g,"")+"</p>"}}I{o+=4B[i]}}}o=R("<p><p>","gi","<p>");o=R("</p></p>","gi","</p>");o=R("<p>s?</p>","gi","");o=R("<p>([^<]+)</(12|c5|2s)>","gi","<p>$1</p></$2>");o=R("<p>(</?"+3D+"[^>]*>)</p>","gi","$1");o=R("<p>(<li.+?)</p>","gi","$1");o=R("<p>s?(</?"+3D+"[^>]*>)","gi","$1");o=R("(</?"+3D+"[^>]*>)s?</p>","gi","$1");o=R("(</?"+3D+"[^>]*>)s?<br />","gi","$1");o=R("<br />(s*</?(?:p|li|12|dl|dd|dt|eN|2r|1g|2q|ol)[^>]*>)","gi","$1");o=R("\\n</p>","gi","</p>");o=R("<li><p>","gi","<li>");o=R("</p></li>","gi","</li>");o=R("</li><p>","gi","</li>");o=R("<p>\\t?\\n?<p>","gi","<p>");o=R("</dt><p>","gi","</dt>");o=R("</dd><p>","gi","</dd>");o=R("<br></p></2h>","gi","</2h>");o=R("<p>\\t*</p>","gi","");$.1u(eG,B(i,s){o=o.G("{G"+i+"}",s)});F $.28(o)},ax:B(o,7x){q 5X="43";if(c.C.5X==="b"){5X="b"}q 5Y="em";if(c.C.5Y==="i"){5Y="i"}o=o.G(/<V 1o="2F-1o: 3r;">([\\w\\W]*?)<\\/V>/gi,"<"+5Y+">$1</"+5Y+">");o=o.G(/<V 1o="2F-71: 3q;">([\\w\\W]*?)<\\/V>/gi,"<"+5X+">$1</"+5X+">");if(c.C.5X==="43"){o=o.G(/<b>([\\w\\W]*?)<\\/b>/gi,"<43>$1</43>")}I{o=o.G(/<43>([\\w\\W]*?)<\\/43>/gi,"<b>$1</b>")}if(c.C.5Y==="em"){o=o.G(/<i>([\\w\\W]*?)<\\/i>/gi,"<em>$1</em>")}I{o=o.G(/<em>([\\w\\W]*?)<\\/em>/gi,"<i>$1</i>")}o=o.G(/<V 1o="Y-bP: 4W;">([\\w\\W]*?)<\\/V>/gi,"<u>$1</u>");if(7x!==N){o=o.G(/<5J>([\\w\\W]*?)<\\/5J>/gi,"<56>$1</56>")}I{o=o.G(/<56>([\\w\\W]*?)<\\/56>/gi,"<5J>$1</5J>")}F o},7S:B(o){if(o==""||1E o=="1I"){F o}q 94=E;if(c.C.5G!==E){94=N}q 2M=94===N?c.C.5G:c.C.7M;q gs=/<\\/?([a-z][a-ep-9]*)\\b[^>]*>/gi;o=o.G(gs,B($0,$1){if(94===N){F $.4L($1.3N(),2M)>"-1"?$0:""}I{F $.4L($1.3N(),2M)>"-1"?"":$0}});o=c.ax(o);F o},bt:B(o,gD){q 2r=o.1S(/<(2r|2o)(.*?)>([\\w\\W]*?)<\\/(2r|2o)>/gi);if(2r!==2J){$.1u(2r,$.M(B(i,s){q 2M=s.1S(/<(2r|2o)(.*?)>([\\w\\W]*?)<\\/(2r|2o)>/i);2M[3]=2M[3].G(/&3u;/g," ");if(gD!==E){2M[3]=c.bZ(2M[3])}2M[3]=2M[3].G(/\\$/g,"&#36;");o=o.G(s,"<"+2M[1]+2M[2]+">"+2M[3]+"</"+2M[1]+">")},c))}F o},bZ:B(4D){4D=4S(4D).G(/&ab;/g,"&").G(/&lt;/g,"<").G(/&gt;/g,">").G(/&gC;/g,\'"\');F 4D.G(/&/g,"&ab;").G(/</g,"&lt;").G(/>/g,"&gt;").G(/"/g,"&gC;")},gy:B(){q $1B=c.$K.1b("li, 1z, a, b, 43, rc, rk, i, em, u, rl, 5J, 56, V, rm");$1B.g3(\'[1o*="9b-8i: g4;"][1o*="9v-21"]\').1f("9b-8i","").1f("9v-21","");$1B.g3(\'[1o*="9b-8i: g4;"]\').1f("9b-8i","");$1B.1f("9v-21","");$.1u($1B,$.M(B(i,s){c.4V(s,"1o")},c));q $bD=c.$K.1b("b, 43, i, em, u, 5J, 56");$bD.1f("2F-1U","");$.1u($bD,$.M(B(i,s){c.4V(s,"1o")},c));c.$K.1b(\'12[1o="Y-54: -4C-4d;"]\').1W().fA();c.$K.1b("2q, ol, li").2C("1o")},gF:B(2o){q i=0,9f=2o.1m,31=0,2u=2J,3p=2J,1d="",1X="",4Y="";c.86=0;3w(;i<9f;i++){31=i;if(-1==2o.4N(i).3W("<")){1X+=2o.4N(i);F c.cu(1X)}3x(31<9f&&2o.5u(31)!="<"){31++}if(i!=31){4Y=2o.4N(i,31-i);if(!4Y.1S(/^\\s{2,}$/g)){if("\\n"==1X.5u(1X.1m-1)){1X+=c.7C()}I{if("\\n"==4Y.5u(0)){1X+="\\n"+c.7C();4Y=4Y.G(/^\\s+/,"")}}1X+=4Y}if(4Y.1S(/\\n/)){1X+="\\n"+c.7C()}}2u=31;3x(31<9f&&">"!=2o.5u(31)){31++}1d=2o.4N(2u,31-2u);i=31;q t;if("!--"==1d.4N(1,3)){if(!1d.1S(/--$/)){3x("-->"!=2o.4N(31,3)){31++}31+=2;1d=2o.4N(2u,31-2u);i=31}if("\\n"!=1X.5u(1X.1m-1)){1X+="\\n"}1X+=c.7C();1X+=1d+">\\n"}I{if("!"==1d[1]){1X=c.9o(1d+">",1X)}I{if("?"==1d[1]){1X+=1d+">\\n"}I{if(t=1d.1S(/^<(3s|1o|2r)/i)){t[1]=t[1].3N();1d=c.ct(1d);1X=c.9o(1d,1X);3p=4S(2o.4N(i+1)).3N().3W("</"+t[1]);if(3p){4Y=2o.4N(i+1,3p);i+=3p;1X+=4Y}}I{1d=c.ct(1d);1X=c.9o(1d,1X)}}}}}F c.cu(1X)},7C:B(){q s="";3w(q j=0;j<c.86;j++){s+="\\t"}F s},cu:B(2o){2o=2o.G(/\\n\\s*\\n/g,"\\n");2o=2o.G(/^[\\s\\n]*/,"");2o=2o.G(/[\\s\\n]*$/,"");2o=2o.G(/<3s(.*?)>\\n<\\/3s>/gi,"<3s$1><\\/3s>");c.86=0;F 2o},ct:B(1d){q 81="";1d=1d.G(/\\n/g," ");1d=1d.G(/\\s{2,}/g," ");1d=1d.G(/^\\s+|\\s+$/g," ");q cS="";if(1d.1S(/\\/$/)){cS="/";1d=1d.G(/\\/+$/,"")}q m;3x(m=/\\s*([^= ]+)(?:=(([\'"\']).*?\\3|[^ ]+))?/.2p(1d)){if(m[2]){81+=m[1].3N()+"="+m[2]}I{if(m[1]){81+=m[1].3N()}}81+=" ";1d=1d.4N(m[0].1m)}F 81.G(/\\s*$/,"")+cS+">"},9o:B(1d,1X){q nl=1d.1S(c.cF);if(1d.1S(c.gL)||nl){1X=1X.G(/\\s*$/,"");1X+="\\n"}if(nl&&"/"==1d.5u(1)){c.86--}if("\\n"==1X.5u(1X.1m-1)){1X+=c.7C()}if(nl&&"/"!=1d.5u(1)){c.86++}1X+=1d;if(1d.1S(c.gI)||1d.1S(c.cF)){1X=1X.G(/ *$/,"");1X+="\\n"}F 1X},gX:B(e){q o=$.28(c.$K.o());if(c.C.1N){if(o==""){e.2x();c.$K.o("");c.2j()}}I{o=o.G(/<br\\s?\\/?>/i,"");q 7f=o.G(/<p>\\s?<\\/p>/gi,"");if(o===""||7f===""){e.2x();q J=$(c.C.5a).2R(0);c.$K.o(J);c.2j()}}c.1j()},5i:B(1d){if(c.1C("3c")&&c.7H()){c.$K.2j()}c.1Z();q 1V=c.3H();c.2e();$.1u(1V,$.M(B(i,J){if(J.Q!=="3f"){q L=$(J).L();if(1d==="p"){if((J.Q==="P"&&L.1U()!=0&&L[0].Q==="3L")||J.Q==="3L"){c.bR();F}I{if(c.C.1N){if(J&&J.Q.4G(/H[1-6]/)==0){$(J).2f(J.3O+"<br>")}I{F}}I{c.6B(1d,J)}}}I{c.6B(1d,J)}}},c));c.1Y();c.1j()},6B:B(1d,1l){if(1l===E){1l=c.2P()}if(1l===E&&c.C.1N===N){c.22("c0",1d);F N}q 1W="";if(1d!=="2r"){1W=$(1l).1W()}I{1W=$(1l).o();if($.28(1W)===""){1W=\'<V id="1J-1M-1"></V>\'}}if(1l.Q==="6w"){1d="p"}if(c.C.1N===N&&1d==="p"){$(1l).2f($("<12>").1h(1W).o()+"<br>")}I{q L=c.2B();q J=$("<"+1d+">").1h(1W);$(1l).2f(J);if(L&&L.Q=="6i"){$(J).gR("<1g>")}}},jH:B(fY,fZ,83){if(83!==E){c.2e()}q 8A=$("<"+fZ+"/>");$(fY).2f(B(){F 8A.1h($(c).1W())});if(83!==E){c.1Y()}F 8A},bR:B(){if(c.1C("3c")&&c.7H()){c.$K.2j()}c.1Z();if(c.C.1N===E){c.2e();q 3D=c.3H();q 2h=E;q fH=3D.1m;if(3D){q 1a="";q 5v="";q G=E;q bT=N;$.1u(3D,B(i,s){if(s.Q!=="P"){bT=E}});$.1u(3D,$.M(B(i,s){if(s.Q==="3L"){c.6B("p",s,E)}I{if(s.Q==="P"){2h=$(s).L();if(2h[0].Q=="3L"){q 7o=$(2h).4k("p").1U();if(7o==1){$(2h).2f(s)}I{if(7o==fH){G="2h";1a+=c.49(s)}I{G="o";1a+=c.49(s);if(i==0){$(s).2y("U-5v").6d();5v=c.49(s)}I{$(s).1w()}}}}I{if(bT===E||3D.1m==1){c.6B("2h",s,E)}I{G="fN";1a+=c.49(s)}}}I{if(s.Q!=="3f"){c.6B("2h",s,E)}}}},c));if(G){if(G=="fN"){$(3D[0]).2f("<2h>"+1a+"</2h>");$(3D).1w()}I{if(G=="2h"){$(2h).2f(1a)}I{if(G=="o"){q o=c.$K.o().G(5v,"</2h>"+1a+"<2h>");c.$K.o(o);c.$K.1b("2h").1u(B(){if($.28($(c).o())==""){$(c).1w()}})}}}}}c.1Y()}I{q 1l=c.2P();if(1l.Q==="3L"){c.2e();q o=$.28($(1l).o());q 1J=$.28(c.jR());o=o.G(/<V(.*?)id="1J-1M(.*?)<\\/V>/gi,"");if(o==1J){$(1l).2f($(1l).o()+"<br>")}I{c.ft("2H");q 2H=c.$K.1b("2H");2H.6d();q fu=c.$K.o().G("<2H></2H>",\'</2h><V id="1J-1M-1">\'+c.C.2i+"</V>"+1J+"<2h>");c.$K.o(fu);2H.1w();c.$K.1b("2h").1u(B(){if($.28($(c).o())==""){$(c).1w()}})}c.1Y();c.$K.1b("V#1J-1M-1").1i("id",E)}I{q 3K=c.cs("2h");q o=$(3K).o();q fw=["2q","ol","1n","3h","7L","3X","cg","dl"];$.1u(fw,B(i,s){o=o.G(2a 2L("<"+s+"(.*?)>","gi"),"");o=o.G(2a 2L("</"+s+">","gi"),"")});q 6Y=c.C.ac;$.1u(6Y,B(i,s){o=o.G(2a 2L("<"+s+"(.*?)>","gi"),"");o=o.G(2a 2L("</"+s+">","gi"),"<br>")});$(3K).o(o);c.cq(3K);q 4l=$(3K).4l();if(4l.1U()!=0&&4l[0].Q==="cP"){4l.1w()}}}c.1j()},rj:B(1i,2t){q 1V=c.3H();$(1V).2C(1i);c.1j()},ri:B(1i,2t){q 1V=c.3H();$(1V).1i(1i,2t);c.1j()},rf:B(5h){q 1V=c.3H();$(1V).1f(5h,"");c.4V(1V,"1o");c.1j()},rh:B(5h,2t){q 1V=c.3H();$(1V).1f(5h,2t);c.1j()},rg:B(2U){q 1V=c.3H();$(1V).3e(2U);c.4V(1V,"1x");c.1j()},rd:B(2U){q 1V=c.3H();$(1V).2y(2U);c.1j()},rb:B(2U){c.2e();c.ck(B(J){$(J).3e(2U);c.4V(J,"1x")});c.1Y();c.1j()},r4:B(2U){q 1r=c.3S();if(!$(1r).3v(2U)){c.as("2y",2U)}},r6:B(5h){c.2e();c.ck(B(J){$(J).1f(5h,"");c.4V(J,"1o")});c.1Y();c.1j()},ra:B(5h,2t){c.as("1f",5h,2t)},qc:B(1i){c.2e();q O=c.3m(),J=c.7U(),1V=c.7b();if(O.4M||O.5T===O.7Q&&J){1V=$(J)}$(1V).2C(1i);c.fm();c.1Y();c.1j()},q1:B(1i,2t){c.as("1i",1i,2t)},as:B(1G,1i,2t){c.1Z();c.2e();q O=c.3m();q el=c.7U();if((O.4M||O.5T===O.7Q)&&el&&!c.7n(el)){$(el)[1G](1i,2t)}I{c.X.22("8a",E,4);q aH=c.$K.1b("2F");$.1u(aH,$.M(B(i,s){c.fL(1G,s,1i,2t)},c))}c.1Y();c.1j()},fL:B(1G,s,1i,2t){q L=$(s).L(),el;q av=c.9V();q aE=$(L).Y();q aR=av==aE;if(aR&&L&&L[0].Q==="c7"&&L[0].or.1m!=0){el=L;$(s).2f($(s).o())}I{el=$("<4o>").1h($(s).1W());$(s).2f(el)}$(el)[1G](1i,2t);F el},ck:B(1e){q O=c.3m(),J=c.7U(),1V=c.7b(),4M;if(O.4M||O.5T===O.7Q&&J){1V=$(J);4M=N}$.1u(1V,$.M(B(i,J){if(!4M&&J.Q!=="c7"){q av=c.9V();q aE=$(J).L().Y();q aR=av==aE;if(aR&&J.4Q.Q==="c7"&&!$(J.4Q).3v("4e")){J=J.4Q}I{F}}1e.5B(c,J)},c))},fm:B(){q $93=c.$K.1b("4o");$.1u($93,$.M(B(i,V){q $V=$(V);if($V.1i("1x")===1I&&$V.1i("1o")===1I){$V.1W().fA()}},c))},ft:B(1d){c.2e();c.X.22("8a",E,4);q aH=c.$K.1b("2F");q 2X;$.1u(aH,B(i,s){q el=$("<"+1d+"/>").1h($(s).1W());$(s).2f(el);2X=el});c.1Y();c.1j()},oq:B(1d){c.2e();q ca=1d.op();q 1V=c.7b();q L=$(c.2B()).L();$.1u(1V,B(i,s){if(s.Q===ca){c.9P(s)}});if(L&&L[0].Q===ca){c.9P(L)}c.1Y();c.1j()},9P:B(el){$(el).2f($(el).1W())},9m:B(o,1j){q 1r=c.3S();q L=1r.4Q;c.4m();c.1Z();q $o=$("<12>").1h($.cc(o));o=$o.o();o=c.cb(o);$o=$("<12>").1h($.cc(o));q c9=c.2P();if($o.1W().1m==1){q 9R=$o.1W()[0].Q;if(9R!="P"&&9R==c9.Q||9R=="6w"){$o=$("<12>").1h(o)}}if(c.C.1N){o=o.G(/<p(.*?)>([\\w\\W]*?)<\\/p>/gi,"$2<br>")}if(!c.C.1N&&$o.1W().1m==1&&$o.1W()[0].4v==3&&(c.bk().1m>2||(!1r||1r.Q=="bp"&&!L||L.Q=="kc"))){o="<p>"+o+"</p>"}o=c.aV(o);if($o.1W().1m>1&&c9||$o.1W().is("p, :aI, 2q, ol, li, 12, 1n, 1g, 2h, 2r, c5, 2m, aI, 44, ke, kf")){if(c.1C("3t")){if(!c.8N()){c.X.1J.5z().cd(o)}I{c.bb(o)}}I{c.X.22("5b",E,o)}}I{c.ah(o,E)}if(c.5O){c.48.2W($.M(B(){if(!c.C.1N){c.7e(c.$K.1W().2X())}I{c.bX()}},c),1)}c.7D();c.7J();if(1j!==E){c.1j()}},ah:B(o,1j){o=c.aV(o);q 1q=c.29();if(1q.41&&1q.51){q O=1q.41(0);O.ay();q el=c.X.4y("12");el.3O=o;q 4f=c.X.cO(),J,5N;3x((J=el.8z)){5N=4f.7c(J)}O.3A(4f);if(5N){O=O.9p();O.9d(5N);O.5I(N);1q.4O();1q.5e(O)}}if(1j!==E){c.1j()}},os:B(o){o=c.aV(o);q J=$(o);q 6s=X.4y("V");6s.3O="\\7m";q O=c.3m();O.3A(6s);O.3A(J[0]);O.5I(E);q 1q=c.29();1q.4O();1q.5e(O);c.1j()},ot:B(o){q $o=$($.cc(o));if($o.1m){o=$o.Y()}c.4m();if(c.1C("3t")){if(!c.8N()){c.X.1J.5z().cd(o)}I{c.bb(o)}}I{c.X.22("5b",E,o)}c.1j()},3A:B(J){J=J[0]||J;if(J.Q=="kt"){q 6G="4o";q au=J.ka;q 5Q=2a 2L("<"+J.Q,"i");q 5A=au.G(5Q,"<"+6G);5Q=2a 2L("</"+J.Q,"i");5A=5A.G(5Q,"</"+6G);J=$(5A)[0]}q 1q=c.29();if(1q.41&&1q.51){O=1q.41(0);O.ay();O.3A(J);O.ov(J);O.9d(J);1q.4O();1q.5e(O)}F J},iS:B(e,J){q O;q x=e.k5,y=e.k7;if(c.X.k8){q 3Y=c.X.k8(x,y);O=c.3m();O.7W(3Y.ou,3Y.2V);O.5I(N);O.3A(J)}I{if(c.X.kh){O=c.X.kh(x,y);O.3A(J)}I{if(1E X.2v.ki!="1I"){O=c.X.2v.ki();O.kr(x,y);q ce=O.oo();ce.kr(x,y);O.om("og",ce);O.3n()}}}},6Q:B(2g,L){if(1E(L)!="1I"){2g=L}if(c.cR()){if(c.C.1N){q 1W=$("<12>").1h($.28(c.$K.o())).1W();q 2X=1W.2X()[0];if(2X.Q=="kt"&&2X.3O==""){2X=1W.4Z()[0]}if(c.49(2X)!=c.49(2g)){F E}}I{if(c.$K.1W().2X()[0]!==2g){F E}}c.cf(2g)}},cf:B(2g){c.1Z();if(c.C.1N===E){q J=$(c.C.5a);$(2g).2E(J);c.4b(J)}I{q J=$(\'<V id="1J-1M-1">\'+c.C.2i+"</V>",c.X)[0];$(2g).2E(J);$(J).2E(c.C.2i);c.1Y();c.$K.1b("V#1J-1M-1").2C("id")}},aD:B(kp){c.2e();q br="<br>";if(kp==N){br="<br><br>"}if(c.1C("3c")){q V=$("<V>").o(c.C.2i);c.$K.1b("#1J-1M-1").3M(br).3M(V).3M(c.C.2i);c.jZ(V[0]);V.1w();c.7V()}I{q L=c.2B();if(L&&L.Q==="A"){q 2V=c.cT(L);q Y=$.28($(L).Y()).G(/\\n\\r\\n/g,"");q 4T=Y.1m;if(2V==4T){c.7V();q J=$(\'<V id="1J-1M-1">\'+c.C.2i+"</V>",c.X)[0];$(L).2E(J);$(J).3M(br+(c.1C("4C")?c.C.2i:""));c.1Y();F N}}c.$K.1b("#1J-1M-1").3M(br+(c.1C("4C")?c.C.2i:""));c.1Y()}},of:B(){c.aD(N)},ko:B(2g){q J=$("<br>"+c.C.2i);$(2g).2f(J);c.4b(J)},kk:B(o){o=c.1e("oe",E,o);if(c.1C("3t")){q 2H=$.28(o);if(2H.4G(/^<a(.*?)>(.*?)<\\/a>$/i)==0){o=o.G(/^<a(.*?)>(.*?)<\\/a>$/i,"$2")}}if(c.C.kj){q 2H=c.X.4y("12");o=o.G(/<br>|<\\/H[1-6]>|<\\/p>|<\\/12>/gi,"\\n");2H.3O=o;o=2H.9X||2H.bC;o=$.28(o);o=o.G("\\n","<br>");o=c.bj(o);c.aL(o);F E}q aO=E;if(c.7s("6i")){aO=N;q 6Y=c.C.ac;6Y.3a("3h");6Y.3a("1n");$.1u(6Y,B(i,s){o=o.G(2a 2L("<"+s+"(.*?)>","gi"),"");o=o.G(2a 2L("</"+s+">","gi"),"<br>")})}if(c.7s("6w")){o=c.kn(o);c.aL(o);F N}o=o.G(/<1z(.*?)v:oh=(.*?)>/gi,"");o=o.G(/<p(.*?)1x="oi"([\\w\\W]*?)<\\/p>/gi,"<2q><li$2</li>");o=o.G(/<p(.*?)1x="ok"([\\w\\W]*?)<\\/p>/gi,"<li$2</li>");o=o.G(/<p(.*?)1x="oj"([\\w\\W]*?)<\\/p>/gi,"<li$2</li></2q>");o=o.G(/<p(.*?)1x="ow"([\\w\\W]*?)<\\/p>/gi,"<2q><li$2</li></2q>");o=o.G(/·/g,"");o=o.G(/<!--[\\s\\S]*?-->|<\\?(?:4P)?[\\s\\S]*?\\?>/gi,"");if(c.C.kl===N){o=o.G(/(&3u;){2,}/gi,"&3u;");o=o.G(/&3u;/gi," ")}o=o.G(/<b\\ox="km-1v-1M(.*?)">([\\w\\W]*?)<\\/b>/gi,"$2");o=o.G(/<b(.*?)id="oJ-km-oI(.*?)">([\\w\\W]*?)<\\/b>/gi,"$3");o=o.G(/<V[^>]*(2F-1o: 3r; 2F-71: 3q|2F-71: 3q; 2F-1o: 3r)[^>]*>/gi,\'<V 1o="2F-71: 3q;"><V 1o="2F-1o: 3r;">\');o=o.G(/<V[^>]*2F-1o: 3r[^>]*>/gi,\'<V 1o="2F-1o: 3r;">\');o=o.G(/<V[^>]*2F-71: 3q[^>]*>/gi,\'<V 1o="2F-71: 3q;">\');o=o.G(/<V[^>]*Y-bP: 4W[^>]*>/gi,\'<V 1o="Y-bP: 4W;">\');o=o.G(/<1g>\\oH*<\\/1g>/gi,"[1g]");o=o.G(/<1g>&3u;<\\/1g>/gi,"[1g]");o=o.G(/<1g><br><\\/1g>/gi,"[1g]");o=o.G(/<1g(.*?)bc="(.*?)"(.*?)>([\\w\\W]*?)<\\/1g>/gi,\'[1g bc="$2"]$4[/1g]\');o=o.G(/<1g(.*?)bh="(.*?)"(.*?)>([\\w\\W]*?)<\\/1g>/gi,\'[1g bh="$2"]$4[/1g]\');o=o.G(/<a(.*?)1R="(.*?)"(.*?)>([\\w\\W]*?)<\\/a>/gi,\'[a 1R="$2"]$4[/a]\');o=o.G(/<1Q(.*?)>([\\w\\W]*?)<\\/1Q>/gi,"[1Q$1]$2[/1Q]");o=o.G(/<3C(.*?)>([\\w\\W]*?)<\\/3C>/gi,"[3C$1]$2[/3C]");o=o.G(/<5t(.*?)>([\\w\\W]*?)<\\/5t>/gi,"[5t$1]$2[/5t]");o=o.G(/<4X(.*?)>([\\w\\W]*?)<\\/4X>/gi,"[4X$1]$2[/4X]");o=o.G(/<3R(.*?)>([\\w\\W]*?)<\\/3R>/gi,"[3R$1]$2[/3R]");o=o.G(/<2K(.*?)>/gi,"[2K$1]");o=o.G(/<1z(.*?)>/gi,"[1z$1]");o=o.G(/ 1x="(.*?)"/gi,"");o=o.G(/<(\\w+)([\\w\\W]*?)>/gi,"<$1>");if(c.C.1N){o=o.G(/<43><\\/43>/gi,"");o=o.G(/<u><\\/u>/gi,"");if(c.C.bV){o=o.G(/<2F(.*?)>([\\w\\W]*?)<\\/2F>/gi,"$2")}o=o.G(/<[^\\/>][^>]*>(\\s*|\\t*|\\n*|&3u;|<br>)<\\/[^>]+>/gi,"<br>")}I{o=o.G(/<[^\\/>][^>]*>(\\s*|\\t*|\\n*|&3u;|<br>)<\\/[^>]+>/gi,"")}o=o.G(/<12>\\s*?\\t*?\\n*?(<2q>|<ol>|<p>)/gi,"$1");o=o.G(/\\[1g bc="(.*?)"\\]([\\w\\W]*?)\\[\\/1g\\]/gi,\'<1g bc="$1">$2</1g>\');o=o.G(/\\[1g bh="(.*?)"\\]([\\w\\W]*?)\\[\\/1g\\]/gi,\'<1g bh="$1">$2</1g>\');o=o.G(/\\[1g\\]/gi,"<1g>&3u;</1g>");o=o.G(/\\[a 1R="(.*?)"\\]([\\w\\W]*?)\\[\\/a\\]/gi,\'<a 1R="$1">$2</a>\');o=o.G(/\\[1Q(.*?)\\]([\\w\\W]*?)\\[\\/1Q\\]/gi,"<1Q$1>$2</1Q>");o=o.G(/\\[3C(.*?)\\]([\\w\\W]*?)\\[\\/3C\\]/gi,"<3C$1>$2</3C>");o=o.G(/\\[5t(.*?)\\]([\\w\\W]*?)\\[\\/5t\\]/gi,"<5t$1>$2</5t>");o=o.G(/\\[4X(.*?)\\]([\\w\\W]*?)\\[\\/4X\\]/gi,"<4X$1>$2</4X>");o=o.G(/\\[3R(.*?)\\]([\\w\\W]*?)\\[\\/3R\\]/gi,"<3R$1>$2</3R>");o=o.G(/\\[2K(.*?)\\]/gi,"<2K$1>");o=o.G(/\\[1z(.*?)\\]/gi,"<1z$1>");if(c.C.aP){o=o.G(/<12(.*?)>([\\w\\W]*?)<\\/12>/gi,"<p>$2</p>");o=o.G(/<\\/12><p>/gi,"<p>");o=o.G(/<\\/p><\\/12>/gi,"</p>");o=o.G(/<p><\\/p>/gi,"<br />")}I{o=o.G(/<12><\\/12>/gi,"<br />")}o=c.7S(o);if(c.7s("3f")){o=o.G(/<p>([\\w\\W]*?)<\\/p>/gi,"$1<br>")}I{if(aO===E){o=c.bj(o)}}o=o.G(/<V(.*?)>([\\w\\W]*?)<\\/V>/gi,"$2");o=o.G(/<1z>/gi,"");o=o.G(/<[^\\/>][^>][^1z|2K|1v|1g][^<]*>(\\s*|\\t*|\\n*| |<br>)<\\/[^>]+>/gi,"");o=o.G(/\\n{3,}/gi,"\\n");o=o.G(/<p><p>/gi,"<p>");o=o.G(/<\\/p><\\/p>/gi,"</p>");o=o.G(/<li>(\\s*|\\t*|\\n*)<p>/gi,"<li>");o=o.G(/<\\/p>(\\s*|\\t*|\\n*)<\\/li>/gi,"</li>");if(c.C.1N===N){o=o.G(/<p(.*?)>([\\w\\W]*?)<\\/p>/gi,"$2<br>")}o=o.G(/<[^\\/>][^>][^1z|2K|1v|1g][^<]*>(\\s*|\\t*|\\n*| |<br>)<\\/[^>]+>/gi,"");o=o.G(/<1z 3l="4C-oK-1L\\:\\/\\/(.*?)"(.*?)>/gi,"");o=o.G(/<1g(.*?)>(\\s*|\\t*|\\n*)<p>([\\w\\W]*?)<\\/p>(\\s*|\\t*|\\n*)<\\/1g>/gi,"<1g$1>$3</1g>");if(c.C.aP){o=o.G(/<12(.*?)>([\\w\\W]*?)<\\/12>/gi,"$2");o=o.G(/<12(.*?)>([\\w\\W]*?)<\\/12>/gi,"$2")}c.bY=E;if(c.1C("3c")){if(c.C.9l){q 2w=o.1S(/<1z 3l="1a:T(.*?)"(.*?)>/gi);if(2w!==2J){c.bY=2w;3w(k in 2w){q 1z=2w[k].G("<1z",\'<1z 1a-3c-aW-T="\'+k+\'" \');o=o.G(2w[k],1z)}}}3x(/<br>$/gi.4c(o)){o=o.G(/<br>$/gi,"")}}o=o.G(/<p>•([\\w\\W]*?)<\\/p>/gi,"<li>$1</li>");if(c.1C("3t")){3x(/<2F>([\\w\\W]*?)<\\/2F>/gi.4c(o)){o=o.G(/<2F>([\\w\\W]*?)<\\/2F>/gi,"$1")}}if(aO===E){o=o.G(/<1g(.*?)>([\\w\\W]*?)<p(.*?)>([\\w\\W]*?)<\\/1g>/gi,"<1g$1>$2$4</1g>");o=o.G(/<1g(.*?)>([\\w\\W]*?)<\\/p>([\\w\\W]*?)<\\/1g>/gi,"<1g$1>$2$3</1g>");o=o.G(/<1g(.*?)>([\\w\\W]*?)<p(.*?)>([\\w\\W]*?)<\\/1g>/gi,"<1g$1>$2$4</1g>");o=o.G(/<1g(.*?)>([\\w\\W]*?)<\\/p>([\\w\\W]*?)<\\/1g>/gi,"<1g$1>$2$3</1g>")}o=o.G(/\\n/g," ");o=o.G(/<p>\\n?<li>/gi,"<li>");c.aL(o)},kn:B(s){s=s.G(/<br>|<\\/H[1-6]>|<\\/p>|<\\/12>/gi,"\\n");q 2H=c.X.4y("12");2H.3O=s;F c.bZ(2H.9X||2H.bC)},aL:B(o){o=c.1e("oL",E,o);if(c.5O){c.$K.o(o);c.cn();c.bX();c.1j()}I{c.9m(o)}c.5O=E;2W($.M(B(){c.85=E;if(c.1C("3c")){c.$K.1b("p:6d").1w()}if(c.bY!==E){c.k3()}},c),3P);if(c.C.4K&&c.cl!==N){$(c.X.2v).3g(c.aX)}I{c.$K.3g(c.aX)}},cL:B(4q){if(c.C.3E!==E&&1E c.C.3E==="3R"){$.1u(c.C.3E,$.M(B(k,v){if(v!=2J&&v.3T().3W("#")===0){v=$(v).1p()}4q[k]=v},c))}F 4q},k3:B(){q jL=c.$K.1b("1z[1a-3c-aW-T]");$.1u(jL,$.M(B(i,s){q $s=$(s);q 2M=s.3l.4i(",");q 4q={eI:2M[0].4i(";")[0].4i(":")[1],1a:2M[1]};4q=c.cL(4q);$.7X(c.C.cJ,4q,$.M(B(1a){q 1K=(1E 1a==="8O"?$.8G(1a):1a);$s.1i("3l",1K.5Z);$s.2C("1a-3c-aW-T");c.1j();c.1e("3Z",$s,1K)},c))},c))},jK:B(e){q 9n=e.1O.9n;q 2M=9n.4i(",");q 4q={eI:2M[0].4i(";")[0].4i(":")[1],1a:2M[1]};if(c.C.9l){4q=c.cL(4q);$.7X(c.C.cJ,4q,$.M(B(1a){q 1K=(1E 1a==="8O"?$.8G(1a):1a);q o=\'<1z 3l="\'+1K.5Z+\'" id="jM-T-1M" />\';c.22("5b",o,E);q T=$(c.$K.1b("1z#jM-T-1M"));if(T.1m){T.2C("id")}I{T=E}c.1j();if(T){c.1e("3Z",T,1K)}},c))}I{c.9m(\'<1z 3l="\'+9n+\'" />\')}},1Z:B(2e){if(2e!==E){c.2e()}c.C.3J.3a(c.$K.o());if(2e!==E){c.7V("3J")}},jN:B(){if(c.C.3J.1m===0){c.4m();F}c.2e();c.C.7Y.3a(c.$K.o());c.1Y(E,N);c.$K.o(c.C.3J.jJ());c.1Y();2W($.M(c.7D,c),3P)},jO:B(){if(c.C.7Y.1m===0){c.4m();F E}c.2e();c.C.3J.3a(c.$K.o());c.1Y(E,N);c.$K.o(c.C.7Y.jJ());c.1Y(N);2W($.M(c.7D,c),4)},7D:B(){c.4F();if(c.C.5F){c.5F()}},5F:B(){c.$K.1b("a").on("25",$.M(c.jD,c));c.$K.on("25.U",$.M(B(e){c.7t(e)},c));$(X).on("25.U",$.M(B(e){c.7t(e)},c))},4F:B(){if(c.C.4F===E){F E}c.$K.1b("1z").1u($.M(B(i,1B){if(c.1C("3t")){$(1B).1i("nC","on")}q L=$(1B).L();if(!L.3v("cH")&&!L.3v("cM")){c.kZ(1B)}},c));c.$K.1b(".cM, .cH").on("25",$.M(c.oM,c))},jD:B(e){q $1s=$(e.1O);q L=$(e.1O).L();if(L.3v("cH")||L.3v("cM")){F}if($1s.1U()==0||$1s[0].Q!=="A"){F}q 3Y=$1s.2V();if(c.C.1Q){q cN=c.$2Z.2V();3Y.1T=cN.1T+(3Y.1T-$(c.X).3g());3Y.1t+=cN.1t}q 4w=$(\'<V 1x="U-1s-4w"></V>\');q 1R=$1s.1i("1R");if(1R===1I){1R=""}if(1R.1m>24){1R=1R.a8(0,24)+"..."}q jF=$(\'<a 1R="\'+$1s.1i("1R")+\'" 1O="6j">\'+1R+"</a>").on("25",$.M(B(e){c.7t(E)},c));q jG=$(\'<a 1R="#">\'+c.C.1F.84+"</a>").on("25",$.M(B(e){e.2x();c.an();c.7t(E)},c));q kv=$(\'<a 1R="#">\'+c.C.1F.67+"</a>").on("25",$.M(B(e){e.2x();c.22("67");c.7t(E)},c));4w.1h(jF);4w.1h(" | ");4w.1h(jG);4w.1h(" | ");4w.1h(kv);4w.1f({1T:(3Y.1T+20)+"px",1t:3Y.1t+"px"});$(".U-1s-4w").1w();$("2v").1h(4w)},7t:B(e){if(e!==E&&e.1O.Q=="A"){F E}$(".U-1s-4w").1w()},29:B(){if(!c.C.4a){F c.X.29()}I{if(!c.C.1Q){F 4a.29()}I{F 4a.29(c.$2Z[0])}}},3m:B(){if(!c.C.4a){if(c.X.29){q 1q=c.29();if(1q.41&&1q.51){F 1q.41(0)}}F c.X.5z()}I{if(!c.C.1Q){F 4a.5z()}I{F 4a.5z(c.bM())}}},cq:B(J){c.jQ(J)},4b:B(J){c.7O(J[0]||J,0,2J,0)},7e:B(J){c.7O(J[0]||J,1,2J,1)},7O:B(4U,cD,8k,9S){if(8k==2J){8k=4U}if(9S==2J){9S=cD}q 1q=c.29();if(!1q){F}if(4U.Q=="P"&&4U.3O==""){4U.3O=c.C.2i}if(4U.Q=="cP"&&c.C.1N===E){q 6r=$(c.C.5a)[0];$(4U).2f(6r);4U=6r;8k=4U}q O=c.3m();O.7W(4U,cD);O.8g(8k,9S);bK{1q.4O()}bI(e){}1q.5e(O)},cs:B(1d){1d=1d.3N();q 1l=c.2P();if(1l){q 3K=c.jH(1l,1d);c.1j();F 3K}q 1q=c.29();q O=1q.41(0);q 3K=X.4y(1d);3K.7c(O.oG());O.3A(3K);c.cq(3K);F 3K},oF:B(){q O=c.3m();O.9K(c.$K[0]);q 1q=c.29();1q.4O();1q.5e(O)},cn:B(){c.29().4O()},cT:B(2g){q co=0;q O=c.3m();q 9z=O.9p();9z.9K(2g);9z.8g(O.7Q,O.jP);co=$.28(9z.3T()).1m;F co},hx:B(){F 2a cp(c.29().41(0))},jQ:B(el,2u,3p){if(1E 3p==="1I"){3p=2u}el=el[0]||el;q O=c.3m();O.9K(el);q 4A=c.cy(el);q 9J=E;q 77=0,78;if(4A.1m==1&&2u){O.7W(4A[0],2u);O.8g(4A[0],3p)}I{3w(q i=0,8l;8l=4A[i++];){78=77+8l.1m;if(!9J&&2u>=77&&(2u<78||(2u==78&&i<4A.1m))){O.7W(8l,2u-77);9J=N}if(9J&&3p<=78){O.8g(8l,3p-77);8h}77=78}}q 1q=c.29();1q.4O();1q.5e(O)},jZ:B(J){c.$K.2j();J=J[0]||J;q O=c.X.5z();q 2u=1;q 3p=-1;O.7W(J,2u);O.8g(J,3p+2);q 1J=c.48.29();q cw=c.X.5z();q al=c.X.8d("\\7m");$(J).2E(al);cw.9d(al);1J.4O();1J.5e(cw);$(al).1w()},cy:B(J){q 4A=[];if(J.4v==3){4A.3a(J)}I{q 4k=J.8p;3w(q i=0,4T=4k.1m;i<4T;++i){4A.3a.cx(4A,c.cy(4k[i]))}}F 4A},3S:B(){q el=E;q 1q=c.29();if(1q&&1q.51>0){el=1q.41(0).5T}F c.4g(el)},2B:B(1B){1B=1B||c.3S();if(1B){F c.4g($(1B).L()[0])}I{F E}},2P:B(J){if(1E J==="1I"){J=c.3S()}3x(J){if(c.7n(J)){if($(J).3v("4e")){F E}F J}J=J.4Q}F E},3H:B(1V){q 8m=[];if(1E 1V=="1I"){q O=c.3m();if(O&&O.4M===N){F[c.2P()]}q 1V=c.7b(O)}$.1u(1V,$.M(B(i,J){if(c.C.1Q===E&&$(J).8y("12.4e").1U()==0){F E}if(c.7n(J)){8m.3a(J)}},c));if(8m.1m===0){8m=[c.2P()]}F 8m},oA:B(J){if(J.4v!=1){F E}F!c.a7.4c(J.jY)},7n:B(J){F J.4v==1&&c.a7.4c(J.jY)},bl:B(1d){F c.a7.4c(1d)},7b:B(O,1d){if(1E O=="1I"||O==E){q O=c.3m()}if(O&&O.4M===N){if(1E 1d==="1I"&&c.bl(1d)){q 1l=c.2P();if(1l.Q==1d){F[1l]}I{F[]}}I{F[c.3S()]}}q 1V=[],4z=[];q 1q=c.X.29();if(!1q.q2){1V=c.bk(1q.41(0))}$.1u(1V,$.M(B(i,J){if(c.C.1Q===E&&$(J).8y("12.4e").1U()==0){F E}if(1E 1d==="1I"){if($.28(J.9X)!=""){4z.3a(J)}}I{if(J.Q==1d){4z.3a(J)}}},c));if(4z.1m==0){if(1E 1d==="1I"&&c.bl(1d)){q 1l=c.2P();if(1l.Q==1d){F 4z.3a(1l)}I{F[]}}I{4z.3a(c.3S())}}q 2X=4z[4z.1m-1];if(c.7n(2X)){4z=4z.k0(0,-1)}F 4z},7U:B(J){if(!J){J=c.3S()}3x(J){if(J.4v==1){if($(J).3v("4e")){F E}F J}J=J.4Q}F E},bk:B(O){O=O||c.3m();q J=O.5T;q bL=O.7Q;if(J==bL){F[J]}q a5=[];3x(J&&J!=bL){a5.3a(J=c.jX(J))}J=O.5T;3x(J&&J!=O.oy){a5.k2(J);J=J.4Q}F a5},jX:B(J){if(J.oB()){F J.8z}I{3x(J&&!J.jS){J=J.4Q}if(!J){F 2J}F J.jS}},9V:B(){F c.29().3T()},jR:B(){q o="";q 1q=c.29();if(1q.51){q bB=c.X.4y("12");q 4T=1q.51;3w(q i=0;i<4T;++i){bB.7c(1q.41(i).oC())}o=bB.3O}F c.bF(o)},2e:B(){if(!c.7H()){c.4m()}if(!c.C.4a){c.jV(c.3m())}I{c.5M=4a.oE()}},jV:B(O,1w){if(!O){F}q 5K=$(\'<V id="1J-1M-1" 1x="U-1J-1M">\'+c.C.2i+"</V>",c.X)[0];q 7P=$(\'<V id="1J-1M-2" 1x="U-1J-1M">\'+c.C.2i+"</V>",c.X)[0];if(O.4M===N){c.9e(O,5K,N)}I{c.9e(O,5K,N);c.9e(O,7P,E)}c.5M=c.$K.o();c.1Y(E,E)},9e:B(O,J,1G){q 9j=O.9p();bK{9j.5I(1G);9j.3A(J);9j.oD()}bI(e){q o=c.C.5a;if(c.C.1N){o="<br>"}c.$K.6v(o);c.2j()}},1Y:B(G,1w){if(!c.C.4a){if(G===N&&c.5M){c.$K.o(c.5M)}q 5K=c.$K.1b("V#1J-1M-1");q 7P=c.$K.1b("V#1J-1M-2");if(c.1C("3c")){c.$K.2j()}I{if(!c.7H()){c.4m()}}if(5K.1m!=0&&7P.1m!=0){c.7O(5K[0],0,7P[0],0)}I{if(5K.1m!=0){c.7O(5K[0],0,2J,0)}}if(1w!==E){c.7V();c.5M=E}}I{4a.od(c.5M)}},7V:B(1G){if(!c.C.4a){$.1u(c.$K.1b("V.U-1J-1M"),B(){q o=$.28($(c).o().G(/[^\\oc-\\nP]/g,""));if(o==""){$(c).1w()}I{$(c).2C("1x").2C("id")}})}I{4a.nO(c.5M)}},l4:B(){c.2e();c.6l(c.C.1F.1n,c.C.hy,nN,$.M(B(){$("#hq").25($.M(c.kY,c));2W(B(){$("#eh").2j()},5l)},c))},kY:B(){c.1Z(E);q ae=$("#eh").1p(),ap=$("#hs").1p(),$bq=$("<12></12>"),bu=4x.jr(4x.j5()*j4),$1n=$(\'<1n id="1n\'+bu+\'"><7L></7L></1n>\'),i,$ak,z,$b9;3w(i=0;i<ae;i++){$ak=$("<3h></3h>");3w(z=0;z<ap;z++){$b9=$("<1g>"+c.C.2i+"</1g>");if(i===0&&z===0){$b9.1h(\'<V id="1J-1M-1">\'+c.C.2i+"</V>")}$($ak).1h($b9)}$1n.1h($ak)}$bq.1h($1n);q o=$bq.o();if(c.C.1N===E&&c.1C("3c")){o+="<p>"+c.C.2i+"</p>"}c.3b();c.1Y();q 1r=c.2P()||c.3S();if(1r&&1r.Q!="bp"){if(1r.Q=="3f"){q 1r=$(1r).2c("2q, ol")}$(1r).2E(o)}I{c.ah(o,E)}c.1Y();q 1n=c.$K.1b("#1n"+bu);c.7K();1n.1b("V#1J-1M-1, 4o#1J-1M-1").1w();1n.2C("id");c.1j()},l6:B(){q $1n=$(c.2B()).2c("1n");if(!c.4g($1n)){F E}if($1n.1U()==0){F E}c.1Z();$1n.1w();c.1j()},kV:B(){q L=c.2B();q $1n=$(L).2c("1n");if(!c.4g($1n)){F E}if($1n.1U()==0){F E}c.1Z();q $4r=$(L).2c("3h");q $bx=$4r.4Z().1m?$4r.4Z():$4r.4l();if($bx.1m){q $bw=$bx.4k("1g").l7();if($bw.1m){$bw.6v(\'<V id="1J-1M-1">\'+c.C.2i+"</V>")}}$4r.1w();c.1Y();$1n.1b("V#1J-1M-1").1w();c.1j()},kW:B(){q L=c.2B();q $1n=$(L).2c("1n");if(!c.4g($1n)){F E}if($1n.1U()==0){F E}c.1Z();q $6e=$(L).2c("1g");if(!($6e.is("1g"))){$6e=$6e.2c("1g")}q 2I=$6e.2R(0).nQ;$1n.1b("3h").1u($.M(B(i,1B){q ld=2I-1<0?2I+1:2I-1;if(i===0){$(1B).1b("1g").eq(ld).6v(\'<V id="1J-1M-1">\'+c.C.2i+"</V>")}$(1B).1b("1g").eq(2I).1w()},c));c.1Y();$1n.1b("V#1J-1M-1").1w();c.1j()},l8:B(){q $1n=$(c.2B()).2c("1n");if(!c.4g($1n)){F E}if($1n.1U()==0){F E}c.1Z();if($1n.1b("3X").1U()!==0){c.d8()}I{q 3h=$1n.1b("3h").l7().6f();3h.1b("1g").o(c.C.2i);$3X=$("<3X></3X>");$3X.1h(3h);$1n.6v($3X);c.1j()}},d8:B(){q $1n=$(c.2B()).2c("1n");if(!c.4g($1n)){F E}q $3X=$1n.1b("3X");if($3X.1U()==0){F E}c.1Z();$3X.1w();c.1j()},lc:B(){c.eE("3M")},la:B(){c.eE("2E")},kT:B(){c.eK("3M")},kR:B(){c.eK("2E")},eE:B(1G){q $1n=$(c.2B()).2c("1n");if(!c.4g($1n)){F E}if($1n.1U()==0){F E}c.1Z();q $4r=$(c.2B()).2c("3h");q bf=$4r.6f();bf.1b("1g").o(c.C.2i);if(1G==="2E"){$4r.2E(bf)}I{$4r.3M(bf)}c.1j()},eK:B(1G){q L=c.2B();q $1n=$(L).2c("1n");if(!c.4g($1n)){F E}if($1n.1U()==0){F E}c.1Z();q 2I=0;q 1r=c.3S();q $4r=$(1r).2c("3h");q $6e=$(1r).2c("1g");$4r.1b("1g").1u($.M(B(i,1B){if($(1B)[0]===$6e[0]){2I=i}},c));$1n.1b("3h").1u($.M(B(i,1B){q $1r=$(1B).1b("1g").eq(2I);q 1g=$1r.6f();1g.o(c.C.2i);1G==="2E"?$1r.2E(1g):$1r.3M(1g)},c));c.1j()},kC:B(){c.2e();c.6l(c.C.1F.3C,c.C.hp,nR,$.M(B(){$("#hv").25($.M(c.kD,c));2W(B(){$("#eT").2j()},5l)},c))},kD:B(){q 1a=$("#eT").1p();1a=c.7S(1a);q 5n=\'<1Q 2l="bW" 21="kQ" 3l="\',6U=\'" c2="0" kO></1Q>\';if(1a.1S(8s)){1a=1a.G(8s,5n+"//bg.bU.6V/4X/$1"+6U)}I{if(1a.1S(8x)){1a=1a.G(8x,5n+"//kH.bN.6V/3C/$2"+6U)}}c.1Y();q 1r=c.2P()||c.3S();if(1r){$(1r).2E(1a)}I{c.ah(1a,E)}c.1j();c.3b()},an:B(){c.2e();q 1e=$.M(B(){if(c.C.eQ!==E){c.ai={};q 4J=c;$.kI(c.C.eQ,B(1a){q $3n=$("#U-hz-hB");$3n.o("");$.1u(1a,B(1k,1p){4J.ai[1k]=1p;$3n.1h($("<47>").1p(1k).o(1p.2n))});$3n.on("5W",B(){q 1k=$(c).1p();q 2n="",1L="";if(1k!=0){2n=4J.ai[1k].2n;1L=4J.ai[1k].1L}$("#8j").1p(1L);$("#aa").1p(2n)});$3n.2N()})}c.7E=E;q 1q=c.29();q 1L="",Y="",1O="";q 1B=c.2B();q 6r=$(1B).L().2R(0);if(6r&&6r.Q==="A"){1B=6r}if(1B&&1B.Q==="A"){1L=1B.1R;Y=$(1B).Y();1O=1B.1O;c.7E=1B}I{Y=1q.3T()}$("#aa").1p(Y);q kE=kB.kA.1R.G(/\\/$/i,"");1L=1L.G(kE,"");1L=1L.G(/^\\/#/,"#");1L=1L.G("er:","");if(c.C.7I===E){q re=2a 2L("^(9W|9u|5C)://"+kB.kA.nT,"i");1L=1L.G(re,"")}$("#8j").1p(1L);if(1O==="6j"){$("#70").aS("aT",N)}c.eB=E;$("#hD").on("25",$.M(c.ky,c));2W(B(){$("#8j").2j()},5l)},c);c.6l(c.C.1F.1s,c.C.hA,nS,1e)},ky:B(){if(c.eB){F}c.eB=N;q 1O="",eS="";q 1s=$("#8j").1p();q Y=$("#aa").1p();if(1s.4G("@")!=-1&&/(9W|9u|5C):\\/\\//i.4c(1s)===E){1s="er:"+1s}I{if(1s.4G("#")!=0){if($("#70").aS("aT")){1O=\' 1O="6j"\';eS="6j"}q eo="((nM--)?[a-ep-9]+(-[a-ep-9]+)*.)+[a-z]{2,}";q re=2a 2L("^(9W|9u|5C)://"+eo,"i");q kM=2a 2L("^"+eo,"i");if(1s.4G(re)==-1&&1s.4G(kM)==0&&c.C.7I){1s=c.C.7I+1s}}}Y=Y.G(/<|>/g,"");q ek="&3u;";if(c.1C("3c")){ek="&3u;"}c.kK(\'<a 1R="\'+1s+\'"\'+1O+">"+Y+"</a>"+ek,$.28(Y),1s,eS)},kK:B(a,Y,1s,1O){c.1Y();if(Y!==""){if(c.7E){c.1Z();$(c.7E).Y(Y).1i("1R",1s);if(1O!==""){$(c.7E).1i("1O",1O)}I{$(c.7E).2C("1O")}}I{q $a=$(a).2y("U-eu-1s");c.2p("5b",c.49($a),E);q 1s=c.$K.1b("a.U-eu-1s");1s.2C("1o").3e("U-eu-1s").1u(B(){if(c.2U==""){$(c).2C("1x")}})}c.1j()}2W($.M(B(){if(c.C.5F){c.5F()}},c),5);c.3b()},kL:B(){c.2e();q 1e=$.M(B(){q 1q=c.29();q Y="";if(c.cv()){Y=1q.Y}I{Y=1q.3T()}$("#dE").1p(Y);if(!c.5c()&&!c.bH()){c.et("#5s",{1L:c.C.7N,3E:c.C.3E,4n:$.M(c.ew,c),3V:$.M(B(7F,1K){c.1e("ho",1K)},c),6x:c.C.dF})}c.e7("5s",{4d:N,1L:c.C.7N,4n:$.M(c.ew,c),3V:$.M(B(7F,1K){c.1e("ho",1K)},c)})},c);c.6l(c.C.1F.26,c.C.iw,bW,1e)},ew:B(1K){c.1Y();if(1K!==E){q Y=$("#dE").1p();if(Y===""){Y=1K.dI}q 1s=\'<a 1R="\'+1K.5Z+\'" id="5Z-1M">\'+Y+"</a>";if(c.1C("4C")&&!!c.48.cC){1s=1s+"&3u;"}c.22("5b",1s,E);q 7T=$(c.$K.1b("a#5Z-1M"));if(7T.1U()!=0){7T.2C("id")}I{7T=E}c.1j();c.1e("7N",7T,1K)}c.3b()},kJ:B(){c.2e();q 1e=$.M(B(){if(c.C.7k){$.kI(c.C.7k,$.M(B(1a){q 7j={},7o=0;$.1u(1a,$.M(B(1k,1p){if(1E 1p.a0!=="1I"){7o++;7j[1p.a0]=7o}},c));q 9c=E;$.1u(1a,$.M(B(1k,1p){q f0="";if(1E 1p.1c!=="1I"){f0=1p.1c}q 9G=0;if(!$.kx(7j)&&1E 1p.a0!=="1I"){9G=7j[1p.a0];if(9c===E){9c=".7h"+9G}}q 1z=$(\'<1z 3l="\'+1p.nL+\'" 1x="7h 7h\'+9G+\'" 4h="\'+1p.T+\'" 1c="\'+f0+\'" />\');$("#dN").1h(1z);$(1z).25($.M(c.iq,c))},c));if(!$.kx(7j)){$(".7h").2T();$(9c).2N();q kw=B(e){$(".7h").2T();$(".7h"+$(e.1O).1p()).2N()};q 3n=$(\'<3n id="nF">\');$.1u(7j,B(k,v){3n.1h($(\'<47 2t="\'+v+\'">\'+k+"</47>"))});$("#dN").3M(3n);3n.5W(kw)}},c))}I{$("#U-53-6A-2").1w()}if(c.C.3Z||c.C.5p){if(!c.5c()&&!c.bH()&&c.C.5p===E){if($("#5s").1m){c.et("#5s",{1L:c.C.3Z,3E:c.C.3E,4n:$.M(c.ed,c),3V:$.M(B(7F,1K){c.1e("by",1K)},c),6x:c.C.ba})}}if(c.C.5p===E){c.e7("5s",{4d:N,1L:c.C.3Z,4n:$.M(c.ed,c),3V:$.M(B(7F,1K){c.1e("by",1K)},c)})}I{$("#5s").on("5W.U",$.M(c.j6,c))}}I{$(".5x").2T();if(!c.C.7k){$("#5m").1w();$("#il").2N()}I{$("#U-53-6A-1").1w();$("#U-53-6A-2").2y("6S");$("#im").2N()}}if(!c.C.kF&&(c.C.3Z||c.C.7k)){$("#U-53-6A-3").2T()}$("#hV").25($.M(c.it,c));if(!c.C.3Z&&!c.C.7k){2W(B(){$("#6b").2j()},5l)}},c);c.6l(c.C.1F.T,c.C.ik,nE,1e)},jC:B(T){q $el=T;q L=$el.L().L();q 1e=$.M(B(){$("#dO").1p($el.1i("87"));$("#nD").1i("1R",$el.1i("3l"));if($el.1f("3i")=="1l"&&$el.1f("5o")=="3o"){$("#aQ").1p("6D")}I{$("#aQ").1p($el.1f("5o"))}if($(L).2R(0).Q==="A"){$("#6b").1p($(L).1i("1R"));if($(L).1i("1O")=="6j"){$("#70").aS("aT",N)}}$("#ii").25($.M(B(){c.d1($el)},c));$("#ij").25($.M(B(){c.l5($el)},c))},c);c.6l(c.C.1F.84,c.C.ip,nG,1e)},d1:B(el){q 8Y=$(el).L().L();q L=$(el).L();q ad=E;if(8Y.1m&&8Y[0].Q==="A"){ad=N;$(8Y).1w()}I{if(L.1m&&L[0].Q==="A"){ad=N;$(L).1w()}I{$(el).1w()}}if(L.1m&&L[0].Q==="P"){c.4m();if(ad===E){c.4b(L)}}c.1e("nH",el);c.3b();c.1j()},l5:B(el){c.62(E);q $el=$(el);q L=$el.L();$el.1i("87",$("#dO").1p());q bd=$("#aQ").1p();q 2G="";if(bd==="1t"){2G="0 "+c.C.8c+" "+c.C.8c+" 0";$el.1f({"5o":"1t",2G:2G})}I{if(bd==="4I"){2G="0 0 "+c.C.8c+" "+c.C.8c+"";$el.1f({"5o":"4I",2G:2G})}I{if(bd==="6D"){$el.1f({"5o":"",3i:"1l",2G:"4d"})}I{$el.1f({"5o":"",3i:"",2G:""})}}}q 1s=$.28($("#6b").1p());if(1s!==""){q 1O=E;if($("#70").aS("aT")){1O=N}if(L.2R(0).Q!=="A"){q a=$(\'<a 1R="\'+1s+\'">\'+c.49(el)+"</a>");if(1O){a.1i("1O","6j")}$el.2f(a)}I{L.1i("1R",1s);if(1O){L.1i("1O","6j")}I{L.2C("1O")}}}I{if(L.2R(0).Q==="A"){L.2f(c.49(el))}}c.3b();c.4F();c.1j()},62:B(e){if(e!==E&&$(e.1O).L().1U()!=0&&$(e.1O).L()[0].id==="U-T-2A"){F E}q 2Y=c.$K.1b("#U-T-2A");if(2Y.1U()==0){F E}c.$K.1b("#U-T-d6, #U-T-da").1w();2Y.1b("1z").1f({5H:2Y[0].1o.5H,9T:2Y[0].1o.9T,5D:2Y[0].1o.5D,9U:2Y[0].1o.9U});2Y.1f("2G","");2Y.1b("1z").1f("i5","");2Y.2f(B(){F $(c).1W()});$(X).3z("25.U-T-80-2T");c.$K.3z("25.U-T-80-2T");c.$K.3z("5y.U-T-d2");c.1j()},kZ:B(T){q $T=$(T);$T.on("8U",$.M(B(){c.62(E)},c));$T.on("nK",$.M(B(){c.$K.on("6y.U-T-l2-6y",$.M(B(){2W($.M(B(){c.4F();c.$K.3z("6y.U-T-l2-6y");c.1j()},c),1)},c))},c));$T.on("25",$.M(B(e){if(c.$K.1b("#U-T-2A").1U()!=0){F E}q nJ=E,9M,9F,dy=$T.2l()/$T.21(),i9=20,nI=10;q 5k=c.ib($T);q 9B=E;if(5k!==E){5k.on("8U",B(e){9B=N;e.2x();dy=$T.2l()/$T.21();9M=4x.6q(e.dw-$T.eq(0).2V().1t);9F=4x.6q(e.dg-$T.eq(0).2V().1T)});$(c.X.2v).on("jB",$.M(B(e){if(9B){q nU=4x.6q(e.dw-$T.eq(0).2V().1t)-9M;q i6=4x.6q(e.dg-$T.eq(0).2V().1T)-9F;q i7=$T.21();q i8=9C(i7,10)+i6;q 9H=4x.6q(i8*dy);if(9H>i9){$T.2l(9H);if(9H<3P){c.69.1f({5H:"-aM",5D:"-nV",8a:"o7",95:"o6 i1"})}I{c.69.1f({5H:"-92",5D:"-i4",8a:"92",95:"aM 9a"})}}9M=4x.6q(e.dw-$T.eq(0).2V().1t);9F=4x.6q(e.dg-$T.eq(0).2V().1T);c.1j()}},c)).on("9x",B(){9B=E})}c.$K.on("5y.U-T-d2",$.M(B(e){q 1k=e.6h;if(c.2O.9D==1k||c.2O.d3==1k){c.1Z(E);c.62(E);c.d1($T)}},c));$(X).on("25.U-T-80-2T",$.M(c.62,c));c.$K.on("25.U-T-80-2T",$.M(c.62,c))},c))},ib:B($T){q 2Y=$(\'<V id="U-T-2A" 1a-U="8R">\');2Y.1f({3j:"ia",3i:"4o-1l",d7:0,o5:"i3 o8 o9(0, 0, 0, .6)","5o":$T.1f("5o")});2Y.1i("3F",E);if($T[0].1o.2G!="4d"){2Y.1f({5H:$T[0].1o.5H,9T:$T[0].1o.9T,5D:$T[0].1o.5D,9U:$T[0].1o.9U});$T.1f("2G","")}I{2Y.1f({3i:"1l",2G:"4d"})}$T.1f("i5",0.5).2E(2Y);c.69=$(\'<V id="U-T-d6" 1a-U="8R">\'+c.C.1F.84+"</V>");c.69.1f({3j:"8B",8Q:5,1T:"50%",1t:"50%",5H:"-92",5D:"-i4",d7:1,ic:"#ie",8i:"#i2",8a:"92",95:"aM 9a",eM:"ob"});c.69.1i("3F",E);c.69.on("25",$.M(B(){c.jC($T)},c));2Y.1h(c.69);if(c.C.hX){q 5k=$(\'<V id="U-T-da" 1a-U="8R"></V>\');5k.1f({3j:"8B",8Q:2,d7:1,eM:"nw-80",i0:"-oa",4I:"-i1",o4:"i3 o3 #i2",ic:"#ie",2l:"ir",21:"ir"});5k.1i("3F",E);2Y.1h(5k);2Y.1h($T);F 5k}I{2Y.1h($T);F E}},iq:B(e){q 1z=\'<1z id="T-1M" 3l="\'+$(e.1O).1i("4h")+\'" 87="\'+$(e.1O).1i("1c")+\'" />\';q L=c.2B();if(c.C.6a&&$(L).2c("li").1U()==0){1z="<p>"+1z+"</p>"}c.9k(1z,N)},it:B(){q 1p=$("#6b").1p();if(1p!==""){q 1a=\'<1z id="T-1M" 3l="\'+1p+\'" />\';if(c.C.1N===E){1a="<p>"+1a+"</p>"}c.9k(1a,N)}I{c.3b()}},ed:B(1a){c.9k(1a)},9k:B(1K,1s){c.1Y();if(1K!==E){q o="";if(1s!==N){o=\'<1z id="T-1M" 3l="\'+1K.5Z+\'" />\';q L=c.2B();if(c.C.6a&&$(L).2c("li").1U()==0){o="<p>"+o+"</p>"}}I{o=1K}c.22("5b",o,E);q T=$(c.$K.1b("1z#T-1M"));if(T.1m){T.2C("id")}I{T=E}c.1j();1s!==N&&c.1e("3Z",T,1K)}c.3b();c.4F()},dY:B(){if($("#U-eO").1U()!=0){F}c.$aU=$(\'<12 id="U-eO"><V></V></12>\');$(X.2v).1h(c.$aU)},8I:B(){c.dY();c.$aU.nY()},9Z:B(){c.dY();c.$aU.6J(nX)},iu:B(){$.4H(c.C,{iw:4S()+\'<2m id="U-6g-26-4u"><2s id="nW" 5w="7X" 7q="" a6="9q/2s-1a"><2D>\'+c.C.1F.dI+\'</2D><2S 1G="Y" id="dE" 1x="6T" /><12 1o="2G-1T: aM;"><2S 1G="26" id="5s" 2n="\'+c.C.dF+\'" /></12></2s></2m>\',ip:4S()+\'<2m id="U-6g-T-84"><2D>\'+c.C.1F.1c+\'</2D><2S 1G="Y" id="dO" 1x="6T" /><2D>\'+c.C.1F.1s+\'</2D><2S 1G="Y" id="6b" 1x="6T" /><2D><2S 1G="hE" id="70"> \'+c.C.1F.dA+"</2D><2D>"+c.C.1F.io+\'</2D><3n id="aQ"><47 2t="3o">\'+c.C.1F.3o+\'</47><47 2t="1t">\'+c.C.1F.1t+\'</47><47 2t="6D">\'+c.C.1F.6D+\'</47><47 2t="4I">\'+c.C.1F.4I+\'</47></3n></2m><44><1D id="ii" 1x="4t nZ">\'+c.C.1F.ih+\'</1D><1D 1x="4t 6L">\'+c.C.1F.6I+\'</1D><1D id="ij" 1x="4t 6K">\'+c.C.1F.83+"</1D></44>",ik:4S()+\'<2m id="U-6g-T-4u"><12 id="5m"><a 1R="#" id="U-53-6A-1" 1x="6S">\'+c.C.1F.8r+\'</a><a 1R="#" id="U-53-6A-2">\'+c.C.1F.dR+\'</a><a 1R="#" id="U-53-6A-3">\'+c.C.1F.1s+\'</a></12><2s id="o0" 5w="7X" 7q="" a6="9q/2s-1a"><12 id="o2" 1x="5x"><2S 1G="26" id="5s" 2n="\'+c.C.ba+\'" /></12><12 id="im" 1x="5x" 1o="3i: 3o;"><12 id="dN"></12></12></2s><12 id="il" 1x="5x" 1o="3i: 3o;"><2D>\'+c.C.1F.hW+\'</2D><2S 1G="Y" 2n="6b" id="6b" 1x="6T"  /><br><br></12></2m><44><1D 1x="4t 6L">\'+c.C.1F.6I+\'</1D><1D 1x="4t 6K" id="hV">\'+c.C.1F.4u+"</1D></44>",hA:4S()+\'<2m id="U-6g-1s-4u"><3n id="U-hz-hB" 1o="2l: 99.5%; 3i: 3o;"></3n><2D>hC</2D><2S 1G="Y" 1x="6T" id="8j" /><2D>\'+c.C.1F.Y+\'</2D><2S 1G="Y" 1x="6T" id="aa" /><2D><2S 1G="hE" id="70"> \'+c.C.1F.dA+\'</2D></2m><44><1D 1x="4t 6L">\'+c.C.1F.6I+\'</1D><1D id="hD" 1x="4t 6K">\'+c.C.1F.4u+"</1D></44>",hy:4S()+\'<2m id="U-6g-1n-4u"><2D>\'+c.C.1F.ae+\'</2D><2S 1G="Y" 1U="5" 2t="2" id="eh" /><2D>\'+c.C.1F.ap+\'</2D><2S 1G="Y" 1U="5" 2t="3" id="hs" /></2m><44><1D 1x="4t 6L">\'+c.C.1F.6I+\'</1D><1D id="hq" 1x="4t 6K">\'+c.C.1F.4u+"</1D></44>",hp:4S()+\'<2m id="U-6g-3C-4u"><2s id="o1"><2D>\'+c.C.1F.ht+\'</2D><5R id="eT" 1o="2l: 99%; 21: oO;"></5R></2s></2m><44><1D 1x="4t 6L">\'+c.C.1F.6I+\'</1D><1D id="hv" 1x="4t 6K">\'+c.C.1F.4u+"</1D></44>"})},6l:B(1c,3I,2l,1e){c.hL();c.$a1=2l;c.$2Q=$("#fe");if(!c.$2Q.1m){c.$2Q=$(\'<12 id="fe" 1o="3i: 3o;" />\');c.$2Q.1h($(\'<12 id="eR">&oP;</12>\'));c.$2Q.1h($(\'<aI id="9i" />\'));c.$2Q.1h($(\'<12 id="at" />\'));c.$2Q.8D(X.2v)}$("#eR").on("25",$.M(c.3b,c));$(X).58($.M(c.ez,c));c.$K.58($.M(c.ez,c));c.hI(3I);c.hH(1c);c.ix();c.jh();c.hM();c.hJ();c.6Z=c.X.2v.3g;if(c.C.4K===E){c.6Z=c.$K.3g()}if(c.5c()===E){c.hU()}I{c.hO()}if(1E 1e==="B"){1e()}2W($.M(B(){c.1e("pE",c.$2Q)},c),11);$(X).3z("pD.6g");c.$2Q.1b("2S[1G=Y]").on("pC",$.M(B(e){if(e.6h===13){c.$2Q.1b(".6K").25();e.2x()}},c));F c.$2Q},hU:B(){c.$2Q.1f({3j:"9w",1T:"-d4",1t:"50%",2l:c.$a1+"px",5D:"-"+(c.$a1/2)+"px"}).2N();c.dB=$(X.2v).1f("af");$(X.2v).1f("af","8F");2W($.M(B(){q 21=c.$2Q.jk();c.$2Q.1f({1T:"50%",21:"4d",7R:"4d",5H:"-"+(21+10)/2+"px"})},c),15)},hO:B(){c.$2Q.1f({3j:"9w",2l:"3P%",21:"3P%",1T:"0",1t:"0",2G:"0",7R:"pF"}).2N()},hI:B(3I){c.6o=E;if(3I.3W("#")==0){c.6o=$(3I);$("#at").6d().1h(c.6o.o());c.6o.o("")}I{$("#at").6d().1h(3I)}},hH:B(1c){c.$2Q.1b("#9i").o(1c)},hJ:B(){q 42=c.$2Q.1b("44 1D").ar(".pG");q eD=42.1U();if(eD>0){$(42).1f("2l",(c.$a1/eD)+"px")}},hM:B(){c.$2Q.1b(".6L").on("25",$.M(c.3b,c))},hL:B(){if(c.C.eX){c.$8M=$("#eY");if(!c.$8M.1m){c.$8M=$(\'<12 id="eY" 1o="3i: 3o;"></12>\');$("2v").6v(c.$8M)}c.$8M.2N().on("25",$.M(c.3b,c))}},ix:B(){if(1E $.fn.iy!=="1I"){c.$2Q.iy({pI:"#9i"});c.$2Q.1b("#9i").1f("eM","pH")}},jh:B(){q $5m=$("#5m");if(!$5m.1m){F E}q 4J=c;$5m.1b("a").1u(B(i,s){i++;$(s).on("25",B(e){e.2x();$5m.1b("a").3e("6S");$(c).2y("6S");$(".5x").2T();$("#5x"+i).2N();$("#pB").1p(i);if(4J.5c()===E){q 21=4J.$2Q.jk();4J.$2Q.1f("2G-1T","-"+(21+10)/2+"px")}})})},ez:B(e){if(e.2O===c.2O.eA){c.3b();F E}},3b:B(){$("#eR").3z("25",c.3b);$("#fe").6J("pA",$.M(B(){q fb=$("#at");if(c.6o!==E){c.6o.o(fb.o());c.6o=E}fb.o("");if(c.C.eX){$("#eY").2T().3z("25",c.3b)}$(X).je("58",c.jd);c.$K.je("58",c.jd);c.1Y();if(c.C.4K&&c.6Z){$(c.X.2v).3g(c.6Z)}I{if(c.C.4K===E&&c.6Z){c.$K.3g(c.6Z)}}c.1e("pu")},c));if(c.5c()===E){$(X.2v).1f("af",c.dB?c.dB:"j7")}F E},pt:B(dC){$(".5x").2T();$("#5m").1b("a").3e("6S").eq(dC-1).2y("6S");$("#5x"+dC).2N()},j6:B(e){q 7d=e.1O.7d;3w(q i=0,f;f=7d[i];i++){c.e2(f)}},e2:B(26){c.ja(26,$.M(B(j9){c.jz(26,j9)},c))},ja:B(26,1e){q 2k=2a jy();q ee="?";if(c.C.5p.4G(/\\?/)!="-1"){ee="&"}2k.b1("ps",c.C.5p+ee+"2n="+26.2n+"&1G="+26.1G,N);if(2k.jl){2k.jl("Y/pv; pw=x-pz-py")}q 4J=c;2k.pJ=B(e){if(c.jv==4&&c.e9==5l){4J.8I();1e(pK(c.pW))}I{if(c.jv==4&&c.e9!=5l){}}};2k.jp()},ju:B(5w,1L){q 2k=2a jy();if("pV"in 2k){2k.b1(5w,1L,N)}I{if(1E jA!="1I"){2k=2a jA();2k.b1(5w,1L)}I{2k=2J}}F 2k},jz:B(26,1L){q 2k=c.ju("pU",1L);if(!2k){}I{2k.jt=$.M(B(){if(2k.e9==5l){c.9Z();q eg=1L.4i("?");if(!eg[0]){F E}c.1Y();q o="";o=\'<1z id="T-1M" 3l="\'+eg[0]+\'" />\';if(c.C.6a){o="<p>"+o+"</p>"}c.22("5b",o,E);q T=$(c.$K.1b("1z#T-1M"));if(T.1m){T.2C("id")}I{T=E}c.1j();c.1e("3Z",T,E);c.3b();c.4F()}I{}},c);2k.pX=B(){};2k.8r.pY=B(e){};2k.jo("q0-pZ",26.1G);2k.jo("x-pT-pS","pN-pM");2k.jp(26)}},e7:B(el,3Q){c.3B={1L:E,4n:E,3V:E,2u:E,e0:E,4d:E,2S:E};$.4H(c.3B,3Q);q $el=$("#"+el);if($el.1m&&$el[0].Q==="pL"){c.3B.2S=$el;c.el=$($el[0].2s)}I{c.el=$el}c.iC=c.el.1i("7q");if(c.3B.4d){$(c.3B.2S).5W($.M(B(e){c.el.dn(B(e){F E});c.dZ(e)},c))}I{if(c.3B.e0){$("#"+c.3B.e0).25($.M(c.dZ,c))}}},dZ:B(e){c.8I();c.iI(c.2g,c.js())},js:B(){c.id="f"+4x.jr(4x.j5()*j4);q d=c.X.4y("12");q 1Q=\'<1Q 1o="3i:3o" id="\'+c.id+\'" 2n="\'+c.id+\'"></1Q>\';d.3O=1Q;$(d).8D("2v");if(c.3B.2u){c.3B.2u()}$("#"+c.id).iJ($.M(c.iH,c));F c.id},iI:B(f,2n){if(c.3B.2S){q cZ="pO"+c.id,iN="pP"+c.id;c.2s=$(\'<2s  7q="\'+c.3B.1L+\'" 5w="eH" 1O="\'+2n+\'" 2n="\'+cZ+\'" id="\'+cZ+\'" a6="9q/2s-1a" />\');if(c.C.3E!==E&&1E c.C.3E==="3R"){$.1u(c.C.3E,$.M(B(k,v){if(v!=2J&&v.3T().3W("#")===0){v=$(v).1p()}q 8F=$("<2S/>",{1G:"8F",2n:k,2t:v});$(c.2s).1h(8F)},c))}q d0=c.3B.2S;q 8A=$(d0).6f();$(d0).1i("id",iN).3M(8A).8D(c.2s);$(c.2s).1f("3j","8B").1f("1T","-d4").1f("1t","-d4").8D("2v");c.2s.dn()}I{f.1i("1O",2n).1i("5w","eH").1i("a6","9q/2s-1a").1i("7q",c.3B.1L);c.2g.dn()}},iH:B(){q i=$("#"+c.id)[0],d;if(i.iG){d=i.iG}I{if(i.dp){d=i.dp.X}I{d=48.pR[c.id].X}}if(c.3B.4n){c.9Z();if(1E d!=="1I"){q iA=d.2v.3O;q 7g=iA.1S(/\\{(.|\\n)*\\}/)[0];7g=7g.G(/^\\[/,"");7g=7g.G(/\\]$/,"");q 1K=$.8G(7g);if(1E 1K.3V=="1I"){c.3B.4n(1K)}I{c.3B.3V(c,1K);c.3b()}}I{c.3b();pQ("iz pr!")}}c.el.1i("7q",c.iC);c.el.1i("1O","")},et:B(el,3Q){c.5P=$.4H({1L:E,4n:E,3V:E,pq:E,3E:E,Y:c.C.1F.iD,iO:c.C.1F.iF,6x:E},3Q);if(48.eP===1I){F E}c.eU=$(\'<12 1x="p2"></12>\');c.5f=$(\'<12 1x="p1">\'+c.5P.Y+"</12>");c.iP=$(\'<12 1x="p0">\'+c.5P.iO+"</12>");c.eU.1h(c.5f);$(el).3M(c.eU);$(el).3M(c.iP);c.5f.on("p3",$.M(B(){F c.iU()},c));c.5f.on("p4",$.M(B(){F c.hY()},c));c.5f.2R(0).p6=$.M(B(e){e.2x();c.5f.3e("bo").2y("6y");c.8I();c.ey(c.5P.1L,e.8T.7d[0],E,e,c.5P.6x)},c)},ey:B(1L,26,bv,e,6x){if(!bv){q 2k=$.p5.2k();if(2k.8r){2k.8r.oZ("eO",$.M(c.iV,c),E)}$.oY({2k:B(){F 2k}})}c.1e("6y",e);q fd=2a eP();if(6x!==E){fd.1h(6x,26)}I{fd.1h("26",26)}if(c.C.3E!==E&&1E c.C.3E==="3R"){$.1u(c.C.3E,$.M(B(k,v){if(v!=2J&&v.3T().3W("#")===0){v=$(v).1p()}fd.1h(k,v)},c))}$.j2({1L:1L,oS:"o",1a:fd,oR:E,eI:E,oQ:E,1G:"eH",4n:$.M(B(1a){1a=1a.G(/^\\[/,"");1a=1a.G(/\\]$/,"");q 1K=(1E 1a==="8O"?$.8G(1a):1a);c.9Z();if(bv){q $1z=$("<1z>");$1z.1i("3l",1K.5Z).1i("id","iT-T-1M");c.iS(e,$1z[0]);q T=$(c.$K.1b("1z#iT-T-1M"));if(T.1m){T.2C("id")}I{T=E}c.1j();c.4F();if(T){c.1e("3Z",T,1K)}if(1E 1K.3V!=="1I"){c.1e("by",1K)}}I{if(1E 1K.3V=="1I"){c.5P.4n(1K)}I{c.5P.3V(c,1K);c.5P.4n(E)}}},c)})},iU:B(){c.5f.2y("bo");F E},hY:B(){c.5f.3e("bo");F E},iV:B(e,Y){q j0=e.iQ?9C(e.iQ/e.oT*3P,10):e;c.5f.Y("oU "+j0+"% "+(Y||""))},5c:B(){F/(oX|oW|oV|p7)/.4c(6k.7i)},bH:B(){F/p8/.4c(6k.7i)},9g:B(4D){if(1E(4D)==="1I"){F 0}F 9C(4D.G("px",""),10)},49:B(el){F $("<12>").1h($(el).eq(0).6f()).o()},pl:B(o){q 2H=X.4y("8J");2H.3O=o;F 2H.9X||2H.bC||""},iE:B(7F){F pk.5g.3T.5B(7F)=="[3R 4S]"},a4:B(o){o=o.G(/&#aY;|<br>|<br\\/>|&3u;/gi,"");o=o.G(/\\s/g,"");o=o.G(/^<p>[^\\W\\w\\D\\d]*?<\\/p>$/i,"");F o==""},pj:B(){q rv=E;if(6k.pm=="pn pp po"){q 3U=6k.7i;q re=2a 2L("pi ([0-9]{1,}[.0-9]{0,})");if(re.2p(3U)!=2J){rv=pg(2L.$1)}}F rv},8N:B(){F!!6k.7i.1S(/pb\\/7\\./)},1C:B(1C){q 3U=6k.7i.3N();q 1S=/(jc)[\\/]([\\w.]+)/.2p(3U)||/(cC)[ \\/]([\\w.]+)/.2p(3U)||/(4C)[ \\/]([\\w.]+).*(pa)[ \\/]([\\w.]+)/.2p(3U)||/(4C)[ \\/]([\\w.]+)/.2p(3U)||/(8S)(?:.*9y|)[ \\/]([\\w.]+)/.2p(3U)||/(3t) ([\\w.]+)/.2p(3U)||3U.3W("p9")>=0&&/(rv)(?::| )([\\w.]+)/.2p(3U)||3U.3W("pc")<0&&/(3c)(?:.*? rv:([\\w.]+)|)/.2p(3U)||[];if(1C=="9y"){F 1S[2]}if(1C=="4C"){F(1S[1]=="cC"||1S[1]=="4C")}if(1S[1]=="rv"){F 1C=="3t"}if(1S[1]=="jc"){F 1C=="4C"}F 1C==1S[1]},cv:B(){if(c.1C("3t")&&9C(c.1C("9y"),10)<9){F N}F E},hR:B(hQ){q 7a=hQ.pd(N);q 12=c.X.4y("12");12.7c(7a);F 12.3O},cE:B(){q J=c.$K[0];q 4f=c.X.cO();q cQ;3x((cQ=J.8z)){4f.7c(cQ)}F 4f},4g:B(el){if(!el){F E}if(c.C.1Q){F el}if($(el).8y("12.4e").1m==0||$(el).3v("4e")){F E}I{F el}},7s:B(Q){q L=c.2B(),1r=c.3S();F L&&L.Q===Q?L:1r&&1r.Q===Q?1r:E},cR:B(){q 1r=c.2P();q 2V=c.cT(1r);q Y=$.28($(1r).Y()).G(/\\n\\r\\n/g,"");q 4T=Y.1m;if(2V==4T){F N}I{F E}},7H:B(){q el,1q=c.29();if(1q&&1q.51&&1q.51>0){el=1q.41(0).5T}if(!el){F E}if(c.C.1Q){if(c.hx().iv()){F!c.$K.is(el)}I{F N}}F $(el).2c("12.4e").1m!=0},4V:B(el,1i){if($(el).1i(1i)==""){$(el).2C(1i)}},lb:B(9s,2t){q 2I=2J;3x((2I=9s.3W(2t))!==-1){9s.9r(2I,1)}F 9s}};3G.5g.7B.5g=3G.5g;$.3G.fn.c6=B(c8,6P,68,6O,6N){q 1L=/(((5C?|pf?):\\/\\/)|bg[.][^\\s])(.+?\\..+?)([.),]?)(\\s|\\.\\s+|\\)|$)/gi,kU=/(5C?|9u):\\/\\//i,bS=/(5C?:\\/\\/.*\\.(?:le|pe|kP|kN))/gi;q 8p=(c.$K?c.$K.2R(0):c).8p,i=8p.1m;3x(i--){q n=8p[i];if(n.4v===3){q o=n.aZ;if(6O&&o){q 5n=\'<1Q 2l="bW" 21="kQ" 3l="\',6U=\'" c2="0" kO></1Q>\';if(o.1S(8s)){o=o.G(8s,5n+"//bg.bU.6V/4X/$1"+6U);$(n).2E(o).1w()}I{if(o.1S(8x)){o=o.G(8x,5n+"//kH.bN.6V/3C/$2"+6U);$(n).2E(o).1w()}}}if(68&&o&&o.1S(bS)){o=o.G(bS,\'<1z 3l="$1">\');$(n).2E(o).1w()}if(6P&&o&&o.1S(1L)){q 2w=o.1S(1L);3w(q i in 2w){q 1R=2w[i];q Y=1R;q 6s="";if(1R.1S(/\\s$/)!==2J){6s=" "}q cj=c8;if(1R.1S(kU)!==2J){cj=""}if(Y.1m>6N){Y=Y.a8(0,6N)+"..."}Y=Y.G(/&/g,"&ab;").G(/</g,"&lt;").G(/>/g,"&gt;");q ku=Y.G("$","$$$");o=o.G(1R,\'<a 1R="\'+cj+$.28(1R)+\'">\'+$.28(ku)+"</a>"+6s)}$(n).2E(o).1w()}}I{if(n.4v===1&&!/^(a|1D|5R)$/i.4c(n.Q)){$.3G.fn.c6.5B(n,c8,6P,68,6O,6N)}}}}})(oN);',62,1711,'||||||||||||this||||||||||||html||var|||||||||||function|opts||false|return|replace||else|node|editor|parent|proxy|true|range||tagName|||image|redactor|span||document|text||||div||||||||data|find|title|tag|callback|css|td|append|attr|sync|key|block|length|table|style|val|sel|current|link|left|each|source|remove|class|lang|img|toolbar|elem|browser|button|typeof|curLang|type|func|undefined|selection|json|url|marker|linebreaks|target|dropdown|iframe|href|match|top|size|nodes|contents|out|selectionRestore|bufferSet||height|execCommand|cmd||click|file||trim|getSelection|new|btn|closest|btnObject|selectionSave|replaceWith|element|blockquote|invisibleSpace|focus|xhr|width|section|name|code|exec|ul|pre|form|value|start|body|matches|preventDefault|addClass|btnName|box|getParent|removeAttr|label|after|font|margin|tmp|index|null|param|RegExp|arr|show|keyCode|getBlock|redactorModal|get|input|hide|className|offset|setTimeout|last|imageBox|frame|list|point|||||||||push|modalClose|mozilla|air|removeClass|LI|scrollTop|tr|display|position|ctrl|src|getRange|select|none|end|bold|italic|script|msie|nbsp|hasClass|for|while|doc|off|insertNode|uploadOptions|video|blocks|uploadFields|contenteditable|Redactor|getBlocks|content|buffer|wrapper|BLOCKQUOTE|before|toLowerCase|innerHTML|100|options|object|getCurrent|toString|ua|error|indexOf|thead|pos|imageUpload||getRangeAt|buttons|strong|footer|||option|window|outerHtml|rangy|selectionStart|test|auto|redactor_editor|frag|isParentRedactor|rel|split|fullpage|children|next|focusWithSaveScroll|success|inline|elems|postData|current_tr|buttonGet|redactor_modal_btn|insert|nodeType|tooltip|Math|createElement|finalnodes|textNodes|htmls|webkit|str|meta|observeImages|search|extend|right|that|autoresize|inArray|collapsed|substr|removeAllRanges|php|parentNode|placeholder|String|len|orgn|removeEmptyAttr|underline|embed|cont|prev||rangeCount|keys|tab|align|blockElem|del|redactor_placeholder|keyup||emptyHtml|inserthtml|isMobile|shiftKey|addRange|dropareabox|prototype|rule|formatBlocks|deleted|imageResizer|200|redactor_tabs|iframeStart|float|s3|listTag|alignmentTags|redactor_file|audio|charAt|replaced|method|redactor_tab|keydown|createRange|newTag|call|https|marginLeft|listCurrent|observeLinks|allowedTags|marginTop|collapse|strike|node1|visual|savedSel|lastNode|selectall|draguploadOptions|regex|textarea|indent|startContainer|join|unorderedlist|change|boldTag|italicTag|filelink|||imageResizeHide|direction|toolbarFixed|||unlink|convertImageLinks|imageEditter|paragraphy|redactor_file_link|formatting|empty|current_td|clone|modal|which|TD|_blank|navigator|modalInit|dir|modified|modalcontent|shortcuts|round|par|space|event|orderedlist|prepend|PRE|uploadParam|drop|textareamode|control|formatBlock|Insert|center|enter|listParent|replacementTag|instance|cancel|fadeOut|redactor_modal_action_btn|redactor_btn_modal_close|buttonActive|linkSize|convertVideoLinks|convertLinks|insertAfterLastElement|tmpList|redactor_tabs_act|redactor_input|iframeEnd|com|outdent|buttonBuild|blocksElems|saveModalScroll|redactor_link_blank|weight|isFunction||possible|etags|item|charCount|endCharCount|cleanRemoveSpaces|cloned|getNodes|appendChild|files|selectionEnd|thtml|jsonString|redactorfolder|userAgent|folders|imageGetJson|phpMatches|u200B|nodeTestBlocks|count|autosave|action|hideHandler|currentOrParentIs|linkObserverTooltipClose|dropact|newblock|modif|set|tabindex|uuid|fullpageDoctype|init|cleanGetTabs|observeStart|insert_link_node|obj|elements|isFocused|linkProtocol|setNonEditable|buttonActiveObserver|tbody|deniedTags|fileUpload|selectionSet|node2|endContainer|minHeight|cleanStripTags|linkmarker|getElement|selectionRemoveMarkers|setStart|post|rebuffer|tagblock|resize|tagout|insertunorderedlist|save|edit|rtePaste|cleanlevel|alt|alignmentSet|dblEnter|fontSize|horizontalrule|imageFloatMargin|createTextNode|phpTags|insertorderedlist|setEnd|break|color|redactor_link_url|focn|textNode|newnodes|tmpLi|Delete|childNodes|Header|upload|reUrlYoutube|redactor_act|typewriter|boxTop|toolbarFixedTarget|reUrlVimeo|parents|firstChild|newElement|absolute|character|appendTo|shift|hidden|parseJSON|keyPosition|showProgressBar|DIV|metaKey|Add|redactorModalOverlay|isIe11|string|toolbarExternal|zIndex|verified|opera|dataTransfer|mousedown|specialKey|hdlHideDropDown|special|parentLink|one|||11px|spans|allowed|padding|||||10px|background|folderclass|setStartAfter|selectionSetMarker|codeLength|normalize|airBindMousemoveHide|redactor_modal_header|boundaryRange|imageInsert|clipboardUpload|insertHtml|result|placeTag|cloneRange|multipart|splice|array|dropdownHideAll|ftp|line|fixed|mouseup|version|preCaretRange||isResizing|parseInt|BACKSPACE|focusSet|start_y|folderkey|new_w|javascript|foundStart|selectNodeContents|documentElement|start_x|autosaveInterval|case|inlineRemoveFormatReplace|direct|htmlTagName|foco|marginBottom|marginRight|getSelectionText|http|textContent|alignment|hideProgressBar|folder|redactorModalWidth|template|toolbarFixedTopOffset|isEmpty|rangeNodes|enctype|rTestBlock|substring|strip|redactor_link_url_text|amp|blockLevelElements|parentEl|rows|overflow|tabSpaces|insertHtmlAdvanced|predefinedLinksStorage|separator|row|emptyElement|ASIDE|linkShow|ARTICLE|columns|clickedElement|not|inlineMethods|redactor_modal_inner|outer|selectionHtml|ctrlKey|cleanConvertInlineTags|deleteContents|wrapperHtml|methodVal|activeButtons|listText|insertLineBreak|parentHtml|sourceHeight|insertAfter|fonts|header|indentValue|head|pasteInsert|7px|setSpansVerified|tablePaste|convertDivs|redactor_form_image_align|selected|prop|checked|progressBar|setSpansVerifiedHtml|paste|saveScroll|x200b|nodeValue|buildCodearea|open|ADDRESS|ENTER|toolbarFixedBox|icon|HEADER|classname|FOOTER|column|imageUploadParam|execPasteFrag|colspan|floating||new_tr|www|rowspan|SECTION|cleanParagraphy|getRangeSelectedNodes|tagTestBlock|iframePage|oldsafari|hover|BODY|table_box||redactor_toolbar|cleanSavePreCode|tableId|directupload|focus_td|focus_tr|imageUploadError|buttonsHideOnMobile|Table|container|innerText|elem2|savedHtml|syncClean|Column|isIPad|catch|Row|try|endNode|iframeDoc|vimeo|blur|decoration|placeholderText|formatQuote|urlImage|paragraphsOnly|youtube|cleanFontTag|500|focusEnd|pasteClipboardMozilla|cleanEncodeEntities|formatblock|placeholderOnBlur|frameborder|placeholderOnFocus|placeholderGet|address|formatLinkify|INLINE|protocol|currBlock|utag|cleanRemoveEmptyTags|parseHTML|pasteHTML|endRange|insertingAfterLastElement|tfoot|maxHeight|buildEnable|addProtocol|inlineEachNodes|fullscreen|buildAfter|selectionRemove|caretOffset|Range|selectionElement|hotkeysShiftNums|selectionWrap|cleanTag|cleanFinish|oldIE|cursorRange|apply|getTextNodesIn|setFullpageOnInit|toggle|dnbImageTypes|chrome|orgo|extractContent|cleannewLevel|iframeLoad|royalSlider|iframeAddCss|clipboardUploadUrl|buildOptions|pasteClipboardAppendFields|fotorama|posFrame|createDocumentFragment|BR|child|isEndOfElement|suffix|getCaretOffset|buildBindKeyboard|toolbarObserveScroll|delete_table|delete_row|delete_column|formId|oldElement|imageRemove|delete|DELETE|2000px|templateVars|editter|lineHeight|tableDeleteHead|delete_head|resizer|cleanConverters|btnHeight||cleanConvertProtected|RedactorPlugins|pageY|arrSearch|etagsInline|align_right|alignright||redactor_button_disabled|submit|removeEmptyTags|contentWindow|dropdownWidth|indentingIndent|align_center||align_left|alignleft|pageX|aligncenter|ratio|indentingOutdent|link_new_tab|modalSaveBodyOveflow|num|add_head|redactor_filename|fileUploadParam|alignmentCenter|insideOutdent|filename|rBlockTest|justify|alignmentJustify|contOwnLine|redactor_image_box|redactor_file_alt|FIGCAPTION|alignmentLeft|choose|insert_table|alignmentRight|ownLine|buttonName|buttonInactive|insert_row_above|buildProgressBar|uploadSubmit|trigger|dropdownHide|s3uploadFile|insert_column_right|indentingStart|cleanEmpty|plugins|uploadInit|dropdownShow|status|insert_row_below|childList|insert_column_left|imageCallback|mark|buildEventKeydownInsertLineBreak|s3image|redactor_table_rows|align_justify|modify|extra||||pattern|z0||mailto|reader|draguploadInit|added|DOWN|fileCallback|lineOrWord|dragUploadAjax|modalCloseHandler|ESC|linkInsertPressed|placeholderRemove|buttonsSize|tableAddRow|activeButtonsStates|safes|POST|contentType|link_insert|tableAddColumn|clipboardData|cursor|th|progress|FormData|predefinedLinks|redactor_modal_close|targetBlank|redactor_insert_video_area|droparea|airShow|merge|modalOverlay|redactor_modal_overlay|sourceOld|thumbtitle|||||||||||redactorModalInner|focusElem||redactor_modal|commentsMatches|iframeAppend|superscript|textareaKeydownCallback|shortcutsHandler|Array|beforekey|inlineUnwrapSpan||subscript|placeholderFocus|redactor_dropdown_link|dragUpload|buildEventKeyup|inlineFormat|newhtml|tabFocus|blocksElemsRemove|buildEventKeydownTab|focusCallback|firstParent|unwrap|placeholderRemoveFromEditor|strikethrough|formattingPre|execLists|TAB|execUnlink|blocksLen|wym|UL|buttonInactiveAll|inlineSetMethods|placeholderBlur|paragraphs|min|redactor_editor_wym|toolbarInit|clonedHtml|placeholderRemoveFromCode|buildEventKeydownPre|link_edit|buttonActiveToggle|buildEventKeydownBackspace|inserthorizontalrule|fromElement|toTagName|afterkey|command|buildEventDrop|filter|transparent|close|toolbarBuild|toolbar_fixed_box|airButtons|buildEventKeydown|fieldset|airEnable|buildEventPaste|LEFT_WIN|mod|clearInterval|redactor_air|altKey||area|map|cleanup|clipboardFilePaste|scroll|originalEvent|10005|toolbarOverflow|redactor_air_|tags||iframeCreate|buildEventClipboardUpload|write|visibility|cleanUnverified|items|buttonSource|formattingTags|quot|encode|pastedFrag|cleanHtml|timer|219|cleanlineAfter|buttonImage|hotkeysSpecialKeys|cleanlineBefore|cleanReConvertProtected|listCurrentText|placeholderStart|returnValue|redactor_dropdown|wrapAll|buildEventKeyupConverters|buttonActiveVisual|dropdownBuild|placeholderInit|buildPlugins|formatEmpty|double|innerHeight|tidyHtml|||||||airBindHide|toggleCode|indenting|textareaIndenting|keyboard|setInterval|innerWidth|collapseToStart|dropdownObject|origHandler|buffer_|cleanReplacer|arguments|u200D|toggleVisual|uFEFF|buttonInactiveVisual|fileUploadError|modal_video|redactor_insert_table_btn||redactor_table_columns|video_html_code|H5|redactor_insert_video_btn|header5|getCaretOffsetRange|modal_table|predefined|modal_link|links|URL|redactor_insert_link_btn|checkbox|H6|header4|modalSetTitle|modalSetContent|modalSetButtonsWidth|DL|modalSetOverlay|modalOnCloseButton|DD|modalShowOnMobile|header3|fragment|getFragmentHtml|header2|header1|modalShowOnDesktop|redactor_upload_btn|image_web_link|imageResizable|draguploadOndragleave|H4|bottom|5px|fff|1px|18px|opacity|mouse_y|div_h|new_h|min_w|relative|imageResizeControls|backgroundColor||000|||_delete|redactor_image_delete_btn|redactorSaveBtn|modal_image|redactor_tab3|redactor_tab2||image_position|modal_image_edit|imageThumbClick|8px||imageCallbackLink|modalTemplatesInit|equals|modal_file|modalSetDraggable|draggable|Upload|rawString|Center|element_action|drop_file_here|isString|or_choose|contentDocument|uploadLoaded|uploadForm|load|the|to|Align|fileId|atext|dropalternative|loaded|Left|insertNodeToCaretPositionFromPoint|drag|draguploadOndrag|uploadProgress|Right|Image|Video|Code|percent|Head|ajax|Link|99999|random|s3handleFileSelect|visible|arrAdd|signedURL|s3executeOnSignedUrl|newLevel|opr|hdlModalClose|unbind|paragraph|quote|modalLoadTabs|shortcutsAdd|langs|outerHeight|overrideMimeType|DT|Color|setRequestHeader|send|List|floor|uploadFrame|onload|s3createCORSRequest|readyState|OUTPUT|Edit|XMLHttpRequest|s3uploadToS3|XDomainRequest|mousemove|imageEdit|linkObserver|linkNofollow|aLink|aEdit|formatChangeTag|nofollow|pop|pasteClipboardUpload|imgs|clipboard|bufferUndo|bufferRedo|endOffset|setCaret|getSelectionHtml|nextSibling|xhtmlTags|xhtml|selectionCreateMarker|args|nextNode|nodeName|setCaretAfter|slice|setFullpageDoctype|unshift|pasteClipboardUploadMozilla|noeditable|clientX|buildContent|clientY|caretPositionFromPoint|buildMobile|outerHTML|buildFromTextarea|HTML|buildAddClasses|aside|article|buildFromElement|caretRangeFromPoint|createTextRange|pastePlainText|pasteClean|cleanSpaces|internal|pastePre|replaceLineBreak|twice|mobile|moveToPoint|buildStart|SPAN|escapedBackReferences|aUnlink|onchangeFunc|isEmptyObject|linkProcess|removeFormat|location|self|videoShow|videoInsert|thref|imageTabLink|H1|player|getJSON|imageShow|linkInsert|fileShow|re2|gif|allowfullscreen|jpeg|281|tableAddColumnRight|setEditor|tableAddColumnLeft|rProtocol|tableDeleteRow|tableDeleteColumn|setCodeIframe|tableInsert|imageResize|H3|getCodeIframe|inside|doctype|tableShow|imageSave|tableDeleteTable|first|tableAddHead|H2|tableAddRowBelow|removeFromArrayByValue|tableAddRowAbove|focusIndex|png|Indent|Outdent|ownerDocument||frameset|103|Cancel|104|Save|107|Rows|defaultView|Columns|Title||Below|106|Above|102|stylesheet|105|Back|Normal|Formatting|Unlink|ltr|Quote|backspace|applet|use|strict|VERSION|such|removeChild|youtu|startOffset|isArray|eval|Bold|Italic|pagedown|pageup|esc|home|up|101|down|capslock|Ordered|Font|fontcolor|backcolor|No|Unordered|pause|noscript|redactor_format_h2|getAsFile|FileReader|getToolbar|readAsDataURL|about|blank|Chrome|536|noneditable|getIframe|word|getEditor|getObject|removeData|destroy|redo|getBox|collapseToEnd|109|undo|syncBefore|syncAfter|dash|u2010|mdash|u2014|enableInlineTableEditing|enableObjectResizing|45px|TEXTAREA|redactor_box|hellip|u2026|blurCallback|8203|default|switch|1class|u2122|copy|u00a9|trade|Callback|TH|Horizontal|Justify|here|Drop|Rule|Deleted|Open|Anchor|anchor|Or|Choose|Email|Text|Web|None|web|Embed|Download|download|File|Underline|Alignment|redactor_format_h5|redactor_format_h4|redactor_format_h3|redactor_|bull|separator_drop1|alignjustify||separator_drop3|separator_drop2|redactor_format_h1|redactor_format_pre|META|CTRL||optional|Name|LEFT||dropdowns|redactor_format_blockquote|u00a0|sourceWidth|Position|unselectable|redactor_image_edit_src|610|redactor_image_box_select|380|imageDelete|min_h|clicked|dragstart|thumb|xn|300|removeMarkers|u1C7F|cellIndex|600|460|host|mouse_x|13px|redactorUploadFileForm|1500|fadeIn|redactor_modal_delete_btn|redactorInsertImageForm|redactorInsertVideoForm|redactor_tab1|solid|border|outline|3px|9px|dashed|rgba|4px|pointer|u0000|restoreSelection|pasteBefore|insertDoubleLineBreak|EndToEnd|shapes|MsoListParagraphCxSpFirst|MsoListParagraphCxSpLast|MsoListParagraphCxSpMiddle||setEndPoint||duplicate|toUpperCase|inlineRemoveFormat|attributes|insertBeforeCursor|insertText|offsetNode|setEndAfter|MsoListParagraph|sid|commonAncestorContainer|110|isInlineNode|hasChildNodes|cloneContents|detach|saveSelection|selectionAll|extractContents|u200b|guid|docs|fake|pasteAfter|editGallery|jQuery|160px|times|processData|cache|dataType|total|Loading|BlackBerry|iPod|iPhone|ajaxSetup|addEventListener|redactor_dropalternative|redactor_dropareabox|redactor_droparea|dragover|dragleave|ajaxSettings|ondrop|Android|iPad|trident|safari|Trident|compatible|cloneNode|jpg|ftps|parseFloat||MSIE|getInternetExplorerVersion|Object|stripHtml|appName|Microsoft|Explorer|Internet|preview|failed|GET|modalSetTab|modalClosed|plain|charset||defined|user|fast|redactor_tab_selected|keypress|focusin|modalOpened|300px|redactor_modal_btn_hidden|move|handle|onreadystatechange|decodeURIComponent|INPUT|read|public|redactorUploadForm|redactorUploadFile|alert|frames|acl|amz|PUT|withCredentials|responseText|onerror|onprogress|Type|Content|inlineSetAttr|isCollapsed|external|redactor_toolbar_|focusNode|redactor_separator_drop|dropdownShown|redactor_dropdown_|1000|autosaveError|fromCharCode|inlineRemoveAttr|Key|escape|encodeURIComponent|touchstart|stopPropagation|buttonAddBefore|buttonAddAfter|buttonRemove|firstNode|SUB|SUP|buttonAddFirst|buttonAdd|buttonTagToActiveState|redactor_dropdown_box_|buttonChangeIcon|buttonRemoveIcon|buttonAwesome|222|221|119|118|120|121|122|f10|117|116|112|111|113|114|115|f11|123|189|188|190|191|220|192|187|186|144|f12|numlock|145|173|OL|_|details|menu|hasOwnProperty|inlineSetClass|figcaption|inlineRemoveStyle|hgroup|nav|figure|inlineSetStyle|inlineRemoveClass|sub|blockSetClass||blockRemoveStyle|blockRemoveClass|blockSetStyle|blockSetAttr|blockRemoveAttr|sup|small|cite|legend|summary|caption|concat|col|JustifyCenter|JustifyFull|gallery||comment|colgroup|JustifyRight|math|JustifyLeft'.split('|'),0,{}))
\ No newline at end of file
+eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('(C($){\'mI m0\';if(!g7.5w.aH){g7.5w.aH=C(eW){E fn=q;F C(){F fn.9E(eW)}}}E 2G=0;$.fn.I=C(44){E 2K=[];E eI=9F.5w.ct.6e(fA,1);if(1s 44===\'6Z\'){q.1B(C(){E 6u=$.1f(q,\'I\');E 1C;if(44.3N(/\\./)!=\'-1\'){1C=44.4o(\'.\');if(1s 6u[1C[0]]!=\'1y\'){1C=6u[1C[0]][1C[1]]}}N{1C=6u[44]}if(1s 6u!==\'1y\'&&$.5k(1C)){E 9L=1C.9E(6u,eI);if(9L!==1y&&9L!==6u){2K.2N(9L)}}N{$.6U(\'lX lL bx "\'+44+\'" 2B 47\')}})}N{q.1B(C(){$.1f(q,\'I\',{});$.1f(q,\'I\',47(q,44))})}if(2K.12===0)F q;N if(2K.12===1)F 2K[0];N F 2K};C 47(el,44){F 2a 47.5w.3Q(el,44)}$.47=47;$.47.l2=\'10.2.2\';$.47.d4=[\'3b\',\'3r\',\'R\',\'25\',\'2o\',\'1t\',\'1V\',\'1x\',\'1e\',\'1X\',\'1n\',\'22\',\'2n\',\'M\',\'3y\',\'28\',\'1Y\',\'1D\',\'2s\',\'1H\',\'3K\',\'V\',\'2z\',\'2e\',\'1a\',\'1R\',\'2V\',\'5C\',\'3f\',\'5E\',\'J\',\'6m\',\'2H\',\'1I\',\'1A\',\'1v\',\'L\'];$.47.G={1H:\'en\',d7:\'l1\',8r:O,2n:O,fP:O,3f:O,6r:1q,6S:O,aG:O,aI:O,1M:O,5Q:1q,2V:1q,fh:O,al:1q,jJ:1q,hj:1q,hL:O,3r:O,d5:O,9m:60,d6:O,9O:O,j4:1q,5c:\'8s\',hR:O,cs:50,dq:1q,hE:1q,hH:1q,8x:\'lE\',dp:1q,75:4e,k1:\'22\',lh:O,fB:1q,76:4e,k2:\'22\',fF:1q,7V:O,a2:1q,c1:1q,bX:1q,93:1q,6v:4,9Y:O,fp:1q,9p:O,1A:1q,iS:1q,6y:1l,6z:0,bf:O,iO:O,4E:1q,4i:[\'B\',\'3a\',\'4n\',\'4m\',\'5Y\',\'5m\',\'5z\',\'7p\',\'3y\',\'M\',\'22\',\'V\',\'3b\',\'b3\'],cw:[],cv:[],3a:[\'p\',\'29\',\'2F\',\'h1\',\'h2\',\'h3\',\'h4\',\'h5\',\'h6\'],dT:O,2H:1q,4c:[\'3V\',\'1o\'],4z:O,ej:[\'3W\',\'1i\',\'2F\',\'64\',\'3p\',\'ol\',\'h1\',\'h2\',\'h3\',\'h4\',\'h5\',\'h6\',\'dl\',\'29\',\'7Y\',\'du\',\'49\',\'87\',\'c8\',\'eE\',\'eS\',\'41\',\'1o\',\'3V\',\'4R\',\'7z\',\'3D\',\'2v\',\'1t\',\'42\',\'iA\',\'fx\',\'lf\',\'hr\',\'iT\',\'lj\',\'l7\',\'pJ\',\'6K\',\'pD\',\'pw\',\'pC\',\'p\'],6G:O,5B:[[\'7c\',\'4h\'],[\'b\',\'5i\']],5A:[[\'3R-7I:\\\\s?4n\',"5i"],[\'3R-1o:\\\\s?4m\',"em"],[\'1g-7a:\\\\s?5x\',"u"],[\'1g-7a:\\\\s?3K-eP\',\'4h\']],5y:O,1K:O,4T:O,6L:[\'1j\'],4f:[\'p\'],b5:[\'5Y\',\'4m\',\'4n\',\'5x\',\'5m\',\'5z\',\'pL\',\'pU\',\'pT\',\'9d\'],cg:{b:\'4n\',5i:\'4n\',i:\'4m\',em:\'4m\',4h:\'5Y\',7c:\'5Y\',3p:\'5m\',ol:\'5z\',u:\'5x\'},6m:{\'48+6E+m, 5h+6E+m\':{1C:\'28.dm\'},\'48+b, 5h+b\':{1C:\'28.30\',6p:[\'4n\']},\'48+i, 5h+i\':{1C:\'28.30\',6p:[\'4m\']},\'48+h, 5h+h\':{1C:\'28.30\',6p:[\'hv\']},\'48+l, 5h+l\':{1C:\'28.30\',6p:[\'hq\']},\'48+k, 5h+k\':{1C:\'V.2O\'},\'48+6E+7\':{1C:\'2e.3m\',6p:[\'5z\']},\'48+6E+8\':{1C:\'2e.3m\',6p:[\'5m\']}},js:O,25:[],9k:[],5S:\'<p>&#9T;</p>\',6o:\'&#9T;\',kb:[\'M/jG\',\'M/pW\',\'M/jN\'],ad:20,cW:[\'a\',\'1L\',\'b\',\'5i\',\'7l\',\'7Q\',\'i\',\'em\',\'u\',\'9f\',\'7c\',\'4h\',\'8S\',\'3p\',\'ol\',\'li\'],4N:[\'5i\',\'b\',\'u\',\'em\',\'i\',\'1e\',\'4h\',\'pX\',\'aN\',\'aS\',\'7Q\',\'7l\',\'6A\',\'E\',\'8S\',\'9f\'],8F:[\'P\',\'dK\',\'di\',\'cS\',\'cR\',\'cI\',\'cH\',\'pM\',\'pc\',\'pd\',\'89\',\'6F\',\'aK\',\'pe\',\'hM\',\'p8\',\'p2\',\'ph\',\'nA\',\'nB\',\'oH\'],8q:[\'8o\',\'8D\',\'8N\',\'5v\'],cA:O,1R:{79:[]},kn:{en:{B:\'oG\',6Y:\'6B gx\',M:\'6B hO\',3W:\'dz\',V:\'cO\',ba:\'6B V\',ca:\'jh V\',6P:\'oD\',3a:\'oW\',j2:\'ox 1g\',ci:\'oe\',1e:\'gS\',jb:\'9H 1\',jH:\'9H 2\',jI:\'9H 3\',jM:\'9H 4\',jD:\'9H 5\',4n:\'ou\',4m:\'qO\',om:\'oo j9\',ok:\'op j9\',5m:\'ov jK\',5z:\'os jK\',7p:\'ob\',3y:\'oy\',it:\'oE\',1Y:\'6B\',3o:\'oF\',hK:\'8w\',nk:\'6B dz\',nj:\'9r dV np\',ng:\'9r dV n3\',nb:\'9r co hU\',nU:\'9r co hS\',nZ:\'8w co\',nV:\'8w dV\',nI:\'8w dz\',nH:\'qj\',q2:\'q6\',qa:\'9r hl\',qt:\'8w hl\',1J:\'qM\',cC:\'qL\',5p:\'qK\',2c:\'hU\',4s:\'hS\',5u:\'gn\',qy:\'hO qB cO\',1g:\'pj\',bY:\'pi\',pn:\'cD\',po:\'gx p4 gS or pR/pK cO\',22:\'6B pI\',1v:\'pH\',lo:\'ld\',gc:\'lH\',lI:\'lA gc\',kY:\'jo 22 jm\',jt:\'gz 1g hB eZ 2c\',jx:\'gn 1g\',jB:\'gz 1g hB eZ 4s\',jA:\'m9 1g\',b3:\'6B m5 mc\',5Y:\'mC\',gV:\'ly\',aL:\'lv V in 2a 5I\',5x:\'lC\',3b:\'lu\',6k:\'lc (nE)\',7L:\'jh\',kD:\'jo 22 jm or \'}},2z:{4v:{72:/5o?:\\/\\/(?:[0-9A-Z-]+\\.)?(?:lx\\.be\\/|72\\.a5\\S*[^\\w\\-\\s])([\\w\\-]{11})(?=[^\\w\\-]|$)(?![?=&+%\\w.\\-]*(?:[\'"][^<>]*>|<\\/a>))[?=&+%\\w.-]*/ig,71:/5o?:\\/\\/(7n\\.)?71.a5\\/(\\d+)($|\\/)/,M:/((5o?|7n)[^\\s]+\\.)(lK?g|jG|jN)(\\?[^\\s-]+)?/ig,2i:/(5o?:\\/\\/(?:7n\\.|(?!7n))[^\\s\\.]+\\.[^\\s]{2,}|7n\\.[^\\s]+\\.[^\\s]{2,})/ig,}},8n:O};47.fn=$.47.5w={3j:{84:8,8Q:46,l0:38,dW:40,70:13,a7:32,b2:27,dx:9,fK:17,fL:91,fU:16,fM:18,eB:39,lD:37,g0:91},3Q:C(el,44){q.$2j=$(el);q.2G=2G++;q.7B=O;q.$5e=O;q.jz(44);q.jy();q.3a={};$.mB(q.G.8q,q.G.8F);q.gm=2a 2l(\'^(\'+q.G.8q.3c(\'|\')+\')$\',\'i\');q.1I.ja();if(q.G.4c!==O){E 2d=[\'B\',\'e0\',\'V\',\'31\',\'5h\',\'mE\'];2B(E i=0;i<2d.12;i++){q.G.4c.2N(2d[i])}}q.1H.2R();$.7E(q.G.6m,q.G.js);q.1X.2u(\'2b\');q.2b=1q;q.2o.ki()},jz:C(44){q.G=$.7E({},$.7E(1q,{},$.47.G),q.$2j.1f(),44)},dg:C(41){F g8.mw(41).bW(C(ai){F 1s 41[ai]==\'C\'})},jy:C(){E 2p=$.47.d4.12;2B(E i=0;i<2p;i++){q.iq($.47.d4[i])}},iq:C(6D){if(1s q[6D]==\'1y\')F;q[6D]=q[6D]();E 5X=q.dg(q[6D]);E 2p=5X.12;2B(E z=0;z<2p;z++){q[6D][5X[z]]=q[6D][5X[z]].aH(q)}},3b:C(){F{2c:C(){q.3b.1U(\'\')},4s:C(){q.3b.1U(\'4s\')},5u:C(){q.3b.1U(\'5u\')},9d:C(){q.3b.1U(\'9d\')},1U:C(1m){if(!q.L.1P(\'2E\'))q.$T.2n();q.25.1U();q.J.3o();q.3b.1Z=q.J.3X();q.3b.1m=1m;if(q.3b.ix()){q.3b.im()}N{q.3b.iy()}q.J.3d();q.1e.1S()},im:C(){E 4M=q.J.5Z(\'1i\');$(4M).1c(\'1f-8b\',\'I\').1O(\'1g-5g\',q.3b.1m)},iy:C(){$.1B(q.3b.1Z,$.1d(C(i,el){E $el=q.L.bh(el);if(!$el)F;if(q.3b.il($el)){q.3b.i6($el)}N{q.3b.ib($el)}},q))},ix:C(){F(q.G.1M&&q.3b.1Z[0]===O)},il:C($el){F(q.3b.1m===\'\'&&1s($el.1f(\'8b\'))!==\'1y\')},i6:C($el){$el.2t($el.B())},ib:C($el){$el.1O(\'1g-5g\',q.3b.1m);q.L.5r($el,\'1o\')}}},3r:C(){F{B:O,9Q:C(){if(!q.G.3r)F;q.3r.1w=(q.G.d5)?q.G.d5:q.$2v.1c(\'1w\');if(q.G.d6)F;q.9m=m3(q.3r.2R,q.G.9m*m1)},h7:C(){if(!q.G.d6)F;q.3r.2R()},2R:C(){q.3r.4E=q.1e.1b();if(q.3r.B===q.3r.4E)F;E 1f={};1f[\'1w\']=q.3r.1w;1f[q.3r.1w]=q.3r.4E;1f=q.3r.6V(1f);E ih=$.lU({2i:q.G.3r,1m:\'lM\',1f:1f});ih.lT(q.3r.iR)},6V:C(1f){if(q.G.9O===O||1s q.G.9O!==\'41\'){F 1f}$.1B(q.G.9O,$.1d(C(k,v){if(v!==4e&&v.4y().4Q(\'#\')===0)v=$(v).2K();1f[k]=v},q));F 1f},iR:C(1f){E 2P;52{2P=$.jY(1f)}51(e){2P=1f}E iY=(1s 2P.6U==\'1y\')?\'3r\':\'mg\';q.1X.2u(iY,q.3r.1w,2P);q.3r.B=q.3r.4E},ml:C(){gu(q.9m)}}},R:C(){F{3a:C(1w){q.R.b1=O;E 1m,1E;if(1s q.3a[1w].1f!=\'1y\')1m=\'1f\';N if(1s q.3a[1w].1c!=\'1y\')1m=\'1c\';N if(1s q.3a[1w][\'1G\']!=\'1y\')1m=\'1G\';if(1s q.3a[1w].8M!=\'1y\'){q.R.b1=1q}if(1m)1E=q.3a[1w][1m];q.R.30(q.3a[1w].Y,1m,1E)},30:C(Y,1m,1E){if(Y==\'ci\')Y=\'29\';E iF=[\'p\',\'2F\',\'29\',\'h1\',\'h2\',\'h3\',\'h4\',\'h5\',\'h6\'];if($.3t(Y,iF)==-1)F;q.R.8I=(Y==\'2F\'||Y.3N(/h[1-6]/i)!=-1);if(!q.L.1P(\'2E\'))q.$T.2n();E B=$.3k(q.$T.B());q.R.3z=q.L.3z(B);if(q.L.1P(\'4b\')&&!q.2n.dO()){if(q.R.3z){E $3I;if(!q.G.1M){$3I=q.$T.3O().3I();q.1V.3P($3I)}}}q.R.1Z=q.J.3X();q.R.7w=q.R.1Z.12;q.R.1m=1m;q.R.1E=1E;q.25.1U();q.J.3o();q.R.1U(Y);q.J.3d();q.1e.1S();q.1R.2R()},1U:C(Y){q.J.1b();q.R.69=q.14.iL.1p;if(q.14.53){q.R.iE(Y)}N{q.R.iB(Y)}},iE:C(Y){if(q.G.1M&&q.R.3z&&Y!=\'p\'){E Q=1l.3w(Y);q.$T.B(Q);q.1V.3P(Q);F}E R=q.R.1Z[0];if(R===O)F;if(R.1p==\'5v\'){if(Y!=\'29\')F;q.R.aF();F}E 9P=(q.R.69==\'6F\'||q.R.69==\'dn\');if(9P&&!q.G.1M){1l.3Z(\'9S\',O,\'<\'+Y+\'>\');R=q.J.4a();q.R.3m($(R))}N if(R.1p.3e()!=Y){if(q.G.1M&&Y==\'p\'){$(R).1F(\'<br>\');q.L.6i(R)}N{E $1W=q.L.4I(R,Y);q.R.3m($1W);if(Y!=\'p\'&&Y!=\'29\')$1W.1h(\'1L\').1u();if(q.R.8I)q.L.4Z($1W);if(Y==\'p\'||q.R.d2)$1W.1h(\'p\').26().3Y();q.R.d0($1W)}}N if(Y==\'29\'&&R.1p.3e()==Y){if(q.G.1M){$(R).1F(\'<br>\');q.L.6i(R)}N{E $el=q.L.4I(R,\'p\');q.R.3m($el)}}N if(R.1p.3e()==Y){q.R.3m($(R))}if(1s q.R.1m==\'1y\'&&1s q.R.1E==\'1y\'){$(R).1K(\'1G\').1K(\'1o\')}},iB:C(Y){E R=q.R.1Z[0];E 9P=(q.R.69==\'6F\'||q.R.69==\'dn\');if(R!==O&&q.R.7w===1){if(R.1p.3e()==Y&&Y==\'29\'){if(q.G.1M){$(R).1F(\'<br>\');q.L.6i(R)}N{E $el=q.L.4I(R,\'p\');q.R.3m($el)}}N if(R.1p==\'5v\'){if(Y!=\'29\')F;q.R.aF()}N if(q.R.69==\'aK\'){q.R.kS(Y)}N if(q.G.1M&&((9P)||(q.14.iL!=R))){q.R.aE(Y)}N{if(q.G.1M&&Y==\'p\'){$(R).6t(\'<br>\').1F(\'<br>\');q.L.6i(R)}N if(R.1p===\'6F\'){q.R.aE(Y)}N{E $1W=q.L.4I(R,Y);q.R.3m($1W);if(q.R.8I)q.L.4Z($1W);if(Y==\'p\'||q.R.d2)$1W.1h(\'p\').26().3Y()}}}N{if(q.G.1M||Y!=\'p\'){if(Y==\'29\'){E d3=0;2B(E i=0;i<q.R.7w;i++){if(q.R.1Z[i].1p==\'aK\')d3++}if(d3==q.R.7w){$.1B(q.R.1Z,$.1d(C(i,s){E $1W=O;if(q.G.1M){$(s).6t(\'<br>\').1F(\'<br>\');$1W=q.L.6i(s)}N{$1W=q.L.4I(s,\'p\')}if($1W&&1s q.R.1m==\'1y\'&&1s q.R.1E==\'1y\'){$1W.1K(\'1G\').1K(\'1o\')}},q));F}}q.R.aE(Y)}N{E 8B=0;E 5G=O;if(q.R.1m==\'1G\'){5G=\'3m\';8B=$(q.R.1Z).bW(\'.\'+q.R.1E).12;if(q.R.7w==8B)5G=\'3m\';N if(q.R.7w>8B)5G=\'1U\';N if(8B===0)5G=\'1U\'}E 9y=[\'3p\',\'ol\',\'li\',\'2Y\',\'5P\',\'dl\',\'dt\',\'dd\'];$.1B(q.R.1Z,$.1d(C(i,s){if($.3t(s.1p.3e(),9y)!=-1)F;E $1W=q.L.4I(s,Y);if(5G){if(5G==\'3m\')q.R.3m($1W);N if(5G==\'1u\')q.R.1u($1W);N if(5G==\'1U\')q.R.kt($1W)}N q.R.3m($1W);if(Y!=\'p\'&&Y!=\'29\')$1W.1h(\'1L\').1u();if(q.R.8I)q.L.4Z($1W);if(Y==\'p\'||q.R.d2)$1W.1h(\'p\').26().3Y();if(1s q.R.1m==\'1y\'&&1s q.R.1E==\'1y\'){$1W.1K(\'1G\').1K(\'1o\')}},q))}}},kt:C($el){if(q.R.b1){$el.1K(\'1G\').1K(\'1o\')}if(q.R.1m==\'1G\'){$el.2f(q.R.1E);F}N if(q.R.1m==\'1c\'||q.R.1m==\'1f\'){$el.1c(q.R.1E.1w,q.R.1E.1E);F}},3m:C($el){if(q.R.b1){$el.1K(\'1G\').1K(\'1o\')}if(q.R.1m==\'1G\'){$el.7O(q.R.1E);F}N if(q.R.1m==\'1c\'||q.R.1m==\'1f\'){if($el.1c(q.R.1E.1w)==q.R.1E.1E){$el.1K(q.R.1E.1w)}N{$el.1c(q.R.1E.1w,q.R.1E.1E)}F}N{$el.1K(\'1o 1G\');F}},1u:C($el){$el.2S(q.R.1E)},aF:C(){E R=$(q.R.1Z[0]).2q(\'3p, ol\',q.$T[0]);$(R).1h(\'3p, ol\').26().3Y();$(R).1h(\'li\').1F($(\'<br>\')).26().3Y();E $el=q.L.4I(R,\'29\');q.R.3m($el)},kS:C(Y){1l.3Z(\'7p\');1l.3Z(\'9S\',O,Y);q.1x.4r();q.$T.1h(\'p:kh\').1u();E 1W=q.J.4a();if(Y!=\'p\'){$(1W).1h(\'1L\').1u()}if(!q.G.1M){q.R.3m($(1W))}q.$T.1h(\'3p, ol, 6O, 29, p\').1B($.1d(q.L.4f,q));if(q.G.1M&&Y==\'p\'){q.L.6i(1W)}},aE:C(Y){if(q.R.69==\'8D\'||q.R.69==\'8N\'){if(Y==\'29\'){q.R.aF()}N{F}}E 1W=q.J.5Z(Y);if(1W===O)F;E $1W=$(1W);q.R.d0($1W);E $jR=$1W.1h(q.G.8q.3c(\',\')+\', 2Y, 3W, e7, ee, e3, 5P, 6O\');$jR.26().3Y();if(Y!=\'p\'&&Y!=\'29\')$1W.1h(\'1L\').1u();$.1B(q.R.1Z,$.1d(q.L.4f,q));$1W.1F(q.J.6Q(2));if(!q.G.1M){q.R.3m($1W)}q.$T.1h(\'3p, ol, 6O, 29, p\').1B($.1d(q.L.4f,q));$1W.1h(\'29:kh\').1u();if(q.R.8I){q.L.4Z($1W)}if(q.G.1M&&Y==\'p\'){q.L.6i($1W)}if(q.G.1M){E $1N=$1W.1N().1N();if($1N.9B()!=0&&$1N[0].1p===\'58\'){$1N.1u()}}},d0:C($1W){if($1W.2q(\'3W\',q.$T[0]).12===0)F;if($1W.2q(\'6O\',q.$T[0]).12===0)$1W.5Z(\'<6O>\');if($1W.2q(\'2Y\',q.$T[0]).12===0&&$1W.2q(\'5P\').12===0){$1W.5Z(\'<2Y>\')}},gv:C(1w,1E){E 1Z=q.J.3X();$(1Z).1K(\'1f-\'+1w);q.1e.1S()},mZ:C(1w,1E){E 1Z=q.J.3X();$(1Z).1c(\'1f-\'+1w,1E);q.1e.1S()},mY:C(1w,1E){E 1Z=q.J.3X();$.1B(1Z,C(){if($(q).1c(\'1f-\'+1w)){$(q).1K(\'1f-\'+1w)}N{$(q).1c(\'1f-\'+1w,1E)}})},1K:C(1c,1E){E 1Z=q.J.3X();$(1Z).1K(1c);q.1e.1S()},mX:C(1c,1E){E 1Z=q.J.3X();$(1Z).1c(1c,1E);q.1e.1S()},mW:C(1c,1E){E 1Z=q.J.3X();$.1B(1Z,C(){if($(q).1c(1w)){$(q).1K(1w)}N{$(q).1c(1w,1E)}})},2S:C(3E){E 1Z=q.J.3X();$(1Z).2S(3E);q.L.5r(1Z,\'1G\');q.1e.1S()},8g:C(3E){E 1Z=q.J.3X();$(1Z).2f(3E);q.1e.1S()},7O:C(3E){E 1Z=q.J.3X();$(1Z).7O(3E);q.1e.1S()}}},25:C(){F{1U:C(1m){if(1s 1m==\'1y\'||1m==\'9l\'){q.25.kV()}N{q.25.kM()}},kV:C(){q.J.3o();q.G.25.2N(q.$T.B());q.J.3d()},kM:C(){q.J.3o();q.G.9k.2N(q.$T.B());q.J.3d()},kd:C(){q.$T.B(q.G.25.kW())},kj:C(){q.$T.B(q.G.9k.kW())},cQ:C(){q.G.25.2N(q.$T.B())},9l:C(){if(q.G.25.12===0)F;q.25.1U(\'a9\');q.25.kd();q.J.3d();3A($.1d(q.1R.2R,q),50)},a9:C(){if(q.G.9k.12===0)F;q.25.1U(\'9l\');q.25.kj();q.J.3d();3A($.1d(q.1R.2R,q),50)}}},2o:C(){F{ki:C(){q.2o.jW();q.2o.kz();q.2o.kK();q.2o.kF();q.2o.k5()},aa:C(){F(q.$2j[0].1p===\'mu\')},jW:C(){q.$2Q=$(\'<1i 1G="I-2Q" 9c="mq" />\')},k7:C(){q.$2v=$(\'<2v />\').1c(\'1w\',q.2o.ku())},ku:C(){F((1s(1w)==\'1y\'))?\'d1-\'+q.2G:q.$2j.1c(\'id\')},kz:C(){E 1C=(q.2o.aa())?\'2K\':\'B\';q.d1=$.3k(q.$2j[1C]())},kF:C(){q.$T.1c({\'5d\':1q,\'kU\':q.G.d7})},kK:C(){E 1C=(q.2o.aa())?\'kk\':\'ke\';q.2o[1C]()},kk:C(){q.$T=$(\'<1i />\');q.$2v=q.$2j;q.$2Q.jX(q.$2j).1F(q.$T).1F(q.$2j);q.$T.2f(\'I-T\');q.$2j.3v()},ke:C(){q.$T=q.$2j;q.2o.k7();q.$2Q.jX(q.$T).1F(q.$T).1F(q.$2v);q.$T.2f(\'I-T\');q.$2v.3v()},k5:C(){q.1e.1U(q.d1);q.2o.kO();q.2o.kH();if(q.G.6r)F;3A($.1d(q.1e.dJ,q),bt)},kH:C(){q.2o.eD();q.2o.ex();q.2o.fD();q.2o.fR();if(q.G.1A){q.G.1A=q.1A.3Q();q.1A.2o()}q.1a.iW();q.2o.8r();3A($.1d(q.1R.2R,q),4);q.1X.2u(\'3Q\')},kO:C(){$(q.$2v).1c(\'kU\',q.G.d7);if(q.G.1M)q.$T.2f(\'I-1M\');if(q.G.6S)q.$T.1c(\'6S\',q.G.6S);if(q.G.aG)q.$T.1O(\'aG\',q.G.aG);if(q.G.aI)q.$T.1O(\'aI\',q.G.aI)},fg:C(e){e.2w();if(!q.G.fB||!q.G.fF)F;E 5R=e.bG.5R;q.1v.kQ(5R[0],e)},ft:C(e){q.1e.1S();3A(q.1x.4r,1);q.1X.2u(\'57\',e)},fD:C(){q.$T.on(\'57.I\',$.1d(C(e){e=e.7T||e;if(3l.b6===1y||!e.bG)F 1q;if(e.bG.5R.12===0){F q.2o.ft(e)}N{q.2o.fg(e)}3A(q.1x.4r,1);q.1X.2u(\'57\',e)},q));q.$T.on(\'2y.I\',$.1d(C(e){E 8e=q.1X.7g();E 1m=(8e==\'2y\'||8e==\'4W\')?O:\'2y\';q.1X.9K(1m);q.L.8U();q.1X.2u(\'2y\',e)},q));q.$T.on(\'5C.I\',$.1d(q.5C.3Q,q));q.$T.on(\'l4.I\',$.1d(q.1e.1S,q));q.$T.on(\'1D.I\',$.1d(q.1D.3Q,q));q.$T.on(\'2s.I\',$.1d(q.2s.3Q,q));if($.5k(q.G.fo)){q.$2v.on(\'1D.I-2v\',$.1d(q.G.fo,q))}if($.5k(q.G.fZ)){q.$2v.on(\'2s.I-2v\',$.1d(q.G.fZ,q))}if($.5k(q.G.fW)){q.$T.on(\'2n.I\',$.1d(q.G.fW,q))}$(1l).on(\'6R.I.\'+q.2G,$.1d(C(e){q.8h=e.1Q},q));q.$T.on(\'fI.I\',$.1d(C(e){if(q.2b)F;if(q.7B)F;if(!q.2o.fO())F;q.L.8U();if($.5k(q.G.mr))q.1X.2u(\'fI\',e)},q))},fO:C(){if(q.8h===1q)F 1q;E $el=$(q.8h);F(!$el.3i(\'I-1A, I-1n\')&&!$el.is(\'#I-1a\')&&$el.dL(\'.I-1A, .I-1n, #I-1a\').12===0)},fR:C(){if(q.2z.aD()){q.2z.30()}q.3f.9Q();if(q.G.2n)3A(q.2n.2W,88);if(q.G.fP)3A(q.2n.3P,88)},8r:C(){if(!q.G.8r)F;$.1B(q.G.8r,$.1d(C(i,s){E 1C=(1s df!==\'1y\'&&1s df[s]!==\'1y\')?df:47.fn;if(!$.5k(1C[s])){F}q[s]=1C[s]();E 5X=q.dg(q[s]);E 2p=5X.12;2B(E z=0;z<2p;z++){q[s][5X[z]]=q[s][5X[z]].aH(q)}if($.5k(q[s].3Q)){q[s].3Q()}},q))},eD:C(){if(!q.L.1P(\'4b\'))F;52{1l.3Z(\'lZ\',O,O);1l.3Z(\'lN\',O,O)}51(e){}},ex:C(){if(!q.L.1P(\'2E\'))F;1l.3Z("lS",O,O)}}},1t:C(){F{2o:C(2g,2X){E $1t=$(\'<a 2k="#" 1G="3J-8R 3J-\'+2g+\'" 3T="\'+2g+\'" />\').1c({\'9c\':\'1t\',\'4K-2L\':2X.1J,\'6S\':\'-1\'});if(2X.1C||2X.4A||2X.1n){q.1t.eu($1t,2g,2X)}if(2X.1n){$1t.2f(\'I-1A-V-1n\').1c(\'4K-eY\',1q);E $1n=$(\'<1i 1G="I-1n I-1n-\'+q.2G+\' I-1n-2Q-\'+2g+\'" 1o="6N: 5p;">\');$1t.1f(\'1n\',$1n);q.1n.2o(2g,$1n,2X.1n)}if(q.L.7b()){q.1t.et($1t,2g,2X.1J)}F $1t},eu:C($1t,2g,2X){$1t.on(\'5H 2y\',$.1d(C(e){if($1t.3i(\'I-1t-7o\'))F O;E 1m=\'1C\';E 2A=2X.1C;if(2X.4A){1m=\'4A\';2A=2X.4A}N if(2X.1n){1m=\'1n\';2A=O}q.1t.ac(e,2g,1m,2A)},q))},et:C($1t,1w,1J){E $3q=$(\'<1j>\').2f(\'I-1A-3q I-1A-3q-\'+q.2G+\' I-1A-3q-\'+1w).3v().B(1J);$3q.aU(\'31\');$1t.on(\'ay\',C(){if($(q).3i(\'I-1t-7o\'))F;E 3s=$1t.2I();$3q.2O();$3q.1O({2U:(3s.2U+$1t.6c())+\'px\',2c:(3s.2c+$1t.7P()/2-$3q.7P()/2)+\'px\'})});$1t.on(\'az\',C(){$3q.3v()})},ac:C(e,2g,1m,2A){q.1t.mj=q.1V.ar();e.2w();if(q.L.1P(\'2E\'))e.8l=O;if(1m==\'4A\')q.28.30(2A);N if(1m==\'1n\')q.1n.2O(e,2g);N q.1t.ep(e,2A,2g)},ep:C(e,2A,2g){E 1C;q.8h=1q;if($.5k(2A))2A.6e(q,2g);N if(2A.3N(/\\./)!=\'-1\'){1C=2A.4o(\'.\');if(1s q[1C[0]]==\'1y\')F;q[1C[0]][1C[1]](2g)}N q[2A](2g);q.1R.4i(e,2g)},1b:C(1k){F q.$1A.1h(\'a.3J-\'+1k)},9q:C(1k){q.1t.1b(1k).2f(\'I-8A\')},ge:C(1k){q.1t.1b(1k).2S(\'I-8A\')},cj:C(1k){if(1s 1k===\'1y\'){q.$1A.1h(\'a.3J-8R\').2S(\'I-8A\')}N{q.$1A.1h(\'a.3J-8R\').6h(\'.3J-\'+1k).2S(\'I-8A\')}},gf:C(){q.$1A.1h(\'a.3J-8R\').6h(\'a.3J-B, a.3J-fe\').2S(\'I-1t-7o\')},gT:C(){q.$1A.1h(\'a.3J-8R\').6h(\'a.3J-B, a.3J-fe\').2f(\'I-1t-7o\')},mm:C(1k,av){q.1t.1b(1k).2f(\'3J-\'+av)},mf:C(1k,av){q.1t.1b(1k).2S(\'3J-\'+av)},m7:C(1k,1w){E $1t=q.1t.1b(1k);$1t.2S(\'I-21-M\').2f(\'fa-I-21\');$1t.B(\'<i 1G="fa \'+1w+\'"></i>\')},cB:C($21,2A){if($21=="25")F;E 1m=(2A==\'1n\')?\'1n\':\'1C\';E 1k=$21.1c(\'3T\');$21.on(\'5H 2y\',$.1d(C(e){if($21.3i(\'I-1t-7o\'))F O;q.1t.ac(e,1k,1m,2A)},q))},cc:C($21,1n){$21.2f(\'I-1A-V-1n\').1c(\'4K-eY\',1q);E 1k=$21.1c(\'3T\');q.1t.cB($21,\'1n\');E $1n=$(\'<1i 1G="I-1n I-1n-\'+q.2G+\' I-1n-2Q-\'+1k+\'" 1o="6N: 5p;">\');$21.1f(\'1n\',$1n);if(1n)q.1n.2o(1k,$1n,1n);F $1n},cQ:C(1k,1J){if(!q.G.1A)F;if(q.1t.9v(1k))F"25";E 21=q.1t.2o(1k,{1J:1J});21.2f(\'I-21-M\');q.$1A.1F($(\'<li>\').1F(21));F 21},ma:C(1k,1J){if(!q.G.1A)F;if(q.1t.9v(1k))F"25";E 21=q.1t.2o(1k,{1J:1J});21.2f(\'I-21-M\');q.$1A.6t($(\'<li>\').1F(21));F 21},mt:C(fb,1k,1J){if(!q.G.1A)F;if(q.1t.9v(1k))F"25";E 21=q.1t.2o(1k,{1J:1J});21.2f(\'I-21-M\');E $21=q.1t.1b(fb);if($21.12!==0)$21.1r().3F($(\'<li>\').1F(21));N q.$1A.1F($(\'<li>\').1F(21));F 21},mb:C(f0,1k,1J){if(!q.G.1A)F;if(q.1t.9v(1k))F"25";E 21=q.1t.2o(1k,{1J:1J});21.2f(\'I-21-M\');E $21=q.1t.1b(f0);if($21.12!==0)$21.1r().a6($(\'<li>\').1F(21));N q.$1A.1F($(\'<li>\').1F(21));F 21},1u:C(1k){q.1t.1b(1k).1u()},9v:C(1k){F(1k=="9l"||1k=="a9")&&!q.L.7b()}}},1V:C(){F{2W:C(Q){if(!q.L.6X(Q)){E 3g=q.L.bH();$(Q).6t(3g);q.1V.3P(3g)}N{q.1V.1U(Q,0,Q,0)}},3P:C(Q){Q=Q[0]||Q;if(Q.eR.a8==1){F q.1V.54(Q.eR)}q.1V.1U(Q,1,Q,1)},1U:C(4P,eL,77,hs){4P=4P[0]||4P;77=77[0]||77;if(q.L.7J(4P.1p)&&4P.3x===\'\'){4P.3x=q.G.6o}if(4P.1p==\'58\'&&q.G.1M===O){E 1r=$(q.G.5S)[0];$(4P).2t(1r);4P=1r;77=4P}q.J.1b();52{q.14.2W(4P,eL);q.14.3P(77,hs)}51(e){}q.J.4l()},54:C(Q){52{E Y=$(Q)[0].1p;if(Y!=\'58\'&&!q.L.6X(Q)){E 3g=q.L.bH();$(Q).3F(3g);q.1V.3P(3g)}N{if(Y!=\'58\'&&q.L.1P(\'2E\')){q.1V.2W($(Q).1N())}N{q.1V.d9(Q,\'3F\')}}}51(e){E 3g=q.L.bH();$(Q).3F(3g);q.1V.3P(3g)}},bT:C(Q){if(q.L.6X(Q)){q.1V.3P($(Q).4G())}N{q.1V.d9(Q,\'a6\')}},d9:C(Q,1m){if(!q.L.1P(\'2E\'))q.$T.2n();Q=Q[0]||Q;q.J.1b();if(1m==\'3F\'){52{q.14.9I(Q);q.14.mn(Q)}51(e){}}N{52{q.14.mk(Q);q.14.ks(Q)}51(e){}}q.14.43(O);q.J.4l()},b4:C(Q){Q=Q[0]||Q;q.J.1b();E at=q.14.7S();at.9j(Q);at.3P(q.14.bQ,q.14.bO);F $.3k(at.4y()).12},ar:C(){E 2I=0;E 2T=3l.4D();if(2T.7A>0){E 14=3l.4D().6l(0);E aJ=14.7S();aJ.9j(q.$T[0]);aJ.3P(14.bQ,14.bO);2I=aJ.4y().12}F 2I},eh:C(2b,2h){if(1s 2h==\'1y\')2h=2b;if(!q.2n.dO())q.2n.2W();E 2T=q.J.1b();E Q,2I=0;E hn=1l.mh(q.$T[0],mi.lR,4e,4e);56(Q==hn.lQ()){2I+=Q.5O.12;if(2I>2b){q.14.2W(Q,Q.5O.12+2b-2I);2b=lP}if(2I>=2h){q.14.3P(Q,Q.5O.12+2h-2I);6n}}q.14.43(O);q.J.4l()},lO:C(2b,2h){q.1V.eh(2b,2h)},m2:C(){F q.1V.ar()}}},1x:C(){F{gH:C(B){B=B.K(/<3V(.*?[^>]?)>([\\w\\W]*?)<\\/3V>/gi,\'<2F 1G="I-3V-Y" 1o="6N: 5p;" $1>$2</2F>\');B=B.K(/\\$/g,\'&#36;\');B=B.K(/<a 2k="(.*?[^>]?)®(.*?[^>]?)">/gi,\'<a 2k="$1&lW$2">\');if(q.G.5Q)B=q.1x.5Q(B);if(q.G.1M)B=q.1x.8C(B);B=q.1x.cT(B);E $1i=$(\'<1i>\');$1i.B(B);E da=$1i.1h(\'3R[1o]\');if(da.12!==0){da.2t(C(){E $el=$(q);E $1j=$(\'<1j>\').1c(\'1o\',$el.1c(\'1o\'));F $1j.1F($el.26())});B=$1i.B()}$1i.1u();B=B.K(/<3R(.*?[^<])>/gi,\'\');B=B.K(/<\\/3R>/gi,\'\');B=q.1I.2R(B);if(q.G.2V)B=q.2V.2R(B);B=q.1x.7R(B);B=q.1x.cU(B);B=B.K(/&9o;/g,\'&\');F B},9R:C(B){B=B.K(/\\6g/g,\'\');B=B.K(/&#9T;/gi,\'\');if(q.G.hj){B=B.K(/&5s;/gi,\' \')}if(B.3N(/^<p>(||\\s||<br\\s?\\/?>||&5s;)<\\/p>$/i)!=-1){F\'\'}B=B.K(/<2F 1G="I-3V-Y" 1o="6N: 5p;"(.*?[^>]?)>([\\w\\W]*?)<\\/2F>/gi,\'<3V$1>$2</3V>\');B=q.1x.gG(B);E hZ={\'\\mo\':\'&mp;\',\'\\mQ\':\'&mR;\',\'\\mK\':\'&mL;\',\'\\mM\':\'&n0;\',\'\\mU\':\'&mV;\'};$.1B(hZ,C(i,s){B=B.K(2a 2l(i,\'g\'),s)});if(q.L.1P(\'4b\')){B=B.K(/<br\\s?\\/?>$/gi,\'\')}B=B.K(2a 2l(\'<br\\\\s?/?></li>\',\'gi\'),\'</li>\');B=B.K(2a 2l(\'</li><br\\\\s?/?>\',\'gi\'),\'</li>\');B=B.K(/<(.*?)3T="\\s*?"(.*?[^>]?)>/gi,\'<$1$2">\');B=B.K(/<(.*?)1o="\\s*?"(.*?[^>]?)>/gi,\'<$1$2">\');B=B.K(/="">/gi,\'>\');B=B.K(/""">/gi,\'">\');B=B.K(/"">/gi,\'">\');B=B.K(/<1i(.*?[^>]) 1f-8b="I"(.*?[^>])>/gi,\'<1i$1$2>\');B=B.K(/<(.*?) 1f-3H="I"(.*?[^>])>/gi,\'<$1$2>\');E $1i=$("<1i/>").B($.dQ(B,1l,1q));$1i.1h("1j").1K("3T");$1i.1h(\'2F .I-7s-3g\').1B(C(){$(q).26().3Y()});B=$1i.B();B=B.K(/<1L(.*?[^>])3T="(.*?[^>])"(.*?[^>])>/gi,\'<1L$1$3>\');B=B.K(/<1j 1G="I-7s-3g">(.*?)<\\/1j>/gi,\'$1\');B=B.K(/ 1f-3o-2i="(.*?[^>])"/gi,\'\');B=B.K(/<1j(.*?)id="I-M-2Q"(.*?[^>])>([\\w\\W]*?)<1L(.*?)><\\/1j>/gi,\'$3<1L$4>\');B=B.K(/<1j(.*?)id="I-M-8f"(.*?[^>])>(.*?)<\\/1j>/gi,\'\');B=B.K(/<1j(.*?)id="I-M-5l"(.*?[^>])>(.*?)<\\/1j>/gi,\'\');B=B.K(/<3R(.*?[^<])>/gi,\'\');B=B.K(/<\\/3R>/gi,\'\');B=q.1I.2R(B);if(q.G.hR){B=B.K(/<a(.*?)3T="hG"(.*?[^>])>/gi,\'<a$1$2>\');B=B.K(/<a(.*?[^>])>/gi,\'<a$1 3T="hG">\')}B=B.K(/\\mv-I-(Y|1G|1o)="(.*?[^>])"/gi,\'\');B=B.K(2a 2l(\'<(.*?) 1f-3H="I"(.*?[^>])>\',\'gi\'),\'<$1$2>\');B=B.K(2a 2l(\'<(.*?) 1f-3H="I">\',\'gi\'),\'<$1>\');B=B.K(/&9o;/g,\'&\');F B},8y:C(B,8i){B=$.3k(B);B=B.K(/\\$/g,\'&#36;\');B=B.K(/<1j 1G="s[0-9]">/gi,\'<1j>\');B=B.K(/<1j 1G="hN-mx-3g">&5s;<\\/1j>/gi,\' \');B=B.K(/<1j 1G="hN-5I-1j"[^>]*>\\t<\\/1j>/gi,\'\\t\');B=B.K(/<1j[^>]*>(\\s|&5s;)<\\/1j>/gi,\' \');if(q.G.hL){F q.1x.aj(B)}if(!q.L.9w()&&1s 8i==\'1y\'){if(q.L.4S([\'hM\',\'A\'])){F q.1x.aj(B,O)}if(q.L.4S(\'8o\')){B=B.K(/”/g,\'"\');B=B.K(/“/g,\'"\');B=B.K(/‘/g,\'\\\'\');B=B.K(/’/g,\'\\\'\');F q.1x.gK(B)}if(q.L.4S([\'aK\',\'dK\',\'di\',\'cS\',\'cR\',\'cI\',\'cH\'])){B=q.1x.gL(B);if(!q.L.1P(\'2E\')){E R=q.J.4a();if(R&&R.1p==\'P\'){B=B.K(/<1L(.*?)>/gi,\'<p><1L$1></p>\')}}F B}if(q.L.4S([\'6F\'])){B=q.1x.b0(B,\'2Y\');if(q.G.1M)B=q.1x.8C(B);B=q.1x.gF(B);F B}if(q.L.4S([\'5v\'])){F q.1x.b0(B,\'li\')}}B=q.1x.gP(B,8i);if(!q.1x.9D){if(q.G.1M)B=q.1x.8C(B);if(q.G.5Q)B=q.1x.5Q(B);B=q.1x.cT(B)}B=q.1x.ha(B);B=q.1x.gb(B);B=q.1x.b0(B,\'gB\');if(!q.1x.9D&&q.G.2V){B=q.2V.2R(B)}B=q.1x.gO(B);B=q.1x.gZ(B);B=q.1x.h0(B);B=q.1x.cU(B);F B},ha:C(B){B=B.K(/<!--[\\s\\S]*?-->/gi,\'\');B=B.K(/<1o[^>]*>[\\s\\S]*?<\\/1o>/gi,\'\');B=B.K(/<o\\:p[^>]*>[\\s\\S]*?<\\/o\\:p>/gi,\'\');if(B.1T(/1G="?gd|1o="[^"]*\\gr-|1o=\'[^\'\']*\\gr-|w:ms/i)){B=B.K(/<!--[\\s\\S]+?-->/gi,\'\');B=B.K(/<(!|3V[^>]*>.*?<\\/3V(?=[>\\s])|\\/?(\\?my(:\\w+)?|1L|5h|V|1o|\\w:\\w+)(?=[\\s\\/>]))[^>]*>/gi,\'\');B=B.K(/<(\\/?)s>/gi,"<$mz>");B=B.K(/ /gi,\' \');B=B.K(/<1j\\s+1o\\s*=\\s*"\\s*aY-mF\\s*:\\s*mG\\s*;?\\s*"\\s*>([\\s\\9Z]*)<\\/1j>/gi,C(5f,aW){F(aW.12>0)?aW.K(/./," ").ct(8d.mH(aW.12/2)).4o("").3c("\\9Z"):\'\'});B=q.1x.gQ(B);B=B.K(/<1L(.*?)v:mA=(.*?)>/gi,\'\');B=B.K(/4t="22\\:\\/\\/(.*?)"/,\'4t=""\');E $1i=$("<1i/>").B(B);E 5T=O;E 8V=1;E cY=[];$1i.1h("p[1o]").1B(C(){E 3M=$(q).1c(\'1o\').1T(/aY\\-2e\\:l([0-9]+)\\n1([0-9]+)/);if(3M){E 5V=5L(3M[1]);E 5W=5L(3M[2]);E cX=$(q).B().1T(/^[\\w]+\\./)?"ol":"3p";E $li=$("<li/>").B($(q).B());$li.B($li.B().K(/^([\\w\\.]+)</,\'<\'));$li.1h("1j:3I").1u();if(5W==1&&$.3t(5V,cY)==-1){E $2e=$("<"+cX+"/>").1c({"1f-7h":5W,"1f-2e":5V}).B($li);$(q).2t($2e);5T=5V;cY.2N(5V)}N{if(5W>8V){E $aV=$1i.1h(\'[1f-7h="\'+8V+\'"][1f-2e="\'+5T+\'"]\');E $5T=$aV;2B(E i=8V;i<5W;i++){$2e=$("<"+cX+"/>");$2e.aU($5T.1h("li").2Z());$5T=$2e}$5T.1c({"1f-7h":5W,"1f-2e":5V}).B($li)}N{E $aV=$1i.1h(\'[1f-7h="\'+5W+\'"][1f-2e="\'+5V+\'"]\').2Z();$aV.1F($li)}8V=5W;5T=5V;$(q).1u()}}});$1i.1h(\'[1f-7h][1f-2e]\').1K(\'1f-7h 1f-2e\');B=$1i.B();B=B.K(/·/g,\'\');B=B.K(/<p 1G="gd(.*?)"/gi,\'<p\');B=B.K(/ 1G=\\"(aY[^\\"]*)\\"/gi,"");B=B.K(/ 1G=(aY\\w+)/gi,"");B=B.K(/<o:p(.*?)>([\\w\\W]*?)<\\/o:p>/gi,\'$2\');B=B.K(/\\n/g,\' \');B=B.K(/<p>\\n?<li>/gi,\'<li>\')}F B},gb:C(B){B=B.K(/<b\\l6="g9-4E-2r(.*?)">([\\w\\W]*?)<\\/b>/gi,"$2");B=B.K(/<b(.*?)id="l5-g9-l9(.*?)">([\\w\\W]*?)<\\/b>/gi,"$3");B=B.K(/<1j[^>]*(3R-1o: 4m; 3R-7I: 4n|3R-7I: 4n; 3R-1o: 4m)[^>]*>/gi,\'<1j 1o="3R-7I: 4n;"><1j 1o="3R-1o: 4m;">\');B=B.K(/<1j[^>]*3R-1o: 4m[^>]*>/gi,\'<1j 1o="3R-1o: 4m;">\');B=B.K(/<1j[^>]*3R-7I: 4n[^>]*>/gi,\'<1j 1o="3R-7I: 4n;">\');B=B.K(/<1j[^>]*1g-7a: 5x[^>]*>/gi,\'<1j 1o="1g-7a: 5x;">\');B=B.K(/<1L>/gi,\'\');B=B.K(/\\n{3,}/gi,\'\\n\');B=B.K(/<3R(.*?)>([\\w\\W]*?)<\\/3R>/gi,\'$2\');B=B.K(/<p><p>/gi,\'<p>\');B=B.K(/<\\/p><\\/p>/gi,\'</p>\');B=B.K(/<li>(\\s*|\\t*|\\n*)<p>/gi,\'<li>\');B=B.K(/<\\/p>(\\s*|\\t*|\\n*)<\\/li>/gi,\'</li>\');B=B.K(/<\\/p>\\s<p/gi,\'<\\/p><p\');B=B.K(/<1L 4t="7q-lz-2i\\:\\/\\/(.*?)"(.*?)>/gi,\'\');B=B.K(/<p>•([\\w\\W]*?)<\\/p>/gi,\'<li>$1</li>\');if(q.L.1P(\'4b\')){B=B.K(/<br\\s?\\/?>$/gi,\'\')}F B},b0:C(B,1m){E 2d=[\'1j\',\'a\',\'2F\',\'29\',\'9f\',\'em\',\'5i\',\'1e\',\'aS\',\'6A\',\'du\',\'8S\',\'E\',\'aN\',\'cL\',\'7Q\',\'7l\',\'b\',\'i\',\'u\',\'4h\',\'ol\',\'3p\',\'li\',\'dl\',\'dt\',\'dd\',\'p\',\'br\',\'6Y\',\'8Y\',\'4R\',\'7i\',\'8J\',\'41\',\'1L\',\'3W\',\'2Y\',\'5P\',\'6O\',\'ee\',\'e3\',\'e7\',\'h1\',\'h2\',\'h3\',\'h4\',\'h5\',\'h6\'];E cK=O;E cN=[[\'a\',\'*\'],[\'1L\',[\'4t\',\'80\']],[\'1j\',[\'1G\',\'3T\',\'1f-3H\']],[\'4R\',\'*\'],[\'6Y\',\'*\'],[\'8Y\',\'*\'],[\'7i\',\'*\'],[\'41\',\'*\'],[\'8J\',\'*\'],[\'4E\',\'*\']];if(1m==\'gB\'){cK=[\'p\',\'1j\',\'h1\',\'h2\',\'h3\',\'h4\',\'h5\',\'h6\'];cN=[[\'3W\',\'1G\'],[\'2Y\',[\'lw\',\'la\']],[\'a\',\'*\'],[\'1L\',[\'4t\',\'80\',\'1f-I-8m-M\']],[\'1j\',[\'1G\',\'3T\',\'1f-3H\']],[\'4R\',\'*\'],[\'6Y\',\'*\'],[\'8Y\',\'*\'],[\'7i\',\'*\'],[\'41\',\'*\'],[\'8J\',\'*\'],[\'4E\',\'*\']]}N if(1m==\'2Y\'){2d=[\'3p\',\'ol\',\'li\',\'1j\',\'a\',\'9f\',\'em\',\'5i\',\'1e\',\'aS\',\'6A\',\'8S\',\'E\',\'aN\',\'cL\',\'7Q\',\'7l\',\'b\',\'i\',\'u\',\'4h\',\'ol\',\'3p\',\'li\',\'dl\',\'dt\',\'dd\',\'br\',\'4R\',\'6Y\',\'8Y\',\'7i\',\'8J\',\'41\',\'1L\',\'h1\',\'h2\',\'h3\',\'h4\',\'h5\',\'h6\']}N if(1m==\'li\'){2d=[\'3p\',\'ol\',\'li\',\'1j\',\'a\',\'9f\',\'em\',\'5i\',\'1e\',\'aS\',\'6A\',\'8S\',\'E\',\'aN\',\'cL\',\'7Q\',\'7l\',\'b\',\'i\',\'u\',\'4h\',\'br\',\'4R\',\'6Y\',\'8Y\',\'7i\',\'8J\',\'41\',\'1L\']}E 44={4c:(q.G.4c)?q.G.4c:O,4z:(q.G.4z)?q.G.4z:2d,6G:1q,lg:1q,1K:(q.G.1K)?q.G.1K:O,4T:(q.G.4T)?q.G.4T:cN,4f:cK};F q.1I.2R(B,44)},h0:C(B){B=B.K(/<(p|h[1-6])>(|\\s|\\n|\\t|<br\\s?\\/?>)<\\/(p|h[1-6])>/gi,\'\');if(!q.G.1M)B=B.K(/<br>$/i,\'\');F B},gZ:C(B){B=B.K(/<1j>(.*?)<\\/1j>/gi,\'$1\');B=B.K(/<1j[^>]*>\\s|&5s;<\\/1j>/gi,\' \');F B},gQ:C(B){if(!q.L.1P(\'2E\'))F B;E 5q=$.3k(B);if(5q.3N(/^<a(.*?)>(.*?)<\\/a>$/i)===0){B=B.K(/^<a(.*?)>(.*?)<\\/a>$/i,"$2")}F B},gP:C(B,8i){q.1x.9D=O;if(!q.L.9w()&&1s 8i==\'1y\'){E 1Z=q.G.8q.3c(\'|\').K(\'P|\',\'\').K(\'89|\',\'\');E gD=B.1T(2a 2l(\'</(\'+1Z+\')>\',\'gi\'));E aQ=B.1T(/<\\/(p|1i)>/gi);if(!gD&&(aQ===4e||(aQ&&aQ.12<=1))){E gI=B.1T(/<br\\s?\\/?>/gi);if(!gI){q.1x.9D=1q;B=B.K(/<\\/?(p|1i)(.*?)>/gi,\'\')}}}F B},lq:C(3D,5J){5J=(((5J||\'\')+\'\').3e().1T(/<[a-z][a-7t-9]*>/g)||[]).3c(\'\');E 2d=/<\\/?([a-z][a-7t-9]*)\\b[^>]*>/gi;F 3D.K(2d,C($0,$1){F 5J.4Q(\'<\'+$1.3e()+\'>\')>-1?$0:\'\'})},lr:C(B){B=q.1x.gJ(B);B=q.1x.gN(B);B=q.1x.gM(B);F B},gJ:C(B){E 2F=B.1T(/<2F(.*?)>([\\w\\W]*?)<\\/2F>/gi);if(2F!==4e){$.1B(2F,$.1d(C(i,s){E 2C=s.1T(/<2F(.*?)>([\\w\\W]*?)<\\/2F>/i);2C[2]=2C[2].K(/<br\\s?\\/?>/g,\'\\n\');2C[2]=2C[2].K(/&5s;/g,\' \');if(q.G.6v){2C[2]=2C[2].K(/\\t/g,9F(q.G.6v+1).3c(\' \'))}2C[2]=q.1x.as(2C[2]);2C[2]=2C[2].K(/\\$/g,\'&#36;\');B=B.K(s,\'<2F\'+2C[1]+\'>\'+2C[2]+\'</2F>\')},q))}F B},gN:C(B){E 1e=B.1T(/<1e(.*?)>([\\w\\W]*?)<\\/1e>/gi);if(1e!==4e){$.1B(1e,$.1d(C(i,s){E 2C=s.1T(/<1e(.*?)>([\\w\\W]*?)<\\/1e>/i);2C[2]=2C[2].K(/&5s;/g,\' \');2C[2]=q.1x.as(2C[2]);2C[2]=2C[2].K(/\\$/g,\'&#36;\');B=B.K(s,\'<1e\'+2C[1]+\'>\'+2C[2]+\'</1e>\')},q))}F B},gM:C(B){B=B.K(/&lt;1j id=&5N;J-2r-([0-9])&5N; 1G=&5N;I-J-2r&5N; 1f-3H=&5N;I&5N;&gt;​&lt;\\/1j&gt;/g,\'<1j id="J-2r-$1" 1G="I-J-2r" 1f-3H="I">​</1j>\');F B},cG:C(B){B=B.K(/<br\\s?\\/?>|<\\/H[1-6]>|<\\/p>|<\\/1i>|<\\/li>|<\\/2Y>/gi,\'\\n\');E 5q=1l.3w(\'1i\');5q.3x=B;B=5q.ll||5q.lm;F $.3k(B)},aj:C(B,2V){B=q.1x.cG(B);B=B.K(/\\n/g,\'<br />\');if(q.G.2V&&1s 2V==\'1y\'&&!q.L.1P(\'4b\')){B=q.2V.2R(B)}F B},gK:C(B){B=B.K(/<1L(.*?) 1o="(.*?)"(.*?[^>])>/gi,\'<1L$1$3>\');B=B.K(/<1L(.*?)>/gi,\'&lt;1L$1&gt;\');B=q.1x.cG(B);if(q.G.6v){B=B.K(/\\t/g,9F(q.G.6v+1).3c(\' \'))}B=q.1x.as(B);F B},gL:C(B){B=B.K(/<1L(.*?)>/gi,\'[1L$1]\');B=B.K(/<([pY]*?)>/gi,\'\');B=B.K(/\\[1L(.*?)\\]/gi,\'<1L$1>\');F B},pG:C(B){B=B.K(/<a(.*?)2k="(.*?)"(.*?)>([\\w\\W]*?)<\\/a>/gi,\'[a 2k="$2"]$4[/a]\');B=B.K(/<1L(.*?)>/gi,\'[1L$1]\');B=B.K(/<(.*?)>/gi,\'\');B=B.K(/\\[a 2k="(.*?)"\\]([\\w\\W]*?)\\[\\/a\\]/gi,\'<a 2k="$1">$2</a>\');B=B.K(/\\[1L(.*?)\\]/gi,\'<1L$1>\');F B},as:C(5f){5f=6f(5f).K(/&9o;/g,\'&\').K(/&lt;/g,\'<\').K(/&gt;/g,\'>\').K(/&5N;/g,\'"\');F 5f.K(/&/g,\'&9o;\').K(/</g,\'&lt;\').K(/>/g,\'&gt;\').K(/"/g,\'&5N;\')},gO:C(B){if(q.L.1P(\'2E\'))F B;E 1i=1l.3w(\'1i\');1i.3x=B;q.1x.cV($(1i));B=1i.3x;$(1i).1u();F B},4r:C(){if(q.L.1P(\'2E\'))F;q.1x.cV(q.$T);E cP=q.$T.1h(\'h1, h2, h3, h4, h5, h6\');cP.1h(\'1j\').1K(\'1o\');cP.1h(q.G.cW.3c(\', \')).1K(\'1o\');q.1e.1S()},cV:C($T){$T.1h(q.G.cW.3c(\', \')).1K(\'1o\');$T.1h(\'1j\').6h(\'[1f-3H="I"]\').1K(\'1o\');$T.1h(\'1j[1f-3H="I"], 1L[1f-3H="I"]\').1B(C(i,s){E $s=$(s);$s.1c(\'1o\',$s.1c(\'3T\'))})},pz:C(){},7R:C(B){if(q.L.1P(\'2E\'))F B;B=B.K(2a 2l(\'<1L(.*?[^>])>\',\'gi\'),\'<1L$1 1f-3H="I">\');B=B.K(2a 2l(\'<1j(.*?[^>])>\',\'gi\'),\'<1j$1 1f-3H="I">\');E 3M=B.1T(2a 2l(\'<(1j|1L)(.*?)1o="(.*?)"(.*?[^>])>\',\'gi\'));if(3M){E 2p=3M.12;2B(E i=0;i<2p;i++){52{E gE=3M[i].K(/1o="(.*?)"/i,\'1o="$1" 3T="$1"\');B=B.K(3M[i],gE)}51(e){}}}F B},cU:C(B){E $1i=$(\'<1i />\').B(B);E 2d=q.G.4N;2d.2N(\'1j\');$1i.1h(2d.3c(\',\')).1B(C(){E $el=$(q);E Y=q.1p.3e();$el.1c(\'1f-I-Y\',Y);if(Y==\'1j\'){if($el.1c(\'1o\'))$el.1c(\'1f-I-1o\',$el.1c(\'1o\'));N if($el.1c(\'1G\'))$el.1c(\'1f-I-1G\',$el.1c(\'1G\'))}});B=$1i.B();$1i.1u();F B},ap:C(){q.$T.1h(\'li\').1B(C(i,s){E $1N=$(s).1N();if($1N.12!==0&&($1N[0].1p==\'8D\'||$1N[0].1p==\'8N\')){$(s).1F($1N)}})},dH:C(B){B=B.K(/\\n/g,\'\');B=B.K(/[\\t]*/g,\'\');B=B.K(/\\n\\s*\\n/g,"\\n");B=B.K(/^[\\s\\n]*/g,\' \');B=B.K(/[\\s\\n]*$/g,\' \');B=B.K(/>\\s{2,}</g,\'> <\');B=B.K(/\\n\\n/g,"\\n");B=B.K(/\\6g/g,\'\');F B},5Q:C(B){if(q.G.1M){B=B.K(/<1i><br\\s?\\/?><\\/1i>/gi,\'<br />\');B=B.K(/<1i(.*?)>([\\w\\W]*?)<\\/1i>/gi,\'$2<br />\')}N{B=B.K(/<1i(.*?)>([\\w\\W]*?)<\\/1i>/gi,\'<p$1>$2</p>\')}B=B.K(/<1i(.*?[^>])>/gi,\'\');B=B.K(/<\\/1i>/gi,\'\');F B},gF:C(B){B=B.K(/<1i\\s(.*?)>/gi,\'<p>\');B=B.K(/<1i><br\\s?\\/?><\\/1i>/gi,\'<br /><br />\');B=B.K(/<1i>([\\w\\W]*?)<\\/1i>/gi,\'$1<br /><br />\');F B},8C:C(B){B=B.K(/<p\\s(.*?)>/gi,\'<p>\');B=B.K(/<p><br\\s?\\/?><\\/p>/gi,\'<br />\');B=B.K(/<p>([\\w\\W]*?)<\\/p>/gi,\'$1<br /><br />\');B=B.K(/(<br\\s?\\/?>){1,}\\n?<\\/29>/gi,\'</29>\');F B},cT:C(B){F B.K(/<64(.*?)>([\\w\\W]*?)<\\/64>/gi,\'<49$1 3T="I-64-Y">$2</49>\')},gG:C(B){F B.K(/<49(.*?) 3T="I-64-Y"(.*?)>([\\w\\W]*?)<\\/49>/gi,\'<64$1$2>$3</64>\')}}},1e:C(){F{1U:C(B){B=$.3k(B.4y());B=q.1x.gH(B);if(q.L.1P(\'2E\')){B=B.K(/<1j(.*?)id="J-2r-(1|2)"(.*?)><\\/1j>;/gi,\'\')}q.$T.B(B);q.1e.1S();if(B!==\'\')q.3f.1u();3A($.1d(q.25.cQ,q),15);if(q.2b===O)q.1R.2R()},1b:C(){E 1e=q.$2v.2K();if(q.G.5Q)1e=q.1x.5Q(1e);if(q.G.1M)1e=q.1x.8C(1e);1e=q.2H.1b(1e);F 1e},1S:C(){3A($.1d(q.1e.gY,q),10)},gY:C(){E B=q.$T.B();if(q.1e.dh&&q.1e.dh==B){F}q.1e.dh=B;B=q.1X.2u(\'pV\',B);B=q.1x.9R(B);q.$2v.2K(B);q.1X.2u(\'1S\',B);if(q.2b===O){q.1X.2u(\'kR\',B)}q.2b=O;if(q.3r.B==O){q.3r.B=q.1e.1b()}if(q.G.8n){q.$2v.1N(\'.3C\').1B(C(i,el){el.3C.gX(B)})}q.3r.h7();q.3r.9Q()},3m:C(){if(q.G.6r){q.1e.dJ()}N{q.1e.gU()}},dJ:C(){q.J.3o();q.1e.2I=q.1V.ar();E 6b=$(3l).3U();E 2J=q.$T.7P(),3n=q.$T.6c();q.$T.3v();E B=q.$2v.2K();q.gk=q.1x.dH(B);B=q.2H.1b(B);E 2b=0,2h=0;E $6W=$("<1i/>").1F($.dQ(q.1x.9R(q.$T.B()),1l,1q));E $a4=$6W.1h("1j.I-J-2r");if($a4.12>0){E a3=q.2H.1b($6W.B()).K(/&9o;/g,\'&\');if($a4.12==1){2b=q.L.bF(a3,$6W.1h("#J-2r-1").6j("7v"));2h=2b}N if($a4.12==2){2b=q.L.bF(a3,$6W.1h("#J-2r-1").6j("7v"));2h=q.L.bF(a3,$6W.1h("#J-2r-2").6j("7v"))-$6W.1h("#J-2r-1").6j("7v").4y().12}}q.J.8p();q.$2v.2K(B);if(q.G.8n){q.$2v.1N(\'.3C\').1B(C(i,el){$(el).2O();el.3C.gX(B);el.3C.pO(\'88%\',3n);el.3C.pP();if(2b==2h){el.3C.pQ(el.3C.7d(2b).3K,el.3C.7d(2h).ch)}N{el.3C.pv({3K:el.3C.7d(2b).3K,ch:el.3C.7d(2b).ch},{3K:el.3C.7d(2h).3K,ch:el.3C.7d(2h).ch})}el.3C.2n()})}N{q.$2v.3n(3n).2O().2n();q.$2v.on(\'1D.I-2v-gl\',q.1e.ga);$(3l).3U(6b);if(q.$2v[0].gR){q.$2v[0].gR(2b,2h)}q.$2v[0].3U=0}q.G.6r=O;q.1t.gT();q.1t.9q(\'B\');q.1X.2u(\'4E\',B)},gU:C(){E B;if(q.G.6r)F;E 2b=0,2h=0;if(q.G.8n){E J;q.$2v.1N(\'.3C\').1B(C(i,el){J=el.3C.pu();2b=el.3C.gC(J[0].gV);2h=el.3C.gC(J[0].e0);B=el.3C.pb()})}N{2b=q.$2v.1b(0).dE;2h=q.$2v.1b(0).dF;B=q.$2v.3v().2K()}if(2b>2h&&2h>0){E gh=2h;E gg=2b;2b=gh;2h=gg}2b=q.1e.dG(B,2b);2h=q.1e.dG(B,2h);B=B.4x(0,2b)+q.J.5a(1)+B.4x(2b);if(2h>2b){E dI=q.J.5a(1).4y().12;B=B.4x(0,2h+dI)+q.J.5a(2)+B.4x(2h+dI)}if(q.gk!==q.1x.dH(B)){q.1e.1U(B)}if(q.G.8n){q.$2v.1N(\'.3C\').3v()}q.$T.2O();if(!q.L.3z(B)){q.3f.1u()}q.J.3d();q.$2v.3h(\'1D.I-2v-gl\');q.1t.gf();q.1t.ge(\'B\');q.1R.2R();q.G.6r=1q;q.1X.2u(\'6r\',B)},ga:C(e){if(e.3j!==9)F 1q;E $el=q.$2v;E 2b=$el.1b(0).dE;$el.2K($el.2K().aB(0,2b)+"\\t"+$el.2K().aB($el.1b(0).dF));$el.1b(0).dE=$el.1b(0).dF=2b+1;F O},dG:C(B,2I){E dM=B.12;E c=0;if(B[2I]==\'>\'){c++}N{2B(E i=2I;i<=dM;i++){c++;if(B[i]==\'>\'){6n}N if(B[i]==\'<\'||i==dM){c=0;6n}}}F 2I+c}}},1X:C(){F{p5:C(){F $.7E({},q)},p7:C(){F q.$T},p6:C(){F q.$2Q},pf:C(){F q.$2j},pg:C(){F q.$2v},pq:C(){F(q.$1A)?q.$1A:O},9K:C(1w){q.1X.8e=1w},7g:C(){F q.1X.8e},2u:C(1m,e,1f){E 9V=1m+\'pp\';E gy=\'I\';E 2A=q.G[9V];if(q.$2v){E 8l=O;E 9t=$.pr(q.$2v[0],\'9t\');if(1s 9t!=\'1y\'&&1s 9t[9V]!=\'1y\'){$.1B(9t[9V],$.1d(C(1k,1E){if(1E[\'pt\']==gy){E 1f=(1s 1f==\'1y\')?[e]:[e,1f];8l=(1s 1f==\'1y\')?1E.7N.6e(q,e):1E.7N.6e(q,e,1f)}},q))}if(8l)F 8l}if($.5k(2A)){F(1s 1f==\'1y\')?2A.6e(q,e):2A.6e(q,e,1f)}N{F(1s 1f==\'1y\')?e:1f}},gA:C(){q.G.jL=1q;q.1X.2u(\'gA\');q.$2j.3h(\'.I\').gv(\'I\');q.$T.3h(\'.I\');$(1l).3h(\'6R.I.\'+q.2G);$(1l).3h(\'2y.I-M-83.\'+q.2G);$(1l).3h(\'2y.I-M-4J-3v.\'+q.2G);$(1l).3h(\'5H.I.\'+q.2G+\' 2y.I.\'+q.2G);$("31").3h(\'6b.I.\'+q.2G);$(q.G.6y).3h(\'6b.I.\'+q.2G);q.$T.2S(\'I-T I-1M I-3f\');q.$T.1K(\'5d\');E B=q.1e.1b();if(q.G.1A){q.$1A.1h(\'a\').1B(C(){E $el=$(q);if($el.1f(\'1n\')){$el.1f(\'1n\').1u();$el.1f(\'1n\',{})}})}if(q.2o.aa()){q.$2Q.3F(q.$2j);q.$2Q.1u();q.$2j.2K(B).2O()}N{q.$2Q.3F(q.$T);q.$2Q.1u();q.$2j.B(B).2O()}if(q.$5e)q.$5e.1u();if(q.$4F)q.$4F.1u();if(q.$74)q.$74.1u();$(\'.I-1A-3q-\'+q.2G).1u();gu(q.9m)}}},1n:C(){F{2o:C(1w,$1n,dU){if(1w==\'3a\'&&q.G.dT){$.1B(q.G.dT,$.1d(C(i,s){E 1w=s.Y,1C;if(1s s[\'1G\']!=\'1y\'){1w=1w+\'-\'+s[\'1G\']}s.1m=(q.L.7J(s.Y))?\'R\':\'28\';if(1s s.1C!=="1y"){1C=s.1C}N{1C=(s.1m==\'28\')?\'28.3a\':\'R.3a\'}if(q.G.1M&&s.1m==\'R\'&&s.Y==\'p\')F;q.3a[1w]={Y:s.Y,1o:s.1o,\'1G\':s[\'1G\'],1c:s.1c,1f:s.1f,8M:s.8M};dU[1w]={1C:1C,1J:s.1J}},q))}$.1B(dU,$.1d(C(2g,2X){E $2x=$(\'<a 2k="#" 1G="I-1n-\'+2g+\'" 9c="1t">\'+2X.1J+\'</a>\');if(1w==\'3a\')$2x.2f(\'I-3a-\'+2g);$2x.on(\'2y\',$.1d(C(e){e.2w();E 1m=\'1C\';E 2A=2X.1C;if(2X.4A){1m=\'4A\';2A=2X.4A}N if(2X.1n){1m=\'1n\';2A=2X.1n}if($(e.1Q).3i(\'I-1n-V-c9\'))F;q.1t.ac(e,2g,1m,2A);q.1n.7C()},q));q.1R.cc($2x,2g,2X);$1n.1F($2x)},q))},2O:C(e,1k){if(!q.G.6r){e.2w();F O}E $1t=q.1t.1b(1k);E $1n=$1t.1f(\'1n\').aU(1l.31);if(q.G.cA){$1n.2f("I-1n-jP")}if($1t.3i(\'7y\')){q.1n.7C()}N{q.1n.7C();q.1R.79();q.1X.2u(\'pm\',{1n:$1n,1k:1k,1t:$1t});q.1t.9q(1k);$1t.2f(\'7y\');E 7x=$1t.2I();E dS=$1n.2J();if((7x.2c+dS)>$(1l).2J()){7x.2c=8d.pl(0,7x.2c-dS)}E 2c=7x.2c+\'px\';if(q.$1A.3i(\'1A-82-2Q\')){E 2U=q.$1A.6c()+q.G.6z;E 4g=\'82\';if(q.G.6y!==1l){2U=(q.$1A.6c()+q.$1A.2I().2U)+q.G.6z;4g=\'8u\'}$1n.1O({4g:4g,2c:2c,2U:2U+\'px\'}).2O()}N{E 2U=($1t.6c()+7x.2U)+\'px\';$1n.1O({4g:\'8u\',2c:2c,2U:2U}).2O()}q.1X.2u(\'qP\',{1n:$1n,1k:1k,1t:$1t});q.$1n=$1n}$(1l).dR(\'2y.I-1n\',$.1d(q.1n.3v,q));q.$T.dR(\'2y.I-1n\',$.1d(q.1n.3v,q));$(1l).dR(\'2s.I-1n\',$.1d(q.1n.9a,q));$1n.on(\'ay.I-1n\',$.1d(q.L.d8,q)).on(\'az.I-1n\',$.1d(q.L.bJ,q));e.fw()},9a:C(e){if(e.7U!=q.3j.b2)F;q.1n.7C();q.$T.2n()},7C:C(){q.$1A.1h(\'a.7y\').2S(\'I-8A\').2S(\'7y\');q.L.bJ();$(\'.I-1n-\'+q.2G).3v();$(\'.I-1n-V-h9\').2S(\'I-1n-V-h9\');if(q.$1n){q.$1n.3h(\'.I-1n\');q.1X.2u(\'qC\',q.$1n);q.$1n=O}},3v:C(e){E $1n=$(e.1Q);if(!$1n.3i(\'7y\')&&!$1n.3i(\'I-1n-V-c9\')){$1n.2S(\'7y\');$1n.3h(\'ay az\');q.1n.7C()}}}},22:C(){F{2O:C(){q.1a.2R(\'22\',q.1H.1b(\'22\'),hP);q.1v.3Q(\'#I-1a-22-1v\',q.G.76,q.22.1Y);q.J.3o();q.J.1b();E 1g=q.2T.4y();$(\'#I-6k\').2K(1g);q.1a.2O()},1Y:C(2P,4Y,e){if(1s 2P.6U!=\'1y\'){q.1a.4j();q.J.3d();q.1X.2u(\'qG\',2P);F}E V;if(1s 2P==\'6Z\'){V=2P}N{E 1g=$(\'#I-6k\').2K();if(1s 1g==\'1y\'||1g===\'\')1g=2P.6k;V=\'<a 2k="\'+2P.9b+\'" id="9b-2r">\'+1g+\'</a>\'}if(4Y){q.J.8p();E 2r=q.J.6Q();q.1Y.dA(e,2r)}N{q.1a.4j()}q.J.3d();q.25.1U();q.1Y.fc(V);if(1s 2P==\'6Z\')F;E 8t=$(q.$T.1h(\'a#9b-2r\'));if(8t.12!==0){8t.1K(\'id\').1K(\'1o\')}N 8t=O;q.1X.2u(\'76\',8t,2P)}}},2n:C(){F{2W:C(){q.$T.2n();E 3I=q.$T.3O().3I();if(3I.12===0)F;if(3I[0].12===0||3I[0].1p==\'58\'||3I[0].a8==3){F}if(3I[0].1p==\'8D\'||3I[0].1p==\'8N\'){E 4w=3I.1h(\'li\').3I();if(!q.L.6X(4w)&&4w.1g()===\'\'){q.1V.2W(4w);F}}if(q.G.1M&&!q.L.7J(3I[0].1p)){q.J.1b();q.14.2W(q.$T[0],0);q.14.3P(q.$T[0],0);q.J.4l();F}q.1V.2W(3I)},3P:C(){E 2Z=q.$T.3O().2Z();q.$T.2n();if(2Z.9B()===0)F;if(q.L.3z(q.$T.B())){q.J.1b();q.14.43(1q);q.14.9I(2Z[0]);q.14.3P(2Z[0],0);q.J.4l()}N{q.J.1b();q.14.9j(2Z[0]);q.14.43(O);q.J.4l()}},dO:C(){E 8v=1l.4D().8v;if(8v===4e)F O;if(q.G.1M&&$(8v.e4).3i(\'I-1M\'))F 1q;N if(!q.L.4p(8v.e4))F O;F q.$T.is(\':2n\')}}},M:C(){F{2O:C(){q.1a.2R(\'M\',q.1H.1b(\'M\'),hP);q.1v.3Q(\'#I-1a-M-4d\',q.G.75,q.M.1Y);q.J.3o();q.1a.2O()},hc:C($M){E $V=$M.2q(\'a\',q.$T[0]);q.1a.2R(\'iX\',q.1H.1b(\'7L\'),qv);q.1a.cy();q.M.hJ=q.1a.io(q.1H.1b(\'hK\'));q.M.hF=q.1a.cz(q.1H.1b(\'3o\'));q.M.hJ.on(\'2y\',$.1d(C(){q.M.1u($M)},q));q.M.hF.on(\'2y\',$.1d(C(){q.M.i0($M)},q));$(\'.I-V-3q\').1u();$(\'#I-M-1J\').2K($M.1c(\'80\'));if(!q.G.hE)$(\'.I-M-V-42\').3v();N{E $dP=$(\'#I-M-V\');$dP.1c(\'2k\',$M.1c(\'4t\'));if($V.12!==0){$dP.2K($V.1c(\'2k\'));if($V.1c(\'1Q\')==\'7K\')$(\'#I-M-V-7Z\').6j(\'a0\',1q)}}if(!q.G.hH)$(\'.I-M-4g-42\').3v();N{E hI=($M.1O(\'6N\')==\'R\'&&$M.1O(\'7f\')==\'5p\')?\'5u\':$M.1O(\'7f\');$(\'#I-M-5g\').2K(hI)}q.1a.2O();$(\'#I-M-1J\').2n()},i1:C($M){E hQ=$(\'#I-M-5g\').2K();E ag=\'\';E dD=\'\';E 8P=\'\';eJ(hQ){9G\'2c\':ag=\'2c\';8P=\'0 \'+q.G.8x+\' \'+q.G.8x+\' 0\';6n;9G\'4s\':ag=\'4s\';8P=\'0 0 \'+q.G.8x+\' \'+q.G.8x;6n;9G\'5u\':dD=\'R\';8P=\'bj\';6n}$M.1O({\'7f\':ag,6N:dD,4B:8P});$M.1c(\'3T\',$M.1c(\'1o\'))},i0:C($M){q.M.7W();q.25.1U();E $V=$M.2q(\'a\',q.$T[0]);E 1J=$(\'#I-M-1J\').2K().K(/(<([^>]+)>)/ig,"");$M.1c(\'80\',1J);q.M.i1($M);E V=$.3k($(\'#I-M-V\').2K());E V=V.K(/(<([^>]+)>)/ig,"");if(V!==\'\'){E 7D=\'((jT--)?[a-7t-9]+(-[a-7t-9]+)*\\\\.)+[a-z]{2,}\';E 3J=2a 2l(\'^(8s|8K|5o)://\'+7D,\'i\');E a1=2a 2l(\'^\'+7D,\'i\');if(V.3N(3J)==-1&&V.3N(a1)===0&&q.G.5c){V=q.G.5c+\'://\'+V}E 1Q=($(\'#I-M-V-7Z\').6j(\'a0\'))?1q:O;if($V.12===0){E a=$(\'<a 2k="\'+V+\'">\'+q.L.6q($M)+\'</a>\');if(1Q)a.1c(\'1Q\',\'7K\');$M.2t(a)}N{$V.1c(\'2k\',V);if(1Q){$V.1c(\'1Q\',\'7K\')}N{$V.1K(\'1Q\')}}}N if($V.12!==0){$V.2t(q.L.6q($M))}q.1a.4j();q.1R.au();q.1e.1S()},j1:C($M){if(q.G.dq){$M.on(\'qH\',$.1d(q.M.bn,q))}E 7N=$.1d(C(e){q.1R.M=$M;q.M.8f=q.M.hk($M);$(1l).on(\'6R.I-M-4J-3v.\'+q.2G,$.1d(q.M.7W,q));if(!q.G.dp)F;q.M.8f.on(\'6R.I 5H.I\',$.1d(C(e){q.M.i2(e,$M)},q))},q);$M.3h(\'6R.I\').on(\'6R.I\',$.1d(q.M.7W,q));$M.3h(\'2y.I 5H.I\').on(\'2y.I 5H.I\',7N)},i2:C(e,$M){e.2w();q.M.4k={x:e.hY,y:e.ah,el:$M,dC:$M.2J()/$M.3n(),h:$M.3n()};e=e.7T||e;if(e.8G){q.M.4k.x=e.8G[0].hY;q.M.4k.y=e.8G[0].ah}q.M.hX()},hX:C(){$(1l).on(\'qI.I-M-4J qT.I-M-4J\',$.1d(q.M.hT,q));$(1l).on(\'iZ.I-M-4J qU.I-M-4J\',$.1d(q.M.hW,q))},hT:C(e){e.2w();e=e.7T||e;E 3n=q.M.4k.h;if(e.8G)3n+=(e.8G[0].ah-q.M.4k.y);N 3n+=(e.ah-q.M.4k.y);E 2J=8d.hV(3n*q.M.4k.dC);if(3n<50||2J<88)F;E 3n=8d.hV(q.M.4k.el.2J()/q.M.4k.dC);q.M.4k.el.1c({2J:2J,3n:3n});q.M.4k.el.2J(2J);q.M.4k.el.3n(3n);q.1e.1S()},hW:C(){q.iw=O;$(1l).3h(\'.I-M-4J\');q.M.7W()},bn:C(e){if(q.$T.1h(\'#I-M-2Q\').12!==0){e.2w();F O}q.$T.on(\'57.I-M-hD-57\',$.1d(C(){3A($.1d(q.M.bs,q),1)},q))},bs:C(){q.M.hC();q.1R.au();q.$T.3h(\'57.I-M-hD-57\');q.1x.4r();q.1e.1S()},hC:C(){q.$T.1h(\'1L[1f-3o-2i]\').1B(C(){E $el=$(q);$el.1c(\'4t\',$el.1c(\'1f-3o-2i\'));$el.1K(\'1f-3o-2i\')})},7W:C(e){if(e&&$(e.1Q).2q(\'#I-M-2Q\',q.$T[0]).12!==0)F;if(e&&e.1Q.1p==\'aA\'){E $M=$(e.1Q);$M.1c(\'1f-3o-2i\',$M.1c(\'4t\'))}E 3G=q.$T.1h(\'#I-M-2Q\');if(3G.12===0)F;$(\'#I-M-5l\').1u();$(\'#I-M-8f\').1u();3G.1h(\'1L\').1O({63:3G[0].1o.63,af:3G[0].1o.af,ae:3G[0].1o.ae,ab:3G[0].1o.ab});3G.1O(\'4B\',\'\');3G.1h(\'1L\').1O(\'hg\',\'\');3G.2t(C(){F $(q).26()});$(1l).3h(\'6R.I-M-4J-3v.\'+q.2G);if(1s q.M.4k!==\'1y\'){q.M.4k.el.1c(\'3T\',q.M.4k.el.1c(\'1o\'))}q.1e.1S()},hd:C($M,3G){if(q.G.dp&&!q.L.6M()){E 8a=$(\'<1j id="I-M-8f" 1f-I="3H"></1j>\');if(!q.L.7b()){8a.1O({2J:\'hi\',3n:\'hi\'})}8a.1c(\'5d\',O);3G.1F(8a);3G.1F($M);F 8a}N{3G.1F($M);F O}},hk:C($M){E 3G=$(\'<1j id="I-M-2Q" 1f-I="3H">\');3G.1O(\'7f\',$M.1O(\'7f\')).1c(\'5d\',O);if($M[0].1o.4B!=\'bj\'){3G.1O({63:$M[0].1o.63,af:$M[0].1o.af,ae:$M[0].1o.ae,ab:$M[0].1o.ab});$M.1O(\'4B\',\'\')}N{3G.1O({\'6N\':\'R\',\'4B\':\'bj\'})}$M.1O(\'hg\',\'.5\').3F(3G);if(q.G.dq){q.M.5l=$(\'<1j id="I-M-5l" 1f-I="3H">\'+q.1H.1b(\'7L\')+\'</1j>\');q.M.5l.1c(\'5d\',O);q.M.5l.on(\'2y\',$.1d(C(){q.M.hc($M)},q));3G.1F(q.M.5l);E hb=q.M.5l.7P();q.M.5l.1O(\'4B-2c\',\'-\'+hb/2+\'px\')}F q.M.hd($M,3G)},1u:C(M){E $M=$(M);E $V=$M.2q(\'a\',q.$T[0]);E $6K=$M.2q(\'6K\',q.$T[0]);E $1r=$M.1r();if($(\'#I-M-2Q\').12!==0){$1r=$(\'#I-M-2Q\').1r()}E $1N;if($6K.12!==0){$1N=$6K.1N();$6K.1u()}N if($V.12!==0){$1r=$V.1r();$V.1u()}N{$M.1u()}$(\'#I-M-2Q\').1u();if($6K.12!==0){q.1V.2W($1N)}N{q.1V.2W($1r)}q.1X.2u(\'dw\',$M[0].4t,$M);q.1a.4j();q.1e.1S()},1Y:C(2P,4Y,e){if(1s 2P.6U!=\'1y\'){q.1a.4j();q.J.3d();q.1X.2u(\'qs\',2P);F}E $1L;if(1s 2P==\'6Z\'){$1L=$(2P).1c(\'1f-I-8m-M\',\'1q\')}N{$1L=$(\'<1L>\');$1L.1c(\'4t\',2P.9b).1c(\'1f-I-8m-M\',\'1q\')}E Q=$1L;E do=q.L.4S(\'P\');if(do){Q=$(\'<29 />\').1F($1L)}if(4Y){q.J.8p();E 2r=q.J.6Q();q.1Y.dA(e,2r)}N{q.1a.4j()}q.J.3d();q.25.1U();q.1Y.B(q.L.6q(Q),O);E $M=q.$T.1h(\'1L[1f-I-8m-M=1q]\').1K(\'1f-I-8m-M\');if(do){$M.1r().26().3Y().5Z(\'<p />\')}N if(q.G.1M){if(!q.L.3z(q.1e.1b())){$M.a6(\'<br>\')}$M.3F(\'<br>\')}if(1s 2P==\'6Z\')F;q.1X.2u(\'75\',$M,2P)}}},3y:C(){F{bo:C(){if(!q.L.1P(\'2E\'))q.$T.2n();q.25.1U();q.J.3o();E R=q.J.4a();if(R&&R.1p==\'5v\'){q.3y.ho()}N if(R===O&&q.G.1M){q.3y.hx()}N{q.3y.hy()}q.J.3d();q.1e.1S()},ho:C(){1l.3Z(\'3y\');q.3y.aO();q.1x.ap();q.1x.4r()},hy:C(){$.1B(q.J.3X(),$.1d(C(i,4V){if(4V.1p===\'6F\'||4V.1p===\'dn\')F;E $el=q.L.bh(4V);E 2c=q.L.dr($el.1O(\'4B-2c\'))+q.G.ad;$el.1O(\'4B-2c\',2c+\'px\')},q))},hx:C(){E 4M=q.J.5Z(\'1i\');$(4M).1c(\'1f-8b\',\'I\');$(4M).1O(\'4B-2c\',q.G.ad+\'px\')},95:C(){q.25.1U();q.J.3o();E R=q.J.4a();if(R&&R.1p==\'5v\'){q.3y.hz()}N{q.3y.hA()}q.J.3d();q.1e.1S()},hz:C(){1l.3Z(\'7p\');E 1z=q.J.3B();E $2x=$(1z).2q(\'li\',q.$T[0]);q.3y.aO();if(!q.G.1M&&$2x.12===0){1l.3Z(\'9S\',O,\'p\');q.$T.1h(\'3p, ol, 29, p\').1B($.1d(q.L.4f,q))}q.1x.4r()},hA:C(){$.1B(q.J.3X(),$.1d(C(i,4V){E $el=q.L.bh(4V);E 2c=q.L.dr($el.1O(\'4B-2c\'))-q.G.ad;if(2c<=0){if(q.G.1M&&1s($el.1f(\'8b\'))!==\'1y\'){$el.2t($el.B()+\'<br />\')}N{$el.1O(\'4B-2c\',\'\');q.L.5r($el,\'1o\')}}N{$el.1O(\'4B-2c\',2c+\'px\')}},q))},aO:C(){E R=q.J.4a();if(q.14.53&&R&&R.1p==\'5v\'&&q.L.3z($(R).1g())){E $R=$(R);$R.1h(\'1j\').6h(\'.I-J-2r\').26().3Y();$R.1F(\'<br>\')}}}},28:C(){F{3a:C(1w){E 1m,1E;if(1s q.3a[1w].1o!=\'1y\')1m=\'1o\';N if(1s q.3a[1w][\'1G\']!=\'1y\')1m=\'1G\';if(1m)1E=q.3a[1w][1m];q.28.30(q.3a[1w].Y,1m,1E)},30:C(Y,1m,1E){E 1z=q.J.3B();if(1z&&1z.1p===\'qc\')F;q.8h=1q;if(q.L.4S(\'8o\')||q.L.h8())F;E 2d=[\'b\',\'4n\',\'i\',\'4m\',\'5x\',\'eM\',\'5Y\',\'hv\',\'hq\'];E hu=[\'5i\',\'5i\',\'em\',\'em\',\'u\',\'4h\',\'4h\',\'7Q\',\'7l\'];2B(E i=0;i<2d.12;i++){if(Y==2d[i])Y=hu[i]}if(q.G.4z){if($.3t(Y,q.G.4z)==-1)F}N{if($.3t(Y,q.G.4c)!==-1)F}q.28.1m=1m||O;q.28.1E=1E||O;q.25.1U();if(!q.L.1P(\'2E\')){q.$T.2n()}q.J.1b();if(q.14.53){q.28.i3(Y)}N{q.28.fS(Y)}},i3:C(Y){E 1z=q.J.3B();E $1r=$(1z).2q(Y+\'[1f-I-Y=\'+Y+\']\',q.$T[0]);if($1r.12!==0&&(q.28.1m!=\'1o\'&&$1r[0].1p!=\'6w\')){if(q.L.3z($1r.1g())){q.1V.54($1r[0]);$1r.1u();q.1e.1S()}N if(q.L.6T($1r)){q.1V.54($1r[0])}F}E Q=$(\'<\'+Y+\'>\').1c(\'1f-3H\',\'I\').1c(\'1f-I-Y\',Y);Q.B(q.G.6o);Q=q.28.dj(Q);E Q=q.1Y.Q(Q);q.1V.3P(Q);q.1e.1S()},fS:C(Y){q.28.eF(Y);q.J.3o();1l.3Z(\'eM\');q.$T.1h(\'7c\').1B($.1d(C(i,s){E $el=$(s);q.28.eG($el,Y);E $1j;if(q.28.1m){$1j=$(\'<1j>\').1c(\'1f-I-Y\',Y).1c(\'1f-3H\',\'I\');$1j=q.28.dj($1j)}N{$1j=$(\'<\'+Y+\'>\').1c(\'1f-I-Y\',Y).1c(\'1f-3H\',\'I\')}$el.2t($1j.B($el.26()));E $1r=$1j.1r();if($1r&&$1r[0].1p===\'U\'){$1j.1r().2t($1j)}if(Y==\'1j\'){if($1r&&$1r[0].1p===\'6w\'&&q.28.1m===\'1o\'){E 2C=q.28.1E.4o(\';\');2B(E z=0;z<2C.12;z++){if(2C[z]===\'\')F;E 1o=2C[z].4o(\':\');$1r.1O(1o[0],\'\');if(q.L.5r($1r,\'1o\')){$1r.2t($1r.26())}}}}},q));if(Y!=\'1j\'){q.$T.1h(q.G.4N.3c(\', \')).1B($.1d(C(i,s){E $el=$(s);if(s.1p===\'U\'&&s.4u.12===0){$el.2t($el.26());F}E ai=$el.1O(\'1g-7a\');if(ai===\'3K-eP\'){$el.1O(\'1g-7a\',\'\');q.L.5r($el,\'1o\')}},q))}if(Y!=\'4h\'){E eK=q;q.$T.1h(\'28\').1B(C(i,s){eK.L.4I(s,\'4h\')})}q.J.3d();q.1e.1S()},eG:C($el,Y){E 3u=q;$el.3O(Y).1B(C(){E $4w=$(q);if(!$4w.3i(\'I-J-2r\')){if(3u.28.1m==\'1o\'){E 2C=3u.28.1E.4o(\';\');2B(E z=0;z<2C.12;z++){if(2C[z]===\'\')F;E 1o=2C[z].4o(\':\');$4w.1O(1o[0],\'\');if(3u.L.5r($4w,\'1o\')){$4w.2t($4w.26())}}}N{$4w.26().3Y()}}})},eF:C(Y){q.J.3o();E 1h=\'\';if(q.28.1m==\'1G\')1h=\'[1f-I-1G=\'+q.28.1E+\']\';N if(q.28.1m==\'1o\'){1h=\'[1f-I-1o="\'+q.28.1E+\'"]\'}E 3u=q;if(Y!=\'4h\'){q.$T.1h(\'4h\').1B(C(i,s){3u.L.4I(s,\'28\')})}if(Y!=\'1j\'){q.$T.1h(Y).1B(C(){E $el=$(q);$el.2t($(\'<7c />\').B($el.26()))})}q.$T.1h(\'[1f-I-Y="\'+Y+\'"]\'+1h).1B(C(){if(1h===\'\'&&Y==\'1j\'&&q.1p.3e()==Y)F;E $el=$(q);$el.2t($(\'<7c />\').B($el.26()))});q.J.3d()},dj:C(Q){eJ(q.28.1m){9G\'1G\':if(Q.3i(q.28.1E)){Q.2S(q.28.1E);Q.1K(\'1f-I-1G\')}N{Q.2f(q.28.1E);Q.1c(\'1f-I-1G\',q.28.1E)}6n;9G\'1o\':Q[0].1o.q4=q.28.1E;Q.1c(\'1f-I-1o\',q.28.1E);6n}F Q},qd:C(){q.25.1U();E 1z=q.J.3B();E 23=q.J.e8();q.J.3o();if(1z&&1z.1p===\'6w\'){E $s=$(1z);$s.1K(\'1o\');if($s[0].4u.12===0){$s.2t($s.26())}}$.1B(23,$.1d(C(i,s){E $s=$(s);if($.3t(s.1p.3e(),q.G.4N)!=-1&&!$s.3i(\'I-J-2r\')){$s.1K(\'1o\');if($s[0].4u.12===0){$s.2t($s.26())}}},q));q.J.3d();q.1e.1S()},qe:C(1w){q.25.1U();E 1r=q.J.67();E 23=q.J.e8();q.J.3o();if(1r&&1r.1p===\'6w\'){E $s=$(1r);$s.1O(1w,\'\');q.L.5r($s,\'1o\');if($s[0].4u.12===0){$s.2t($s.26())}}$.1B(23,$.1d(C(i,s){E $s=$(s);if($.3t(s.1p.3e(),q.G.4N)!=-1&&!$s.3i(\'I-J-2r\')){$s.1O(1w,\'\');q.L.5r($s,\'1o\');if($s[0].4u.12===0){$s.2t($s.26())}}},q));q.J.3d();q.1e.1S()},dm:C(){q.25.1U();E 1z=q.J.3B();q.J.3o();1l.3Z(\'dm\');if(1z&&1z.1p===\'6w\'){$(1z).2t($(1z).26())}$.1B(q.J.6I(),$.1d(C(i,s){E $s=$(s);if($.3t(s.1p.3e(),q.G.4N)!=-1&&!$s.3i(\'I-J-2r\')){$s.2t($s.26())}},q));q.J.3d();q.1e.1S()},7O:C(3E){q.28.30(\'1j\',\'1G\',3E)},qn:C(1E){q.28.30(\'1j\',\'1o\',1E)}}},1Y:C(){F{1U:C(B,1x){q.3f.1u();B=q.1x.7R(B);if(1s 1x==\'1y\'){B=q.1x.8y(B,O)}q.$T.B(B);q.J.1u();q.2n.3P();q.1x.ap();q.1e.1S();q.1R.2R();if(1s 1x==\'1y\'){3A($.1d(q.1x.4r,q),10)}},1g:C(1g){q.3f.1u();1g=1g.4y();1g=$.3k(1g);1g=q.1x.aj(1g,O);q.$T.2n();if(q.L.1P(\'2E\')){q.1Y.ds(1g)}N{q.J.1b();q.14.55();E el=1l.3w("1i");el.3x=1g;E 4L=1l.9M(),Q,5K;56((Q=el.9N)){5K=4L.81(Q)}q.14.3L(4L);if(5K){E 14=q.14.7S();14.9I(5K);14.43(1q);q.2T.8T();q.2T.4l(14)}}q.1e.1S();q.1x.4r()},fc:C(B){q.1Y.B(B,O)},B:C(B,1x){q.3f.1u();if(1s 1x==\'1y\')1x=1q;q.$T.2n();B=q.1x.7R(B);if(1x){B=q.1x.8y(B)}if(q.L.1P(\'2E\')){q.1Y.ds(B)}N{if(q.1x.9D)q.1Y.eV(B);N 1l.3Z(\'qr\',O,B);q.1Y.eX()}q.1x.ap();if(!q.G.1M){q.$T.1h(\'p\').1B($.1d(q.L.4f,q))}q.1e.1S();q.1R.2R();if(1x){q.1x.4r()}},eX:C(){if(!q.L.1P(\'4b\'))F;E $1N=$(q.J.4a()).1N();if($1N.12>0&&$1N[0].1p==\'P\'&&$1N.B()===\'\'){$1N.1u()}},ds:C(B){if(q.L.gj()){E 1r=q.L.4S(\'P\');E $B=$(\'<1i>\').1F(B);E eU=$B.26().is(\'p, :87, dl, 3p, ol, 1i, 3W, 2Y, 29, 2F, du, 49, 87, c8, eE, eS\');if(1r&&eU)q.1Y.ey(1r,B);N q.1Y.ew(B);F}1l.J.92().gw(B)},eV:C(B){B=q.1x.7R(B);q.J.1b();q.14.55();E el=1l.3w(\'1i\');el.3x=B;E 4L=1l.9M(),Q,5K;56((Q=el.9N)){5K=4L.81(Q)}q.14.3L(4L);q.14.43(1q);q.1V.54(5K)},Q:C(Q,55){Q=Q[0]||Q;E B=q.L.6q(Q);B=q.1x.7R(B);if(B.1T(/</g)!==4e){Q=$(B)[0]}q.J.1b();if(55!==O){q.14.55()}q.14.3L(Q);q.14.43(O);q.J.4l();F Q},qf:C(Q,x,y){Q=Q[0]||Q;q.J.1b();E 14;if(1l.aq){E 3s=1l.aq(x,y);q.14.2W(3s.er,3s.2I);q.14.43(1q);q.14.3L(Q)}N if(1l.ao){14=1l.ao(x,y);14.3L(Q)}N if(1s 1l.31.an!="1y"){14=1l.31.an();14.ak(x,y);E 7e=14.eo();7e.ak(x,y);14.eA("ez",7e);14.7z()}},dA:C(e,Q){Q=Q[0]||Q;E 14;E x=e.qi,y=e.qk;if(1l.aq){E 3s=1l.aq(x,y);E 2T=1l.4D();14=2T.6l(0);14.2W(3s.er,3s.2I);14.43(1q);14.3L(Q)}N if(1l.ao){14=1l.ao(x,y);14.3L(Q)}N if(1s 1l.31.an!="1y"){14=1l.31.an();14.ak(x,y);E 7e=14.eo();7e.ak(x,y);14.eA("ez",7e);14.7z()}},ey:C(1r,B){E Q=1l.3w(\'1j\');Q.3E=\'I-ie-5C\';q.1Y.Q(Q);E 7m=$(1r).B();7m=\'<p>\'+7m.K(/<1j 1G="I-ie-5C"><\\/1j>/gi,\'</p>\'+B+\'<p>\')+\'</p>\';7m=7m.K(/<p><\\/p>/gi,\'\');$(1r).2t(7m)},ew:C(B){q.J.1b();q.14.55();E el=1l.3w("1i");el.3x=B;E 4L=1l.9M(),Q,5K;56((Q=el.9N)){5K=4L.81(Q)}q.14.3L(4L);q.14.43(O);q.J.4l()}}},1D:C(){F{3Q:C(e){if(q.7B)F;E 1k=e.7U;E 4W=(1k>=37&&1k<=40);q.1D.48=e.9u||e.6J;q.1D.1z=q.J.3B();q.1D.1r=q.J.67();q.1D.R=q.J.4a();q.1D.2F=q.L.bB(q.1D.1z,\'2F\');q.1D.29=q.L.bB(q.1D.1z,\'29\');q.1D.7Y=q.L.bB(q.1D.1z,\'7Y\');q.6m.3Q(e,1k);if(q.L.7b()){q.1D.fN(4W,1k);q.1D.g6(e,1k)}q.1D.fV(4W);q.1D.fY(e,1k);E eC=q.1X.2u(\'1D\',e);if(eC===O){e.2w();F O}if(q.G.al&&(q.L.1P(\'2E\')||q.L.1P(\'4b\'))&&(1k===q.3j.dW||1k===q.3j.eB)){E dy=O;E $3W=O;if(q.1D.R&&q.1D.R.1p===\'6F\'){$3W=$(q.1D.R).2q(\'3W\',q.$T[0])}if($3W&&$3W.1h(\'2Y\').2Z()[0]===q.1D.R){dy=1q}if(q.L.6T()&&dy){E Q=$(q.G.5S);$3W.3F(Q);q.1V.2W(Q)}}if(q.G.al&&1k===q.3j.dW){q.1D.fH()}if(!q.G.al&&1k===q.3j.70){e.2w();if(!q.14.53)q.14.55();F}if(1k==q.3j.70&&!e.6s&&!e.9u&&!e.6J){E fQ=q.1X.2u(\'nz\',e);if(fQ===O){e.2w();F O}if(q.1D.29&&q.1D.fk(e)===1q){F O}E 1z,$1N;if(q.1D.2F){F q.1D.fE(e)}N if(q.1D.29||q.1D.7Y){1z=q.J.3B();$1N=$(1z).1N();if($1N.12!==0&&$1N[0].1p==\'58\'){F q.1D.6x(e)}N if(q.L.6T()&&(1z&&1z!=\'6w\')){F q.1D.9x(e)}N{F q.1D.6x(e)}}N if(q.G.1M&&!q.1D.R){1z=q.J.3B();$1N=$(q.1D.1z).1N();if($1N.12!==0&&$1N[0].1p==\'58\'){F q.1D.6x(e)}N if(1z!==O&&$(1z).3i(\'I-7s-3g\')){q.1V.54(1z);$(1z).26().3Y();F q.1D.9x(e)}N{if(q.L.dN()){F q.1D.9x(e)}N if($1N.12===0&&1z===O&&1s $1N.ny!=\'1y\'){F q.1D.6x(e)}F q.1D.6x(e)}}N if(q.G.1M&&q.1D.R){3A($.1d(q.1D.fs,q),1)}N if(!q.G.1M&&q.1D.R){3A($.1d(q.1D.fm,q),1);if(q.1D.R.1p===\'5v\'){1z=q.J.3B();E $1r=$(1z).2q(\'li\',q.$T[0]);E $2e=$1r.2q(\'3p,ol\',q.$T[0]);if($1r.12!==0&&q.L.3z($1r.B())&&$2e.1N().12===0&&q.L.3z($2e.1h("li").2Z().B())){$2e.1h("li").2Z().1u();E Q=$(q.G.5S);$2e.3F(Q);q.1V.2W(Q);F O}}}N if(!q.G.1M&&!q.1D.R){F q.1D.fj(e)}}if(1k===q.3j.70&&(e.9u||e.6s)){F q.1D.i4(e)}if(1k===q.3j.dx||e.6J&&1k===ek||e.6J&&1k===e5){F q.1D.fG(e,1k)}if(1k===q.3j.84||1k===q.3j.8Q){E 23=q.J.6I();if(23){E 2p=23.12;E 2Z;2B(E i=0;i<2p;i++){E 3O=$(23[i]).3O(\'1L\');if(3O.12!==0){E 3u=q;$.1B(3O,C(z,s){E $s=$(s);if($s.1O(\'7f\')!=\'5p\')F;3u.1X.2u(\'dw\',s.4t,$s);2Z=s})}N if(23[i].1p==\'aA\'){if(2Z!=23[i]){q.1X.2u(\'dw\',23[i].4t,$(23[i]));2Z=23[i]}}}}}if(1k===q.3j.84){E R=q.J.4a();E ff=($(R).1O(\'4B-2c\')!==\'nC\');if(R&&ff&&q.14.53&&q.L.gp()){q.3y.95();e.2w();F}if(q.L.1P(\'4b\')){E 4G=q.J.iG();E am=$(4G).4G()[0];if(4G&&4G.1p===\'fT\')$(4G).1u();if(am&&am.1p===\'fT\')$(am).1u()}q.1D.hp();q.1D.ij(e)}q.1e.1S()},fN:C(4W,1k){if(!4W&&(q.1X.7g()==\'2y\'||q.1X.7g()==\'4W\')){q.1X.9K(O);if(q.1D.fJ(1k)){q.25.1U()}}},fJ:C(1k){E k=q.3j;E 4X=[k.84,k.8Q,k.70,k.a7,k.b2,k.dx,k.fK,k.fL,k.fM,k.fU];F($.3t(1k,4X)==-1)?1q:O},fV:C(4W){if(!4W)F;if((q.1X.7g()==\'2y\'||q.1X.7g()==\'4W\')){q.1X.9K(O);F}q.1X.9K(\'4W\')},g6:C(e,1k){if(q.1D.48&&1k===90&&!e.6s&&!e.g1&&q.G.25.12){e.2w();q.25.9l();F}N if(q.1D.48&&1k===90&&e.6s&&!e.g1&&q.G.9k.12!==0){e.2w();q.25.a9();F}N if(!q.1D.48){if(1k==q.3j.84||1k==q.3j.8Q||(1k==q.3j.70&&!e.9u&&!e.6s)||1k==q.3j.a7){q.25.1U()}}},fY:C(e,1k){if(q.1D.48&&1k===65){q.L.gW()}N if(1k!=q.3j.g0&&!q.1D.48){q.L.8U()}},fH:C(){E 2d=[q.1D.29,q.1D.2F,q.1D.7Y];2B(E i=0;i<2d.12;i++){if(2d[i]){q.1D.fC(2d[i]);F O}}},i4:C(e){q.25.1U();if(q.L.6T()){F q.1D.9x(e)}F q.1D.6x(e)},fG:C(e,1k){if(!q.G.fp)F 1q;if(q.L.3z(q.1e.1b())&&q.G.9Y===O)F 1q;e.2w();E Q;if(q.1D.2F&&!e.6s){Q=(q.G.6v)?1l.8W(9F(q.G.6v+1).3c(\'\\9Z\')):1l.8W(\'\\t\');q.1Y.Q(Q);q.1e.1S()}N if(q.G.9Y!==O){Q=1l.8W(9F(q.G.9Y+1).3c(\'\\9Z\'));q.1Y.Q(Q);q.1e.1S()}N{if(e.6J&&1k===e5)q.3y.95();N if(e.6J&&1k===ek)q.3y.bo();N if(!e.6s)q.3y.bo();N q.3y.95()}F O},fs:C(){E 4C=q.J.4a();E 9X=4C.3x.K(/<br\\s?\\/?>/gi,\'\');if((4C.1p===\'89\'||4C.1p===\'P\')&&9X===\'\'&&!$(4C).3i(\'I-T\')){E br=1l.3w(\'br\');$(4C).2t(br);q.1V.bT(br);q.1e.1S();F O}},fm:C(){E 4C=q.J.4a();E 9X=4C.3x.K(/<br\\s?\\/?>/gi,\'\');if(4C.1p===\'89\'&&q.L.3z(9X)&&!$(4C).3i(\'I-T\')){E p=1l.3w(\'p\');p.3x=q.G.6o;$(4C).2t(p);q.1V.2W(p);q.1e.1S();F O}N if(q.G.fh&&4C.1p==\'P\'){$(4C).1K(\'1G\').1K(\'1o\')}},fj:C(e){e.2w();q.J.1b();E p=1l.3w(\'p\');p.3x=q.G.6o;q.14.55();q.14.3L(p);q.1V.2W(p);q.1e.1S();F O},fk:C(e){if(!q.L.6T())F;E 5q=$.3k($(q.1D.R).B());if(5q.3N(/(<br\\s?\\/?>){2}$/i)!=-1){e.2w();if(q.G.1M){E br=1l.3w(\'br\');$(q.1D.29).3F(br);q.1V.bT(br);$(q.1D.R).B(5q.K(/<br\\s?\\/?>$/i,\'\'))}N{E Q=$(q.G.5S);$(q.1D.29).3F(Q);q.1V.2W(Q)}F 1q}F},fC:C(2j){if(!q.L.6T())F;q.25.1U();if(q.G.1M){E 26=$(\'<1i>\').1F($.3k(q.$T.B())).26();E 2Z=26.2Z()[0];if(2Z.1p==\'6w\'&&2Z.3x===\'\'){2Z=26.4G()[0]}if(q.L.6q(2Z)!=q.L.6q(2j))F;E br=1l.3w(\'br\');$(2j).3F(br);q.1V.54(br)}N{if(q.$T.26().2Z()[0]!==2j)F;E Q=$(q.G.5S);$(2j).3F(Q);q.1V.2W(Q)}},fE:C(e){e.2w();E Q=1l.8W(\'\\n\');q.J.1b();q.14.55();q.14.3L(Q);q.1V.54(Q);q.1e.1S();F O},6x:C(e){F q.1D.bR(e)},9x:C(e){F q.1D.bR(e,1q)},bR:C(e,fv){e.fw();q.J.1b();E 5t=1l.3w(\'br\');if(q.L.1P(\'2E\')){q.14.43(O);q.14.3P(q.14.bQ,q.14.bO)}N{q.14.55()}q.14.3L(5t);E $9W=$(5t).1r("a");if($9W.12>0){$9W.1h(5t).1u();$9W.3F(5t)}if(fv===1q){E $1N=$(5t).1N();if($1N.12!==0&&$1N[0].1p===\'58\'&&q.L.dN()){q.1V.54(5t);q.1e.1S();F O}E bP=1l.3w(\'br\');q.14.3L(bP);q.1V.54(bP)}N{if(q.L.1P(\'2E\')){E 3g=1l.3w(\'1j\');3g.3x=\'&#9T;\';$(5t).3F(3g);q.1V.54(3g);$(3g).1u()}N{E 14=1l.92();14.9I(5t);14.43(1q);E J=3l.4D();J.8T();J.4l(14)}}q.1e.1S();F O},hp:C(){E $1z=$(q.1D.1z);if($1z.1g().3N(/^\\6g$/g)===0){$1z.1u()}},ij:C(e){E $1z=$(q.1D.1z);E $1r=$(q.1D.1r);E 2Y=$1z.2q(\'2Y\',q.$T[0]);if(2Y.12!==0&&$1z.2q(\'li\',q.$T[0])&&$1r.3O(\'li\').12===1){if(!q.L.3z($1z.1g()))F;e.2w();$1z.1u();$1r.1u();q.1V.2W(2Y)}}}},2s:C(){F{3Q:C(e){if(q.7B)F;E 1k=e.7U;q.2s.1z=q.J.3B();q.2s.1r=q.J.67();E $1r=q.L.4p($(q.2s.1r).1r());E kA=q.1X.2u(\'2s\',e);if(kA===O){e.2w();F O}if(!q.G.1M&&q.2s.1z.a8==3&&q.2s.1z.12<=1&&(q.2s.1r===O||q.2s.1r.1p==\'e6\')){q.2s.9U()}if(!q.G.1M&&q.L.4p(q.2s.1z)&&q.2s.1z.1p===\'89\'){q.2s.9U(O)}if(!q.G.1M&&$(q.2s.1r).3i(\'I-7s-3g\')&&($1r===O||$1r[0].1p==\'e6\')){$(q.2s.1r).26().3Y();q.2s.9U()}if(q.2z.aD()&&q.2z.kB(1k))q.2z.30();if(1k===q.3j.8Q||1k===q.3j.84){if(q.L.1P(\'4b\')){E 2Y=$(q.1D.1z).2q(\'2Y\',q.$T[0]);if(2Y.9B()!==0&&2Y.1g()!==\'\'){e.2w();F O}}q.1x.4r();if(q.1R.M){e.2w();q.M.7W();q.25.1U();q.M.1u(q.1R.M);q.1R.M=O;F O}q.$T.1h(\'p\').1B($.1d(C(i,s){q.L.4f(i,$(s).B())},q));if(q.G.1M&&q.2s.1z&&q.2s.1z.1p==\'89\'&&q.L.3z(q.2s.1z.3x)){$(q.2s.1z).3F(q.J.5a());q.J.3d();$(q.2s.1z).1u()}F q.2s.kN(e)}},9U:C(8O){E $1z=$(q.2s.1z);E Q;if(8O===O){Q=$(\'<p>\').1F($1z.B())}N{Q=$(\'<p>\').1F($1z.8O())}$1z.2t(Q);E 1N=$(Q).1N();if(1s(1N[0])!==\'1y\'&&1N[0].1p==\'58\'){1N.1u()}q.1V.3P(Q)},kN:C(e){E B=$.3k(q.$T.B());if(!q.L.3z(B))F;e.2w();if(q.G.1M){q.$T.B(q.J.5a());q.J.3d()}N{q.$T.B(q.G.5S);q.2n.2W()}q.1e.1S();F O}}},1H:C(){F{2R:C(){q.G.bU=q.G.kn[q.G.1H]},1b:C(1w){F(1s q.G.bU[1w]!=\'1y\')?q.G.bU[1w]:\'\'}}},3K:C(){F{1Y:C(){q.25.1U();E 1Z=q.J.3X();if(1Z[0]!==O&&q.3K.jZ(1Z)){if(!q.L.1P(\'2E\'))q.$T.2n();F}if(q.L.1P(\'2E\')){q.3K.aR()}N{q.3K.kc()}},jZ:C(1Z){E 9y=[\'li\',\'2Y\',\'5P\',\'29\',\'7Y\',\'2F\',\'dl\',\'dt\',\'dd\'];E 3I=1Z[0].1p.3e();E 2Z=q.J.kJ();2Z=(1s 2Z==\'1y\')?3I:2Z.1p.3e();E bN=$.3t(3I,9y)!=-1;E kl=$.3t(2Z,9y)!=-1;if((bN&&kl)||bN){F 1q}},aR:C(){q.L.8z();q.25.1U();q.1Y.Q(1l.3w(\'hr\'));q.L.bk();q.1e.1S()},kc:C(){q.25.1U();E bV=\'<p id="I-1Y-3K"><br /></p>\';if(q.G.1M)bV=\'<br id="I-1Y-3K">\';1l.3Z(\'na\',O,\'<hr>\'+bV);q.3K.k9();q.1e.1S()},k9:C(){E Q=q.$T.1h(\'#I-1Y-3K\');E 1N=$(Q).1N()[0];E 1Q=1N;if(q.L.1P(\'4b\')&&1N&&1N.3x===\'\'){1Q=$(1N).1N()[0];$(1N).1u()}if(1Q){Q.1u();if(!q.G.1M){q.$T.2n();q.3K.2W(1Q)}}N{Q.1K(\'id\');q.3K.2W(Q[0])}},2W:C(Q){if(1s Q===\'1y\')F;E jQ=1l.8W(\'\\6g\');q.J.1b();q.14.2W(Q,0);q.14.3L(jQ);q.14.43(1q);q.J.4l()}}},V:C(){F{2O:C(e){if(1s e!=\'1y\'&&e.2w)e.2w();if(!q.1R.c7(\'a\')){q.1a.2R(\'V\',q.1H.1b(\'ba\'),jS)}N{q.1a.2R(\'V\',q.1H.1b(\'ca\'),jS)}q.1a.cy();E kE=!q.1R.c7(\'a\')?q.1H.1b(\'1Y\'):q.1H.1b(\'7L\');q.V.kL=q.1a.cz(kE);q.J.1b();q.V.kG();q.V.kg();if(q.V.1Q==\'7K\')$(\'#I-V-7Z\').6j(\'a0\',1q);q.V.$9n=$(\'#I-V-2i\');q.V.$bZ=$(\'#I-V-2i-1g\');q.V.$bZ.2K(q.V.1g);q.V.$9n.2K(q.V.2i);q.V.kL.on(\'2y\',$.1d(q.V.1Y,q));$(\'.I-V-3q\').1u();q.J.3o();q.1a.2O();q.V.$9n.2n()},kg:C(){E jV=3u.jU.2k.K(/\\/$/i,\'\');if(1s q.V.2i!=="1y"){q.V.2i=q.V.2i.K(jV,\'\');q.V.2i=q.V.2i.K(/^\\/#/,\'#\');q.V.2i=q.V.2i.K(\'bY:\',\'\');if(!q.G.5c){E 3J=2a 2l(\'^(8s|8K|5o)://\'+3u.jU.n4,\'i\');q.V.2i=q.V.2i.K(3J,\'\')}}},kG:C(){q.V.$Q=O;E $el=$(q.J.3B()).2q(\'a\',q.$T[0]);if($el.12!==0&&$el[0].1p===\'A\'){q.V.$Q=$el;q.V.2i=$el.1c(\'2k\');q.V.1g=$el.1g();q.V.1Q=$el.1c(\'1Q\')}N{q.V.1g=q.2T.4y();q.V.2i=\'\';q.V.1Q=\'\'}},1Y:C(){q.3f.1u();E 1Q=\'\';E V=q.V.$9n.2K();E 1g=q.V.$bZ.2K().K(/(<([^>]+)>)/ig,"");if($.3k(V)===\'\'){q.V.$9n.2f(\'I-3D-6U\').on(\'2s\',C(){$(q).2S(\'I-3D-6U\');$(q).3h(\'2s\')});F}if(V.3N(\'@\')!=-1&&/(8s|8K|5o):\\/\\//i.bE(V)===O){V=\'bY:\'+V}N if(V.3N(\'#\')!==0){if($(\'#I-V-7Z\').6j(\'a0\')){1Q=\'7K\'}E 7D=\'((jT--)?[a-7t-9]+(-[a-7t-9]+)*\\\\.)+[a-z]{2,}\';E 3J=2a 2l(\'^(8s|8K|5o)://\'+7D,\'i\');E a1=2a 2l(\'^\'+7D,\'i\');E km=2a 2l(\'\\.(B|nh)$\',\'i\');if(V.3N(3J)==-1&&V.3N(km)==-1&&V.3N(a1)===0&&q.G.5c){V=q.G.5c+\'://\'+V}}q.V.1U(1g,V,1Q);q.1a.4j()},1U:C(1g,V,1Q){1g=$.3k(1g.K(/<|>/g,\'\'));q.J.3d();E 1Z=q.J.3X();if(1g===\'\'&&V===\'\')F;if(1g===\'\'&&V!==\'\')1g=V;if(q.V.$Q){q.25.1U();E $V=q.V.$Q,$el=$V.3O();if($el.12>0){56($el.12){$el=$el.3O()}$el=$el.2h()}N{$el=$V}$V.1c(\'2k\',V);$el.1g(1g);if(1Q!==\'\'){$V.1c(\'1Q\',1Q)}N{$V.1K(\'1Q\')}q.J.9g($V);q.1e.1S()}N{if(q.L.1P(\'4b\')&&q.V.1g===\'\'){E $a=$(\'<a />\').1c(\'2k\',V).1g(1g);if(1Q!==\'\')$a.1c(\'1Q\',1Q);q.1Y.Q($a);q.J.9g($a)}N{E $a;if(q.L.1P(\'2E\')){$a=$(\'<a 2k="\'+V+\'">\').1g(1g);if(1Q!==\'\')$a.1c(\'1Q\',1Q);$a=$(q.1Y.Q($a));if(q.J.e1().1T(/\\s$/)){$a.3F(" ")}q.J.9g($a)}N{1l.3Z(\'nt\',O,V);$a=$(q.J.3B()).2q(\'a\',q.$T[0]);if(q.L.1P(\'4b\')){$a=$(\'a[k3=""]\')}if(1Q!==\'\')$a.1c(\'1Q\',1Q);$a.1K(\'1o\').1K(\'k3\');if(q.J.e1().1T(/\\s$/)){$a.3F(" ")}if(q.V.1g!==\'\'||q.V.1g!=1g){if(!q.G.1M&&1Z&&1Z.12<=1){$a.1g(1g)}q.J.9g($a)}}}q.1e.1S();q.1X.2u(\'nu\',$a)}3A($.1d(C(){q.1R.5n()},q),5)},6P:C(e){if(1s e!=\'1y\'&&e.2w){e.2w()}E 23=q.J.6I();if(!23)F;q.25.1U();E 2p=23.12;E 5n=[];2B(E i=0;i<2p;i++){if(23[i].1p===\'A\'){5n.2N(23[i])}E $Q=$(23[i]).2q(\'a\',q.$T[0]);$Q.2t($Q.26())}q.1X.2u(\'nv\',5n);$(\'.I-V-3q\').1u();q.1e.1S()},7O:C(3E){q.V.8g(3E,\'7O\')},2f:C(3E){q.V.8g(3E,\'2f\')},2S:C(3E){q.V.8g(3E,\'2S\')},8g:C(3E,1C){E 5n=q.J.kq([\'a\']);if(5n===O)F;$.1B(5n,C(){$(q)[1C](3E)})}}},2z:C(){F{kB:C(1k){F 1k==q.3j.70||1k==q.3j.a7},aD:C(){F q.G.a2&&(q.G.c1||q.G.bX||q.G.93)&&!q.L.4S(\'8o\')},30:C(){E 2z=q.2z,G=q.G;q.$T.1h(":6h(4R,1L,a,2F)").ni().26().bW(C(){F q.a8===3&&$.3k(q.5O)!=""&&!$(q).1r().is("2F")&&(q.5O.1T(G.2z.4v.72)||q.5O.1T(G.2z.4v.71)||q.5O.1T(G.2z.4v.M)||q.5O.1T(G.2z.4v.2i))}).1B(C(){E 1g=$(q).1g(),B=1g;if(G.93&&(B.1T(G.2z.4v.72)||B.1T(G.2z.4v.71))){B=2z.93(B)}N if(G.bX&&B.1T(G.2z.4v.M)){B=2z.kT(B)}N if(G.c1){B=2z.a2(B)}$(q).a6(1g.K(1g,B)).1u()});E ka=q.$T.1h(\'.I-2z-41\').1B(C(){E $el=$(q);$el.2S(\'I-2z-41\');if($el.1c(\'1G\')===\'\')$el.1K(\'1G\');F $el[0]});q.1X.2u(\'2z\',ka);q.1e.1S()},93:C(B){E bM=\'<4R 1G="I-2z-41" 2J="nm" 3n="no" 4t="\',bK=\'" nn="0" o0></4R>\';if(B.1T(q.G.2z.4v.72)){B=B.K(q.G.2z.4v.72,bM+\'//7n.72.a5/7i/$1\'+bK)}if(B.1T(q.G.2z.4v.71)){B=B.K(q.G.2z.4v.71,bM+\'//oI.71.a5/6Y/$2\'+bK)}F B},kT:C(B){E 3M=B.1T(q.G.2z.4v.M);if(3M){B=B.K(B,\'<1L 4t="\'+3M+\'" 1G="I-2z-41" />\');if(q.G.1M){if(!q.L.3z(q.1e.1b())){B=\'<br>\'+B}}B+=\'<br>\'}F B},a2:C(B){E 3M=B.1T(q.G.2z.4v.2i);if(3M){3M=$.oJ(3M,C(v,k){F $.3t(v,3M)===k});E 12=3M.12;2B(E i=0;i<12;i++){E 2k=3M[i],1g=2k,5c=q.G.5c+\'://\';if(2k.1T(/(5o?|8K):\\/\\//i)!==4e){5c=""}if(1g.12>q.G.cs){1g=1g.aB(0,q.G.cs)+\'...\'}1g=fl(1g);E cu="\\\\b";if($.3t(2k.ct(-1),["/","&","="])!=-1){cu=""}E kx=2a 2l(\'(\'+2k.K(/[\\-\\[\\]\\/\\{\\}\\(\\)\\*\\+\\?\\.\\\\\\^\\$\\|]/g,"\\\\$&")+cu+\')\',\'g\');B=B.K(kx,\'<a 2k="\'+5c+$.3k(2k)+\'" 1G="I-2z-41">\'+$.3k(1g)+\'</a>\')}}F B}}},2e:C(){F{3m:C(4H){q.3f.1u();if(!q.L.1P(\'2E\'))q.$T.2n();q.25.1U();q.J.3o();E 1r=q.J.67();E $2e=$(1r).2q(\'ol, 3p\',q.$T[0]);if(!q.L.4p($2e)&&$2e.12!==0){$2e=O}E cq,cn;E 1u=O;if($2e&&$2e.12){1u=1q;E cr=$2e[0].1p;cq=(4H===\'5z\'&&cr===\'8D\');cn=(4H===\'5m\'&&cr===\'8N\')}if(cq){q.L.4I($2e,\'ol\')}N if(cn){q.L.4I($2e,\'3p\')}N{if(1u){q.2e.1u(4H,$2e)}N{q.2e.1Y(4H)}}q.J.3d();q.1e.1S()},1Y:C(4H){E 1z=q.J.3B();E $2Y=$(1z).2q(\'2Y, 5P\',q.$T[0]);if(q.L.1P(\'2E\')&&q.G.1M){q.2e.aR(4H)}N{1l.3Z(\'1Y\'+4H)}E 1r=q.J.67();E $2e=$(1r).2q(\'ol, 3p\',q.$T[0]);if($2Y.12!==0){E iK=$2Y.8O();$2Y.3F(iK).1u(\'\')}if(q.L.3z($2e.1h(\'li\').1g())){E $3O=$2e.3O(\'li\');$3O.1h(\'br\').1u();$3O.1F(q.J.5a());if(q.G.1M&&q.L.1P(\'4b\')&&$3O.9B()==2&&q.L.3z($3O.eq(1).1g())){$3O.eq(1).1u()}}if($2e.12){E $7X=$2e.1r();if(q.L.4p($7X)&&$7X[0].1p!=\'5v\'&&q.L.6X($7X[0])){$7X.2t($7X.26())}}if(!q.L.1P(\'2E\')){q.$T.2n()}q.1x.4r()},aR:C(4H){E 4M=q.J.5Z(\'1i\');E aP=$(4M).B();E 85=(4H==\'5z\')?$(\'<ol>\'):$(\'<3p>\');E 8c=$(\'<li>\');if($.3k(aP)===\'\'){8c.1F(q.J.5a());85.1F(8c);q.$T.1h(\'#J-2r-1\').2t(85)}N{E 8k=aP.4o(/<br\\s?\\/?>/gi);if(8k){2B(E i=0;i<8k.12;i++){if($.3k(8k[i])!==\'\'){85.1F($(\'<li>\').B(8k[i]))}}}N{8c.1F(aP);85.1F(8c)}$(4M).2t(85)}},1u:C(4H,$2e){if($.3t(\'3p\',q.J.3X()))4H=\'5m\';1l.3Z(\'1Y\'+4H);E $1z=$(q.J.3B());q.3y.aO();if(!q.G.1M&&$1z.2q(\'li, 5P, 2Y\',q.$T[0]).12===0){1l.3Z(\'9S\',O,\'p\');q.$T.1h(\'3p, ol, 29\').1B($.1d(q.L.4f,q))}E $3W=$(q.J.3B()).2q(\'3W\',q.$T[0]);E $4G=$3W.4G();if(!q.G.1M&&$3W.12!==0&&$4G.12!==0&&$4G[0].1p==\'58\'){$4G.1u()}q.1x.4r()}}},1a:C(){F{aM:{},iW:C(){q.G.1a={iX:6f()+\'<49 id="I-1a-M-7L">\'+\'<2L>\'+q.1H.1b(\'1J\')+\'</2L>\'+\'<3D 1m="1g" id="I-M-1J" />\'+\'<2L 1G="I-M-V-42">\'+q.1H.1b(\'V\')+\'</2L>\'+\'<3D 1m="1g" id="I-M-V" 1G="I-M-V-42" 4K-2L="\'+q.1H.1b(\'V\')+\'" />\'+\'<2L 1G="I-M-V-42"><3D 1m="iP" id="I-M-V-7Z" 4K-2L="\'+q.1H.1b(\'aL\')+\'"> \'+q.1H.1b(\'aL\')+\'</2L>\'+\'<2L 1G="I-M-4g-42">\'+q.1H.1b(\'cC\')+\'</2L>\'+\'<7z 1G="I-M-4g-42" id="I-M-5g" 4K-2L="\'+q.1H.1b(\'cC\')+\'">\'+\'<42 1E="5p">\'+q.1H.1b(\'5p\')+\'</42>\'+\'<42 1E="2c">\'+q.1H.1b(\'2c\')+\'</42>\'+\'<42 1E="5u">\'+q.1H.1b(\'5u\')+\'</42>\'+\'<42 1E="4s">\'+q.1H.1b(\'4s\')+\'</42>\'+\'</7z>\'+\'</49>\',M:6f()+\'<49 id="I-1a-M-1Y">\'+\'<1i id="I-1a-M-4d"></1i>\'+\'</49>\',22:6f()+\'<49 id="I-1a-22-1Y">\'+\'<1i id="I-1a-22-1v-2Q">\'+\'<2L>\'+q.1H.1b(\'6k\')+\'</2L>\'+\'<3D 1m="1g" id="I-6k" 4K-2L="\'+q.1H.1b(\'6k\')+\'" /><br><br>\'+\'<1i id="I-1a-22-1v"></1i>\'+\'</1i>\'+\'</49>\',V:6f()+\'<49 id="I-1a-V-1Y">\'+\'<2L>cD</2L>\'+\'<3D 1m="2i" id="I-V-2i" 4K-2L="cD" />\'+\'<2L>\'+q.1H.1b(\'1g\')+\'</2L>\'+\'<3D 1m="1g" id="I-V-2i-1g" 4K-2L="\'+q.1H.1b(\'1g\')+\'" />\'+\'<2L><3D 1m="iP" id="I-V-7Z"> \'+q.1H.1b(\'aL\')+\'</2L>\'+\'</49>\'};$.7E(q.G,q.G.1a)},cB:C(1w,2A){q.1a.aM[1w]=2A},oX:C($1a){q.1a.$94=$(\'<1i>\').1c(\'id\',\'I-1a-94\');$1a.6t(q.1a.$94)},oY:C(id,1w,8Z){E $5I=$(\'<a 2k="#" 3T="5I\'+id+\'">\').1g(1w);if(8Z){$5I.2f(\'8Z\')}E 3u=q;$5I.on(\'2y\',C(e){e.2w();$(\'.I-5I\').3v();$(\'.I-\'+$(q).1c(\'3T\')).2O();3u.1a.$94.1h(\'a\').2S(\'8Z\');$(q).2f(\'8Z\')});q.1a.$94.1F($5I)},oZ:C(1w,ic){q.G.1a[1w]=ic},i9:C(1w){F q.G.1a[1w]},oU:C(){F q.$aX.1h(\'49\')},2R:C(62,1J,2J){q.1a.62=62;q.1a.2J=2J;q.1a.2o();q.1a.jr();q.1a.i7(1J);q.1a.ik();q.1a.i8();if(1s q.1a.aM[62]!=\'1y\'){q.1a.aM[62].6e(q)}},2O:C(){q.L.d8();if(q.L.6M()){q.1a.cx()}N{q.1a.aZ()}if(q.G.cA){q.$4F.2f("I-1a-jP")}q.$74.2O();q.$4F.2O();q.$1a.1c(\'6S\',\'-1\');q.$1a.2n();q.1a.ip();q.L.8z();if(!q.L.6M()){3A($.1d(q.1a.aZ,q),0);$(3l).on(\'4J.I-1a\',$.1d(q.1a.4J,q))}q.1X.2u(\'oP\',q.1a.62,q.$1a);$(1l).3h(\'oO.1a\');q.$1a.1h(\'3D[1m=1g],3D[1m=2i],3D[1m=oQ]\').on(\'1D.I-1a\',$.1d(q.1a.iu,q))},aZ:C(){E 3n=q.$1a.oR();E aT=$(3l).3n();E 7u=$(3l).2J();if(q.1a.2J>7u){q.$1a.1O({2J:\'96%\',63:(aT/2-3n/2)+\'px\'});F}if(3n>aT){q.$1a.1O({2J:q.1a.2J+\'px\',63:\'oS\'})}N{q.$1a.1O({2J:q.1a.2J+\'px\',63:(aT/2-3n/2)+\'px\'})}},cx:C(){q.$1a.1O({2J:\'96%\',63:\'2%\'})},4J:C(){if(q.L.6M()){q.1a.cx()}N{q.1a.aZ()}},i7:C(1J){q.$9e.B(1J)},i8:C(){q.$aX.B(q.1a.i9(q.1a.62))},ik:C(){if(1s $.fn.iv===\'1y\')F;q.$1a.iv({iw:q.$9e});q.$9e.1O(\'oa\',\'oc\')},iu:C(e){if(e.7U!=13)F;e.2w();q.$1a.1h(\'1t.I-1a-7M-21\').2y()},cy:C(){E 1t=$(\'<1t>\').2f(\'I-1a-21 I-1a-4j-21\').B(q.1H.1b(\'it\'));1t.on(\'2y\',$.1d(q.1a.4j,q));q.$9i.1F(1t)},io:C(2L){F q.1a.cm(2L,\'83\')},cz:C(2L){F q.1a.cm(2L,\'7M\')},cm:C(2L,3E){E 1t=$(\'<1t>\').2f(\'I-1a-21\').2f(\'I-1a-\'+3E+\'-21\').B(2L);q.$9i.1F(1t);F 1t},ip:C(){E 4i=q.$9i.1h(\'1t\');E cl=4i.12;if(cl===0)F;4i.1O(\'2J\',(88/cl)+\'%\')},2o:C(){q.1a.jw();q.$4F=$(\'<1i id="I-1a-2Q"/>\').3v();q.$1a=$(\'<1i id="I-1a" 9c="o9" 4K-o8="I-1a-87" />\');q.$9e=$(\'<87 id="I-1a-87"/>\');q.$8X=$(\'<1t 1m="1t" id="I-1a-4j" 6S="1" 4K-2L="o3" />\').B(\'&o2;\');q.$aX=$(\'<1i id="I-1a-31" />\');q.$9i=$(\'<c8 />\');q.$1a.1F(q.$9e);q.$1a.1F(q.$8X);q.$1a.1F(q.$aX);q.$1a.1F(q.$9i);q.$4F.1F(q.$1a);q.$4F.aU(1l.31)},jw:C(){q.$74=$(\'<1i id="I-1a-o5">\').3v();$(\'31\').6t(q.$74)},jr:C(){q.$8X.on(\'2y.I-1a\',$.1d(q.1a.4j,q));$(1l).on(\'2s.I-1a\',$.1d(q.1a.9a,q));q.$T.on(\'2s.I-1a\',$.1d(q.1a.9a,q));q.$4F.on(\'2y.I-1a\',$.1d(q.1a.4j,q))},jC:C(){q.$8X.3h(\'2y.I-1a\');$(1l).3h(\'2s.I-1a\');q.$T.3h(\'2s.I-1a\');q.$4F.3h(\'2y.I-1a\');$(3l).3h(\'4J.I-1a\')},9a:C(e){if(e.7U!=q.3j.b2)F;q.1a.4j(O)},4j:C(e){if(e){if(!$(e.1Q).3i(\'I-1a-4j-21\')&&e.1Q!=q.$8X[0]&&e.1Q!=q.$4F[0]){F}e.2w()}if(!q.$4F)F;q.1a.jC();q.L.bJ();q.$74.1u();q.$4F.iM(\'of\',$.1d(C(){q.$4F.1u();3A($.1d(q.L.bk,q),0);if(e!==1y)q.J.3d();$(1l.31).1O(\'bI\',q.1a.og);q.1X.2u(\'ot\',q.1a.62)},q))}}},1R:C(){F{2R:C(){if(1s q.G.jL!="1y")F;if(q.L.1P(\'2E\')){E 3u=q;q.$T.1h(\'2F, 1e\').on(\'ay\',C(){3u.$T.1c(\'5d\',O);$(q).1c(\'5d\',1q)}).on(\'az\',C(){3u.$T.1c(\'5d\',1q);$(q).1K(\'5d\')})}q.1R.au();q.1R.5n()},1A:C(e,2g){q.1R.4i(e,2g);q.1R.79()},c7:C($el,$1z){if(1s $1z==\'1y\'){E $1z=$(q.J.3B())}F $1z.is($el)||$1z.dL($el).12>0},79:C(){E $1z=$(q.J.3B());$.1B(q.G.1R.79,$.1d(C(1k,1E){E 1R=1E.1R,2j=1R.2j,$2x=1E.2x,c3=1s 1R[\'in\']!=\'1y\'?1R[\'in\']:O,c6=1s 1R[\'2m\']!=\'1y\'?1R[\'2m\']:O;if($1z.2q(2j).9B()>0){q.1R.c4($2x,c3,c6)}N{q.1R.c4($2x,c6,c3)}},q))},c4:C($2x,9s,ax){if(ax&&1s ax[\'1c\']!=\'1y\'){q.1R.c5($2x,ax.1c,1q)}if(1s 9s[\'1c\']!=\'1y\'){q.1R.c5($2x,9s.1c)}if(1s 9s[\'1J\']!=\'1y\'){$2x.1g(9s[\'1J\'])}},c5:C($2x,jq,cb){$.1B(jq,C(1k,1E){if(1k==\'1G\'){if(!cb){$2x.2f(1E)}N{$2x.2S(1E)}}N{if(!cb){$2x.1c(1k,1E)}N{$2x.1K(1k)}}})},cc:C($2x,2g,2X){if(1s 2X.1R=="1y")F;2X.2x=$2x;q.G.1R.79.2N(2X)},4i:C(e,2g){E 1z=q.J.3B();E 1r=q.J.67();if(e!==O){q.1t.cj()}N{q.1t.cj(2g)}if(e===O&&2g!==\'B\'){if($.3t(2g,q.G.b5)!=-1)q.1t.oq(2g);F}$.1B(q.G.cg,$.1d(C(1k,1E){E aw=$(1r).2q(1k,q.$T[0]);E ck=$(1z).2q(1k,q.$T[0]);if(aw.12!==0&&!q.L.4p(aw))F;if(!q.L.4p(ck))F;if(aw.12!==0||ck.2q(1k,q.$T[0]).12!==0){q.1t.9q(1E)}},q));E $1r=$(1r).2q(q.G.8F.4y().3e(),q.$T[0]);if(q.L.4p(1r)&&$1r.12){E 5g=($1r.1O(\'1g-5g\')===\'\')?\'2c\':$1r.1O(\'1g-5g\');q.1t.9q(\'5g\'+5g)}},oi:C(Y,2g){q.G.b5.2N(2g);q.G.cg[Y]=2g},au:C(){q.$T.1h(\'1L\').1B($.1d(C(i,1L){E $1L=$(1L);$1L.2q(\'a\',q.$T[0]).on(\'2y\',C(e){e.2w()});if(q.L.1P(\'2E\'))$1L.1c(\'oh\',\'on\');q.M.j1($1L)},q));$(1l).on(\'2y.I-M-83.\'+q.2G,$.1d(C(e){q.1R.M=O;if(e.1Q.1p==\'aA\'&&q.L.4p(e.1Q)){q.1R.M=(q.1R.M&&q.1R.M==e.1Q)?O:e.1Q}},q))},5n:C(){if(!q.G.j4)F;q.$T.1h(\'a\').on(\'5H.I.\'+q.2G+\' 2y.I.\'+q.2G,$.1d(q.1R.jc,q));q.$T.on(\'5H.I.\'+q.2G+\' 2y.I.\'+q.2G,$.1d(q.1R.cf,q));$(1l).on(\'5H.I.\'+q.2G+\' 2y.I.\'+q.2G,$.1d(q.1R.cf,q))},jl:C($V){F $V.2I()},jc:C(e){E $el=$(e.1Q);if($el[0].1p==\'aA\')F;if($el[0].1p!==\'A\')$el=$el.2q(\'a\',q.$T[0]);if($el[0].1p!==\'A\')F;E $V=$el;E 3s=q.1R.jl($V);E 3q=$(\'<1j 1G="I-V-3q"></1j>\');E 2k=$V.1c(\'2k\');if(2k===1y){2k=\'\'}if(2k.12>24)2k=2k.aB(0,24)+\'...\';E jj=$(\'<a 2k="\'+$V.1c(\'2k\')+\'" 1Q="7K" />\').B(2k).2f(\'I-V-3q-7M\');E jf=$(\'<a 2k="#" />\').B(q.1H.1b(\'7L\')).on(\'2y\',$.1d(q.V.2O,q)).2f(\'I-V-3q-7M\');E je=$(\'<a 2k="#" />\').B(q.1H.1b(\'6P\')).on(\'2y\',$.1d(q.V.6P,q)).2f(\'I-V-3q-7M\');3q.1F(jj).1F(\' | \').1F(jf).1F(\' | \').1F(je);3q.1O({2U:(3s.2U+5L($V.1O(\'3K-3n\'),10))+\'px\',2c:3s.2c+\'px\'});$(\'.I-V-3q\').1u();$(\'31\').1F(3q)},cf:C(e){e=e.7T||e;E 1Q=e.1Q;E $1r=$(1Q).2q(\'a\',q.$T[0]);if($1r.12!==0&&$1r[0].1p===\'A\'&&1Q.1p!==\'A\'){F}N if((1Q.1p===\'A\'&&q.L.4p(1Q))||$(1Q).3i(\'I-V-3q-7M\')){F}$(\'.I-V-3q\').1u()}}},2V:C(){F{2R:C(B){if(q.G.1M)F B;if(B===\'\'||B===\'<p></p>\')F q.G.5S;B=B+"\\n";q.2V.aC=[];q.2V.z=0;B=B.K(/(<br\\s?\\/?>){1,}\\n?<\\/29>/gi,\'</29>\');B=q.2V.jk(B);B=q.2V.j6(B);B=q.2V.jF(B);B=q.2V.jp(B);B=q.2V.8M(B);B=q.2V.j7(B);B=B.K(2a 2l(\'<br\\\\s?/?>\\n?<(\'+q.G.ej.3c(\'|\')+\')(.*?[^>])>\',\'gi\'),\'<p><br /></p>\\n<$1$2>\');F $.3k(B)},jk:C(B){E $1i=$(\'<1i />\').1F(B);$1i.1h(\'29 p\').2t(C(){F $(q).1F(\'<br />\').26()});B=$1i.B();$1i.1h(q.G.ej.3c(\', \')).1B($.1d(C(i,s){q.2V.z++;q.2V.aC[q.2V.z]=s.7v;B=B.K(s.7v,\'\\n{K\'+q.2V.z+\'}\')},q));F B},j6:C(B){E dX=B.1T(/<!--([\\w\\W]*?)-->/gi);if(!dX)F B;$.1B(dX,$.1d(C(i,s){q.2V.z++;q.2V.aC[q.2V.z]=s;B=B.K(s,\'\\n{K\'+q.2V.z+\'}\')},q));F B},j7:C(B){$.1B(q.2V.aC,C(i,s){s=(1s s!==\'1y\')?s.K(/\\$/g,\'&#36;\'):s;B=B.K(\'{K\'+i+\'}\',s)});F B},jp:C(B){E 4O=B.4o(2a 2l(\'\\n\',\'g\'),-1);B=\'\';if(4O){E 2p=4O.12;2B(E i=0;i<2p;i++){if(!4O.ow(i))F;if(4O[i].3N(\'{K\')==-1){4O[i]=4O[i].K(/<p>\\n\\t?<\\/p>/gi,\'\');4O[i]=4O[i].K(/<p><\\/p>/gi,\'\');if(4O[i]!==\'\'){B+=\'<p>\'+4O[i].K(/^\\n+|\\n+$/g,"")+"</p>"}}N B+=4O[i]}}F B},jF:C(B){B=B.K(/<br \\/>\\s*<br \\/>/gi,"\\n\\n");B=B.K(/<br\\s?\\/?>\\n?<br\\s?\\/?>/gi,"\\n<br /><br />");B=B.K(2a 2l("\\r\\n",\'g\'),"\\n");B=B.K(2a 2l("\\r",\'g\'),"\\n");B=B.K(2a 2l("/\\n\\n+/"),\'g\',"\\n\\n");F B},8M:C(B){B=B.K(2a 2l(\'</29></p>\',\'gi\'),\'</29>\');B=B.K(2a 2l(\'<p></29>\',\'gi\'),\'</29>\');B=B.K(2a 2l(\'<p><29>\',\'gi\'),\'<29>\');B=B.K(2a 2l(\'<29></p>\',\'gi\'),\'<29>\');B=B.K(2a 2l(\'<p><p \',\'gi\'),\'<p \');B=B.K(2a 2l(\'<p><p>\',\'gi\'),\'<p>\');B=B.K(2a 2l(\'</p></p>\',\'gi\'),\'</p>\');B=B.K(2a 2l(\'<p>\\\\s?</p>\',\'gi\'),\'\');B=B.K(2a 2l("\\n</p>",\'gi\'),\'</p>\');B=B.K(2a 2l(\'<p>\\t?\\t?\\n?<p>\',\'gi\'),\'<p>\');B=B.K(2a 2l(\'<p>\\t*</p>\',\'gi\'),\'\');F B}}},5C:C(){F{3Q:C(e){if(!q.G.jJ){3A($.1d(q.1e.1S,q),1);F}q.7B=1q;q.25.1U();q.J.3o();q.L.8z();q.5C.ju();$(3l).on(\'6b.I-jv\',$.1d(C(){$(3l).3U(q.bm)},q));3A($.1d(C(){E B=q.$5e.B();q.$5e.1u();q.J.3d();q.L.bk();q.5C.1Y(B);$(3l).3h(\'6b.I-jv\');if(q.2z.aD())q.2z.30()},q),1)},ju:C(){q.$5e=$(\'<1i>\').B(\'\').1c(\'5d\',\'1q\').1O({4g:\'82\',2J:0,2U:0,2c:\'-o4\'});if(q.L.1P(\'2E\')){q.$2Q.1F(q.$5e)}N{if($(\'.1a-31\').12>0){$(\'.1a-31\').1F(q.$5e)}N{$(\'31\').1F(q.$5e)}}q.$5e.2n()},1Y:C(B){B=q.1X.2u(\'od\',B);B=(q.L.9w())?q.1x.8y(B,O):q.1x.8y(B);B=q.1X.2u(\'5C\',B);if(q.L.9w()){q.1Y.1U(B,O)}N{q.1Y.B(B,O)}q.L.8U();q.7B=O;3A($.1d(q.1x.4r,q),10);3A($.1d(C(){E ii=q.$T.1h(\'1j\');$.1B(ii,C(i,s){E B=s.3x.K(/\\6g/,\'\');if(B===\'\'&&s.4u.12===0)$(s).1u()})},q),10)}}},3f:C(){F{9Q:C(){if(!q.3f.is())F;q.$T.1c(\'3f\',q.$2j.1c(\'3f\'));q.3f.3m();q.$T.on(\'1D.I-3f\',$.1d(q.3f.3m,q))},3m:C(){3A($.1d(C(){E 1C=q.L.3z(q.$T.B(),O)?\'2f\':\'2S\';q.$T[1C](\'I-3f\')},q),5)},1u:C(){q.$T.2S(\'I-3f\')},is:C(){if(q.G.3f){F q.$2j.1c(\'3f\',q.G.3f)}N{F!(1s q.$2j.1c(\'3f\')==\'1y\'||q.$2j.1c(\'3f\')===\'\')}}}},5E:C(){F{2O:C(){$(1l.31).1F($(\'<1i id="I-5E"><1j></1j></1i>\'));$(\'#I-5E\').oN()},3v:C(){$(\'#I-5E\').iM(oM,C(){$(q).1u()})}}},J:C(){F{1b:C(){q.2T=1l.4D();if(1l.4D&&q.2T.6l&&q.2T.7A){q.14=q.2T.6l(0)}N{q.14=1l.92()}},4l:C(){52{q.2T.8T()}51(e){}q.2T.4l(q.14)},3B:C(){E el=O;q.J.1b();if(q.2T&&q.2T.7A>0){el=q.2T.6l(0).ko}F q.L.4p(el)},67:C(4V){4V=4V||q.J.3B();if(4V){F q.L.4p($(4V).1r()[0])}F O},iG:C(){F 3l.4D().iI.oB},oz:C(){F 3l.4D().iI.oA},4a:C(Q){Q=Q||q.J.3B();56(Q){if(q.L.7J(Q.1p)){F($(Q).3i(\'I-T\'))?O:Q}Q=Q.e4}F O},e8:C(23,2d){q.J.1b();if(q.14&&q.14.53){F O}E 5U=[];23=(1s 23==\'1y\'||23===O)?q.J.6I():23;E 4N=q.G.4N;4N.2N(\'1j\');if(1s 2d!==\'1y\'){2B(E i=0;i<2d.12;i++){4N.2N(2d[i])}}$.1B(23,$.1d(C(i,Q){if($.3t(Q.1p.3e(),4N)!=-1){5U.2N(Q)}},q));F(5U.12===0)?O:5U},kq:C(2d){q.J.1b();if(q.14&&q.14.53){F O}E 5U=[];E 23=q.J.6I();$.1B(23,$.1d(C(i,Q){if($.3t(Q.1p.3e(),2d)!=-1){5U.2N(Q)}},q));F(5U.12===0)?O:5U},3X:C(23){q.J.1b();if(q.14&&q.14.53){F[q.J.4a()]}E 1Z=[];23=(1s 23==\'1y\')?q.J.6I():23;$.1B(23,$.1d(C(i,Q){if(q.L.6X(Q)){q.J.kI=Q;1Z.2N(Q)}},q));F(1Z.12===0)?[q.J.4a()]:1Z},kJ:C(){F q.J.kI},6I:C(){q.J.1b();E 78=q.J.ei(1);E 9J=q.J.ei(2);if(q.14.53===O){if(3l.4D){E 2T=3l.4D();if(2T.7A>0){E 14=2T.6l(0);E kf=14.ko,ef=14.ef;E 7F=14.7S();7F.43(O);7F.3L(9J);7F.2W(kf,ef);7F.43(1q);7F.3L(78);14.9I(78);14.ks(9J);2T.8T();2T.4l(14)}}}N{q.J.k0(q.14,78,1q);9J=78}E 23=[];E 9z=0;E 3u=q;q.$T.1h(\'*\').1B(C(){if(q==78){E 1r=$(q).1r();if(1r.12!==0&&1r[0].1p!=\'e6\'&&3u.L.4p(1r[0])){23.2N(1r[0])}23.2N(q);9z=1}N{if(9z>0){23.2N(q);9z=9z+1}}if(q==9J){F O}});E eb=[];E 2p=23.12;2B(E i=0;i<2p;i++){if(23[i].id!=\'23-2r-1\'&&23[i].id!=\'23-2r-2\'){eb.2N(23[i])}}q.J.kp();F eb},ei:C(6d){F $(\'<1j id="23-2r-\'+6d+\'" 1G="I-23-2r" 1f-3H="I">\'+q.G.6o+\'</1j>\')[0]},k0:C(14,Q,1m){E 14=14.7S();52{14.43(1m);14.3L(Q)}51(e){}},kp:C(){$(1l).1h(\'1j.I-23-2r\').1u();q.$T.1h(\'1j.I-23-2r\').1u()},nQ:C(2b,2h){q.1V.eh(2b,2h)},5Z:C(Y){q.J.1b();if(q.14.53)F O;E 4M=1l.3w(Y);4M.81(q.14.nT());q.14.3L(4M);F 4M},9g:C(Q){if(q.L.1P(\'4b\')){Q=Q[0]||Q;E 14=1l.92();14.9j(Q)}N{q.1V.1U(Q,0,Q,1)}},bd:C(){q.J.1b();q.14.9j(q.$T[0]);q.J.4l()},1u:C(){q.J.1b();q.2T.8T()},3o:C(){q.J.g2()},g2:C(){q.J.1b();E 5M=q.J.6Q(1);q.J.ed(q.14,5M,1q);if(q.14.53===O){E 9C=q.J.6Q(2);q.J.ed(q.14,9C,O)}q.eT=q.$T.B()},6Q:C(6d){if(1s 6d==\'1y\')6d=1;F $(\'<1j id="J-2r-\'+6d+\'" 1G="I-J-2r"  1f-3H="I">\'+q.G.6o+\'</1j>\')[0]},5a:C(6d){F q.L.6q(q.J.6Q(6d))},ed:C(14,Q,1m){14=14.7S();52{14.43(1m);14.3L(Q)}51(e){q.2n.2W()}},3d:C(){E 5M=q.$T.1h(\'1j#J-2r-1\');E 9C=q.$T.1h(\'1j#J-2r-2\');if(q.L.1P(\'4b\')){q.$T.2n()}if(5M.12!==0&&9C.12!==0){q.1V.1U(5M,0,9C,0)}N if(5M.12!==0){q.1V.1U(5M,0,5M,0)}N{q.$T.2n()}q.J.8p();q.eT=O},8p:C(){q.$T.1h(\'1j.I-J-2r\').1B(C(i,s){E 1g=$(s).1g().K(/\\6g/g,\'\');if(1g===\'\')$(s).1u();N $(s).2t(C(){F $(q).26()})})},e1:C(){q.J.1b();F q.2T.4y()},q5:C(){E B=\'\';q.J.1b();if(q.2T.7A){E dY=1l.3w(\'1i\');E 2p=q.2T.7A;2B(E i=0;i<2p;++i){dY.81(q.2T.6l(i).qN())}B=dY.3x}F q.1x.9R(B)},go:C(B){q.J.1b();q.14.55();E 1i=1l.3w("1i");1i.3x=B;E 4L=1l.9M(),4w;56((4w=1i.9N)){4L.81(4w)}q.14.3L(4L)},qE:C(B){B=q.J.5a(1)+B+q.J.5a(2);q.J.1b();if(3l.4D&&3l.4D().6l){q.J.go(B)}N if(1l.J&&1l.J.92){q.14.gw(B)}q.J.3d();q.1e.1S()}}},6m:C(){F{3Q:C(e,1k){if(!q.G.6m){if((e.9u||e.6J)&&(1k===66||1k===73))e.2w();F O}$.1B(q.G.6m,$.1d(C(5f,4A){E 4X=5f.4o(\',\');E 2p=4X.12;2B(E i=0;i<2p;i++){if(1s 4X[i]===\'6Z\'){q.6m.7N(e,$.3k(4X[i]),$.1d(C(){E 1C;if(4A.1C.3N(/\\./)!=\'-1\'){1C=4A.1C.4o(\'.\');if(1s q[1C[0]]!=\'1y\'){q[1C[0]][1C[1]].9E(q,4A.6p)}}N{q[4A.1C].9E(q,4A.6p)}},q))}}},q))},7N:C(e,4X,fq){E g5={8:"pS",9:"5I",10:"F",13:"F",16:"6E",17:"48",18:"80",19:"pB",20:"pA",27:"py",32:"3g",33:"pE",34:"pF",35:"2h",36:"lb",37:"2c",38:"ls",39:"4s",40:"l8",45:"1Y",46:"4h",59:";",61:"=",96:"0",97:"1",98:"2",99:"3",88:"4",mN:"5",mS:"6",mT:"7",ln:"8",le:"9",mP:"*",lY:"+",me:"-",md:".",m4:"/",lV:"f1",mJ:"f2",q1:"f3",mD:"f4",pN:"f5",pa:"f6",p9:"f7",p3:"f8",ps:"f9",pk:"pZ",qD:"qF",qA:"qu",qw:"qx",qz:"6b",qS:"-",qR:";",qJ:"=",q9:",",q8:"-",qo:".",ql:"/",qh:"`",e5:"[",nG:"\\\\",ek:"]",nF:"\'"};E e9={"`":"~","1":"!","2":"@","3":"#","4":"$","5":"%","6":"^","7":"&","8":"*","9":"(","0":")","-":"nM","=":"+",";":": ","\'":"\\"",",":"<",".":">","/":"?","\\\\":"|"};4X=4X.3e().4o(" ");E bu=g5[e.3j],9h=6f.nW(e.7U).3e(),86="",7j={};$.1B(["80","48","5h","6E"],C(68,bz){if(e[bz+\'nX\']&&bu!==bz){86+=bz+\'+\'}});if(bu)7j[86+bu]=1q;if(9h){7j[86+9h]=1q;7j[86+e9[9h]]=1q;if(86==="6E+"){7j[e9[9h]]=1q}}2B(E i=0,2p=4X.12;i<2p;i++){if(7j[4X[i]]){e.2w();F fq.9E(q,fA)}}}}},2H:C(){F{1b:C(1e){if(!q.G.2H)F 1e;E dZ=[\'fx\',\'31\',\'e0\',\'hr\',\'i?n9\',\'V\',\'5h\',\'n5\',\'1o\',\'3V\',\'3W\',\'ee\',\'e7\',\'e3\'];E e2=[\'li\',\'dt\',\'dt\',\'h[1-6]\',\'42\',\'3V\'];E 8H=[\'p\',\'29\',\'1i\',\'dl\',\'iT\',\'64\',\'oV\',\'iA\',\'ol\',\'2F\',\'7z\',\'2Y\',\'5P\',\'6O\',\'3p\'];q.2H.jE=2a 2l(\'^<(/?\'+dZ.3c(\'|/?\')+\'|\'+e2.3c(\'|\')+\')[ >]\');q.2H.j8=2a 2l(\'^<(br|/?\'+dZ.3c(\'|/?\')+\'|/\'+e2.3c(\'|/\')+\')[ >]\');q.2H.8H=2a 2l(\'^</?(\'+8H.3c(\'|\')+\')[ >]\');E i=0,bl=1e.12,3S=0,2b=4e,2h=4e,Y=\'\',2m=\'\',5b=\'\';q.2H.8L=0;2B(;i<bl;i++){3S=i;if(-1==1e.4x(i).4Q(\'<\')){2m+=1e.4x(i);F q.2H.eg(2m)}56(3S<bl&&1e.6a(3S)!=\'<\'){3S++}if(i!=3S){5b=1e.4x(i,3S-i);if(!5b.1T(/^\\s{2,}$/g)){if(\'\\n\'==2m.6a(2m.12-1))2m+=q.2H.7r();N if(\'\\n\'==5b.6a(0)){2m+=\'\\n\'+q.2H.7r();5b=5b.K(/^\\s+/,\'\')}2m+=5b}if(5b.1T(/\\n/))2m+=\'\\n\'+q.2H.7r()}2b=3S;56(3S<bl&&\'>\'!=1e.6a(3S)){3S++}Y=1e.4x(2b,3S-2b);i=3S;E t;if(\'!--\'==Y.4x(1,3)){if(!Y.1T(/--$/)){56(\'-->\'!=1e.4x(3S,3)){3S++}3S+=2;Y=1e.4x(2b,3S-2b);i=3S}if(\'\\n\'!=2m.6a(2m.12-1))2m+=\'\\n\';2m+=q.2H.7r();2m+=Y+\'>\\n\'}N if(\'!\'==Y[1]){2m=q.2H.bD(Y+\'>\',2m)}N if(\'?\'==Y[1]){2m+=Y+\'>\\n\'}N if(t=Y.1T(/^<(3V|1o|2F)/i)){t[1]=t[1].3e();Y=q.2H.ec(Y);2m=q.2H.bD(Y,2m);2h=6f(1e.4x(i+1)).3e().4Q(\'</\'+t[1]);if(2h){5b=1e.4x(i+1,2h);i+=2h;2m+=5b}}N{Y=q.2H.ec(Y);2m=q.2H.bD(Y,2m)}}F q.2H.eg(2m)},7r:C(){E s=\'\';2B(E j=0;j<q.2H.8L;j++){s+=\'\\t\'}F s},eg:C(1e){1e=1e.K(/\\n\\s*\\n/g,\'\\n\');1e=1e.K(/^[\\s\\n]*/,\'\');1e=1e.K(/[\\s\\n]*$/,\'\');1e=1e.K(/<3V(.*?)>\\n<\\/3V>/gi,\'<3V$1></3V>\');q.2H.8L=0;F 1e},ec:C(Y){E 8E=\'\';Y=Y.K(/\\n/g,\' \');Y=Y.K(/\\s{2,}/g,\' \');Y=Y.K(/^\\s+|\\s+$/g,\' \');E ea=\'\';if(Y.1T(/\\/$/)){ea=\'/\';Y=Y.K(/\\/+$/,\'\')}E m;56(m=/\\s*([^= ]+)(?:=(([\'"\']).*?\\3|[^ ]+))?/.5j(Y)){if(m[2])8E+=m[1].3e()+\'=\'+m[2];N if(m[1])8E+=m[1].3e();8E+=\' \';Y=Y.4x(m[0].12)}F 8E.K(/\\s*$/,\'\')+ea+\'>\'},bD:C(Y,2m){E nl=Y.1T(q.2H.8H);if(Y.1T(q.2H.jE)||nl){2m=2m.K(/\\s*$/,\'\');2m+=\'\\n\'}if(nl&&\'/\'==Y.6a(1))q.2H.8L--;if(\'\\n\'==2m.6a(2m.12-1))2m+=q.2H.7r();if(nl&&\'/\'!=Y.6a(1))q.2H.8L++;2m+=Y;if(Y.1T(q.2H.j8)||Y.1T(q.2H.8H)){2m=2m.K(/ *$/,\'\')}F 2m}}},1I:C(){F{ja:C(){if(q.G.4z)q.G.4c=O;if(q.G.4T)q.G.1K=O;if(q.G.1M)F;E 2d=[\'p\',\'49\'];if(q.G.4z)q.1I.jd(2d);if(q.G.4c)q.1I.jg(2d)},jd:C(2d){E 2p=2d.12;2B(E i=0;i<2p;i++){if($.3t(2d[i],q.G.4z)==-1){q.G.4z.2N(2d[i])}}},jg:C(2d){E 2p=2d.12;2B(E i=0;i<2p;i++){E 3s=$.3t(2d[i],q.G.4c);if(3s!=-1){q.G.4c.bi(3s,1)}}},2R:C(B,44){q.1I.2D={4c:q.G.4c,4z:q.G.4z,6G:q.G.6G,5B:q.G.5B,5A:q.G.5A,5y:q.G.5y,1K:q.G.1K,4T:q.G.4T,6L:q.G.6L,4f:q.G.4f};$.7E(q.1I.2D,44);B=q.1I.6G(B);q.1I.$1i=$(\'<1i />\').1F(B);q.1I.5B();q.1I.5A();q.1I.ji();q.1I.1K();q.1I.4f();q.1I.j5();q.1I.5y();q.1I.6L();B=q.1I.$1i.B();q.1I.$1i.1u();F B},6G:C(B){if(!q.1I.2D.6G)F B;F B.K(/<!--[\\s\\S]*?-->/gi,\'\')},5B:C(B){if(!q.1I.2D.5B)F B;E 2p=q.1I.2D.5B.12;E 5F=[],dB=[];2B(E i=0;i<2p;i++){dB.2N(q.1I.2D.5B[i][1]);5F.2N(q.1I.2D.5B[i][0])}$.1B(5F,$.1d(C(1k,1E){q.1I.$1i.1h(1E).2t(C(){F $("<"+dB[1k]+" />",{B:$(q).B()})})},q))},5A:C(){if(!q.1I.2D.5A)F;E 2p=q.1I.2D.5A.12;q.1I.$1i.1h(\'1j\').1B($.1d(C(n,s){E $el=$(s);E 1o=$el.1c(\'1o\');2B(E i=0;i<2p;i++){if(1o&&1o.1T(2a 2l(\'^\'+q.1I.2D.5A[i][0],\'i\'))){E 1p=q.1I.2D.5A[i][1];$el.2t(C(){E Y=1l.3w(1p);F $(Y).1F($(q).26())})}}},q))},ji:C(){if(!q.1I.2D.4c&&q.1I.2D.4z){q.1I.$1i.1h(\'*\').6h(q.1I.2D.4z.3c(\',\')).1B(C(i,s){if(s.3x===\'\')$(s).1u();N $(s).26().3Y()})}if(q.1I.2D.4c){q.1I.$1i.1h(q.1I.2D.4c.3c(\',\')).1B(C(i,s){if($(s).3i(\'I-3V-Y\')||$(s).3i(\'I-J-2r\'))F;if(s.3x===\'\')$(s).1u();N $(s).26().3Y()})}},1K:C(){E 2p;if(!q.1I.2D.1K&&q.1I.2D.4T){E ce=[],cd=[];2p=q.1I.2D.4T.12;2B(E i=0;i<2p;i++){ce.2N(q.1I.2D.4T[i][0]);cd.2N(q.1I.2D.4T[i][1])}q.1I.$1i.1h(\'*\').1B($.1d(C(n,s){E $el=$(s);E 3s=$.3t($el[0].1p.3e(),ce);E 5D=q.1I.jn(3s,cd,$el);if(5D){$.1B(5D,C(z,f){$el.1K(f)})}},q))}if(q.1I.2D.1K){2p=q.1I.2D.1K.12;2B(E i=0;i<2p;i++){E 8j=q.1I.2D.1K[i][1];if($.bc(8j))8j=8j.3c(\' \');q.1I.$1i.1h(q.1I.2D.1K[i][0]).1K(8j)}}},jn:C(3s,5J,$el){E 5D=[];if(3s==-1){$.1B($el[0].4u,C(i,2x){5D.2N(2x.1w)})}N if(5J[3s]==\'*\'){5D=[]}N{$.1B($el[0].4u,C(i,2x){if($.bc(5J[3s])){if($.3t(2x.1w,5J[3s])==-1){5D.2N(2x.1w)}}N if(5J[3s]!=2x.1w){5D.2N(2x.1w)}})}F 5D},j3:C(el,bv){bv=2a 2l(bv,"g");F el.1B(C(){E 3u=$(q);E 2p=q.4u.12-1;2B(E i=2p;i>=0;i--){E 2x=q.4u[i];if(2x&&2x.oj&&2x.1w.3N(bv)>=0){3u.1K(2x.1w)}}})},4f:C(){if(!q.1I.2D.4f)F;q.1I.$1i.1h(q.1I.2D.4f.3c(\',\')).1B(C(){E $el=$(q);E 1g=$el.1g();1g=1g.K(/\\6g/g,\'\');1g=1g.K(/&5s;/gi,\'\');1g=1g.K(/\\s/g,\'\');if(1g===\'\'&&$el.3O().12===0){$el.1u()}})},j5:C(){q.1I.$1i.1h(\'li p\').26().3Y()},5y:C(){if(!q.1I.2D.5y)F;E 2d=q.1I.2D.5y;if($.bc(q.1I.2D.5y))2d=q.1I.2D.5y.3c(\',\');q.1I.j3(q.1I.$1i.1h(2d),\'^(1f-)\')},6L:C(){if(!q.1I.2D.6L)F;q.1I.$1i.1h(q.1I.2D.6L.3c(\',\')).1B(C(){if(q.4u.12===0){$(q).26().3Y()}})}}},1A:C(){F{3Q:C(){F{B:{1J:q.1H.1b(\'B\'),1C:\'1e.3m\'},3a:{1J:q.1H.1b(\'3a\'),1n:{p:{1J:q.1H.1b(\'j2\'),1C:\'R.30\'},29:{1J:q.1H.1b(\'ci\'),1C:\'R.30\'},2F:{1J:q.1H.1b(\'1e\'),1C:\'R.30\'},h1:{1J:q.1H.1b(\'jb\'),1C:\'R.30\'},h2:{1J:q.1H.1b(\'jH\'),1C:\'R.30\'},h3:{1J:q.1H.1b(\'jI\'),1C:\'R.30\'},h4:{1J:q.1H.1b(\'jM\'),1C:\'R.30\'},h5:{1J:q.1H.1b(\'jD\'),1C:\'R.30\'}}},4n:{1J:q.1H.1b(\'4n\'),1C:\'28.30\'},4m:{1J:q.1H.1b(\'4m\'),1C:\'28.30\'},5Y:{1J:q.1H.1b(\'5Y\'),1C:\'28.30\'},5x:{1J:q.1H.1b(\'5x\'),1C:\'28.30\'},5m:{1J:\'&o6; \'+q.1H.1b(\'5m\'),1C:\'2e.3m\'},5z:{1J:\'1. \'+q.1H.1b(\'5z\'),1C:\'2e.3m\'},7p:{1J:\'< \'+q.1H.1b(\'7p\'),1C:\'3y.95\'},3y:{1J:\'> \'+q.1H.1b(\'3y\'),1C:\'3y.bo\'},M:{1J:q.1H.1b(\'M\'),1C:\'M.2O\'},22:{1J:q.1H.1b(\'22\'),1C:\'22.2O\'},V:{1J:q.1H.1b(\'V\'),1n:{V:{1J:q.1H.1b(\'ba\'),1C:\'V.2O\',1R:{2j:\'a\',in:{1J:q.1H.1b(\'ca\'),},2m:{1J:q.1H.1b(\'ba\')}}},6P:{1J:q.1H.1b(\'6P\'),1C:\'V.6P\',1R:{2j:\'a\',2m:{1c:{\'1G\':\'I-1n-V-c9\',\'4K-7o\':1q}}}}}},3b:{1J:q.1H.1b(\'3b\'),1n:{2c:{1J:q.1H.1b(\'jt\'),1C:\'3b.2c\'},5u:{1J:q.1H.1b(\'jx\'),1C:\'3b.5u\'},4s:{1J:q.1H.1b(\'jB\'),1C:\'3b.4s\'},9d:{1J:q.1H.1b(\'jA\'),1C:\'3b.9d\'}}},b3:{1J:q.1H.1b(\'b3\'),1C:\'3K.1Y\'}}},2o:C(){q.1A.iV();q.1A.iN();q.1A.iU();if(q.G.4i.12===0)F;q.$1A=q.1A.ir();q.1A.iQ();q.1A.1F();q.1A.i5();q.1A.ia();q.1A.iz();if(q.G.b5){q.$T.on(\'iZ.I 2s.I 2n.I\',$.1d(q.1R.1A,q))}},ir:C(){F $(\'<3p>\').2f(\'I-1A\').1c({\'id\':\'I-1A-\'+q.2G,\'9c\':\'1A\'})},i5:C(){$.1B(q.G.1A.3a.1n,$.1d(C(i,s){if($.3t(i,q.G.3a)==-1)83 q.G.1A.3a.1n[i]},q))},ia:C(){$.1B(q.G.4i,$.1d(C(i,2g){if(!q.G.1A[2g])F;if(2g===\'22\'){if(q.G.76===O)F;N if(!q.G.76&&q.G.7V===O)F}if(2g===\'M\'){if(q.G.75===O)F;N if(!q.G.75&&q.G.7V===O)F}E 2X=q.G.1A[2g];q.$1A.1F($(\'<li>\').1F(q.1t.2o(2g,2X)))},q))},1F:C(){if(q.G.bf){q.$1A.2f(\'I-1A-oT\');$(q.G.bf).B(q.$1A)}N{q.$2Q.6t(q.$1A)}},iz:C(){if(!q.L.7b())F;if(q.G.bf)F;if(!q.G.iS)F;q.1A.cp();$(q.G.6y).on(\'6b.I.\'+q.2G,$.1d(q.1A.cp,q))},iQ:C(){if(q.L.6M()&&q.G.iO){q.$1A.2f(\'I-1A-bI\')}},iU:C(){if(q.G.4E)F;E 68=q.G.4i.4Q(\'B\');if(68!==-1){q.G.4i.bi(68,1)}},iV:C(){if(q.G.cw.12===0)F;$.1B(q.G.cw,$.1d(C(i,s){E 68=q.G.4i.4Q(s);q.G.4i.bi(68,1)},q))},iN:C(){if(!q.L.6M()||q.G.cv.12===0)F;$.1B(q.G.cv,$.1d(C(i,s){E 68=q.G.4i.4Q(s);q.G.4i.bi(68,1)},q))},cp:C(){E 3U=$(q.G.6y).3U();E 6C=1;if(q.G.6y===1l){6C=q.$2Q.2I().2U}if((3U+q.G.6z)>6C){q.1A.iD(3U,6C)}N{q.1A.iC()}},iD:C(3U,6C){E 2U=q.G.6z+3U-6C;E 2c=0;E 2h=6C+q.$2Q.3n()-32;E 2J=q.$2Q.7P();q.$1A.2f(\'1A-82-2Q\');q.$1A.1O({4g:\'8u\',2J:2J,2U:2U+\'px\',2c:2c});if(3U>2h)$(\'.I-1n-\'+q.2G+\':c2\').3v();q.1A.iJ();q.$1A.1O(\'iH\',(3U<2h)?\'c2\':\'k4\')},iC:C(){q.$1A.1O({4g:\'oC\',2J:\'bj\',2U:0,2c:0,iH:\'c2\'});q.1A.jO();q.$1A.2S(\'1A-82-2Q\')},iJ:C(){E 2U=q.$1A.6c()+q.G.6z;E 4g=\'82\';if(q.G.6y!==1l){2U=(q.$1A.6c()+q.$1A.2I().2U)+q.G.6z;4g=\'8u\'}$(\'.I-1n-\'+q.2G).1B(C(){$(q).1O({4g:4g,2U:2U+\'px\'})})},jO:C(){E 2U=(q.$1A.6c()+q.$1A.2I().2U);$(\'.I-1n-\'+q.2G).1B(C(){$(q).1O({4g:\'8u\',2U:2U+\'px\'})})}}},1v:C(){F{3Q:C(id,2i,2A){q.1v.4Y=O;q.1v.2A=2A;q.1v.2i=2i;q.1v.$el=$(id);q.1v.$4d=$(\'<1i id="I-4d" />\');q.1v.$cE=$(\'<1i id="I-4d-3f" />\').1g(q.1H.1b(\'kD\'));q.1v.$3D=$(\'<3D 1m="22" 1w="22" />\');q.1v.$cE.1F(q.1v.$3D);q.1v.$4d.1F(q.1v.$cE);q.1v.$el.1F(q.1v.$4d);q.1v.$4d.3h(\'I.1v\');q.1v.$3D.3h(\'I.1v\');q.1v.$4d.on(\'oL.I.1v\',$.1d(q.1v.bn,q));q.1v.$4d.on(\'oK.I.1v\',$.1d(q.1v.k6,q));q.1v.$3D.on(\'kR.I.1v\',$.1d(C(e){e=e.7T||e;q.1v.b9(q.1v.$3D[0].5R[0],e)},q));q.1v.$4d.on(\'57.I.1v\',$.1d(C(e){e.2w();q.1v.$4d.2S(\'7k-c0\').2f(\'7k-57\');q.1v.bs(e)},q))},kQ:C(22,e){q.1v.4Y=1q;q.1v.b9(22,e)},bs:C(e){e=e.7T||e;E 5R=e.bG.5R;q.1v.b9(5R[0],e)},b9:C(22,e){if(q.G.7V){q.1v.bL(22);q.1v.kr(22);F}E 4q=!!3l.b6?2a b6():4e;if(3l.b6){q.1v.bL(22);E 1w=(q.1v.1m==\'M\')?q.G.k1:q.G.k2;4q.1F(1w,22)}q.5E.2O();q.1X.2u(\'o1\',e,4q);q.1v.ky(4q,e)},bL:C(22){q.1v.k8(22);if(q.1v.4Y){q.1v.2i=(q.1v.1m==\'M\')?q.G.75:q.G.76;q.1v.2A=(q.1v.1m==\'M\')?q.M.1Y:q.22.1Y}},k8:C(22){q.1v.1m=\'M\';if(q.G.kb.4Q(22.1m)==-1){q.1v.1m=\'22\'}},6V:C(7H,fd){if(7H===O||1s 7H!==\'41\')F fd;$.1B(7H,$.1d(C(k,v){if(v!==4e&&v.4y().4Q(\'#\')===0)v=$(v).2K();fd.1F(k,v)},q));F fd},ky:C(4q,e){if(q.1v.1m==\'M\'){4q=q.1v.6V(q.G.nq,4q);4q=q.1v.6V(q.1v.b7,4q)}N{4q=q.1v.6V(q.G.nr,4q);4q=q.1v.6V(q.1v.bC,4q)}E 2M=2a bq();2M.bw(\'ns\',q.1v.2i);2M.cF("X-n7-n8","bq");2M.fu=$.1d(C(){if(2M.bS==4){E 1f=2M.fr;1f=1f.K(/^\\[/,\'\');1f=1f.K(/\\]$/,\'\');E 2P;52{2P=(1s 1f===\'6Z\'?$.jY(1f):1f)}51(n6){2P={6U:1q}}q.5E.3v();if(!q.1v.4Y){q.1v.$4d.2S(\'7k-57\')}q.1v.2A(2P,q.1v.4Y,e)}},q);2M.dk(4q)},bn:C(e){e.2w();q.1v.$4d.2f(\'7k-c0\')},k6:C(e){e.2w();q.1v.$4d.2S(\'7k-c0\')},nf:C(){q.1v.b7={}},ne:C(1w,1E){q.1v.b7[1w]=1E},nd:C(1w){83 q.1v.b7[1w]},nc:C(){q.1v.bC={}},nw:C(1w,1E){q.1v.bC[1w]=1E},nx:C(1w){83 q.1v.bC[1w]},kr:C(22){q.1v.fz(22,$.1d(C(kw){q.1v.g4(22,kw)},q))},fz:C(22,2A){E 2M=2a bq();E 6A=\'?\';if(q.G.7V.3N(/\\?/)!=\'-1\')6A=\'&\';2M.bw(\'nR\',q.G.7V+6A+\'1w=\'+22.1w+\'&1m=\'+22.1m,1q);if(2M.fy)2M.fy(\'1g/nS; nP=x-nN-nO\');E fi=q;2M.fu=C(e){if(q.bS==4&&q.dv==bt){fi.5E.2O();2A(fl(q.fr))}N if(q.bS==4&&q.dv!=bt){}};2M.dk()},g3:C(bx,2i){E 2M=2a bq();if("nY"in 2M){2M.bw(bx,2i,1q)}N if(1s fX!="1y"){2M=2a fX();2M.bw(bx,2i)}N{2M=4e}F 2M},g4:C(22,2i){E 2M=q.1v.g3(\'nL\',2i);if(!2M){}N{2M.nD=$.1d(C(){if(2M.dv==bt){q.5E.3v();E bA=2i.4o(\'?\');if(!bA[0]){F O}if(!q.1v.4Y){q.1v.$4d.2S(\'7k-57\')}E 2P={9b:bA[0]};if(q.1v.1m==\'22\'){E 2C=bA[0].4o(\'/\');2P.6k=2C[2C.12-1]}q.1v.2A(2P,q.1v.4Y,O)}N{}},q);2M.nK=C(){};2M.1v.nJ=C(e){};2M.cF(\'p0-p1\',22.1m);2M.cF(\'x-qg-qm\',\'qq-qp\');2M.dk(22)}}}},L:C(){F{6M:C(){F/(eQ|eH|eO|ht)/.bE(b8.bb)},7b:C(){F!/(eQ|eH|q3|eO|ht)/.bE(b8.bb)},q0:C(7H){F g8.5w.4y.6e(7H)==\'[41 6f]\'},3z:C(B,he){B=B.K(/[\\6g-\\q7\\qb]/g,\'\');B=B.K(/&5s;/gi,\'\');B=B.K(/<\\/?br\\s?\\/?>/g,\'\');B=B.K(/\\s/g,\'\');B=B.K(/^<p>[^\\W\\w\\D\\d]*?<\\/p>$/i,\'\');B=B.K(/<4R(.*?[^>])>$/i,\'4R\');B=B.K(/<4E(.*?[^>])>$/i,\'4E\');if(he!==O){B=B.K(/<[^\\/>][^>]*><\\/[^>]+>/gi,\'\');B=B.K(/<[^\\/>][^>]*><\\/[^>]+>/gi,\'\')}B=$.3k(B);F B===\'\'},dr:C(5f){if(1s(5f)===\'1y\')F 0;F 5L(5f.K(\'px\',\'\'),10)},n2:C(6H){if(1s 6H==\'1y\')F;if(6H.3N(/^#/)==-1)F 6H;E hm=/^#?([a-f\\d])([a-f\\d])([a-f\\d])$/i;6H=6H.K(hm,C(m,r,g,b){F r+r+g+g+b+b});E bg=/^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.5j(6H);F\'qQ(\'+5L(bg[1],16)+\', \'+5L(bg[2],16)+\', \'+5L(bg[3],16)+\')\'},6q:C(el){F $(\'<1i>\').1F($(el).eq(0).8O()).B()},bh:C(el){if($.3t(el.1p,q.G.8F)!==-1){F $(el)}N{F $(el).2q(q.G.8F.4y().3e(),q.$T[0])}},5r:C(el,1c){E $el=$(el);if(1s $el.1c(1c)==\'1y\'){F 1q}if($el.1c(1c)===\'\'){$el.1K(1c);F 1q}F O},4f:C(i,s){E $s=$($.dQ(s));$s.1h(\'.I-7s-3g\').1K(\'1o\').1K(\'1G\');if($s.1h(\'hr, br, 1L, 4R, 4E\').12!==0)F;E 1g=$.3k($s.1g());if(q.L.3z(1g,O)){$s.1u()}},8z:C(){q.gs=q.$T.3U();q.bm=$(3l).3U();if(q.G.9p)q.gq=$(q.G.9p).3U()},bk:C(){if(1s q.8z===\'1y\'&&1s q.bm===\'1y\')F;$(3l).3U(q.bm);q.$T.3U(q.gs);if(q.G.9p)$(q.G.9p).3U(q.gq)},bH:C(){E 3g=1l.3w(\'1j\');3g.3E=\'I-7s-3g\';3g.3x=q.G.6o;F 3g},4Z:C(Q){E 2d=q.G.4N;2d.2N(\'1j\');if(Q.1p==\'8o\')2d.2N(\'a\');$(Q).1h(2d.3c(\',\')).6h(\'1j.I-J-2r\').26().3Y()},6i:C(Q,4Z){E 3u=q;$(Q).2t(C(){if(4Z===1q)3u.L.4Z(q);F $(q).26()});F $(Q)},4I:C(Q,Y,4Z){E 5F;E 3u=q;$(Q).2t(C(){5F=$(\'<\'+Y+\' />\').1F($(q).26());2B(E i=0;i<q.4u.12;i++){5F.1c(q.4u[i].1w,q.4u[i].1E)}if(4Z===1q)3u.L.4Z(5F);F 5F});F 5F},gp:C(){E R=q.J.4a();if(!R)F O;E 2I=q.1V.b4(R);F(2I===0)?1q:O},6T:C(2j){if(1s 2j==\'1y\'){E 2j=q.J.4a();if(!2j)F O}E 2I=q.1V.b4(2j);E 1g=$.3k($(2j).1g()).K(/\\n\\r\\n/g,\'\');F(2I==1g.12)?1q:O},dN:C(){E R=q.$T[0];E 2I=q.1V.b4(R);E 1g=$.3k($(R).B().K(/(<([^>]+)>)/gi,\'\'));F(2I==1g.12)?1q:O},6X:C(R){R=R[0]||R;F R&&q.L.7J(R.1p)},7J:C(Y){if(1s Y==\'1y\')F O;F q.gm.bE(Y)},bB:C(1z,Y){E 2j=$(1z).2q(Y,q.$T[0]);if(2j.12==1){F 2j[0]}F O},9w:C(){F q.bd},gW:C(){q.bd=1q},8U:C(){q.bd=O},4p:C(el){if(!el){F O}if($(el).dL(\'.I-T\').12===0||$(el).3i(\'I-T\')){F O}F el},h8:C(){F q.L.4S([\'dK\',\'di\',\'cS\',\'cR\',\'cI\',\'cH\'])},4S:C(1p){E 1r=q.J.67();E 1z=q.J.3B();if($.bc(1p)){E cJ=0;$.1B(1p,$.1d(C(i,s){if(q.L.cM(1z,1r,s)){cJ++}},q));F(cJ===0)?O:1q}N{F q.L.cM(1z,1r,1p)}},cM:C(1z,1r,1p){1p=1p.lF();F 1r&&1r.1p===1p?1r:1z&&1z.1p===1p?1z:O},lG:C(){F(q.L.1P(\'2E\')&&5L(q.L.1P(\'bp\'),10)<9)?1q:O},lJ:C(){F(q.L.1P(\'2E\')&&5L(q.L.1P(\'bp\'),10)<10)?1q:O},gj:C(){F!!b8.bb.1T(/lB\\/7\\./)},1P:C(1P){E 4U=b8.bb.3e();E 1T=/(de)[\\/]([\\w.]+)/.5j(4U)||/(hw)[ \\/]([\\w.]+)/.5j(4U)||/(7q)[ \\/]([\\w.]+).*(db)[ \\/]([\\w.]+)/.5j(4U)||/(7q)[ \\/]([\\w.]+)/.5j(4U)||/(kX)(?:.*bp|)[ \\/]([\\w.]+)/.5j(4U)||/(2E) ([\\w.]+)/.5j(4U)||4U.4Q("l3")>=0&&/(dc)(?::| )([\\w.]+)/.5j(4U)||4U.4Q("kZ")<0&&/(4b)(?:.*? dc:([\\w.]+)|)/.5j(4U)||[];if(1P==\'db\')F(1s 1T[3]!=\'1y\')?1T[3]==\'db\':O;if(1P==\'bp\')F 1T[2];if(1P==\'7q\')F(1T[1]==\'hw\'||1T[1]==\'de\'||1T[1]==\'7q\');if(1T[1]==\'dc\')F 1P==\'2E\';if(1T[1]==\'de\')F 1P==\'7q\';F 1P==1T[1]},bF:C(ev,es,2I){E i=ev.4Q(es,2I);F i>=0?i:O},d8:C(){E $31=$(\'B\');E 7u=3l.7P;if(!7u){E cZ=1l.lk.lp();7u=cZ.4s-8d.mO(cZ.2c)}E kP=1l.31.kC<7u;E by=q.L.kv();$31.1O(\'bI\',\'k4\');if(kP)$31.1O(\'hh-4s\',by)},kv:C(){E $31=$(\'31\');E 7G=1l.3w(\'1i\');7G.3E=\'I-hf-j0\';$31.1F(7G);E by=7G.m8-7G.kC;$31[0].m6(7G);F by},bJ:C(){$(\'B\').1O({\'bI\':\'\',\'hh-4s\':\'\'});$(\'31\').1u(\'I-hf-j0\')}}}};$(3l).on(\'2R.eN.I\',C(){$(\'[1f-eN="I"]\').I()});47.5w.3Q.5w=47.5w})(o7);',62,1669,'||||||||||||||||||||||||||this|||||||||||html|function||var|return|opts||redactor|selection|replace|utils|image|else|false||node|block||editor||link|||tag||||length||range||||||modal|get|attr|proxy|code|data|text|find|div|span|key|document|type|dropdown|style|tagName|true|parent|typeof|button|remove|upload|name|clean|undefined|current|toolbar|each|func|keydown|value|append|class|lang|tidy|title|removeAttr|img|linebreaks|next|css|browser|target|observe|sync|match|set|caret|formatted|core|insert|blocks||btn|file|nodes||buffer|contents||inline|blockquote|new|start|left|tags|list|addClass|btnName|end|url|element|href|RegExp|out|focus|build|len|closest|marker|keyup|replaceWith|setCallback|textarea|preventDefault|item|click|linkify|callback|for|arr|settings|msie|pre|uuid|tabifier|offset|width|val|label|xhr|push|show|json|box|load|removeClass|sel|top|paragraphize|setStart|btnObject|td|last|format|body|||||||||formatting|alignment|join|restore|toLowerCase|placeholder|space|off|hasClass|keyCode|trim|window|toggle|height|save|ul|tooltip|autosave|pos|inArray|self|hide|createElement|innerHTML|indent|isEmpty|setTimeout|getCurrent|CodeMirror|input|className|after|imageBox|verified|first|re|line|insertNode|matches|search|children|setEnd|init|font|point|rel|scrollTop|script|table|getBlocks|unwrap|execCommand||object|option|collapse|options|||Redactor|ctrl|section|getBlock|mozilla|deniedTags|droparea|null|removeEmpty|position|del|buttons|close|resizeHandle|addRange|italic|bold|split|isRedactorParent|formData|clearUnverified|right|src|attributes|regexps|child|substr|toString|allowedTags|command|margin|blockElem|getSelection|source|modalBox|prev|cmd|replaceToTag|resize|aria|frag|wrapper|inlineTags|htmls|orgn|indexOf|iframe|isCurrentOrParent|allowedAttr|ua|elem|arrow|keys|direct|removeInlineTags||catch|try|collapsed|setAfter|deleteContents|while|drop|BR||getMarkerAsHtml|cont|linkProtocol|contenteditable|pasteBox|str|align|meta|strong|exec|isFunction|editter|unorderedlist|links|https|none|tmp|removeEmptyAttr|nbsp|br1|center|LI|prototype|underline|removeDataAttr|orderedlist|replaceStyles|replaceTags|paste|attributesRemove|progress|replacement|toggleType|touchstart|tab|allowed|lastNode|parseInt|node1|quot|nodeValue|th|replaceDivs|files|emptyHtml|lastList|inlines|currentList|currentLevel|methods|deleted|wrap|||templateName|marginTop|form|||getParent|index|containerTag|charAt|scroll|innerHeight|num|call|String|u200B|not|replaceWithContents|prop|filename|getRangeAt|shortcuts|break|invisibleSpace|params|getOuterHtml|visual|shiftKey|prepend|instance|preSpaces|SPAN|insertBreakLine|toolbarFixedTarget|toolbarFixedTopOffset|mark|Insert|boxTop|module|shift|TD|removeComments|hex|getNodes|metaKey|figure|removeWithoutAttr|isMobile|display|tr|unlink|getMarker|mousedown|tabindex|isEndOfElement|error|getHiddenFields|editorDiv|isBlock|video|string|ENTER|vimeo|youtube||modalOverlay|imageUpload|fileUpload|focn|startNode|dropdowns|decoration|isDesktop|strike|posFromIndex|endRange|float|getEvent|level|embed|possible|drag|sub|parHtml|www|disabled|outdent|webkit|getTabs|invisible|z0|windowWidth|outerHTML|blocksSize|keyPosition|dropact|select|rangeCount|rtePaste|hideAll|pattern|extend|boundaryRange|scrollDiv|obj|weight|isBlockTag|_blank|edit|action|handler|toggleClass|innerWidth|sup|setVerified|cloneRange|originalEvent|which|s3|hideResize|listParent|figcaption|blank|alt|appendChild|fixed|delete|BACKSPACE|tmpList|modif|header|100|DIV|imageResizer|tagblock|tmpLi|Math|event|resizer|setClass|blurClickedElement|setMode|attrs|items|returnValue|inserted|codemirror|PRE|removeMarkers|blockLevelElements|plugins|http|linkmarker|absolute|focusNode|Delete|imageFloatMargin|onPaste|saveScroll|act|classSize|replaceParagraphsToBr|UL|tagout|alignmentTags|targetTouches|newLevel|isRemoveInline|param|ftp|cleanlevel|clear|OL|clone|imageMargin|DELETE|icon|cite|removeAllRanges|disableSelectAll|lastLevel|createTextNode|modalClose|audio|active|||createRange|convertVideoLinks|tabber|decrease|||||closeHandler|filelink|role|justify|modalHeader|small|selectElement|character|modalFooter|selectNodeContents|rebuffer|undo|autosaveInterval|inputUrl|amp|scrollTarget|setActive|Add|addProperties|events|ctrlKey|isMobileUndoRedo|isSelectAll|insertDblBreakLine|exceptTags|counter||size|node2|singleLine|apply|Array|case|Header|setStartAfter|endNode|addEvent|methodVal|createDocumentFragment|firstChild|autosaveFields|isContainerTable|enable|onSync|formatblock|x200b|replaceToParagraph|eventName|parentA|blockHtml|tabAsSpaces|u00a0|checked|re2|convertLinks|editorHtml|selectionMarkers|com|before|SPACE|nodeType|redo|isTextarea|marginRight|onClick|indentValue|marginLeft|marginBottom|imageFloat|pageY|property|getPlainText|moveToPoint|enterKey|prev2|createTextRange|caretRangeFromPoint|normalizeLists|caretPositionFromPoint|getOffset|encodeEntities|cloned|images|classname|parentEl|deleteProperties|mouseover|mouseout|IMG|substring|safes|isEnabled|formatWrap|formatListToBlockquote|minHeight|bind|maxHeight|caretRange|BLOCKQUOTE|link_new_tab|callbacks|samp|fixEmptyIndent|wrapperHtml|matchContainers|insertInIe|kbd|windowHeight|appendTo|prevList|spaces|modalBody|mso|showOnDesktop|onPasteTidy|clearStyle|ESC|horizontalrule|getOffsetOfElement|activeButtons|FormData|imageFields|navigator|traverseFile|link_insert|userAgent|isArray|selectAll||toolbarExternal|result|getAlignmentElement|splice|auto|restoreScroll|codeLength|saveBodyScroll|onDrag|increase|version|XMLHttpRequest||onDrop|200|special|regex|open|method|scrollbarWidth|specialKey|s3file|isTag|fileFields|placeTag|test|strpos|dataTransfer|createSpaceElement|overflow|enableBodyScroll|iframeEnd|setConfig|iframeStart|firstFound|endOffset|br2|endContainer|insertBreakLineProcessing|readyState|setBefore|curLang|extra|filter|convertImageLinks|mailto|inputText|hover|convertUrlLinks|visible|inValues|setDropdownProperties|setDropdownAttr|outValues|isCurrent|footer|inactive|link_edit|isDelete|addDropdown|allowedAttrData|allowedAttrTags|closeTooltip|activeButtonsStates||quote|setInactiveAll|currentEl|buttonsSize|createButton|isOrderedCmdUnordered|Column|observeScroll|isUnorderedCmdOrdered|listTag|linkSize|slice|regexB|buttonsHideOnMobile|buttonsHide|showOnMobile|createCancelButton|createActionButton|highContrast|addCallback|image_position|URL|placeholdler|setRequestHeader|getTextFromHtml|H6|H5|matched|tagsEmpty|dfn|isCurrentOrParentOne|attrAllowed|Link|headers|add|H4|H3|saveFormTags|convertInline|clearUnverifiedRemove|verifiedTags|listType|listsIds|documentElementRect|formatTableWrapping|content|headTag|count|modules|autosaveName|autosaveOnChange|direction|disableBodyScroll|setAfterOrBefore|fonts|safari|rv||opr|RedactorPlugins|getModuleMethods|syncCode|H2|setFormat|send||removeFormat|TH|isP|imageResizable|imageEditable|normalize|htmlIe||address|status|imageDelete|TAB|isEndOfTable|Table|nodeToCaretPositionFromPoint|rTags|ratio|imageDisplay|selectionStart|selectionEnd|enlargeOffset|removeSpaces|markerLength|showCode|H1|parents|htmlLength|isEndOfEditor|isFocused|redactorImageLink|parseHTML|one|dropdownWidth|formattingAdd|dropdownObject|Row|DOWN|commentsMatches|container|ownLine|head|getText|contOwnLine|tfoot|parentNode|219|BODY|thead|getInlines|hotkeysShiftNums|suffix|finalNodes|cleanTag|setMarker|tbody|startOffset|finish|setOffset|getNodesMarker|paragraphizeBlocks|221||||duplicate|onClickCallback||offsetNode|needle|createTooltip|setEvent|haystack|ie11PasteFrag|disableIeLinks|ie11FixInserting|EndToEnd|setEndPoint|RIGHT|keydownStop|disableMozillaEditing|aside|formatConvert|formatRemoveSameChildren|iPod|args|switch|_this|orgo|strikethrough|tools|BlackBerry|through|iPhone|lastChild|article|savedSel|blocksMatch|execHtml|scope|htmlFixMozilla|haspopup|the|beforekey|||||||||||afterkey|htmlWithoutClean||fullscreen|indented|setEventDropUpload|cleanStyleOnEnter|that|insertParagraph|exitFromBlockquote|decodeURIComponent|replaceDivToParagraph||codeKeydownCallback|tabKey|origHandler|responseText|replaceDivToBreakLine|setEventDrop|onreadystatechange|dbl|stopPropagation|area|overrideMimeType|s3executeOnSignedUrl|arguments|dragImageUpload|insertAfterLastElement|setEvents|insertNewLine|dragFileUpload|onTab|onArrowDown|blur|checkKeyEvents|CTRL|META|ALT|checkEvents|isBlured|focusEnd|stop|setHelpers|formatMultiple|HR|SHIFT|addArrowsEvent|focusCallback|XDomainRequest|setupSelectAll|codeKeyupCallback|LEFT_WIN|altKey|createMarkers|s3createCORSRequest|s3uploadToS3|hotkeysSpecialKeys|setupBuffer|Function|Object|internal|textareaIndenting|onPasteExtra|choose|Mso|setInactive|setActiveInVisual|tempEnd|tempStart||isIe11|modified|indenting|reIsBlock|Center|replaceSelection|isStartOfElement|saveTargetScroll|bmso|saveEditorScroll||clearInterval|removeData|pasteHTML|Video|eventNamespace|Align|destroy|all|indexFromPos|matchBlocks|newTag|replaceDivsToBr|restoreFormTags|onSet|matchBR|savePreFormatting|getPreCode|getOnlyImages|restoreSelectionMarker|saveCodeFormatting|removeDirtyStyles|isSingleLine|onPasteIeFixLinks|setSelectionRange|Code|setInactiveInCode|showVisual|anchor|enableSelectAll|setValue|startSync|onPasteRemoveSpans|onPasteRemoveEmpty|||||||onChange|isCurrentOrParentHeader|selected|onPasteWord|editerWidth|showEdit|loadResizableControls|removeEmptyTags|scrollbar|opacity|padding|15px|cleanSpaces|loadEditableControls|Head|shorthandRegex|walker|increaseLists|removeInvisibleSpace|subscript||foco|Android|replaced|superscript|chrome|increaseText|increaseBlocks|decreaseLists|decreaseBlocks|to|fixImageSourceAfterDrop|inside|imageLink|buttonSave|nofollow|imagePosition|floatValue|buttonDelete|_delete|pastePlainText|FIGCAPTION|Apple|Image|700|floating|linkNofollow|Right|moveResize|Left|round|stopResize|startResize|pageX|chars|update|setFloating|setResizable|formatCollapsed|onShiftEnter|setFormattingTags|replaceElement|setTitle|setContent|getTemplate|loadButtons|alignElement|template|||||jsxhr|spans|removeEmptyListInTable|setDraggable|isNeedReplaceElement|setText||createDeleteButton|setButtonsWidth|bindModuleMethods|createContainer||cancel|setEnter|draggable|handle|isLinebreaksOrNoBlocks|setBlocks|setFixed|map|setMultiple|observeScrollDisable|observeScrollEnable|setCollapsed|formatTags|getPrev|visibility|anchorNode|setDropdownsFixed|newTd|commonAncestorContainer|fadeOut|hideButtonsOnMobile|toolbarOverflow|checkbox|setOverflow|success|toolbarFixed|fieldset|isButtonSourceNeeded|hideButtons|loadTemplates|imageEdit|callbackName|mouseup|measure|setEditable|paragraph|removeAttrs|linkTooltip|removeParagraphsInLists|getSafesComments|restoreSafes|lineAfter|Color|setupAllowed|header1|showTooltip|addToAllowed|aUnlink|aEdit|removeFromDenied|Edit|removeTags|aLink|getSafes|getTooltipPosition|here|removeAttrGetRemoves|Drop|replaceBreaksToParagraphs|properties|enableEvents|shortcutsAdd|align_left|createPasteBox|freeze|buildOverlay|align_center|loadModules|loadOptions|align_justify|align_right|disableEvents|header5|lineBefore|replaceBreaksToNewLines|png|header2|header3|cleanOnPaste|List|destroyed|header4|gif|unsetDropdownsFixed|contrast|textNode|elements|600|xn|location|thref|createContainerBox|insertAfter|parseJSON|isExceptLastOrFirst|setNodesMarker|imageUploadParam|fileUploadParam|_moz_dirty|hidden|setCodeAndCall|onDragLeave|createTextarea|getType|setFocus|objects|imageTypes|insertInOthersBrowsers|getUndo|fromElement|startPointNode|cleanUrl|empty|run|getRedo|fromTextarea|lastFound|re3|langs|startContainer|removeNodesMarkers|getInlinesTags|s3uploadFile|setEndBefore|setForce|getTextareaName|measureScrollbar|signedURL|regexp|sendData|loadContent|keyupStop|isKey|clientWidth|upload_label|buttonText|enableEditor|getData|callEditor|lastBlock|getLastBlock|loadEditor|buttonInsert|setRedo|formatEmpty|setOptions|isOverflowing|directUpload|change|formatBlockquote|convertImages|dir|setUndo|pop|opera|drop_file_here|compatible|UP|ltr|VERSION|trident|cut|docs|sid|hgroup|down|guid|rowspan|home|Name|Download|105|math|removePhp|uploadImageField||legend|documentElement|textContent|innerText|104|download|getBoundingClientRect|stripTags|savePreCode|up||Alignment|Open|colspan|youtu|Anchor|fake|Or|Trident|Underline|LEFT|10px|toUpperCase|isOldIe|Choose|or_choose|isLessIe10|jpe|such|post|enableInlineTableEditing|setToPoint|Infinity|nextNode|SHOW_TEXT|AutoUrlDetect|done|ajax|112|reg|No|107|enableObjectResizing|strict|1000|getCoords|setInterval|111|Horizontal|removeChild|setAwesome|offsetWidth|Justify|addFirst|addBefore|Rule|110|109|removeIcon|autosaveError|createTreeWalker|NodeFilter|caretOffset|setStartBefore|disable|changeIcon|setEndAfter|u2122|trade|application|blurCallback|WordDocument|addAfter|TEXTAREA|sdata|getOwnPropertyNames|converted|xml|1strike|shapes|merge|Deleted|115|applet|spacerun|yes|floor|use|113|u2026|hellip|u2014|101|abs|106|u00a9|copy|102|103|u2010|dash|toggleAttr|setAttr|toggleData|setData|mdash|slevel|hexToRgb|Below|host|noscript|err|Requested|With|frame|insertHtml|insert_column_left|clearFileFields|removeImageFields|addImageFields|clearImageFields|insert_row_below|php|addBack|insert_row_above|insert_table||500|frameborder|281|Above|uploadImageFields|uploadFileFields|POST|createLink|insertedLink|deletedLink|addFileFields|removeFileFields|context|enter|FOOTER|ASIDE|0px|onload|optional|222|220|rows|delete_table|onprogress|onerror|PUT|_|user|defined|charset|fromPoint|GET|plain|extractContents|insert_column_right|delete_row|fromCharCode|Key|withCredentials|delete_column|allowfullscreen|uploadStart|times|Close|9999px|overlay|bull|jQuery|labelledby|dialog|cursor|Outdent|move|pasteBefore|Quote|fast|bodyOveflow|unselectable|addButton|specified|backcolor||fontcolor||Font|Back|toggleActive||Ordered|modalClosed|Bold|Unordered|hasOwnProperty|Normal|Indent|getNext|nextSibling|previousSibling|relative|Unlink|Cancel|Save|HTML|ARTICLE|player|grep|dragleave|dragover|1500|fadeIn|focusin|modalOpened|email|outerHeight|20px|external|getModal|frameset|Formatting|createTabber|addTab|addTemplate|Content|Type|SECTION|119|Embed|getObject|getBox|getEditor|ADDRESS|118|117|getValue|DT|DD|OUTPUT|getElement|getTextarea|HEADER|Email|Text|121|max|dropdownShow|web|video_html_code|Callback|getToolbar|_data|120|namespace|listSelections|setSelection|menu||esc|cleanEmptyParagraph|capslock|pause|summary|details|pageup|pagedown|getOnlyLinksAndImages|Upload|File|nav|Vimeo|alignleft|DL|116|setSize|refresh|setCursor|Youtube|backspace|alignright|aligncenter|syncBefore|jpeg|ins|Ss|f10|isString|114|columns|iPad|cssText|getHtml|Columns|u200D|189|188|add_head|uFEFF|TR|removeStyle|removeStyleRule|nodeToPoint|amz|192|clientX|Rows|clientY|191|acl|toggleStyle|190|read|public|insertHTML|imageUploadError|delete_head|f12|705|144|numlock|image_web_link|145|123|Web|dropdownHide|122|replaceWithHtml|f11|fileUploadError|dragstart|mousemove|187|None|Position|Title|cloneContents|Italic|dropdownShown|rgb|186|173|touchmove|touchend'.split('|'),0,{}))
\ No newline at end of file
diff --git a/js/select2.min.js b/js/select2.min.js
new file mode 100644
index 0000000000000000000000000000000000000000..49a988c7abc5552c39e941de320b1de7d847bd93
--- /dev/null
+++ b/js/select2.min.js
@@ -0,0 +1,2 @@
+/*! Select2 4.0.0 | https://github.com/select2/select2/blob/master/LICENSE.md */!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):a("object"==typeof exports?require("jquery"):jQuery)}(function(a){var b=function(){if(a&&a.fn&&a.fn.select2&&a.fn.select2.amd)var b=a.fn.select2.amd;var b;return function(){if(!b||!b.requirejs){b?c=b:b={};var a,c,d;!function(b){function e(a,b){return u.call(a,b)}function f(a,b){var c,d,e,f,g,h,i,j,k,l,m,n=b&&b.split("/"),o=s.map,p=o&&o["*"]||{};if(a&&"."===a.charAt(0))if(b){for(n=n.slice(0,n.length-1),a=a.split("/"),g=a.length-1,s.nodeIdCompat&&w.test(a[g])&&(a[g]=a[g].replace(w,"")),a=n.concat(a),k=0;k<a.length;k+=1)if(m=a[k],"."===m)a.splice(k,1),k-=1;else if(".."===m){if(1===k&&(".."===a[2]||".."===a[0]))break;k>0&&(a.splice(k-1,2),k-=2)}a=a.join("/")}else 0===a.indexOf("./")&&(a=a.substring(2));if((n||p)&&o){for(c=a.split("/"),k=c.length;k>0;k-=1){if(d=c.slice(0,k).join("/"),n)for(l=n.length;l>0;l-=1)if(e=o[n.slice(0,l).join("/")],e&&(e=e[d])){f=e,h=k;break}if(f)break;!i&&p&&p[d]&&(i=p[d],j=k)}!f&&i&&(f=i,h=j),f&&(c.splice(0,h,f),a=c.join("/"))}return a}function g(a,c){return function(){return n.apply(b,v.call(arguments,0).concat([a,c]))}}function h(a){return function(b){return f(b,a)}}function i(a){return function(b){q[a]=b}}function j(a){if(e(r,a)){var c=r[a];delete r[a],t[a]=!0,m.apply(b,c)}if(!e(q,a)&&!e(t,a))throw new Error("No "+a);return q[a]}function k(a){var b,c=a?a.indexOf("!"):-1;return c>-1&&(b=a.substring(0,c),a=a.substring(c+1,a.length)),[b,a]}function l(a){return function(){return s&&s.config&&s.config[a]||{}}}var m,n,o,p,q={},r={},s={},t={},u=Object.prototype.hasOwnProperty,v=[].slice,w=/\.js$/;o=function(a,b){var c,d=k(a),e=d[0];return a=d[1],e&&(e=f(e,b),c=j(e)),e?a=c&&c.normalize?c.normalize(a,h(b)):f(a,b):(a=f(a,b),d=k(a),e=d[0],a=d[1],e&&(c=j(e))),{f:e?e+"!"+a:a,n:a,pr:e,p:c}},p={require:function(a){return g(a)},exports:function(a){var b=q[a];return"undefined"!=typeof b?b:q[a]={}},module:function(a){return{id:a,uri:"",exports:q[a],config:l(a)}}},m=function(a,c,d,f){var h,k,l,m,n,s,u=[],v=typeof d;if(f=f||a,"undefined"===v||"function"===v){for(c=!c.length&&d.length?["require","exports","module"]:c,n=0;n<c.length;n+=1)if(m=o(c[n],f),k=m.f,"require"===k)u[n]=p.require(a);else if("exports"===k)u[n]=p.exports(a),s=!0;else if("module"===k)h=u[n]=p.module(a);else if(e(q,k)||e(r,k)||e(t,k))u[n]=j(k);else{if(!m.p)throw new Error(a+" missing "+k);m.p.load(m.n,g(f,!0),i(k),{}),u[n]=q[k]}l=d?d.apply(q[a],u):void 0,a&&(h&&h.exports!==b&&h.exports!==q[a]?q[a]=h.exports:l===b&&s||(q[a]=l))}else a&&(q[a]=d)},a=c=n=function(a,c,d,e,f){if("string"==typeof a)return p[a]?p[a](c):j(o(a,c).f);if(!a.splice){if(s=a,s.deps&&n(s.deps,s.callback),!c)return;c.splice?(a=c,c=d,d=null):a=b}return c=c||function(){},"function"==typeof d&&(d=e,e=f),e?m(b,a,c,d):setTimeout(function(){m(b,a,c,d)},4),n},n.config=function(a){return n(a)},a._defined=q,d=function(a,b,c){b.splice||(c=b,b=[]),e(q,a)||e(r,a)||(r[a]=[a,b,c])},d.amd={jQuery:!0}}(),b.requirejs=a,b.require=c,b.define=d}}(),b.define("almond",function(){}),b.define("jquery",[],function(){var b=a||$;return null==b&&console&&console.error&&console.error("Select2: An instance of jQuery or a jQuery-compatible library was not found. Make sure that you are including jQuery before Select2 on your web page."),b}),b.define("select2/utils",["jquery"],function(a){function b(a){var b=a.prototype,c=[];for(var d in b){var e=b[d];"function"==typeof e&&"constructor"!==d&&c.push(d)}return c}var c={};c.Extend=function(a,b){function c(){this.constructor=a}var d={}.hasOwnProperty;for(var e in b)d.call(b,e)&&(a[e]=b[e]);return c.prototype=b.prototype,a.prototype=new c,a.__super__=b.prototype,a},c.Decorate=function(a,c){function d(){var b=Array.prototype.unshift,d=c.prototype.constructor.length,e=a.prototype.constructor;d>0&&(b.call(arguments,a.prototype.constructor),e=c.prototype.constructor),e.apply(this,arguments)}function e(){this.constructor=d}var f=b(c),g=b(a);c.displayName=a.displayName,d.prototype=new e;for(var h=0;h<g.length;h++){var i=g[h];d.prototype[i]=a.prototype[i]}for(var j=(function(a){var b=function(){};a in d.prototype&&(b=d.prototype[a]);var e=c.prototype[a];return function(){var a=Array.prototype.unshift;return a.call(arguments,b),e.apply(this,arguments)}}),k=0;k<f.length;k++){var l=f[k];d.prototype[l]=j(l)}return d};var d=function(){this.listeners={}};return d.prototype.on=function(a,b){this.listeners=this.listeners||{},a in this.listeners?this.listeners[a].push(b):this.listeners[a]=[b]},d.prototype.trigger=function(a){var b=Array.prototype.slice;this.listeners=this.listeners||{},a in this.listeners&&this.invoke(this.listeners[a],b.call(arguments,1)),"*"in this.listeners&&this.invoke(this.listeners["*"],arguments)},d.prototype.invoke=function(a,b){for(var c=0,d=a.length;d>c;c++)a[c].apply(this,b)},c.Observable=d,c.generateChars=function(a){for(var b="",c=0;a>c;c++){var d=Math.floor(36*Math.random());b+=d.toString(36)}return b},c.bind=function(a,b){return function(){a.apply(b,arguments)}},c._convertData=function(a){for(var b in a){var c=b.split("-"),d=a;if(1!==c.length){for(var e=0;e<c.length;e++){var f=c[e];f=f.substring(0,1).toLowerCase()+f.substring(1),f in d||(d[f]={}),e==c.length-1&&(d[f]=a[b]),d=d[f]}delete a[b]}}return a},c.hasScroll=function(b,c){var d=a(c),e=c.style.overflowX,f=c.style.overflowY;return e!==f||"hidden"!==f&&"visible"!==f?"scroll"===e||"scroll"===f?!0:d.innerHeight()<c.scrollHeight||d.innerWidth()<c.scrollWidth:!1},c.escapeMarkup=function(a){var b={"\\":"&#92;","&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#47;"};return"string"!=typeof a?a:String(a).replace(/[&<>"'\/\\]/g,function(a){return b[a]})},c.appendMany=function(b,c){if("1.7"===a.fn.jquery.substr(0,3)){var d=a();a.map(c,function(a){d=d.add(a)}),c=d}b.append(c)},c}),b.define("select2/results",["jquery","./utils"],function(a,b){function c(a,b,d){this.$element=a,this.data=d,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('<ul class="select2-results__options" role="tree"></ul>');return this.options.get("multiple")&&b.attr("aria-multiselectable","true"),this.$results=b,b},c.prototype.clear=function(){this.$results.empty()},c.prototype.displayMessage=function(b){var c=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var d=a('<li role="treeitem" class="select2-results__option"></li>'),e=this.options.get("translations").get(b.message);d.append(c(e(b.args))),this.$results.append(d)},c.prototype.append=function(a){this.hideLoading();var b=[];if(null==a.results||0===a.results.length)return void(0===this.$results.children().length&&this.trigger("results:message",{message:"noResults"}));a.results=this.sort(a.results);for(var c=0;c<a.results.length;c++){var d=a.results[c],e=this.option(d);b.push(e)}this.$results.append(b)},c.prototype.position=function(a,b){var c=b.find(".select2-results");c.append(a)},c.prototype.sort=function(a){var b=this.options.get("sorter");return b(a)},c.prototype.setClasses=function(){var b=this;this.data.current(function(c){var d=a.map(c,function(a){return a.id.toString()}),e=b.$results.find(".select2-results__option[aria-selected]");e.each(function(){var b=a(this),c=a.data(this,"data"),e=""+c.id;null!=c.element&&c.element.selected||null==c.element&&a.inArray(e,d)>-1?b.attr("aria-selected","true"):b.attr("aria-selected","false")});var f=e.filter("[aria-selected=true]");f.length>0?f.first().trigger("mouseenter"):e.first().trigger("mouseenter")})},c.prototype.showLoading=function(a){this.hideLoading();var b=this.options.get("translations").get("searching"),c={disabled:!0,loading:!0,text:b(a)},d=this.option(c);d.className+=" loading-results",this.$results.prepend(d)},c.prototype.hideLoading=function(){this.$results.find(".loading-results").remove()},c.prototype.option=function(b){var c=document.createElement("li");c.className="select2-results__option";var d={role:"treeitem","aria-selected":"false"};b.disabled&&(delete d["aria-selected"],d["aria-disabled"]="true"),null==b.id&&delete d["aria-selected"],null!=b._resultId&&(c.id=b._resultId),b.title&&(c.title=b.title),b.children&&(d.role="group",d["aria-label"]=b.text,delete d["aria-selected"]);for(var e in d){var f=d[e];c.setAttribute(e,f)}if(b.children){var g=a(c),h=document.createElement("strong");h.className="select2-results__group";{a(h)}this.template(b,h);for(var i=[],j=0;j<b.children.length;j++){var k=b.children[j],l=this.option(k);i.push(l)}var m=a("<ul></ul>",{"class":"select2-results__options select2-results__options--nested"});m.append(i),g.append(h),g.append(m)}else this.template(b,c);return a.data(c,"data",b),c},c.prototype.bind=function(b){var c=this,d=b.id+"-results";this.$results.attr("id",d),b.on("results:all",function(a){c.clear(),c.append(a.data),b.isOpen()&&c.setClasses()}),b.on("results:append",function(a){c.append(a.data),b.isOpen()&&c.setClasses()}),b.on("query",function(a){c.showLoading(a)}),b.on("select",function(){b.isOpen()&&c.setClasses()}),b.on("unselect",function(){b.isOpen()&&c.setClasses()}),b.on("open",function(){c.$results.attr("aria-expanded","true"),c.$results.attr("aria-hidden","false"),c.setClasses(),c.ensureHighlightVisible()}),b.on("close",function(){c.$results.attr("aria-expanded","false"),c.$results.attr("aria-hidden","true"),c.$results.removeAttr("aria-activedescendant")}),b.on("results:toggle",function(){var a=c.getHighlightedResults();0!==a.length&&a.trigger("mouseup")}),b.on("results:select",function(){var a=c.getHighlightedResults();if(0!==a.length){var b=a.data("data");"true"==a.attr("aria-selected")?c.trigger("close"):c.trigger("select",{data:b})}}),b.on("results:previous",function(){var a=c.getHighlightedResults(),b=c.$results.find("[aria-selected]"),d=b.index(a);if(0!==d){var e=d-1;0===a.length&&(e=0);var f=b.eq(e);f.trigger("mouseenter");var g=c.$results.offset().top,h=f.offset().top,i=c.$results.scrollTop()+(h-g);0===e?c.$results.scrollTop(0):0>h-g&&c.$results.scrollTop(i)}}),b.on("results:next",function(){var a=c.getHighlightedResults(),b=c.$results.find("[aria-selected]"),d=b.index(a),e=d+1;if(!(e>=b.length)){var f=b.eq(e);f.trigger("mouseenter");var g=c.$results.offset().top+c.$results.outerHeight(!1),h=f.offset().top+f.outerHeight(!1),i=c.$results.scrollTop()+h-g;0===e?c.$results.scrollTop(0):h>g&&c.$results.scrollTop(i)}}),b.on("results:focus",function(a){a.element.addClass("select2-results__option--highlighted")}),b.on("results:message",function(a){c.displayMessage(a)}),a.fn.mousewheel&&this.$results.on("mousewheel",function(a){var b=c.$results.scrollTop(),d=c.$results.get(0).scrollHeight-c.$results.scrollTop()+a.deltaY,e=a.deltaY>0&&b-a.deltaY<=0,f=a.deltaY<0&&d<=c.$results.height();e?(c.$results.scrollTop(0),a.preventDefault(),a.stopPropagation()):f&&(c.$results.scrollTop(c.$results.get(0).scrollHeight-c.$results.height()),a.preventDefault(),a.stopPropagation())}),this.$results.on("mouseup",".select2-results__option[aria-selected]",function(b){var d=a(this),e=d.data("data");return"true"===d.attr("aria-selected")?void(c.options.get("multiple")?c.trigger("unselect",{originalEvent:b,data:e}):c.trigger("close")):void c.trigger("select",{originalEvent:b,data:e})}),this.$results.on("mouseenter",".select2-results__option[aria-selected]",function(){var b=a(this).data("data");c.getHighlightedResults().removeClass("select2-results__option--highlighted"),c.trigger("results:focus",{data:b,element:a(this)})})},c.prototype.getHighlightedResults=function(){var a=this.$results.find(".select2-results__option--highlighted");return a},c.prototype.destroy=function(){this.$results.remove()},c.prototype.ensureHighlightVisible=function(){var a=this.getHighlightedResults();if(0!==a.length){var b=this.$results.find("[aria-selected]"),c=b.index(a),d=this.$results.offset().top,e=a.offset().top,f=this.$results.scrollTop()+(e-d),g=e-d;f-=2*a.outerHeight(!1),2>=c?this.$results.scrollTop(0):(g>this.$results.outerHeight()||0>g)&&this.$results.scrollTop(f)}},c.prototype.template=function(b,c){var d=this.options.get("templateResult"),e=this.options.get("escapeMarkup"),f=d(b);null==f?c.style.display="none":"string"==typeof f?c.innerHTML=e(f):a(c).append(f)},c}),b.define("select2/keys",[],function(){var a={BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46};return a}),b.define("select2/selection/base",["jquery","../utils","../keys"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,b.Observable),d.prototype.render=function(){var b=a('<span class="select2-selection" role="combobox" aria-autocomplete="list" aria-haspopup="true" aria-expanded="false"></span>');return this._tabindex=0,null!=this.$element.data("old-tabindex")?this._tabindex=this.$element.data("old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),b.attr("title",this.$element.attr("title")),b.attr("tabindex",this._tabindex),this.$selection=b,b},d.prototype.bind=function(a){var b=this,d=(a.id+"-container",a.id+"-results");this.container=a,this.$selection.on("focus",function(a){b.trigger("focus",a)}),this.$selection.on("blur",function(a){b.trigger("blur",a)}),this.$selection.on("keydown",function(a){b.trigger("keypress",a),a.which===c.SPACE&&a.preventDefault()}),a.on("results:focus",function(a){b.$selection.attr("aria-activedescendant",a.data._resultId)}),a.on("selection:update",function(a){b.update(a.data)}),a.on("open",function(){b.$selection.attr("aria-expanded","true"),b.$selection.attr("aria-owns",d),b._attachCloseHandler(a)}),a.on("close",function(){b.$selection.attr("aria-expanded","false"),b.$selection.removeAttr("aria-activedescendant"),b.$selection.removeAttr("aria-owns"),b.$selection.focus(),b._detachCloseHandler(a)}),a.on("enable",function(){b.$selection.attr("tabindex",b._tabindex)}),a.on("disable",function(){b.$selection.attr("tabindex","-1")})},d.prototype._attachCloseHandler=function(b){a(document.body).on("mousedown.select2."+b.id,function(b){var c=a(b.target),d=c.closest(".select2"),e=a(".select2.select2-container--open");e.each(function(){var b=a(this);if(this!=d[0]){var c=b.data("element");c.select2("close")}})})},d.prototype._detachCloseHandler=function(b){a(document.body).off("mousedown.select2."+b.id)},d.prototype.position=function(a,b){var c=b.find(".selection");c.append(a)},d.prototype.destroy=function(){this._detachCloseHandler(this.container)},d.prototype.update=function(){throw new Error("The `update` method must be defined in child classes.")},d}),b.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(a,b,c){function d(){d.__super__.constructor.apply(this,arguments)}return c.Extend(d,b),d.prototype.render=function(){var a=d.__super__.render.call(this);return a.addClass("select2-selection--single"),a.html('<span class="select2-selection__rendered"></span><span class="select2-selection__arrow" role="presentation"><b role="presentation"></b></span>'),a},d.prototype.bind=function(a){var b=this;d.__super__.bind.apply(this,arguments);var c=a.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",c),this.$selection.attr("aria-labelledby",c),this.$selection.on("mousedown",function(a){1===a.which&&b.trigger("toggle",{originalEvent:a})}),this.$selection.on("focus",function(){}),this.$selection.on("blur",function(){}),a.on("selection:update",function(a){b.update(a.data)})},d.prototype.clear=function(){this.$selection.find(".select2-selection__rendered").empty()},d.prototype.display=function(a){var b=this.options.get("templateSelection"),c=this.options.get("escapeMarkup");return c(b(a))},d.prototype.selectionContainer=function(){return a("<span></span>")},d.prototype.update=function(a){if(0===a.length)return void this.clear();var b=a[0],c=this.display(b),d=this.$selection.find(".select2-selection__rendered");d.empty().append(c),d.prop("title",b.title||b.text)},d}),b.define("select2/selection/multiple",["jquery","./base","../utils"],function(a,b,c){function d(){d.__super__.constructor.apply(this,arguments)}return c.Extend(d,b),d.prototype.render=function(){var a=d.__super__.render.call(this);return a.addClass("select2-selection--multiple"),a.html('<ul class="select2-selection__rendered"></ul>'),a},d.prototype.bind=function(){var b=this;d.__super__.bind.apply(this,arguments),this.$selection.on("click",function(a){b.trigger("toggle",{originalEvent:a})}),this.$selection.on("click",".select2-selection__choice__remove",function(c){var d=a(this),e=d.parent(),f=e.data("data");b.trigger("unselect",{originalEvent:c,data:f})})},d.prototype.clear=function(){this.$selection.find(".select2-selection__rendered").empty()},d.prototype.display=function(a){var b=this.options.get("templateSelection"),c=this.options.get("escapeMarkup");return c(b(a))},d.prototype.selectionContainer=function(){var b=a('<li class="select2-selection__choice"><span class="select2-selection__choice__remove" role="presentation">&times;</span></li>');return b},d.prototype.update=function(a){if(this.clear(),0!==a.length){for(var b=[],d=0;d<a.length;d++){var e=a[d],f=this.display(e),g=this.selectionContainer();g.append(f),g.prop("title",e.title||e.text),g.data("data",e),b.push(g)}var h=this.$selection.find(".select2-selection__rendered");c.appendMany(h,b)}},d}),b.define("select2/selection/placeholder",["../utils"],function(){function a(a,b,c){this.placeholder=this.normalizePlaceholder(c.get("placeholder")),a.call(this,b,c)}return a.prototype.normalizePlaceholder=function(a,b){return"string"==typeof b&&(b={id:"",text:b}),b},a.prototype.createPlaceholder=function(a,b){var c=this.selectionContainer();return c.html(this.display(b)),c.addClass("select2-selection__placeholder").removeClass("select2-selection__choice"),c},a.prototype.update=function(a,b){var c=1==b.length&&b[0].id!=this.placeholder.id,d=b.length>1;if(d||c)return a.call(this,b);this.clear();var e=this.createPlaceholder(this.placeholder);this.$selection.find(".select2-selection__rendered").append(e)},a}),b.define("select2/selection/allowClear",["jquery","../keys"],function(a,b){function c(){}return c.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),null==this.placeholder&&this.options.get("debug")&&window.console&&console.error&&console.error("Select2: The `allowClear` option should be used in combination with the `placeholder` option."),this.$selection.on("mousedown",".select2-selection__clear",function(a){d._handleClear(a)}),b.on("keypress",function(a){d._handleKeyboardClear(a,b)})},c.prototype._handleClear=function(a,b){if(!this.options.get("disabled")){var c=this.$selection.find(".select2-selection__clear");if(0!==c.length){b.stopPropagation();for(var d=c.data("data"),e=0;e<d.length;e++){var f={data:d[e]};if(this.trigger("unselect",f),f.prevented)return}this.$element.val(this.placeholder.id).trigger("change"),this.trigger("toggle")}}},c.prototype._handleKeyboardClear=function(a,c,d){d.isOpen()||(c.which==b.DELETE||c.which==b.BACKSPACE)&&this._handleClear(c)},c.prototype.update=function(b,c){if(b.call(this,c),!(this.$selection.find(".select2-selection__placeholder").length>0||0===c.length)){var d=a('<span class="select2-selection__clear">&times;</span>');d.data("data",c),this.$selection.find(".select2-selection__rendered").prepend(d)}},c}),b.define("select2/selection/search",["jquery","../utils","../keys"],function(a,b,c){function d(a,b,c){a.call(this,b,c)}return d.prototype.render=function(b){var c=a('<li class="select2-search select2-search--inline"><input class="select2-search__field" type="search" tabindex="-1" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" role="textbox" /></li>');this.$searchContainer=c,this.$search=c.find("input");var d=b.call(this);return d},d.prototype.bind=function(a,b,d){var e=this;a.call(this,b,d),b.on("open",function(){e.$search.attr("tabindex",0),e.$search.focus()}),b.on("close",function(){e.$search.attr("tabindex",-1),e.$search.val(""),e.$search.focus()}),b.on("enable",function(){e.$search.prop("disabled",!1)}),b.on("disable",function(){e.$search.prop("disabled",!0)}),this.$selection.on("focusin",".select2-search--inline",function(a){e.trigger("focus",a)}),this.$selection.on("focusout",".select2-search--inline",function(a){e.trigger("blur",a)}),this.$selection.on("keydown",".select2-search--inline",function(a){a.stopPropagation(),e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented();var b=a.which;if(b===c.BACKSPACE&&""===e.$search.val()){var d=e.$searchContainer.prev(".select2-selection__choice");if(d.length>0){var f=d.data("data");e.searchRemoveChoice(f),a.preventDefault()}}}),this.$selection.on("input",".select2-search--inline",function(){e.$selection.off("keyup.search")}),this.$selection.on("keyup.search input",".select2-search--inline",function(a){e.handleSearch(a)})},d.prototype.createPlaceholder=function(a,b){this.$search.attr("placeholder",b.text)},d.prototype.update=function(a,b){this.$search.attr("placeholder",""),a.call(this,b),this.$selection.find(".select2-selection__rendered").append(this.$searchContainer),this.resizeSearch()},d.prototype.handleSearch=function(){if(this.resizeSearch(),!this._keyUpPrevented){var a=this.$search.val();this.trigger("query",{term:a})}this._keyUpPrevented=!1},d.prototype.searchRemoveChoice=function(a,b){this.trigger("unselect",{data:b}),this.trigger("open"),this.$search.val(b.text+" ")},d.prototype.resizeSearch=function(){this.$search.css("width","25px");var a="";if(""!==this.$search.attr("placeholder"))a=this.$selection.find(".select2-selection__rendered").innerWidth();else{var b=this.$search.val().length+1;a=.75*b+"em"}this.$search.css("width",a)},d}),b.define("select2/selection/eventRelay",["jquery"],function(a){function b(){}return b.prototype.bind=function(b,c,d){var e=this,f=["open","opening","close","closing","select","selecting","unselect","unselecting"],g=["opening","closing","selecting","unselecting"];b.call(this,c,d),c.on("*",function(b,c){if(-1!==a.inArray(b,f)){c=c||{};var d=a.Event("select2:"+b,{params:c});e.$element.trigger(d),-1!==a.inArray(b,g)&&(c.prevented=d.isDefaultPrevented())}})},b}),b.define("select2/translation",["jquery","require"],function(a,b){function c(a){this.dict=a||{}}return c.prototype.all=function(){return this.dict},c.prototype.get=function(a){return this.dict[a]},c.prototype.extend=function(b){this.dict=a.extend({},b.all(),this.dict)},c._cache={},c.loadPath=function(a){if(!(a in c._cache)){var d=b(a);c._cache[a]=d}return new c(c._cache[a])},c}),b.define("select2/diacritics",[],function(){var a={"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ω":"ω","ς":"σ"};return a}),b.define("select2/data/base",["../utils"],function(a){function b(){b.__super__.constructor.call(this)}return a.Extend(b,a.Observable),b.prototype.current=function(){throw new Error("The `current` method must be defined in child classes.")},b.prototype.query=function(){throw new Error("The `query` method must be defined in child classes.")},b.prototype.bind=function(){},b.prototype.destroy=function(){},b.prototype.generateResultId=function(b,c){var d=b.id+"-result-";return d+=a.generateChars(4),d+=null!=c.id?"-"+c.id.toString():"-"+a.generateChars(4)},b}),b.define("select2/data/select",["./base","../utils","jquery"],function(a,b,c){function d(a,b){this.$element=a,this.options=b,d.__super__.constructor.call(this)}return b.Extend(d,a),d.prototype.current=function(a){var b=[],d=this;this.$element.find(":selected").each(function(){var a=c(this),e=d.item(a);b.push(e)}),a(b)},d.prototype.select=function(a){var b=this;if(a.selected=!0,c(a.element).is("option"))return a.element.selected=!0,void this.$element.trigger("change");if(this.$element.prop("multiple"))this.current(function(d){var e=[];a=[a],a.push.apply(a,d);for(var f=0;f<a.length;f++){var g=a[f].id;-1===c.inArray(g,e)&&e.push(g)}b.$element.val(e),b.$element.trigger("change")});else{var d=a.id;this.$element.val(d),this.$element.trigger("change")}},d.prototype.unselect=function(a){var b=this;if(this.$element.prop("multiple"))return a.selected=!1,c(a.element).is("option")?(a.element.selected=!1,void this.$element.trigger("change")):void this.current(function(d){for(var e=[],f=0;f<d.length;f++){var g=d[f].id;g!==a.id&&-1===c.inArray(g,e)&&e.push(g)}b.$element.val(e),b.$element.trigger("change")})},d.prototype.bind=function(a){var b=this;this.container=a,a.on("select",function(a){b.select(a.data)}),a.on("unselect",function(a){b.unselect(a.data)})},d.prototype.destroy=function(){this.$element.find("*").each(function(){c.removeData(this,"data")})},d.prototype.query=function(a,b){var d=[],e=this,f=this.$element.children();f.each(function(){var b=c(this);if(b.is("option")||b.is("optgroup")){var f=e.item(b),g=e.matches(a,f);null!==g&&d.push(g)}}),b({results:d})},d.prototype.addOptions=function(a){b.appendMany(this.$element,a)},d.prototype.option=function(a){var b;a.children?(b=document.createElement("optgroup"),b.label=a.text):(b=document.createElement("option"),void 0!==b.textContent?b.textContent=a.text:b.innerText=a.text),a.id&&(b.value=a.id),a.disabled&&(b.disabled=!0),a.selected&&(b.selected=!0),a.title&&(b.title=a.title);var d=c(b),e=this._normalizeItem(a);return e.element=b,c.data(b,"data",e),d},d.prototype.item=function(a){var b={};
+if(b=c.data(a[0],"data"),null!=b)return b;if(a.is("option"))b={id:a.val(),text:a.text(),disabled:a.prop("disabled"),selected:a.prop("selected"),title:a.prop("title")};else if(a.is("optgroup")){b={text:a.prop("label"),children:[],title:a.prop("title")};for(var d=a.children("option"),e=[],f=0;f<d.length;f++){var g=c(d[f]),h=this.item(g);e.push(h)}b.children=e}return b=this._normalizeItem(b),b.element=a[0],c.data(a[0],"data",b),b},d.prototype._normalizeItem=function(a){c.isPlainObject(a)||(a={id:a,text:a}),a=c.extend({},{text:""},a);var b={selected:!1,disabled:!1};return null!=a.id&&(a.id=a.id.toString()),null!=a.text&&(a.text=a.text.toString()),null==a._resultId&&a.id&&null!=this.container&&(a._resultId=this.generateResultId(this.container,a)),c.extend({},b,a)},d.prototype.matches=function(a,b){var c=this.options.get("matcher");return c(a,b)},d}),b.define("select2/data/array",["./select","../utils","jquery"],function(a,b,c){function d(a,b){var c=b.get("data")||[];d.__super__.constructor.call(this,a,b),this.addOptions(this.convertToOptions(c))}return b.Extend(d,a),d.prototype.select=function(a){var b=this.$element.find("option").filter(function(b,c){return c.value==a.id.toString()});0===b.length&&(b=this.option(a),this.addOptions(b)),d.__super__.select.call(this,a)},d.prototype.convertToOptions=function(a){function d(a){return function(){return c(this).val()==a.id}}for(var e=this,f=this.$element.find("option"),g=f.map(function(){return e.item(c(this)).id}).get(),h=[],i=0;i<a.length;i++){var j=this._normalizeItem(a[i]);if(c.inArray(j.id,g)>=0){var k=f.filter(d(j)),l=this.item(k),m=(c.extend(!0,{},l,j),this.option(l));k.replaceWith(m)}else{var n=this.option(j);if(j.children){var o=this.convertToOptions(j.children);b.appendMany(n,o)}h.push(n)}}return h},d}),b.define("select2/data/ajax",["./array","../utils","jquery"],function(a,b,c){function d(b,c){this.ajaxOptions=this._applyDefaults(c.get("ajax")),null!=this.ajaxOptions.processResults&&(this.processResults=this.ajaxOptions.processResults),a.__super__.constructor.call(this,b,c)}return b.Extend(d,a),d.prototype._applyDefaults=function(a){var b={data:function(a){return{q:a.term}},transport:function(a,b,d){var e=c.ajax(a);return e.then(b),e.fail(d),e}};return c.extend({},b,a,!0)},d.prototype.processResults=function(a){return a},d.prototype.query=function(a,b){function d(){var d=f.transport(f,function(d){var f=e.processResults(d,a);e.options.get("debug")&&window.console&&console.error&&(f&&f.results&&c.isArray(f.results)||console.error("Select2: The AJAX results did not return an array in the `results` key of the response.")),b(f)},function(){});e._request=d}var e=this;null!=this._request&&(c.isFunction(this._request.abort)&&this._request.abort(),this._request=null);var f=c.extend({type:"GET"},this.ajaxOptions);"function"==typeof f.url&&(f.url=f.url(a)),"function"==typeof f.data&&(f.data=f.data(a)),this.ajaxOptions.delay&&""!==a.term?(this._queryTimeout&&window.clearTimeout(this._queryTimeout),this._queryTimeout=window.setTimeout(d,this.ajaxOptions.delay)):d()},d}),b.define("select2/data/tags",["jquery"],function(a){function b(b,c,d){var e=d.get("tags"),f=d.get("createTag");if(void 0!==f&&(this.createTag=f),b.call(this,c,d),a.isArray(e))for(var g=0;g<e.length;g++){var h=e[g],i=this._normalizeItem(h),j=this.option(i);this.$element.append(j)}}return b.prototype.query=function(a,b,c){function d(a,f){for(var g=a.results,h=0;h<g.length;h++){var i=g[h],j=null!=i.children&&!d({results:i.children},!0),k=i.text===b.term;if(k||j)return f?!1:(a.data=g,void c(a))}if(f)return!0;var l=e.createTag(b);if(null!=l){var m=e.option(l);m.attr("data-select2-tag",!0),e.addOptions([m]),e.insertTag(g,l)}a.results=g,c(a)}var e=this;return this._removeOldTags(),null==b.term||null!=b.page?void a.call(this,b,c):void a.call(this,b,d)},b.prototype.createTag=function(b,c){var d=a.trim(c.term);return""===d?null:{id:d,text:d}},b.prototype.insertTag=function(a,b,c){b.unshift(c)},b.prototype._removeOldTags=function(){var b=(this._lastTag,this.$element.find("option[data-select2-tag]"));b.each(function(){this.selected||a(this).remove()})},b}),b.define("select2/data/tokenizer",["jquery"],function(a){function b(a,b,c){var d=c.get("tokenizer");void 0!==d&&(this.tokenizer=d),a.call(this,b,c)}return b.prototype.bind=function(a,b,c){a.call(this,b,c),this.$search=b.dropdown.$search||b.selection.$search||c.find(".select2-search__field")},b.prototype.query=function(a,b,c){function d(a){e.select(a)}var e=this;b.term=b.term||"";var f=this.tokenizer(b,this.options,d);f.term!==b.term&&(this.$search.length&&(this.$search.val(f.term),this.$search.focus()),b.term=f.term),a.call(this,b,c)},b.prototype.tokenizer=function(b,c,d,e){for(var f=d.get("tokenSeparators")||[],g=c.term,h=0,i=this.createTag||function(a){return{id:a.term,text:a.term}};h<g.length;){var j=g[h];if(-1!==a.inArray(j,f)){var k=g.substr(0,h),l=a.extend({},c,{term:k}),m=i(l);e(m),g=g.substr(h+1)||"",h=0}else h++}return{term:g}},b}),b.define("select2/data/minimumInputLength",[],function(){function a(a,b,c){this.minimumInputLength=c.get("minimumInputLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){return b.term=b.term||"",b.term.length<this.minimumInputLength?void this.trigger("results:message",{message:"inputTooShort",args:{minimum:this.minimumInputLength,input:b.term,params:b}}):void a.call(this,b,c)},a}),b.define("select2/data/maximumInputLength",[],function(){function a(a,b,c){this.maximumInputLength=c.get("maximumInputLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){return b.term=b.term||"",this.maximumInputLength>0&&b.term.length>this.maximumInputLength?void this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:b.term,params:b}}):void a.call(this,b,c)},a}),b.define("select2/data/maximumSelectionLength",[],function(){function a(a,b,c){this.maximumSelectionLength=c.get("maximumSelectionLength"),a.call(this,b,c)}return a.prototype.query=function(a,b,c){var d=this;this.current(function(e){var f=null!=e?e.length:0;return d.maximumSelectionLength>0&&f>=d.maximumSelectionLength?void d.trigger("results:message",{message:"maximumSelected",args:{maximum:d.maximumSelectionLength}}):void a.call(d,b,c)})},a}),b.define("select2/dropdown",["jquery","./utils"],function(a,b){function c(a,b){this.$element=a,this.options=b,c.__super__.constructor.call(this)}return b.Extend(c,b.Observable),c.prototype.render=function(){var b=a('<span class="select2-dropdown"><span class="select2-results"></span></span>');return b.attr("dir",this.options.get("dir")),this.$dropdown=b,b},c.prototype.position=function(){},c.prototype.destroy=function(){this.$dropdown.remove()},c}),b.define("select2/dropdown/search",["jquery","../utils"],function(a){function b(){}return b.prototype.render=function(b){var c=b.call(this),d=a('<span class="select2-search select2-search--dropdown"><input class="select2-search__field" type="search" tabindex="-1" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" role="textbox" /></span>');return this.$searchContainer=d,this.$search=d.find("input"),c.prepend(d),c},b.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),this.$search.on("keydown",function(a){e.trigger("keypress",a),e._keyUpPrevented=a.isDefaultPrevented()}),this.$search.on("input",function(){a(this).off("keyup")}),this.$search.on("keyup input",function(a){e.handleSearch(a)}),c.on("open",function(){e.$search.attr("tabindex",0),e.$search.focus(),window.setTimeout(function(){e.$search.focus()},0)}),c.on("close",function(){e.$search.attr("tabindex",-1),e.$search.val("")}),c.on("results:all",function(a){if(null==a.query.term||""===a.query.term){var b=e.showSearch(a);b?e.$searchContainer.removeClass("select2-search--hide"):e.$searchContainer.addClass("select2-search--hide")}})},b.prototype.handleSearch=function(){if(!this._keyUpPrevented){var a=this.$search.val();this.trigger("query",{term:a})}this._keyUpPrevented=!1},b.prototype.showSearch=function(){return!0},b}),b.define("select2/dropdown/hidePlaceholder",[],function(){function a(a,b,c,d){this.placeholder=this.normalizePlaceholder(c.get("placeholder")),a.call(this,b,c,d)}return a.prototype.append=function(a,b){b.results=this.removePlaceholder(b.results),a.call(this,b)},a.prototype.normalizePlaceholder=function(a,b){return"string"==typeof b&&(b={id:"",text:b}),b},a.prototype.removePlaceholder=function(a,b){for(var c=b.slice(0),d=b.length-1;d>=0;d--){var e=b[d];this.placeholder.id===e.id&&c.splice(d,1)}return c},a}),b.define("select2/dropdown/infiniteScroll",["jquery"],function(a){function b(a,b,c,d){this.lastParams={},a.call(this,b,c,d),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return b.prototype.append=function(a,b){this.$loadingMore.remove(),this.loading=!1,a.call(this,b),this.showLoadingMore(b)&&this.$results.append(this.$loadingMore)},b.prototype.bind=function(b,c,d){var e=this;b.call(this,c,d),c.on("query",function(a){e.lastParams=a,e.loading=!0}),c.on("query:append",function(a){e.lastParams=a,e.loading=!0}),this.$results.on("scroll",function(){var b=a.contains(document.documentElement,e.$loadingMore[0]);if(!e.loading&&b){var c=e.$results.offset().top+e.$results.outerHeight(!1),d=e.$loadingMore.offset().top+e.$loadingMore.outerHeight(!1);c+50>=d&&e.loadMore()}})},b.prototype.loadMore=function(){this.loading=!0;var b=a.extend({},{page:1},this.lastParams);b.page++,this.trigger("query:append",b)},b.prototype.showLoadingMore=function(a,b){return b.pagination&&b.pagination.more},b.prototype.createLoadingMore=function(){var b=a('<li class="option load-more" role="treeitem"></li>'),c=this.options.get("translations").get("loadingMore");return b.html(c(this.lastParams)),b},b}),b.define("select2/dropdown/attachBody",["jquery","../utils"],function(a,b){function c(a,b,c){this.$dropdownParent=c.get("dropdownParent")||document.body,a.call(this,b,c)}return c.prototype.bind=function(a,b,c){var d=this,e=!1;a.call(this,b,c),b.on("open",function(){d._showDropdown(),d._attachPositioningHandler(b),e||(e=!0,b.on("results:all",function(){d._positionDropdown(),d._resizeDropdown()}),b.on("results:append",function(){d._positionDropdown(),d._resizeDropdown()}))}),b.on("close",function(){d._hideDropdown(),d._detachPositioningHandler(b)}),this.$dropdownContainer.on("mousedown",function(a){a.stopPropagation()})},c.prototype.position=function(a,b,c){b.attr("class",c.attr("class")),b.removeClass("select2"),b.addClass("select2-container--open"),b.css({position:"absolute",top:-999999}),this.$container=c},c.prototype.render=function(b){var c=a("<span></span>"),d=b.call(this);return c.append(d),this.$dropdownContainer=c,c},c.prototype._hideDropdown=function(){this.$dropdownContainer.detach()},c.prototype._attachPositioningHandler=function(c){var d=this,e="scroll.select2."+c.id,f="resize.select2."+c.id,g="orientationchange.select2."+c.id,h=this.$container.parents().filter(b.hasScroll);h.each(function(){a(this).data("select2-scroll-position",{x:a(this).scrollLeft(),y:a(this).scrollTop()})}),h.on(e,function(){var b=a(this).data("select2-scroll-position");a(this).scrollTop(b.y)}),a(window).on(e+" "+f+" "+g,function(){d._positionDropdown(),d._resizeDropdown()})},c.prototype._detachPositioningHandler=function(c){var d="scroll.select2."+c.id,e="resize.select2."+c.id,f="orientationchange.select2."+c.id,g=this.$container.parents().filter(b.hasScroll);g.off(d),a(window).off(d+" "+e+" "+f)},c.prototype._positionDropdown=function(){var b=a(window),c=this.$dropdown.hasClass("select2-dropdown--above"),d=this.$dropdown.hasClass("select2-dropdown--below"),e=null,f=(this.$container.position(),this.$container.offset());f.bottom=f.top+this.$container.outerHeight(!1);var g={height:this.$container.outerHeight(!1)};g.top=f.top,g.bottom=f.top+g.height;var h={height:this.$dropdown.outerHeight(!1)},i={top:b.scrollTop(),bottom:b.scrollTop()+b.height()},j=i.top<f.top-h.height,k=i.bottom>f.bottom+h.height,l={left:f.left,top:g.bottom};c||d||(e="below"),k||!j||c?!j&&k&&c&&(e="below"):e="above",("above"==e||c&&"below"!==e)&&(l.top=g.top-h.height),null!=e&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+e),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+e)),this.$dropdownContainer.css(l)},c.prototype._resizeDropdown=function(){this.$dropdownContainer.width();var a={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(a.minWidth=a.width,a.width="auto"),this.$dropdown.css(a)},c.prototype._showDropdown=function(){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},c}),b.define("select2/dropdown/minimumResultsForSearch",[],function(){function a(b){for(var c=0,d=0;d<b.length;d++){var e=b[d];e.children?c+=a(e.children):c++}return c}function b(a,b,c,d){this.minimumResultsForSearch=c.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),a.call(this,b,c,d)}return b.prototype.showSearch=function(b,c){return a(c.data.results)<this.minimumResultsForSearch?!1:b.call(this,c)},b}),b.define("select2/dropdown/selectOnClose",[],function(){function a(){}return a.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),b.on("close",function(){d._handleSelectOnClose()})},a.prototype._handleSelectOnClose=function(){var a=this.getHighlightedResults();a.length<1||this.trigger("select",{data:a.data("data")})},a}),b.define("select2/dropdown/closeOnSelect",[],function(){function a(){}return a.prototype.bind=function(a,b,c){var d=this;a.call(this,b,c),b.on("select",function(a){d._selectTriggered(a)}),b.on("unselect",function(a){d._selectTriggered(a)})},a.prototype._selectTriggered=function(a,b){var c=b.originalEvent;c&&c.ctrlKey||this.trigger("close")},a}),b.define("select2/i18n/en",[],function(){return{errorLoading:function(){return"The results could not be loaded."},inputTooLong:function(a){var b=a.input.length-a.maximum,c="Please delete "+b+" character";return 1!=b&&(c+="s"),c},inputTooShort:function(a){var b=a.minimum-a.input.length,c="Please enter "+b+" or more characters";return c},loadingMore:function(){return"Loading more results…"},maximumSelected:function(a){var b="You can only select "+a.maximum+" item";return 1!=a.maximum&&(b+="s"),b},noResults:function(){return"No results found"},searching:function(){return"Searching…"}}}),b.define("select2/defaults",["jquery","require","./results","./selection/single","./selection/multiple","./selection/placeholder","./selection/allowClear","./selection/search","./selection/eventRelay","./utils","./translation","./diacritics","./data/select","./data/array","./data/ajax","./data/tags","./data/tokenizer","./data/minimumInputLength","./data/maximumInputLength","./data/maximumSelectionLength","./dropdown","./dropdown/search","./dropdown/hidePlaceholder","./dropdown/infiniteScroll","./dropdown/attachBody","./dropdown/minimumResultsForSearch","./dropdown/selectOnClose","./dropdown/closeOnSelect","./i18n/en"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C){function D(){this.reset()}D.prototype.apply=function(l){if(l=a.extend({},this.defaults,l),null==l.dataAdapter){if(l.dataAdapter=null!=l.ajax?o:null!=l.data?n:m,l.minimumInputLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,r)),l.maximumInputLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,s)),l.maximumSelectionLength>0&&(l.dataAdapter=j.Decorate(l.dataAdapter,t)),l.tags&&(l.dataAdapter=j.Decorate(l.dataAdapter,p)),(null!=l.tokenSeparators||null!=l.tokenizer)&&(l.dataAdapter=j.Decorate(l.dataAdapter,q)),null!=l.query){var C=b(l.amdBase+"compat/query");l.dataAdapter=j.Decorate(l.dataAdapter,C)}if(null!=l.initSelection){var D=b(l.amdBase+"compat/initSelection");l.dataAdapter=j.Decorate(l.dataAdapter,D)}}if(null==l.resultsAdapter&&(l.resultsAdapter=c,null!=l.ajax&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,x)),null!=l.placeholder&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,w)),l.selectOnClose&&(l.resultsAdapter=j.Decorate(l.resultsAdapter,A))),null==l.dropdownAdapter){if(l.multiple)l.dropdownAdapter=u;else{var E=j.Decorate(u,v);l.dropdownAdapter=E}if(0!==l.minimumResultsForSearch&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,z)),l.closeOnSelect&&(l.dropdownAdapter=j.Decorate(l.dropdownAdapter,B)),null!=l.dropdownCssClass||null!=l.dropdownCss||null!=l.adaptDropdownCssClass){var F=b(l.amdBase+"compat/dropdownCss");l.dropdownAdapter=j.Decorate(l.dropdownAdapter,F)}l.dropdownAdapter=j.Decorate(l.dropdownAdapter,y)}if(null==l.selectionAdapter){if(l.selectionAdapter=l.multiple?e:d,null!=l.placeholder&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,f)),l.allowClear&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,g)),l.multiple&&(l.selectionAdapter=j.Decorate(l.selectionAdapter,h)),null!=l.containerCssClass||null!=l.containerCss||null!=l.adaptContainerCssClass){var G=b(l.amdBase+"compat/containerCss");l.selectionAdapter=j.Decorate(l.selectionAdapter,G)}l.selectionAdapter=j.Decorate(l.selectionAdapter,i)}if("string"==typeof l.language)if(l.language.indexOf("-")>0){var H=l.language.split("-"),I=H[0];l.language=[l.language,I]}else l.language=[l.language];if(a.isArray(l.language)){var J=new k;l.language.push("en");for(var K=l.language,L=0;L<K.length;L++){var M=K[L],N={};try{N=k.loadPath(M)}catch(O){try{M=this.defaults.amdLanguageBase+M,N=k.loadPath(M)}catch(P){l.debug&&window.console&&console.warn&&console.warn('Select2: The language file for "'+M+'" could not be automatically loaded. A fallback will be used instead.');continue}}J.extend(N)}l.translations=J}else{var Q=k.loadPath(this.defaults.amdLanguageBase+"en"),R=new k(l.language);R.extend(Q),l.translations=R}return l},D.prototype.reset=function(){function b(a){function b(a){return l[a]||a}return a.replace(/[^\u0000-\u007E]/g,b)}function c(d,e){if(""===a.trim(d.term))return e;if(e.children&&e.children.length>0){for(var f=a.extend(!0,{},e),g=e.children.length-1;g>=0;g--){var h=e.children[g],i=c(d,h);null==i&&f.children.splice(g,1)}return f.children.length>0?f:c(d,f)}var j=b(e.text).toUpperCase(),k=b(d.term).toUpperCase();return j.indexOf(k)>-1?e:null}this.defaults={amdBase:"./",amdLanguageBase:"./i18n/",closeOnSelect:!0,debug:!1,dropdownAutoWidth:!1,escapeMarkup:j.escapeMarkup,language:C,matcher:c,minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:!1,sorter:function(a){return a},templateResult:function(a){return a.text},templateSelection:function(a){return a.text},theme:"default",width:"resolve"}},D.prototype.set=function(b,c){var d=a.camelCase(b),e={};e[d]=c;var f=j._convertData(e);a.extend(this.defaults,f)};var E=new D;return E}),b.define("select2/options",["require","jquery","./defaults","./utils"],function(a,b,c,d){function e(b,e){if(this.options=b,null!=e&&this.fromElement(e),this.options=c.apply(this.options),e&&e.is("input")){var f=a(this.get("amdBase")+"compat/inputData");this.options.dataAdapter=d.Decorate(this.options.dataAdapter,f)}}return e.prototype.fromElement=function(a){var c=["select2"];null==this.options.multiple&&(this.options.multiple=a.prop("multiple")),null==this.options.disabled&&(this.options.disabled=a.prop("disabled")),null==this.options.language&&(a.prop("lang")?this.options.language=a.prop("lang").toLowerCase():a.closest("[lang]").prop("lang")&&(this.options.language=a.closest("[lang]").prop("lang"))),null==this.options.dir&&(this.options.dir=a.prop("dir")?a.prop("dir"):a.closest("[dir]").prop("dir")?a.closest("[dir]").prop("dir"):"ltr"),a.prop("disabled",this.options.disabled),a.prop("multiple",this.options.multiple),a.data("select2Tags")&&(this.options.debug&&window.console&&console.warn&&console.warn('Select2: The `data-select2-tags` attribute has been changed to use the `data-data` and `data-tags="true"` attributes and will be removed in future versions of Select2.'),a.data("data",a.data("select2Tags")),a.data("tags",!0)),a.data("ajaxUrl")&&(this.options.debug&&window.console&&console.warn&&console.warn("Select2: The `data-ajax-url` attribute has been changed to `data-ajax--url` and support for the old attribute will be removed in future versions of Select2."),a.attr("ajax--url",a.data("ajaxUrl")),a.data("ajax--url",a.data("ajaxUrl")));var e={};e=b.fn.jquery&&"1."==b.fn.jquery.substr(0,2)&&a[0].dataset?b.extend(!0,{},a[0].dataset,a.data()):a.data();var f=b.extend(!0,{},e);f=d._convertData(f);for(var g in f)b.inArray(g,c)>-1||(b.isPlainObject(this.options[g])?b.extend(this.options[g],f[g]):this.options[g]=f[g]);return this},e.prototype.get=function(a){return this.options[a]},e.prototype.set=function(a,b){this.options[a]=b},e}),b.define("select2/core",["jquery","./options","./utils","./keys"],function(a,b,c,d){var e=function(a,c){null!=a.data("select2")&&a.data("select2").destroy(),this.$element=a,this.id=this._generateId(a),c=c||{},this.options=new b(c,a),e.__super__.constructor.call(this);var d=a.attr("tabindex")||0;a.data("old-tabindex",d),a.attr("tabindex","-1");var f=this.options.get("dataAdapter");this.dataAdapter=new f(a,this.options);var g=this.render();this._placeContainer(g);var h=this.options.get("selectionAdapter");this.selection=new h(a,this.options),this.$selection=this.selection.render(),this.selection.position(this.$selection,g);var i=this.options.get("dropdownAdapter");this.dropdown=new i(a,this.options),this.$dropdown=this.dropdown.render(),this.dropdown.position(this.$dropdown,g);var j=this.options.get("resultsAdapter");this.results=new j(a,this.options,this.dataAdapter),this.$results=this.results.render(),this.results.position(this.$results,this.$dropdown);var k=this;this._bindAdapters(),this._registerDomEvents(),this._registerDataEvents(),this._registerSelectionEvents(),this._registerDropdownEvents(),this._registerResultsEvents(),this._registerEvents(),this.dataAdapter.current(function(a){k.trigger("selection:update",{data:a})}),a.addClass("select2-hidden-accessible"),a.attr("aria-hidden","true"),this._syncAttributes(),a.data("select2",this)};return c.Extend(e,c.Observable),e.prototype._generateId=function(a){var b="";return b=null!=a.attr("id")?a.attr("id"):null!=a.attr("name")?a.attr("name")+"-"+c.generateChars(2):c.generateChars(4),b="select2-"+b},e.prototype._placeContainer=function(a){a.insertAfter(this.$element);var b=this._resolveWidth(this.$element,this.options.get("width"));null!=b&&a.css("width",b)},e.prototype._resolveWidth=function(a,b){var c=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if("resolve"==b){var d=this._resolveWidth(a,"style");return null!=d?d:this._resolveWidth(a,"element")}if("element"==b){var e=a.outerWidth(!1);return 0>=e?"auto":e+"px"}if("style"==b){var f=a.attr("style");if("string"!=typeof f)return null;for(var g=f.split(";"),h=0,i=g.length;i>h;h+=1){var j=g[h].replace(/\s/g,""),k=j.match(c);if(null!==k&&k.length>=1)return k[1]}return null}return b},e.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container),this.selection.bind(this,this.$container),this.dropdown.bind(this,this.$container),this.results.bind(this,this.$container)},e.prototype._registerDomEvents=function(){var b=this;this.$element.on("change.select2",function(){b.dataAdapter.current(function(a){b.trigger("selection:update",{data:a})})}),this._sync=c.bind(this._syncAttributes,this),this.$element[0].attachEvent&&this.$element[0].attachEvent("onpropertychange",this._sync);var d=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;null!=d?(this._observer=new d(function(c){a.each(c,b._sync)}),this._observer.observe(this.$element[0],{attributes:!0,subtree:!1})):this.$element[0].addEventListener&&this.$element[0].addEventListener("DOMAttrModified",b._sync,!1)},e.prototype._registerDataEvents=function(){var a=this;this.dataAdapter.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerSelectionEvents=function(){var b=this,c=["toggle"];this.selection.on("toggle",function(){b.toggleDropdown()}),this.selection.on("*",function(d,e){-1===a.inArray(d,c)&&b.trigger(d,e)})},e.prototype._registerDropdownEvents=function(){var a=this;this.dropdown.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerResultsEvents=function(){var a=this;this.results.on("*",function(b,c){a.trigger(b,c)})},e.prototype._registerEvents=function(){var a=this;this.on("open",function(){a.$container.addClass("select2-container--open")}),this.on("close",function(){a.$container.removeClass("select2-container--open")}),this.on("enable",function(){a.$container.removeClass("select2-container--disabled")}),this.on("disable",function(){a.$container.addClass("select2-container--disabled")}),this.on("focus",function(){a.$container.addClass("select2-container--focus")}),this.on("blur",function(){a.$container.removeClass("select2-container--focus")}),this.on("query",function(b){a.isOpen()||a.trigger("open"),this.dataAdapter.query(b,function(c){a.trigger("results:all",{data:c,query:b})})}),this.on("query:append",function(b){this.dataAdapter.query(b,function(c){a.trigger("results:append",{data:c,query:b})})}),this.on("keypress",function(b){var c=b.which;a.isOpen()?c===d.ENTER?(a.trigger("results:select"),b.preventDefault()):c===d.SPACE&&b.ctrlKey?(a.trigger("results:toggle"),b.preventDefault()):c===d.UP?(a.trigger("results:previous"),b.preventDefault()):c===d.DOWN?(a.trigger("results:next"),b.preventDefault()):(c===d.ESC||c===d.TAB)&&(a.close(),b.preventDefault()):(c===d.ENTER||c===d.SPACE||(c===d.DOWN||c===d.UP)&&b.altKey)&&(a.open(),b.preventDefault())})},e.prototype._syncAttributes=function(){this.options.set("disabled",this.$element.prop("disabled")),this.options.get("disabled")?(this.isOpen()&&this.close(),this.trigger("disable")):this.trigger("enable")},e.prototype.trigger=function(a,b){var c=e.__super__.trigger,d={open:"opening",close:"closing",select:"selecting",unselect:"unselecting"};if(a in d){var f=d[a],g={prevented:!1,name:a,args:b};if(c.call(this,f,g),g.prevented)return void(b.prevented=!0)}c.call(this,a,b)},e.prototype.toggleDropdown=function(){this.options.get("disabled")||(this.isOpen()?this.close():this.open())},e.prototype.open=function(){this.isOpen()||(this.trigger("query",{}),this.trigger("open"))},e.prototype.close=function(){this.isOpen()&&this.trigger("close")},e.prototype.isOpen=function(){return this.$container.hasClass("select2-container--open")},e.prototype.enable=function(a){this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("enable")` method has been deprecated and will be removed in later Select2 versions. Use $element.prop("disabled") instead.'),(null==a||0===a.length)&&(a=[!0]);var b=!a[0];this.$element.prop("disabled",b)},e.prototype.data=function(){this.options.get("debug")&&arguments.length>0&&window.console&&console.warn&&console.warn('Select2: Data can no longer be set using `select2("data")`. You should consider setting the value instead using `$element.val()`.');var a=[];return this.dataAdapter.current(function(b){a=b}),a},e.prototype.val=function(b){if(this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("val")` method has been deprecated and will be removed in later Select2 versions. Use $element.val() instead.'),null==b||0===b.length)return this.$element.val();var c=b[0];a.isArray(c)&&(c=a.map(c,function(a){return a.toString()})),this.$element.val(c).trigger("change")},e.prototype.destroy=function(){this.$container.remove(),this.$element[0].detachEvent&&this.$element[0].detachEvent("onpropertychange",this._sync),null!=this._observer?(this._observer.disconnect(),this._observer=null):this.$element[0].removeEventListener&&this.$element[0].removeEventListener("DOMAttrModified",this._sync,!1),this._sync=null,this.$element.off(".select2"),this.$element.attr("tabindex",this.$element.data("old-tabindex")),this.$element.removeClass("select2-hidden-accessible"),this.$element.attr("aria-hidden","false"),this.$element.removeData("select2"),this.dataAdapter.destroy(),this.selection.destroy(),this.dropdown.destroy(),this.results.destroy(),this.dataAdapter=null,this.selection=null,this.dropdown=null,this.results=null},e.prototype.render=function(){var b=a('<span class="select2 select2-container"><span class="selection"></span><span class="dropdown-wrapper" aria-hidden="true"></span></span>');return b.attr("dir",this.options.get("dir")),this.$container=b,this.$container.addClass("select2-container--"+this.options.get("theme")),b.data("element",this.$element),b},e}),b.define("jquery.select2",["jquery","require","./select2/core","./select2/defaults"],function(a,b,c,d){if(b("jquery.mousewheel"),null==a.fn.select2){var e=["open","close","destroy"];a.fn.select2=function(b){if(b=b||{},"object"==typeof b)return this.each(function(){{var d=a.extend({},b,!0);new c(a(this),d)}}),this;if("string"==typeof b){var d=this.data("select2");null==d&&window.console&&console.error&&console.error("The select2('"+b+"') method was called on an element that is not using Select2.");var f=Array.prototype.slice.call(arguments,1),g=d[b](f);return a.inArray(b,e)>-1?this:g}throw new Error("Invalid arguments for Select2: "+b)}}return null==a.fn.select2.defaults&&(a.fn.select2.defaults=d),c}),b.define("jquery.mousewheel",["jquery"],function(a){return a}),{define:b.define,require:b.require}}(),c=b.require("jquery.select2");return a.fn.select2.amd=b,c});
\ No newline at end of file
diff --git a/login.php b/login.php
index a90376ec3ee40072ee0290f4bfaa51b44f95b170..4c7f3b6c12bd668c205d686803b5058d0847fca5 100644
--- a/login.php
+++ b/login.php
@@ -85,9 +85,12 @@ elseif ($_POST && isset($_POST['lticket'])) {
         if (!$cfg->isClientEmailVerificationRequired())
             Http::redirect('tickets.php');
 
+        // This will succeed as it is checked in the authentication backend
+        $ticket = Ticket::lookupByNumber($_POST['lticket']);
+
         // We're using authentication backend so we can guard aganist brute
         // force attempts (which doesn't buy much since the link is emailed)
-        $user->sendAccessLink();
+        $ticket->sendAccessLink($user);
         $msg = sprintf(__("%s - access link sent to your email!"),
             Format::htmlchars($user->getName()->getFirst()));
         $_POST = null;
@@ -124,7 +127,8 @@ elseif ($user = UserAuthenticationBackend::processSignOn($errors, false)) {
         }
     }
     elseif ($user instanceof AuthenticatedUser) {
-        Http::redirect('tickets.php');
+        Http::redirect($_SESSION['_client']['auth']['dest']
+                ?: 'tickets.php');
     }
 }
 
@@ -132,6 +136,10 @@ if (!$nav) {
     $nav = new UserNav();
     $nav->setActiveNav('status');
 }
+
+// Browsers shouldn't suggest saving that username/password
+Http::response(422);
+
 require CLIENTINC_DIR.'header.inc.php';
 require CLIENTINC_DIR.$inc;
 require CLIENTINC_DIR.'footer.inc.php';
diff --git a/manage.php b/manage.php
new file mode 100755
index 0000000000000000000000000000000000000000..25e1fd80e256379c9d585892866915d5d3a6e231
--- /dev/null
+++ b/manage.php
@@ -0,0 +1,83 @@
+#!/usr/bin/env php
+<?php
+/*********************************************************************
+    manage.php
+
+    CLI (command line interface) for osTicket management. Use
+
+    php manage.php --help
+
+    for detailed and updated getting started information.
+
+    Jared Hancock <jared@osticket.com>
+    Copyright (c)  2006-2015 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:
+**********************************************************************/
+if (PHP_SAPI != "cli")
+    die("Management only supported from command-line\n");
+
+require_once 'bootstrap.php';
+require_once CLI_DIR . 'cli.inc.php';
+
+if (!function_exists('noop')) { function noop() {} }
+session_set_save_handler('noop','noop','noop','noop','noop','noop');
+
+class Manager extends Module {
+    var $prologue =
+        "Manage one or more osTicket installations";
+
+    var $arguments = array(
+        'action' => "Action to be managed"
+    );
+
+    var $usage = '$script action [options] [arguments]';
+
+    var $autohelp = false;
+
+    function showHelp() {
+        foreach (glob(CLI_DIR.'modules/*.php') as $script)
+            include_once $script;
+
+        global $registered_modules;
+        $this->epilog =
+            "Currently available modules follow. Use 'manage.php <module>
+            --help' for usage regarding each respective module:";
+
+        parent::showHelp();
+
+        echo "\n";
+        ksort($registered_modules);
+        $width = max(array_map('strlen', array_keys($registered_modules)));
+        foreach ($registered_modules as $name=>$mod)
+            echo str_pad($name, $width + 2) . $mod->prologue . "\n";
+    }
+
+    function run($args, $options) {
+        if ($options['help'] && !$args['action'])
+            $this->showHelp();
+
+        else {
+            $action = $args['action'];
+
+            global $argv;
+            foreach ($argv as $idx=>$val)
+                if ($val == $action)
+                    unset($argv[$idx]);
+
+            require_once CLI_DIR . "modules/{$args['action']}.php";
+            if (($module = Module::getInstance($action)))
+                return $module->_run($args['action']);
+
+            $this->stderr->write("Unknown action given\n");
+            $this->showHelp();
+        }
+    }
+}
+
+$manager = new Manager();
+$manager->_run(basename(__file__), false);
diff --git a/open.php b/open.php
index 2311bc2bc57759bbcd812cdb75ace610fc504439..a081c29c9bb47c4417432a1567e65d5d50fd8fd9 100644
--- a/open.php
+++ b/open.php
@@ -57,7 +57,10 @@ if ($_POST) {
 //page
 $nav->setActiveNav('new');
 if ($cfg->isClientLoginRequired()) {
-    if (!$thisclient) {
+    if ($cfg->getClientRegistrationMode() == 'disabled') {
+        Http::redirect('view.php');
+    }
+    elseif (!$thisclient) {
         require_once 'secure.inc.php';
     }
     elseif ($thisclient->isGuest()) {
@@ -67,13 +70,18 @@ if ($cfg->isClientLoginRequired()) {
 }
 
 require(CLIENTINC_DIR.'header.inc.php');
-if($ticket
-        && (
-            (($topic = $ticket->getTopic()) && ($page = $topic->getPage()))
-            || ($page = $cfg->getThankYouPage())
-        )) {
+if ($ticket
+    && (
+        (($topic = $ticket->getTopic()) && ($page = $topic->getPage()))
+        || ($page = $cfg->getThankYouPage())
+    )
+) {
     // Thank the user and promise speedy resolution!
-    echo Format::viewableImages($ticket->replaceVars($page->getBody()));
+    echo Format::viewableImages(
+        $ticket->replaceVars(
+            $page->getLocalBody()
+        )
+    );
 }
 else {
     require(CLIENTINC_DIR.'open.inc.php');
diff --git a/pages/index.php b/pages/index.php
index ab50a1c586d5793301fdcb0246842c31eba95ec2..e9de487346c43d950020397bdc2df70250d8e526 100644
--- a/pages/index.php
+++ b/pages/index.php
@@ -28,27 +28,35 @@ $slug = Format::slugify($ost->get_path_info());
 $first_word = explode('-', $slug);
 $first_word = $first_word[0];
 
-$sql = 'SELECT id, name FROM '.PAGE_TABLE
-    .' WHERE name LIKE '.db_input("$first_word%");
-$page_id = null;
-
-$res = db_query($sql);
-while (list($id, $name) = db_fetch_row($res)) {
-    if (Format::slugify($name) == $slug) {
-        $page_id = $id;
+$pages = Page::objects()->filter(array(
+    'name__like' => "$first_word%"
+));
+
+$selected_page = null;
+foreach ($pages as $P) {
+    if (Format::slugify($P->name) == $slug) {
+        $selected_page = $P;
         break;
     }
 }
 
-if (!$page_id || !($page = Page::lookup($page_id)))
+if (!$selected_page)
     Http::response(404, __('Page Not Found'));
 
-if (!$page->isActive() || $page->getType() != 'other')
+if (!$selected_page->isActive() || $selected_page->getType() != 'other')
     Http::response(404, __('Page Not Found'));
 
 require(CLIENTINC_DIR.'header.inc.php');
 
-print $page->getBodyWithImages();
+$BUTTONS = false;
+include CLIENTINC_DIR.'templates/sidebar.tmpl.php';
+?>
+<div class="main-content">
+<?php
+print $selected_page->getBodyWithImages();
+?>
+</div>
 
+<?php
 require(CLIENTINC_DIR.'footer.inc.php');
 ?>
diff --git a/pwreset.php b/pwreset.php
index e37e4d264bab3aa20484be28a65613d63e6dcd5d..10a7db2fc36cc073391ddbf238815edf06b224ce 100644
--- a/pwreset.php
+++ b/pwreset.php
@@ -46,11 +46,14 @@ elseif ($_GET['token']) {
     $inc = 'pwreset.login.php';
     $_config = new Config('pwreset');
     if (($id = $_config->get($_GET['token']))
-            && ($acct = ClientAccount::lookup(array('user_id'=>$id)))) {
+            && ($acct = ClientAccount::lookup(array('user_id'=>substr($id,1))))) {
         if (!$acct->isConfirmed()) {
             $inc = 'register.confirmed.inc.php';
             $acct->confirm();
-            // TODO: Log the user in
+            // FIXME: The account has to be uncached in order for the lookup
+            // in the ::processSignOn to detect the confirmation
+            ModelInstanceManager::uncache($acct);
+            // Log the user in
             if ($client = UserAuthenticationBackend::processSignOn($errors)) {
                 if ($acct->hasPassword() && !$acct->get('backend')) {
                     $acct->cancelResetTokens();
diff --git a/scp/admin.inc.php b/scp/admin.inc.php
index 434589acaa0584d7164fa32afc48c6848adea88c..9f701eb0bbeaa7d9e53bb227ba09c1061077cd4d 100644
--- a/scp/admin.inc.php
+++ b/scp/admin.inc.php
@@ -13,7 +13,7 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
-require('staff.inc.php');
+require_once 'staff.inc.php';
 //Make sure config is loaded and the staff is set and of admin type
 if(!$ost or !$thisstaff or !$thisstaff->isAdmin()){
     header('Location: index.php');
diff --git a/scp/ajax.php b/scp/ajax.php
index 6134731257943043d726590e305273d9daf46dc8..f01cf70b4bb00f0b4b4a6f17bab1c0d5d92e5a2c 100644
--- a/scp/ajax.php
+++ b/scp/ajax.php
@@ -37,10 +37,12 @@ $dispatcher = patterns('',
     url('^/kb/', patterns('ajax.kbase.php:KbaseAjaxAPI',
         # Send ticket-id as a query arg => canned-response/33?ticket=83
         url_get('^canned-response/(?P<id>\d+).(?P<format>json|txt)', 'cannedResp'),
-        url_get('^faq/(?P<id>\d+)', 'faq')
+        url('^faq/(?P<id>\d+)/access', 'manageFaqAccess'),
+        url_get('^faq/(?P<id>\d+)$', 'faq')
     )),
     url('^/content/', patterns('ajax.content.php:ContentAjaxAPI',
         url_get('^log/(?P<id>\d+)', 'log'),
+        url_get('^context$', 'context'),
         url_get('^ticket_variables', 'ticket_variables'),
         url_get('^signature/(?P<type>\w+)(?:/(?P<id>\d+))?$', 'getSignature'),
         url_get('^(?P<id>\d+)/(?:(?P<lang>\w+)/)?manage$', 'manageContent'),
@@ -49,7 +51,8 @@ $dispatcher = patterns('',
     )),
     url('^/config/', patterns('ajax.config.php:ConfigAjaxAPI',
         url_get('^scp', 'scp'),
-        url_get('^links', 'templateLinks')
+        url_get('^links', 'templateLinks'),
+        url_get('^date-format', 'dateFormat')
     )),
     url('^/form/', patterns('ajax.forms.php:DynamicFormsAjaxAPI',
         url_get('^help-topic/(?P<id>\d+)$', 'getFormsForHelpTopic'),
@@ -57,11 +60,23 @@ $dispatcher = patterns('',
         url_post('^field-config/(?P<id>\d+)$', 'saveFieldConfiguration'),
         url_delete('^answer/(?P<entry>\d+)/(?P<field>\d+)$', 'deleteAnswer'),
         url_post('^upload/(\d+)?$', 'upload'),
-        url_post('^upload/(\w+)?$', 'attach')
+        url_post('^upload/(\w+)?$', 'attach'),
+        url_get('^(?P<id>\d+)/fields/view$', 'getAllFields')
+    )),
+    url('^/filter/', patterns('ajax.filter.php:FilterAjaxAPI',
+        url_get('^action/(?P<type>\w+)/config$', 'getFilterActionForm')
     )),
     url('^/list/', patterns('ajax.forms.php:DynamicFormsAjaxAPI',
-        url_get('^(?P<list>\w+)/item/(?P<id>\d+)/properties$', 'getListItemProperties'),
-        url_post('^(?P<list>\w+)/item/(?P<id>\d+)/properties$', 'saveListItemProperties')
+        url_get('^(?P<list>\w+)/items$', 'getListItems'),
+        url_get('^(?P<list>\w+)/items/search$', 'searchListItems'),
+        url_get('^(?P<list>\w+)/item/(?P<id>\d+)/update$', 'getListItem'),
+        url_post('^(?P<list>\w+)/item/(?P<id>\d+)/update$', 'saveListItem'),
+        url('^(?P<list>\w+)/item/add$', 'addListItem'),
+        url('^(?P<list>\w+)/import$', 'importListItems'),
+        url('^(?P<list>\w+)/manage$', 'massManageListItems'),
+        url_post('^(?P<list>\w+)/delete$', 'deleteItems'),
+        url_post('^(?P<list>\w+)/disable$', 'disableItems'),
+        url_post('^(?P<list>\w+)/enable$', 'undisableItems')
     )),
     url('^/report/overview/', patterns('ajax.reports.php:OverviewReportAjaxAPI',
         # Send
@@ -122,39 +137,78 @@ $dispatcher = patterns('',
         url_get('^/(?P<id>\d+)/forms/manage$', 'manageForms'),
         url_post('^/(?P<id>\d+)/forms/manage$', 'updateForms')
     )),
+    url('^/lock/', patterns('ajax.tickets.php:TicketsAjaxAPI',
+        url_post('^ticket/(?P<tid>\d+)$', 'acquireLock'),
+        url_post('^(?P<id>\d+)/ticket/(?P<tid>\d+)/renew', 'renewLock'),
+        url_post('^(?P<id>\d+)/release', 'releaseLock')
+    )),
     url('^/tickets/', patterns('ajax.tickets.php:TicketsAjaxAPI',
         url_get('^(?P<tid>\d+)/change-user$', 'changeUserForm'),
         url_post('^(?P<tid>\d+)/change-user$', 'changeUser'),
         url_get('^(?P<tid>\d+)/user$', 'viewUser'),
         url_post('^(?P<tid>\d+)/user$', 'updateUser'),
         url_get('^(?P<tid>\d+)/preview', 'previewTicket'),
-        url_post('^(?P<tid>\d+)/lock$', 'acquireLock'),
-        url_post('^(?P<tid>\d+)/lock/(?P<id>\d+)/renew', 'renewLock'),
-        url_post('^(?P<tid>\d+)/lock/(?P<id>\d+)/release', 'releaseLock'),
-        url_get('^(?P<tid>\d+)/collaborators/preview$', 'previewCollaborators'),
-        url_get('^(?P<tid>\d+)/collaborators$', 'showCollaborators'),
-        url_post('^(?P<tid>\d+)/collaborators$', 'updateCollaborators'),
-        url_get('^(?P<tid>\d+)/add-collaborator/(?P<uid>\d+)$', 'addCollaborator'),
-        url_get('^(?P<tid>\d+)/add-collaborator/auth:(?P<bk>\w+):(?P<id>.+)$', 'addRemoteCollaborator'),
-        url('^(?P<tid>\d+)/add-collaborator$', 'addCollaborator'),
         url_get('^(?P<tid>\d+)/forms/manage$', 'manageForms'),
         url_post('^(?P<tid>\d+)/forms/manage$', 'updateForms'),
         url_get('^(?P<tid>\d+)/canned-resp/(?P<cid>\w+).(?P<format>json|txt)', 'cannedResponse'),
         url_get('^(?P<tid>\d+)/status/(?P<status>\w+)(?:/(?P<sid>\d+))?$', 'changeTicketStatus'),
         url_post('^(?P<tid>\d+)/status$', 'setTicketStatus'),
+        url('^(?P<tid>\d+)/thread/(?P<thread_id>\d+)/(?P<action>\w+)$', 'triggerThreadAction'),
         url_get('^status/(?P<status>\w+)(?:/(?P<sid>\d+))?$', 'changeSelectedTicketsStatus'),
         url_post('^status/(?P<state>\w+)$', 'setSelectedTicketsStatus'),
+        url_get('^(?P<tid>\d+)/tasks$', 'tasks'),
+        url('^(?P<tid>\d+)/add-task$', 'addTask'),
+        url_get('^(?P<tid>\d+)/tasks/(?P<id>\d+)/view$', 'task'),
+        url_post('^(?P<tid>\d+)/tasks/(?P<id>\d+)$', 'task'),
         url_get('^lookup', 'lookup'),
-        url_get('^search', 'search')
+        url('^mass/(?P<action>\w+)(?:/(?P<what>\w+))?', 'massProcess'),
+        url('^(?P<tid>\d+)/transfer$', 'transfer'),
+        url('^(?P<tid>\d+)/assign(?:/(?P<to>\w+))?$', 'assign'),
+        url('^(?P<tid>\d+)/claim$', 'claim'),
+        url('^search', patterns('ajax.search.php:SearchAjaxAPI',
+            url_get('^$', 'getAdvancedSearchDialog'),
+            url_post('^$', 'doSearch'),
+            url_get('^quick$', 'doQuickSearch'),
+            url_get('^/(?P<id>\d+)$', 'loadSearch'),
+            url_post('^/(?P<id>\d+)$', 'saveSearch'),
+            url_delete('^/(?P<id>\d+)$', 'deleteSearch'),
+            url_post('^/create$', 'createSearch'),
+            url_get('^/field/(?P<id>[\w_!:]+)$', 'addField')
+        ))
     )),
-    url('^/collaborators/', patterns('ajax.tickets.php:TicketsAjaxAPI',
-        url_get('^(?P<cid>\d+)/view$', 'viewCollaborator'),
-        url_post('^(?P<cid>\d+)$', 'updateCollaborator')
+    url('^/tasks/', patterns('ajax.tasks.php:TasksAjaxAPI',
+        url_get('^(?P<tid>\d+)/preview$', 'preview'),
+        url_get('^(?P<tid>\d+)/edit', 'edit'),
+        url_post('^(?P<tid>\d+)/edit$', 'edit'),
+        url_get('^(?P<tid>\d+)/transfer', 'transfer'),
+        url_post('^(?P<tid>\d+)/transfer$', 'transfer'),
+        url('^(?P<tid>\d+)/assign(?:/(?P<to>\w+))?$', 'assign'),
+        url('^(?P<tid>\d+)/claim$', 'claim'),
+        url_get('^(?P<tid>\d+)/delete', 'delete'),
+        url_post('^(?P<tid>\d+)/delete$', 'delete'),
+        url('^(?P<tid>\d+)/close', 'close'),
+        url('^(?P<tid>\d+)/reopen', 'reopen'),
+        url_get('^(?P<tid>\d+)/view$', 'task'),
+        url_post('^(?P<tid>\d+)$', 'task'),
+        url('^add$', 'add'),
+        url('^lookup', 'lookup'),
+        url('^mass/(?P<action>\w+)(?:/(?P<what>\w+))?', 'massProcess')
+    )),
+    url('^/thread/', patterns('ajax.thread.php:ThreadAjaxAPI',
+        url_get('^(?P<tid>\d+)/collaborators/preview$', 'previewCollaborators'),
+        url_get('^(?P<tid>\d+)/collaborators$', 'showCollaborators'),
+        url_post('^(?P<tid>\d+)/collaborators$', 'updateCollaborators'),
+        url_get('^(?P<tid>\d+)/add-collaborator/(?P<uid>\d+)$', 'addCollaborator'),
+        url_get('^(?P<tid>\d+)/add-collaborator/auth:(?P<bk>\w+):(?P<id>.+)$', 'addRemoteCollaborator'),
+        url('^(?P<tid>\d+)/add-collaborator$', 'addCollaborator'),
+        url_get('^(?P<tid>\d+)/collaborators/(?P<cid>\d+)/view$', 'viewCollaborator'),
+        url_post('^(?P<tid>\d+)/collaborators/(?P<cid>\d+)$', 'updateCollaborator')
     )),
     url('^/draft/', patterns('ajax.draft.php:DraftAjaxAPI',
         url_post('^(?P<id>\d+)$', 'updateDraft'),
         url_delete('^(?P<id>\d+)$', 'deleteDraft'),
         url_post('^(?P<id>\d+)/attach$', 'uploadInlineImage'),
+        url_post('^(?P<namespace>[\w.]+)/attach$', 'uploadInlineImageEarly'),
         url_get('^(?P<namespace>[\w.]+)$', 'getDraft'),
         url_post('^(?P<namespace>[\w.]+)$', 'createDraft'),
         url_get('^images/browse$', 'getFileList')
@@ -175,13 +229,40 @@ $dispatcher = patterns('',
         url_get('^tips/(?P<namespace>[\w_.]+)$', 'getTipsJson'),
         url_get('^(?P<lang>[\w_]+)?/tips/(?P<namespace>[\w_.]+)$', 'getTipsJsonForLang')
     )),
-    url('^/i18n/(?P<lang>[\w_]+)/', patterns('ajax.i18n.php:i18nAjaxAPI',
-        url_get('(?P<tag>\w+)$', 'getLanguageFile')
+    url('^/i18n/', patterns('ajax.i18n.php:i18nAjaxAPI',
+        url_get('^langs/all$', 'getConfiguredLanguages'),
+        url_get('^langs$', 'getSecondaryLanguages'),
+        url_get('^translate/(?P<tag>\w+)$', 'getTranslations'),
+        url_post('^translate/(?P<tag>\w+)$', 'updateTranslations'),
+        url_get('^(?P<lang>[\w_]+)/(?P<tag>\w+)$', 'getLanguageFile')
+    )),
+    url('^/admin', patterns('ajax.admin.php:AdminAjaxAPI',
+        url('^/quick-add', patterns('ajax.admin.php:AdminAjaxAPI',
+            url('^/department$', 'addDepartment'),
+            url('^/team$', 'addTeam'),
+            url('^/role$', 'addRole'),
+            url('^/staff$', 'addStaff')
+        )),
+        url_get('^/role/(?P<id>\d+)/perms', 'getRolePerms')
+    )),
+    url('^/staff', patterns('ajax.staff.php:StaffAjaxAPI',
+        url('^/(?P<id>\d+)/set-password$', 'setPassword'),
+        url('^/(?P<id>\d+)/change-password$', 'changePassword'),
+        url_get('^/(?P<id>\d+)/perms', 'getAgentPerms'),
+        url('^/reset-permissions', 'resetPermissions'),
+        url('^/change-department', 'changeDepartment'),
+        url('^/(?P<id>\d+)/avatar/change', 'setAvatar')
     ))
 );
 
 Signal::send('ajax.scp', $dispatcher);
 
 # Call the respective function
-print $dispatcher->resolve($ost->get_path_info());
+$rv = $dispatcher->resolve($ost->get_path_info());
+
+// Indicate JSON response content-type
+if (is_string($rv) && $rv[0] == '{')
+    Http::response(200, $rv, 'application/json');
+
+print $rv;
 ?>
diff --git a/scp/apps/dispatcher.php b/scp/apps/dispatcher.php
index ac5ac585040ffdbc0bcbc8d9056c2cd651628e31..63255923a7bc73d2c38989d1f658fb295d5e511e 100644
--- a/scp/apps/dispatcher.php
+++ b/scp/apps/dispatcher.php
@@ -14,19 +14,14 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
-# Override staffLoginPage() defined in staff.inc.php to return an
-# HTTP/Forbidden status rather than the actual login page.
-# XXX: This should be moved to the AjaxController class
-function staffLoginPage($msg='Unauthorized') {
-    Http::response(403,'Must login: '.Format::htmlchars($msg));
-    exit;
-}
+if (basename($_SERVER['SCRIPT_NAME'])==basename(__FILE__))
+    die('Access denied'); //Say hi to our friend..
 
 require('staff.inc.php');
 
 //Clean house...don't let the world see your crap.
-ini_set('display_errors','0'); //Disable error display
-ini_set('display_startup_errors','0');
+#ini_set('display_errors','0'); //Disable error display
+#ini_set('display_startup_errors','0');
 
 //TODO: disable direct access via the browser? i,e All request must have REFER?
 if(!defined('INCLUDE_DIR'))	Http::response(500, 'Server configuration error');
@@ -34,7 +29,17 @@ if(!defined('INCLUDE_DIR'))	Http::response(500, 'Server configuration error');
 require_once INCLUDE_DIR.'/class.dispatcher.php';
 $dispatcher = new Dispatcher();
 
-Signal::send('apps.scp', $dispatcher);
+$PI = $ost->get_path_info();
+if (strpos(strtolower($PI), '/admin/') === 0) {
+    require('admin.inc.php');
+    $PI = substr($PI, 6);
+    Signal::send('apps.admin', $dispatcher);
+}
+else {
+    Signal::send('apps.scp', $dispatcher);
+}
+
+$nav->setActiveTab('apps');
 
 # Call the respective function
-print $dispatcher->resolve($ost->get_path_info());
+print $dispatcher->resolve($PI);
diff --git a/scp/autocron.php b/scp/autocron.php
index d6ff919b359791e87caedd2f9b2c04dac7a397c0..989673a22fa276ef1f889a1ab012b8c12d428b9c 100644
--- a/scp/autocron.php
+++ b/scp/autocron.php
@@ -41,7 +41,7 @@ $caller = $thisstaff->getUserName();
 
 // Agent can call cron once every 3 minutes.
 if ($sec < 180 || !$ost || $ost->isUpgradePending())
-    ob_end_clean();
+    return ob_end_clean();
 
 require_once(INCLUDE_DIR.'class.cron.php');
 
diff --git a/scp/canned.php b/scp/canned.php
index b44b4067f5757e6a6e07ad8a6473ddc9eb4b6bd1..6f2631e2a6f5082397fe430dfd31f733e151bfb8 100644
--- a/scp/canned.php
+++ b/scp/canned.php
@@ -17,7 +17,8 @@ require('staff.inc.php');
 include_once(INCLUDE_DIR.'class.canned.php');
 
 /* check permission */
-if(!$thisstaff || !$thisstaff->canManageCannedResponses()
+if(!$thisstaff
+        || !$thisstaff->getRole()->hasPerm(Canned::PERM_MANAGE, false)
         || !$cfg->isCannedResponseEnabled()) {
     header('Location: kb.php');
     exit;
@@ -29,14 +30,14 @@ $canned=null;
 if($_REQUEST['id'] && !($canned=Canned::lookup($_REQUEST['id'])))
     $errors['err']=sprintf(__('%s: Unknown or invalid ID.'), __('canned response'));
 
-$canned_form = new Form(array(
+$canned_form = new SimpleForm(array(
     'attachments' => new FileUploadField(array('id'=>'attach',
         'configuration'=>array('extensions'=>false,
             'size'=>$cfg->getMaxFileSize())
    )),
 ));
 
-if($_POST && $thisstaff->canManageCannedResponses()) {
+if ($_POST) {
     switch(strtolower($_POST['do'])) {
         case 'update':
             if(!$canned) {
@@ -44,31 +45,20 @@ if($_POST && $thisstaff->canManageCannedResponses()) {
             } elseif($canned->update($_POST, $errors)) {
                 $msg=sprintf(__('Successfully updated %s'),
                     __('this canned response'));
+
                 //Delete removed attachments.
                 //XXX: files[] shouldn't be changed under any circumstances.
+                // Upload NEW attachments IF ANY - TODO: validate attachment types??
                 $keepers = $canned_form->getField('attachments')->getClean();
-                $attachments = $canned->attachments->getSeparates(); //current list of attachments.
-                foreach($attachments as $k=>$file) {
-                    if($file['id'] && !in_array($file['id'], $keepers)) {
-                        $canned->attachments->delete($file['id']);
-                    }
-                }
-
-                //Upload NEW attachments IF ANY - TODO: validate attachment types??
-                if ($keepers)
-                    $canned->attachments->upload($keepers);
+                $canned->attachments->keepOnlyFileIds($keepers, false);
 
                 // Attach inline attachments from the editor
                 if (isset($_POST['draft_id'])
                         && ($draft = Draft::lookup($_POST['draft_id']))) {
-                    $canned->attachments->deleteInlines();
-                    $canned->attachments->upload(
-                        $draft->getAttachmentIds($_POST['response']),
-                        true);
+                    $images = $draft->getAttachmentIds($_POST['response']);
+                    $canned->attachments->keepOnlyFileIds($images, true);
                 }
 
-                $canned->reload();
-
                 // XXX: Handle nicely notifying a user that the draft was
                 // deleted | OR | show the draft for the user on the name
                 // page refresh or a nice bar popup immediately with
@@ -82,18 +72,19 @@ if($_POST && $thisstaff->canManageCannedResponses()) {
             }
             break;
         case 'create':
-            if(($id=Canned::create($_POST, $errors))) {
+            $premade = Canned::create();
+            if ($premade->update($_POST,$errors)) {
                 $msg=sprintf(__('Successfully added %s'), Format::htmlchars($_POST['title']));
                 $_REQUEST['a']=null;
                 //Upload attachments
                 $keepers = $canned_form->getField('attachments')->getClean();
-                if (($c=Canned::lookup($id)) && $keepers)
-                    $c->attachments->upload($keepers);
+                if ($keepers)
+                    $premade->attachments->upload($keepers);
 
                 // Attach inline attachments from the editor
-                if ($c && isset($_POST['draft_id'])
+                if (isset($_POST['draft_id'])
                         && ($draft = Draft::lookup($_POST['draft_id'])))
-                    $c->attachments->upload(
+                    $premade->attachments->upload(
                         $draft->getAttachmentIds($_POST['response']), true);
 
                 // Delete this user's drafts for new canned-responses
diff --git a/scp/categories.php b/scp/categories.php
index 8afdc4c0f47da5ee7d9a52341fe509a9718bea71..3e25c1d672e14f017b26c9da44863cc98f70455b 100644
--- a/scp/categories.php
+++ b/scp/categories.php
@@ -17,7 +17,8 @@ require('staff.inc.php');
 include_once(INCLUDE_DIR.'class.category.php');
 
 /* check permission */
-if(!$thisstaff || !$thisstaff->canManageFAQ()) {
+if(!$thisstaff ||
+        !$thisstaff->hasPerm(FAQ::PERM_MANAGE)) {
     header('Location: kb.php');
     exit;
 }
@@ -40,7 +41,8 @@ if($_POST){
             }
             break;
         case 'create':
-            if(($id=Category::create($_POST,$errors))) {
+            $category = Category::create();
+            if ($category->update($_POST, $errors)) {
                 $msg=sprintf(__('Successfull added %s'), Format::htmlchars($_POST['name']));
                 $_REQUEST['a']=null;
             } elseif(!$errors['err']) {
@@ -55,11 +57,13 @@ if($_POST){
                 $count=count($_POST['ids']);
                 switch(strtolower($_POST['a'])) {
                     case 'make_public':
-                        $sql='UPDATE '.FAQ_CATEGORY_TABLE.' SET ispublic=1 '
-                            .' WHERE category_id IN ('.implode(',', db_input($_POST['ids'])).')';
-
-                        if(db_query($sql) && ($num=db_affected_rows())) {
-                            if($num==$count)
+                        $num = Category::objects()->filter(array(
+                            'category_id__in'=>$_POST['ids']
+                        ))->update(array(
+                            'ispublic'=>true
+                        ));
+                        if ($num > 0) {
+                            if ($num==$count)
                                 $msg = sprintf(__('Successfully made %s PUBLIC'),
                                     _N('selected category', 'selected categories', $count));
                             else
@@ -71,10 +75,12 @@ if($_POST){
                         }
                         break;
                     case 'make_private':
-                        $sql='UPDATE '.FAQ_CATEGORY_TABLE.' SET ispublic=0 '
-                            .' WHERE category_id IN ('.implode(',', db_input($_POST['ids'])).')';
-
-                        if(db_query($sql) && ($num=db_affected_rows())) {
+                        $num = Category::objects()->filter(array(
+                            'category_id__in'=>$_POST['ids']
+                        ))->update(array(
+                            'ispublic'=>false
+                        ));
+                        if ($num > 0) {
                             if($num==$count)
                                 $msg = sprintf(__('Successfully made %s PRIVATE'),
                                     _N('selected category', 'selected categories', $count));
@@ -87,19 +93,17 @@ if($_POST){
                         }
                         break;
                     case 'delete':
-                        $i=0;
-                        foreach($_POST['ids'] as $k=>$v) {
-                            if(($c=Category::lookup($v)) && $c->delete())
-                                $i++;
-                        }
+                        $i = Category::objects()->filter(array(
+                            'category_id__in'=>$_POST['ids']
+                        ))->delete();
 
-                        if($i==$count)
+                        if ($i==$count)
                             $msg = sprintf(__('Successfully deleted %s'),
                                 _N('selected category', 'selected categories', $count));
-                        elseif($i>0)
+                        elseif ($i > 0)
                             $warn = sprintf(__('%1$d of %2$d %3$s deleted'), $i, $count,
                                 _N('selected category', 'selected categories', $count));
-                        elseif(!$errors['err'])
+                        elseif (!$errors['err'])
                             $errors['err'] = sprintf(__('Unable to delete %s'),
                                 _N('selected category', 'selected categories', $count));
                         break;
diff --git a/scp/css/bootstrap.css b/scp/css/bootstrap.css
deleted file mode 100644
index 7a02fb748814898548e67f60e34185991712de67..0000000000000000000000000000000000000000
--- a/scp/css/bootstrap.css
+++ /dev/null
@@ -1,1589 +0,0 @@
-/*!
- * Bootstrap v2.0.4
- *
- * Copyright 2012 Twitter, Inc
- * Licensed under the Apache License v2.0
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Designed and built with all the love in the world @twitter by @mdo and @fat.
- */
-.clearfix {
-  *zoom: 1;
-}
-.clearfix:before,
-.clearfix:after {
-  display: table;
-  content: "";
-}
-.clearfix:after {
-  clear: both;
-}
-.hide-text {
-  font: 0/0 a;
-  color: transparent;
-  text-shadow: none;
-  background-color: transparent;
-  border: 0;
-}
-.input-block-level {
-  display: block;
-  width: 100%;
-  min-height: 28px;
-  -webkit-box-sizing: border-box;
-  -moz-box-sizing: border-box;
-  -ms-box-sizing: border-box;
-  box-sizing: border-box;
-}
-p {
-  margin: 0 0 9px;
-}
-p small {
-  font-size: 11px;
-  color: #999999;
-}
-.lead {
-  margin-bottom: 18px;
-  font-size: 20px;
-  font-weight: 200;
-  line-height: 27px;
-}
-h1,
-h2,
-h3,
-h4,
-h5,
-h6 {
-  margin: 0;
-  font-family: inherit;
-  font-weight: bold;
-  color: inherit;
-  text-rendering: optimizelegibility;
-}
-h1 small,
-h2 small,
-h3 small,
-h4 small,
-h5 small,
-h6 small {
-  font-weight: normal;
-  color: #999999;
-}
-h1 {
-  font-size: 30px;
-  line-height: 36px;
-}
-h1 small {
-  font-size: 18px;
-}
-h2 {
-  font-size: 24px;
-  line-height: 36px;
-}
-h2 small {
-  font-size: 18px;
-}
-h3 {
-  font-size: 18px;
-  line-height: 27px;
-}
-h3 small {
-  font-size: 14px;
-}
-h4,
-h5,
-h6 {
-  line-height: 18px;
-}
-h4 {
-  font-size: 14px;
-}
-h4 small {
-  font-size: 12px;
-}
-h5 {
-  font-size: 12px;
-}
-h6 {
-  font-size: 11px;
-  color: #999999;
-  text-transform: uppercase;
-}
-.page-header {
-  padding-bottom: 17px;
-  margin: 18px 0;
-  border-bottom: 1px solid #eeeeee;
-}
-.page-header h1 {
-  line-height: 1;
-}
-ul,
-ol {
-  padding: 0;
-  margin: 0 0 9px 25px;
-}
-ul ul,
-ul ol,
-ol ol,
-ol ul {
-  margin-bottom: 0;
-}
-ul {
-  list-style: disc;
-}
-ol {
-  list-style: decimal;
-}
-/*li {
-  line-height: 18px;
-}*/
-ul.unstyled,
-ol.unstyled {
-  margin-left: 0;
-  list-style: none;
-}
-dl {
-  margin-bottom: 18px;
-}
-dt,
-dd {
-  line-height: 18px;
-}
-dt {
-  font-weight: bold;
-  line-height: 17px;
-}
-dd {
-  margin-left: 9px;
-}
-.dl-horizontal dt {
-  float: left;
-  width: 120px;
-  clear: left;
-  text-align: right;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
-}
-.dl-horizontal dd {
-  margin-left: 130px;
-}
-hr {
-  margin: 18px 0;
-  border: 0;
-  border-top: 1px solid #eeeeee;
-  border-bottom: 1px solid #ffffff;
-}
-strong {
-  font-weight: bold;
-}
-em {
-  font-style: italic;
-}
-.muted {
-  color: #999999;
-}
-abbr[title] {
-  cursor: help;
-  border-bottom: 1px dotted #999999;
-}
-abbr.initialism {
-  font-size: 90%;
-  text-transform: uppercase;
-}
-blockquote {
-  padding: 0 0 0 15px;
-  margin: 0 0 18px;
-  border-left: 5px solid #eeeeee;
-}
-blockquote p {
-  margin-bottom: 0;
-  font-size: 16px;
-  font-weight: 300;
-  line-height: 22.5px;
-}
-blockquote small {
-  display: block;
-  line-height: 18px;
-  color: #999999;
-}
-blockquote small:before {
-  content: '\2014 \00A0';
-}
-blockquote.pull-right {
-  float: right;
-  padding-right: 15px;
-  padding-left: 0;
-  border-right: 5px solid #eeeeee;
-  border-left: 0;
-}
-blockquote.pull-right p,
-blockquote.pull-right small {
-  text-align: right;
-}
-q:before,
-q:after,
-blockquote:before,
-blockquote:after {
-  content: "";
-}
-address {
-  display: block;
-  margin-bottom: 18px;
-  font-style: normal;
-  line-height: 18px;
-}
-small {
-  font-size: 100%;
-}
-cite {
-  font-style: normal;
-}
-.label,
-.badge {
-  font-size: 10.998px;
-  font-weight: bold;
-  line-height: 14px;
-  color: #ffffff;
-  vertical-align: baseline;
-  white-space: nowrap;
-  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
-  background-color: #999999;
-}
-.label {
-  padding: 1px 4px 2px;
-  -webkit-border-radius: 3px;
-  -moz-border-radius: 3px;
-  border-radius: 3px;
-}
-.badge {
-  padding: 1px 9px 2px;
-  -webkit-border-radius: 9px;
-  -moz-border-radius: 9px;
-  border-radius: 9px;
-}
-a.label:hover,
-a.badge:hover {
-  color: #ffffff;
-  text-decoration: none;
-  cursor: pointer;
-}
-.label-important,
-.badge-important {
-  background-color: #b94a48;
-}
-.label-important[href],
-.badge-important[href] {
-  background-color: #953b39;
-}
-.label-warning,
-.badge-warning {
-  background-color: #f89406;
-}
-.label-warning[href],
-.badge-warning[href] {
-  background-color: #c67605;
-}
-.label-success,
-.badge-success {
-  background-color: #468847;
-}
-.label-success[href],
-.badge-success[href] {
-  background-color: #356635;
-}
-.label-info,
-.badge-info {
-  background-color: #3a87ad;
-}
-.label-info[href],
-.badge-info[href] {
-  background-color: #2d6987;
-}
-.label-inverse,
-.badge-inverse {
-  background-color: #333333;
-}
-.label-inverse[href],
-.badge-inverse[href] {
-  background-color: #1a1a1a;
-}
-table {
-  max-width: 100%;
-  background-color: transparent;
-  border-collapse: collapse;
-  border-spacing: 0;
-}
-.table {
-  width: 100%;
-  margin-bottom: 18px;
-}
-.table th,
-.table td {
-  padding: 8px;
-  line-height: 18px;
-  text-align: left;
-  vertical-align: top;
-  border-top: 1px solid #dddddd;
-}
-.table th {
-  font-weight: bold;
-}
-.table thead th {
-  vertical-align: bottom;
-}
-.table caption + thead tr:first-child th,
-.table caption + thead tr:first-child td,
-.table colgroup + thead tr:first-child th,
-.table colgroup + thead tr:first-child td,
-.table thead:first-child tr:first-child th,
-.table thead:first-child tr:first-child td {
-  border-top: 0;
-}
-.table tbody + tbody {
-  border-top: 2px solid #dddddd;
-}
-.table-condensed th,
-.table-condensed td {
-  padding: 4px 5px;
-}
-.table-bordered {
-  border: 1px solid #dddddd;
-  border-collapse: separate;
-  *border-collapse: collapsed;
-  border-left: 0;
-  -webkit-border-radius: 4px;
-  -moz-border-radius: 4px;
-  border-radius: 4px;
-}
-.table-bordered th,
-.table-bordered td {
-  border-left: 1px solid #dddddd;
-}
-.table-bordered caption + thead tr:first-child th,
-.table-bordered caption + tbody tr:first-child th,
-.table-bordered caption + tbody tr:first-child td,
-.table-bordered colgroup + thead tr:first-child th,
-.table-bordered colgroup + tbody tr:first-child th,
-.table-bordered colgroup + tbody tr:first-child td,
-.table-bordered thead:first-child tr:first-child th,
-.table-bordered tbody:first-child tr:first-child th,
-.table-bordered tbody:first-child tr:first-child td {
-  border-top: 0;
-}
-.table-bordered thead:first-child tr:first-child th:first-child,
-.table-bordered tbody:first-child tr:first-child td:first-child {
-  -webkit-border-top-left-radius: 4px;
-  border-top-left-radius: 4px;
-  -moz-border-radius-topleft: 4px;
-}
-.table-bordered thead:first-child tr:first-child th:last-child,
-.table-bordered tbody:first-child tr:first-child td:last-child {
-  -webkit-border-top-right-radius: 4px;
-  border-top-right-radius: 4px;
-  -moz-border-radius-topright: 4px;
-}
-.table-bordered thead:last-child tr:last-child th:first-child,
-.table-bordered tbody:last-child tr:last-child td:first-child {
-  -webkit-border-radius: 0 0 0 4px;
-  -moz-border-radius: 0 0 0 4px;
-  border-radius: 0 0 0 4px;
-  -webkit-border-bottom-left-radius: 4px;
-  border-bottom-left-radius: 4px;
-  -moz-border-radius-bottomleft: 4px;
-}
-.table-bordered thead:last-child tr:last-child th:last-child,
-.table-bordered tbody:last-child tr:last-child td:last-child {
-  -webkit-border-bottom-right-radius: 4px;
-  border-bottom-right-radius: 4px;
-  -moz-border-radius-bottomright: 4px;
-}
-.table-striped tbody tr:nth-child(odd) td,
-.table-striped tbody tr:nth-child(odd) th {
-  background-color: #f9f9f9;
-}
-.table tbody tr:hover td,
-.table tbody tr:hover th {
-  background-color: #f5f5f5;
-}
-table .span1 {
-  float: none;
-  width: 44px;
-  margin-left: 0;
-}
-table .span2 {
-  float: none;
-  width: 124px;
-  margin-left: 0;
-}
-table .span3 {
-  float: none;
-  width: 204px;
-  margin-left: 0;
-}
-table .span4 {
-  float: none;
-  width: 284px;
-  margin-left: 0;
-}
-table .span5 {
-  float: none;
-  width: 364px;
-  margin-left: 0;
-}
-table .span6 {
-  float: none;
-  width: 444px;
-  margin-left: 0;
-}
-table .span7 {
-  float: none;
-  width: 524px;
-  margin-left: 0;
-}
-table .span8 {
-  float: none;
-  width: 604px;
-  margin-left: 0;
-}
-table .span9 {
-  float: none;
-  width: 684px;
-  margin-left: 0;
-}
-table .span10 {
-  float: none;
-  width: 764px;
-  margin-left: 0;
-}
-table .span11 {
-  float: none;
-  width: 844px;
-  margin-left: 0;
-}
-table .span12 {
-  float: none;
-  width: 924px;
-  margin-left: 0;
-}
-table .span13 {
-  float: none;
-  width: 1004px;
-  margin-left: 0;
-}
-table .span14 {
-  float: none;
-  width: 1084px;
-  margin-left: 0;
-}
-table .span15 {
-  float: none;
-  width: 1164px;
-  margin-left: 0;
-}
-table .span16 {
-  float: none;
-  width: 1244px;
-  margin-left: 0;
-}
-table .span17 {
-  float: none;
-  width: 1324px;
-  margin-left: 0;
-}
-table .span18 {
-  float: none;
-  width: 1404px;
-  margin-left: 0;
-}
-table .span19 {
-  float: none;
-  width: 1484px;
-  margin-left: 0;
-}
-table .span20 {
-  float: none;
-  width: 1564px;
-  margin-left: 0;
-}
-table .span21 {
-  float: none;
-  width: 1644px;
-  margin-left: 0;
-}
-table .span22 {
-  float: none;
-  width: 1724px;
-  margin-left: 0;
-}
-table .span23 {
-  float: none;
-  width: 1804px;
-  margin-left: 0;
-}
-table .span24 {
-  float: none;
-  width: 1884px;
-  margin-left: 0;
-}
-form {
-  margin: 0 0 18px;
-}
-fieldset {
-  padding: 0;
-  margin: 0;
-  border: 0;
-}
-legend {
-  display: block;
-  width: 100%;
-  padding: 0;
-  margin-bottom: 27px;
-  font-size: 19.5px;
-  line-height: 36px;
-  color: #333333;
-  border: 0;
-  border-bottom: 1px solid #e5e5e5;
-}
-legend small {
-  font-size: 13.5px;
-  color: #999999;
-}
-label,
-input,
-button,
-select,
-textarea {
-  font-size: 13px;
-  font-weight: normal;
-  line-height: 18px;
-}
-input,
-button,
-select,
-textarea {
-  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
-}
-label {
-  display: block;
-  margin-bottom: 5px;
-}
-select,
-textarea,
-input[type="text"],
-input[type="password"],
-input[type="datetime"],
-input[type="datetime-local"],
-input[type="date"],
-input[type="month"],
-input[type="time"],
-input[type="week"],
-input[type="number"],
-input[type="email"],
-input[type="url"],
-input[type="search"],
-input[type="tel"],
-input[type="color"],
-.uneditable-input {
-  display: inline-block;
-  height: 18px;
-  padding: 4px;
-  margin-bottom: 9px;
-  font-size: 13px;
-  line-height: 18px;
-  color: #555555;
-}
-input,
-textarea {
-  width: 210px;
-}
-textarea {
-  height: auto;
-}
-textarea,
-input[type="text"],
-input[type="password"],
-input[type="datetime"],
-input[type="datetime-local"],
-input[type="date"],
-input[type="month"],
-input[type="time"],
-input[type="week"],
-input[type="number"],
-input[type="email"],
-input[type="url"],
-input[type="search"],
-input[type="tel"],
-input[type="color"],
-.uneditable-input {
-  background-color: #ffffff;
-  border: 1px solid #cccccc;
-  -webkit-border-radius: 3px;
-  -moz-border-radius: 3px;
-  border-radius: 3px;
-  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-  -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
-  -webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
-  -moz-transition: border linear 0.2s, box-shadow linear 0.2s;
-  -ms-transition: border linear 0.2s, box-shadow linear 0.2s;
-  -o-transition: border linear 0.2s, box-shadow linear 0.2s;
-  transition: border linear 0.2s, box-shadow linear 0.2s;
-}
-textarea:focus,
-input[type="text"]:focus,
-input[type="password"]:focus,
-input[type="datetime"]:focus,
-input[type="datetime-local"]:focus,
-input[type="date"]:focus,
-input[type="month"]:focus,
-input[type="time"]:focus,
-input[type="week"]:focus,
-input[type="number"]:focus,
-input[type="email"]:focus,
-input[type="url"]:focus,
-input[type="search"]:focus,
-input[type="tel"]:focus,
-input[type="color"]:focus,
-.uneditable-input:focus {
-  border-color: rgba(82, 168, 236, 0.8);
-  outline: 0;
-  outline: thin dotted \9;
-  /* IE6-9 */
-
-  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
-  -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
-  box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6);
-}
-input[type="radio"],
-input[type="checkbox"] {
-  margin: 3px 0;
-  *margin-top: 0;
-  /* IE7 */
-
-  line-height: normal;
-  cursor: pointer;
-}
-input[type="submit"],
-input[type="reset"],
-input[type="button"],
-input[type="radio"],
-input[type="checkbox"] {
-  width: auto;
-}
-.uneditable-textarea {
-  width: auto;
-  height: auto;
-}
-select,
-input[type="file"] {
-  height: 28px;
-  /* In IE7, the height of the select element cannot be changed by height, only font-size */
-
-  *margin-top: 4px;
-  /* For IE7, add top margin to align select with labels */
-
-  line-height: 28px;
-}
-select {
-  width: 220px;
-  border: 1px solid #bbb;
-}
-select[multiple],
-select[size] {
-  height: auto;
-}
-select:focus,
-input[type="file"]:focus,
-input[type="radio"]:focus,
-input[type="checkbox"]:focus {
-  outline: thin dotted #333;
-  outline: 5px auto -webkit-focus-ring-color;
-  outline-offset: -2px;
-}
-.radio,
-.checkbox {
-  min-height: 18px;
-  padding-left: 18px;
-}
-.radio input[type="radio"],
-.checkbox input[type="checkbox"] {
-  float: left;
-  margin-left: -18px;
-}
-.controls > .radio:first-child,
-.controls > .checkbox:first-child {
-  padding-top: 5px;
-}
-.radio.inline,
-.checkbox.inline {
-  display: inline-block;
-  padding-top: 5px;
-  margin-bottom: 0;
-  vertical-align: middle;
-}
-.radio.inline + .radio.inline,
-.checkbox.inline + .checkbox.inline {
-  margin-left: 10px;
-}
-.input-mini {
-  width: 60px;
-}
-.input-small {
-  width: 90px;
-}
-.input-medium {
-  width: 150px;
-}
-.input-large {
-  width: 210px;
-}
-.input-xlarge {
-  width: 270px;
-}
-.input-xxlarge {
-  width: 530px;
-}
-input[class*="span"],
-select[class*="span"],
-textarea[class*="span"],
-.uneditable-input[class*="span"],
-.row-fluid input[class*="span"],
-.row-fluid select[class*="span"],
-.row-fluid textarea[class*="span"],
-.row-fluid .uneditable-input[class*="span"] {
-  float: none;
-  margin-left: 0;
-}
-.input-append input[class*="span"],
-.input-append .uneditable-input[class*="span"],
-.input-prepend input[class*="span"],
-.input-prepend .uneditable-input[class*="span"],
-.row-fluid .input-prepend [class*="span"],
-.row-fluid .input-append [class*="span"] {
-  display: inline-block;
-}
-input,
-textarea,
-.uneditable-input {
-  margin-left: 0;
-}
-input.span12, textarea.span12, .uneditable-input.span12 {
-  width: 930px;
-}
-input.span11, textarea.span11, .uneditable-input.span11 {
-  width: 850px;
-}
-input.span10, textarea.span10, .uneditable-input.span10 {
-  width: 770px;
-}
-input.span9, textarea.span9, .uneditable-input.span9 {
-  width: 690px;
-}
-input.span8, textarea.span8, .uneditable-input.span8 {
-  width: 610px;
-}
-input.span7, textarea.span7, .uneditable-input.span7 {
-  width: 530px;
-}
-input.span6, textarea.span6, .uneditable-input.span6 {
-  width: 450px;
-}
-input.span5, textarea.span5, .uneditable-input.span5 {
-  width: 370px;
-}
-input.span4, textarea.span4, .uneditable-input.span4 {
-  width: 290px;
-}
-input.span3, textarea.span3, .uneditable-input.span3 {
-  width: 210px;
-}
-input.span2, textarea.span2, .uneditable-input.span2 {
-  width: 130px;
-}
-input.span1, textarea.span1, .uneditable-input.span1 {
-  width: 50px;
-}
-input[disabled],
-select[disabled],
-textarea[disabled],
-input[readonly],
-select[readonly],
-textarea[readonly] {
-  cursor: not-allowed;
-  background-color: #eeeeee;
-  border-color: #ddd;
-}
-input[type="radio"][disabled],
-input[type="checkbox"][disabled],
-input[type="radio"][readonly],
-input[type="checkbox"][readonly] {
-  background-color: transparent;
-}
-.control-group.warning > label,
-.control-group.warning .help-block,
-.control-group.warning .help-inline {
-  color: #c09853;
-}
-.control-group.warning .checkbox,
-.control-group.warning .radio,
-.control-group.warning input,
-.control-group.warning select,
-.control-group.warning textarea {
-  color: #c09853;
-  border-color: #c09853;
-}
-.control-group.warning .checkbox:focus,
-.control-group.warning .radio:focus,
-.control-group.warning input:focus,
-.control-group.warning select:focus,
-.control-group.warning textarea:focus {
-  border-color: #a47e3c;
-  -webkit-box-shadow: 0 0 6px #dbc59e;
-  -moz-box-shadow: 0 0 6px #dbc59e;
-  box-shadow: 0 0 6px #dbc59e;
-}
-.control-group.warning .input-prepend .add-on,
-.control-group.warning .input-append .add-on {
-  color: #c09853;
-  background-color: #fcf8e3;
-  border-color: #c09853;
-}
-.control-group.error > label,
-.control-group.error .help-block,
-.control-group.error .help-inline {
-  color: #b94a48;
-}
-.control-group.error .checkbox,
-.control-group.error .radio,
-.control-group.error input,
-.control-group.error select,
-.control-group.error textarea {
-  color: #b94a48;
-  border-color: #b94a48;
-}
-.control-group.error .checkbox:focus,
-.control-group.error .radio:focus,
-.control-group.error input:focus,
-.control-group.error select:focus,
-.control-group.error textarea:focus {
-  border-color: #953b39;
-  -webkit-box-shadow: 0 0 6px #d59392;
-  -moz-box-shadow: 0 0 6px #d59392;
-  box-shadow: 0 0 6px #d59392;
-}
-.control-group.error .input-prepend .add-on,
-.control-group.error .input-append .add-on {
-  color: #b94a48;
-  background-color: #f2dede;
-  border-color: #b94a48;
-}
-.control-group.success > label,
-.control-group.success .help-block,
-.control-group.success .help-inline {
-  color: #468847;
-}
-.control-group.success .checkbox,
-.control-group.success .radio,
-.control-group.success input,
-.control-group.success select,
-.control-group.success textarea {
-  color: #468847;
-  border-color: #468847;
-}
-.control-group.success .checkbox:focus,
-.control-group.success .radio:focus,
-.control-group.success input:focus,
-.control-group.success select:focus,
-.control-group.success textarea:focus {
-  border-color: #356635;
-  -webkit-box-shadow: 0 0 6px #7aba7b;
-  -moz-box-shadow: 0 0 6px #7aba7b;
-  box-shadow: 0 0 6px #7aba7b;
-}
-.control-group.success .input-prepend .add-on,
-.control-group.success .input-append .add-on {
-  color: #468847;
-  background-color: #dff0d8;
-  border-color: #468847;
-}
-input:focus:required:invalid,
-textarea:focus:required:invalid,
-select:focus:required:invalid {
-  color: #b94a48;
-  border-color: #ee5f5b;
-}
-input:focus:required:invalid:focus,
-textarea:focus:required:invalid:focus,
-select:focus:required:invalid:focus {
-  border-color: #e9322d;
-  -webkit-box-shadow: 0 0 6px #f8b9b7;
-  -moz-box-shadow: 0 0 6px #f8b9b7;
-  box-shadow: 0 0 6px #f8b9b7;
-}
-.form-actions {
-  padding: 17px 20px 18px;
-  margin-top: 18px;
-  margin-bottom: 18px;
-  background-color: #f5f5f5;
-  border-top: 1px solid #e5e5e5;
-  *zoom: 1;
-}
-.form-actions:before,
-.form-actions:after {
-  display: table;
-  content: "";
-}
-.form-actions:after {
-  clear: both;
-}
-.uneditable-input {
-  overflow: hidden;
-  white-space: nowrap;
-  cursor: not-allowed;
-  background-color: #ffffff;
-  border-color: #eee;
-  -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
-  -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
-  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
-}
-:-moz-placeholder {
-  color: #999999;
-}
-:-ms-input-placeholder {
-  color: #999999;
-}
-::-webkit-input-placeholder {
-  color: #999999;
-}
-.help-block,
-.help-inline {
-  color: #555555;
-}
-.help-block {
-  display: block;
-  margin-bottom: 9px;
-}
-.help-inline {
-  display: inline-block;
-  *display: inline;
-  /* IE7 inline-block hack */
-
-  *zoom: 1;
-  vertical-align: middle;
-  padding-left: 5px;
-}
-.input-prepend,
-.input-append {
-  margin-bottom: 5px;
-}
-.input-prepend input,
-.input-append input,
-.input-prepend select,
-.input-append select,
-.input-prepend .uneditable-input,
-.input-append .uneditable-input {
-  position: relative;
-  margin-bottom: 0;
-  *margin-left: 0;
-  vertical-align: middle;
-  -webkit-border-radius: 0 3px 3px 0;
-  -moz-border-radius: 0 3px 3px 0;
-  border-radius: 0 3px 3px 0;
-}
-.input-prepend input:focus,
-.input-append input:focus,
-.input-prepend select:focus,
-.input-append select:focus,
-.input-prepend .uneditable-input:focus,
-.input-append .uneditable-input:focus {
-  z-index: 2;
-}
-.input-prepend .uneditable-input,
-.input-append .uneditable-input {
-  border-left-color: #ccc;
-}
-.input-prepend .add-on,
-.input-append .add-on {
-  display: inline-block;
-  width: auto;
-  height: 18px;
-  min-width: 16px;
-  padding: 4px 5px;
-  font-weight: normal;
-  line-height: 18px;
-  text-align: center;
-  text-shadow: 0 1px 0 #ffffff;
-  vertical-align: middle;
-  background-color: #eeeeee;
-  border: 1px solid #ccc;
-}
-.input-prepend .add-on,
-.input-append .add-on,
-.input-prepend .btn,
-.input-append .btn {
-  margin-left: -1px;
-  -webkit-border-radius: 0;
-  -moz-border-radius: 0;
-  border-radius: 0;
-}
-.input-prepend .active,
-.input-append .active {
-  background-color: #a9dba9;
-  border-color: #46a546;
-}
-.input-prepend .add-on,
-.input-prepend .btn {
-  margin-right: -1px;
-}
-.input-prepend .add-on:first-child,
-.input-prepend .btn:first-child {
-  -webkit-border-radius: 3px 0 0 3px;
-  -moz-border-radius: 3px 0 0 3px;
-  border-radius: 3px 0 0 3px;
-}
-.input-append input,
-.input-append select,
-.input-append .uneditable-input {
-  -webkit-border-radius: 3px 0 0 3px;
-  -moz-border-radius: 3px 0 0 3px;
-  border-radius: 3px 0 0 3px;
-}
-.input-append .uneditable-input {
-  border-right-color: #ccc;
-  border-left-color: #eee;
-}
-.input-append .add-on:last-child,
-.input-append .btn:last-child {
-  -webkit-border-radius: 0 3px 3px 0;
-  -moz-border-radius: 0 3px 3px 0;
-  border-radius: 0 3px 3px 0;
-}
-.input-prepend.input-append input,
-.input-prepend.input-append select,
-.input-prepend.input-append .uneditable-input {
-  -webkit-border-radius: 0;
-  -moz-border-radius: 0;
-  border-radius: 0;
-}
-.input-prepend.input-append .add-on:first-child,
-.input-prepend.input-append .btn:first-child {
-  margin-right: -1px;
-  -webkit-border-radius: 3px 0 0 3px;
-  -moz-border-radius: 3px 0 0 3px;
-  border-radius: 3px 0 0 3px;
-}
-.input-prepend.input-append .add-on:last-child,
-.input-prepend.input-append .btn:last-child {
-  margin-left: -1px;
-  -webkit-border-radius: 0 3px 3px 0;
-  -moz-border-radius: 0 3px 3px 0;
-  border-radius: 0 3px 3px 0;
-}
-.search-query {
-  padding-right: 14px;
-  padding-right: 4px \9;
-  padding-left: 14px;
-  padding-left: 4px \9;
-  /* IE7-8 doesn't have border-radius, so don't indent the padding */
-
-  margin-bottom: 0;
-  -webkit-border-radius: 14px;
-  -moz-border-radius: 14px;
-  border-radius: 14px;
-}
-.form-search input,
-.form-inline input,
-.form-horizontal input,
-.form-search textarea,
-.form-inline textarea,
-.form-horizontal textarea,
-.form-search select,
-.form-inline select,
-.form-horizontal select,
-.form-search .help-inline,
-.form-inline .help-inline,
-.form-horizontal .help-inline,
-.form-search .uneditable-input,
-.form-inline .uneditable-input,
-.form-horizontal .uneditable-input,
-.form-search .input-prepend,
-.form-inline .input-prepend,
-.form-horizontal .input-prepend,
-.form-search .input-append,
-.form-inline .input-append,
-.form-horizontal .input-append {
-  display: inline-block;
-  *display: inline;
-  /* IE7 inline-block hack */
-
-  *zoom: 1;
-  margin-bottom: 0;
-}
-.form-search .hide,
-.form-inline .hide,
-.form-horizontal .hide {
-  display: none;
-}
-.form-search label,
-.form-inline label {
-  display: inline-block;
-}
-.form-search .input-append,
-.form-inline .input-append,
-.form-search .input-prepend,
-.form-inline .input-prepend {
-  margin-bottom: 0;
-}
-.form-search .radio,
-.form-search .checkbox,
-.form-inline .radio,
-.form-inline .checkbox {
-  padding-left: 0;
-  margin-bottom: 0;
-  vertical-align: middle;
-}
-.form-search .radio input[type="radio"],
-.form-search .checkbox input[type="checkbox"],
-.form-inline .radio input[type="radio"],
-.form-inline .checkbox input[type="checkbox"] {
-  float: left;
-  margin-right: 3px;
-  margin-left: 0;
-}
-.control-group {
-  margin-bottom: 9px;
-}
-legend + .control-group {
-  margin-top: 18px;
-  -webkit-margin-top-collapse: separate;
-}
-.form-horizontal .control-group {
-  margin-bottom: 18px;
-  *zoom: 1;
-}
-.form-horizontal .control-group:before,
-.form-horizontal .control-group:after {
-  display: table;
-  content: "";
-}
-.form-horizontal .control-group:after {
-  clear: both;
-}
-.form-horizontal .control-label {
-  float: left;
-  width: 140px;
-  padding-top: 5px;
-  text-align: right;
-}
-.form-horizontal .controls {
-  *display: inline-block;
-  *padding-left: 20px;
-  margin-left: 160px;
-  *margin-left: 0;
-}
-.form-horizontal .controls:first-child {
-  *padding-left: 160px;
-}
-.form-horizontal .help-block {
-  margin-top: 9px;
-  margin-bottom: 0;
-}
-.form-horizontal .form-actions {
-  padding-left: 160px;
-}
-.nav {
-  margin-left: 0;
-  margin-bottom: 18px;
-  list-style: none;
-}
-.nav > li > a {
-  display: block;
-}
-.nav > li > a:hover {
-  text-decoration: none;
-  background-color: #eeeeee;
-}
-.nav > .pull-right {
-  float: right;
-}
-.nav .nav-header {
-  display: block;
-  padding: 3px 15px;
-  font-size: 11px;
-  font-weight: bold;
-  line-height: 18px;
-  color: #999999;
-  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
-  text-transform: uppercase;
-}
-.nav li + .nav-header {
-  margin-top: 9px;
-}
-.nav-list {
-  padding-left: 15px;
-  padding-right: 15px;
-  margin-bottom: 0;
-}
-.nav-list > li > a,
-.nav-list .nav-header {
-  margin-left: -15px;
-  margin-right: -15px;
-  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
-}
-.nav-list > li > a {
-  padding: 3px 15px;
-}
-.nav-list > .active > a,
-.nav-list > .active > a:hover {
-  color: #ffffff;
-  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
-  background-color: #0088cc;
-}
-.nav-list [class^="icon-"] {
-  margin-right: 2px;
-}
-.nav-list .divider {
-  *width: 100%;
-  height: 1px;
-  margin: 8px 1px;
-  *margin: -5px 0 5px;
-  overflow: hidden;
-  background-color: #e5e5e5;
-  border-bottom: 1px solid #ffffff;
-}
-.nav-tabs,
-.nav-pills {
-  *zoom: 1;
-}
-.nav-tabs:before,
-.nav-pills:before,
-.nav-tabs:after,
-.nav-pills:after {
-  display: table;
-  content: "";
-}
-.nav-tabs:after,
-.nav-pills:after {
-  clear: both;
-}
-.nav-tabs > li,
-.nav-pills > li {
-  display: inline-block;
-}
-.nav-tabs > li > a,
-.nav-pills > li > a {
-  padding-right: 12px;
-  padding-left: 12px;
-  margin-right: 2px;
-  line-height: 14px;
-}
-.nav-tabs {
-  border-bottom: 1px solid #ddd;
-}
-.nav-tabs > li {
-  margin-bottom: -1px;
-}
-.nav-tabs > li > a {
-  padding-top: 8px;
-  padding-bottom: 8px;
-  line-height: 18px;
-  border: 1px solid transparent;
-  -webkit-border-radius: 4px 4px 0 0;
-  -moz-border-radius: 4px 4px 0 0;
-  border-radius: 4px 4px 0 0;
-}
-.nav-tabs > li > a:hover {
-  border-color: #eeeeee #eeeeee #dddddd;
-}
-.nav-tabs > .active > a,
-.nav-tabs > .active > a:hover {
-  color: #555555;
-  background-color: #ffffff;
-  border: 1px solid #ddd;
-  border-bottom-color: transparent;
-  cursor: default;
-}
-.nav-pills > li > a {
-  padding-top: 8px;
-  padding-bottom: 8px;
-  margin-top: 2px;
-  margin-bottom: 2px;
-  -webkit-border-radius: 5px;
-  -moz-border-radius: 5px;
-  border-radius: 5px;
-}
-.nav-pills > .active > a,
-.nav-pills > .active > a:hover {
-  color: #ffffff;
-  background-color: #0088cc;
-}
-.nav-stacked > li {
-  float: none;
-}
-.nav-stacked > li > a {
-  margin-right: 0;
-}
-.nav-tabs.nav-stacked {
-  border-bottom: 0;
-}
-.nav-tabs.nav-stacked > li > a {
-  border: 1px solid #ddd;
-  -webkit-border-radius: 0;
-  -moz-border-radius: 0;
-  border-radius: 0;
-}
-.nav-tabs.nav-stacked > li:first-child > a {
-  -webkit-border-radius: 4px 4px 0 0;
-  -moz-border-radius: 4px 4px 0 0;
-  border-radius: 4px 4px 0 0;
-}
-.nav-tabs.nav-stacked > li:last-child > a {
-  -webkit-border-radius: 0 0 4px 4px;
-  -moz-border-radius: 0 0 4px 4px;
-  border-radius: 0 0 4px 4px;
-}
-.nav-tabs.nav-stacked > li > a:hover {
-  border-color: #ddd;
-  z-index: 2;
-}
-.nav-pills.nav-stacked > li > a {
-  margin-bottom: 3px;
-}
-.nav-pills.nav-stacked > li:last-child > a {
-  margin-bottom: 1px;
-}
-.nav-tabs .dropdown-menu {
-  -webkit-border-radius: 0 0 5px 5px;
-  -moz-border-radius: 0 0 5px 5px;
-  border-radius: 0 0 5px 5px;
-}
-.nav-pills .dropdown-menu {
-  -webkit-border-radius: 4px;
-  -moz-border-radius: 4px;
-  border-radius: 4px;
-}
-.nav-tabs .dropdown-toggle .caret,
-.nav-pills .dropdown-toggle .caret {
-  border-top-color: #0088cc;
-  border-bottom-color: #0088cc;
-  margin-top: 6px;
-}
-.nav-tabs .dropdown-toggle:hover .caret,
-.nav-pills .dropdown-toggle:hover .caret {
-  border-top-color: #005580;
-  border-bottom-color: #005580;
-}
-.nav-tabs .active .dropdown-toggle .caret,
-.nav-pills .active .dropdown-toggle .caret {
-  border-top-color: #333333;
-  border-bottom-color: #333333;
-}
-.nav > .dropdown.active > a:hover {
-  color: #000000;
-  cursor: pointer;
-}
-.nav-tabs .open .dropdown-toggle,
-.nav-pills .open .dropdown-toggle,
-.nav > li.dropdown.open.active > a:hover {
-  color: #ffffff;
-  background-color: #999999;
-  border-color: #999999;
-}
-.nav li.dropdown.open .caret,
-.nav li.dropdown.open.active .caret,
-.nav li.dropdown.open a:hover .caret {
-  border-top-color: #ffffff;
-  border-bottom-color: #ffffff;
-  opacity: 1;
-  filter: alpha(opacity=100);
-}
-.tabs-stacked .open > a:hover {
-  border-color: #999999;
-}
-.tabbable {
-  *zoom: 1;
-}
-.tabbable:before,
-.tabbable:after {
-  display: table;
-  content: "";
-}
-.tabbable:after {
-  clear: both;
-}
-.tab-content {
-  overflow: auto;
-}
-.tabs-below > .nav-tabs,
-.tabs-right > .nav-tabs,
-.tabs-left > .nav-tabs {
-  border-bottom: 0;
-}
-.tab-content > .tab-pane,
-.pill-content > .pill-pane {
-  display: none;
-}
-.tab-content > .active,
-.pill-content > .active {
-  display: block;
-}
-.tabs-below > .nav-tabs {
-  border-top: 1px solid #ddd;
-}
-.tabs-below > .nav-tabs > li {
-  margin-top: -1px;
-  margin-bottom: 0;
-}
-.tabs-below > .nav-tabs > li > a {
-  -webkit-border-radius: 0 0 4px 4px;
-  -moz-border-radius: 0 0 4px 4px;
-  border-radius: 0 0 4px 4px;
-}
-.tabs-below > .nav-tabs > li > a:hover {
-  border-bottom-color: transparent;
-  border-top-color: #ddd;
-}
-.tabs-below > .nav-tabs > .active > a,
-.tabs-below > .nav-tabs > .active > a:hover {
-  border-color: transparent #ddd #ddd #ddd;
-}
-.tabs-left > .nav-tabs > li,
-.tabs-right > .nav-tabs > li {
-  float: none;
-}
-.tabs-left > .nav-tabs > li > a,
-.tabs-right > .nav-tabs > li > a {
-  min-width: 74px;
-  margin-right: 0;
-  margin-bottom: 3px;
-}
-.tabs-left > .nav-tabs {
-  float: left;
-  margin-right: 19px;
-  border-right: 1px solid #ddd;
-}
-.tabs-left > .nav-tabs > li > a {
-  margin-right: -1px;
-  -webkit-border-radius: 4px 0 0 4px;
-  -moz-border-radius: 4px 0 0 4px;
-  border-radius: 4px 0 0 4px;
-}
-.tabs-left > .nav-tabs > li > a:hover {
-  border-color: #eeeeee #dddddd #eeeeee #eeeeee;
-}
-.tabs-left > .nav-tabs .active > a,
-.tabs-left > .nav-tabs .active > a:hover {
-  border-color: #ddd transparent #ddd #ddd;
-  *border-right-color: #ffffff;
-}
-.tabs-right > .nav-tabs {
-  float: right;
-  margin-left: 19px;
-  border-left: 1px solid #ddd;
-}
-.tabs-right > .nav-tabs > li > a {
-  margin-left: -1px;
-  -webkit-border-radius: 0 4px 4px 0;
-  -moz-border-radius: 0 4px 4px 0;
-  border-radius: 0 4px 4px 0;
-}
-.tabs-right > .nav-tabs > li > a:hover {
-  border-color: #eeeeee #eeeeee #eeeeee #dddddd;
-}
-.tabs-right > .nav-tabs .active > a,
-.tabs-right > .nav-tabs .active > a:hover {
-  border-color: #ddd #ddd #ddd transparent;
-  *border-left-color: #ffffff;
-}
-.pagination {
-  height: 36px;
-  margin: 18px 0;
-}
-.pagination ul {
-  display: inline-block;
-  *display: inline;
-  /* IE7 inline-block hack */
-
-  *zoom: 1;
-  margin-left: 0;
-  margin-bottom: 0;
-  -webkit-border-radius: 3px;
-  -moz-border-radius: 3px;
-  border-radius: 3px;
-  -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
-  -moz-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
-  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
-}
-.pagination li {
-  display: inline;
-}
-.pagination a {
-  float: left;
-  padding: 0 14px;
-  line-height: 34px;
-  text-decoration: none;
-  border: 1px solid #ddd;
-  border-left-width: 0;
-}
-.pagination a:hover,
-.pagination .active a {
-  background-color: #f5f5f5;
-}
-.pagination .active a {
-  color: #999999;
-  cursor: default;
-}
-.pagination .disabled span,
-.pagination .disabled a,
-.pagination .disabled a:hover {
-  color: #999999;
-  background-color: transparent;
-  cursor: default;
-}
-.pagination li:first-child a {
-  border-left-width: 1px;
-  -webkit-border-radius: 3px 0 0 3px;
-  -moz-border-radius: 3px 0 0 3px;
-  border-radius: 3px 0 0 3px;
-}
-.pagination li:last-child a {
-  -webkit-border-radius: 0 3px 3px 0;
-  -moz-border-radius: 0 3px 3px 0;
-  border-radius: 0 3px 3px 0;
-}
-.pagination-centered {
-  text-align: center;
-}
-.pagination-right {
-  text-align: right;
-}
-.well {
-  min-height: 20px;
-  padding: 19px;
-  margin-bottom: 20px;
-  background-color: #f5f5f5;
-  border: 1px solid #eee;
-  border: 1px solid rgba(0, 0, 0, 0.05);
-  -webkit-border-radius: 4px;
-  -moz-border-radius: 4px;
-  border-radius: 4px;
-  -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
-  -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
-  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
-}
-.well blockquote {
-  border-color: #ddd;
-  border-color: rgba(0, 0, 0, 0.15);
-}
-.well-large {
-  padding: 24px;
-  -webkit-border-radius: 6px;
-  -moz-border-radius: 6px;
-  border-radius: 6px;
-}
-.well-small {
-  padding: 9px;
-  -webkit-border-radius: 3px;
-  -moz-border-radius: 3px;
-  border-radius: 3px;
-}
diff --git a/scp/css/dashboard.css b/scp/css/dashboard.css
index 43ec338890ac6bcce9d7647f85841657d362fe88..249c43597aa05e73bcd7a3888f651313cfc05d08 100644
--- a/scp/css/dashboard.css
+++ b/scp/css/dashboard.css
@@ -27,20 +27,3 @@ span.label.disabled {
 span.label {
     cursor: pointer;
 }
-#table-here tr :not(:first-child) {
-    text-align: right;
-    padding-right: 2.3em;
-    width: 12%;
-}
-#table-here tr :not(:first-child) div {
-    position: relative;
-    margin-right: -1em;
-}
-#table-here tr :not(:first-child) div div {
-    position: absolute;
-    -moz-border-radius: 1em;
-    -webkit-border-radius: 1em;
-    border-radius: 1em;
-}
-
-
diff --git a/scp/css/dropdown.css b/scp/css/dropdown.css
index 4fb664178aaa2ab61f663d6b9219ad00740bf64d..1106463645a48208b3526afe37ee08423e7db3ff 100644
--- a/scp/css/dropdown.css
+++ b/scp/css/dropdown.css
@@ -5,13 +5,13 @@
 
 .action-dropdown {
   position: absolute;
-  z-index: 9999999;
+  z-index: 9999998;
   display: none;
   margin-top: 8px;
 }
 .action-dropdown ul {
   text-align: left;
-  font-size: 13px;
+  font-size: 0.95em;
   min-width: 140px;
   list-style: none;
   background: #FFF;
@@ -34,14 +34,33 @@
   color: #555;
   text-decoration: none;
   line-height: 18px;
-  padding: 3px 15px;
+  padding: 3px 10px;
   white-space: nowrap;
 }
-.action-dropdown ul li > a:hover {
+.action-dropdown ul.bleed-left li > a {
+  padding-left: 8px;
+}
+.action-dropdown ul li > a i {
+  margin-right: 0.1em;
+}
+.action-dropdown ul li > a:hover,
+.action-dropdown ul li.active > a:hover {
   background-color: #08C;
   color: #FFF !important;
   cursor: pointer;
 }
+.action-dropdown ul li.active > a {
+  background-color: rgba(0, 136, 204, 0.2);
+  color: #08C;
+}
+.action-dropdown ul li.danger > a:hover {
+  background-color: #CF3F3F;
+}
+.action-dropdown ul li > a.disabled {
+  pointer-events: none;
+  color: #999;
+  color: rgba(85,85,85,0.5);
+}
 .action-dropdown hr {
   height: 1px;
   border: none;
@@ -80,54 +99,45 @@
   left: auto;
   right: 10px;
 }
-
-.action-button {
-  -webkit-border-radius: 3px;
-  -moz-border-radius: 3px;
-  border-radius: 3px;
-  -webkit-background-clip: padding-box;
-  -moz-background-clip: padding;
-  background-clip: padding-box;
-  color: #777 !important;
-  display: inline-block;
-  border: 1px solid #aaa;
-  cursor: pointer;
-  font-size: 11px;
-  overflow: hidden;
-  background-color: #dddddd;
-  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #efefef), color-stop(100% #dddddd));
-  background-image: -webkit-linear-gradient(top, #efefef 0%, #dddddd 100%);
-  background-image: -moz-linear-gradient(top, #efefef 0%, #dddddd 100%);
-  background-image: -ms-linear-gradient(top, #efefef 0%, #dddddd 100%);
-  background-image: -o-linear-gradient(top, #efefef 0%, #dddddd 100%);
-  background-image: linear-gradient(top, #efefef 0%, #dddddd 100%);
-  padding: 0 5px;
-  text-decoration: none !important;
-  line-height:18px;
-  margin-left:5px;
-}
 .action-button span,
 .action-button a {
-  color: #777 !important;
+  color: inherit;
   display: inline-block;
   float: left;
 }
 .action-button i.icon-caret-down {
-  background-color: #dddddd;
-  background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #efefef), color-stop(100% #dddddd));
-  background-image: -webkit-linear-gradient(top, #efefef 0%, #dddddd 100%);
-  background-image: -moz-linear-gradient(top, #efefef 0%, #dddddd 100%);
-  background-image: -ms-linear-gradient(top, #efefef 0%, #dddddd 100%);
-  background-image: -o-linear-gradient(top, #efefef 0%, #dddddd 100%);
-  background-image: linear-gradient(top, #efefef 0%, #dddddd 100%);
-  height: 18px;
-  line-height: 18px;
-  margin-right: 0;
-  margin-left: 5px;
-  padding-left: 5px;
-  border-left: 1px solid #aaa;
+    height: 17px;
+    line-height: 100%;
+    margin-right: -1px;
+    margin-left: 5px;
+    padding-left: 5px;
+    margin-top: -1px;
+    padding-top: 7px;
+    display: inline-block;
+    border-left: 1px solid #ccc;
 }
-.action-button a {
-  color: #777;
+.action-button:hover i.icon-caret-down {
+    border-color: inherit;
+}
+a.action-button, .action-button a {
+  color: inherit;
   text-decoration: none;
 }
+.action-buttons {
+    display: inline-block;
+    vertical-align: middle;
+}
+.action-buttons .action-button + .action-button {
+    margin-left: 0;
+    padding-left: 0;
+    border-left: none;
+    border-top-left-radius: 0;
+    border-bottom-left-radius: 0;
+}
+.action-buttons .action-button:not(:last-of-type) {
+    margin-right: 0;
+    padding-right: 0;
+    border-right: none;
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+}
diff --git a/scp/css/login.css b/scp/css/login.css
index a5a682f08d04b513b643b044f817416988895d56..71d6cb7fcedac7dbbb217e0d1d977a14efd711ce 100644
--- a/scp/css/login.css
+++ b/scp/css/login.css
@@ -1,6 +1,7 @@
 input:focus {
-    border: 1px solid rgb(207,16,118);
-    box-shadow: 0 0 4px rgb(207,16,118);
+    border: 1px solid orange;
+    box-shadow: 0 0 4px orange;
+    outline: none;
 }
 
 :-webkit-input-placeholder {
@@ -23,9 +24,6 @@ html {
 
 body {
     -webkit-font-smoothing:antialiased;
-    background:url(../images/login-background.jpg);
-    background-repeat: repeat-x;
-    background-attachment: fixed;
     font-size: 16px;
     font-smoothing:antialiased;
     height:100%;
@@ -34,33 +32,100 @@ body {
     text-align: center;
 }
 
+#brickwall {
+    background-image: url(../logo.php?backdrop);
+    -webkit-background-size: cover;
+       -moz-background-size: cover;
+         -o-background-size: cover;
+            background-size: cover;
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+}
+
+#background-compat {
+    display: none;
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background-color: rgba(255, 165, 0, 0.7);
+}
+#background {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: -1;
+    background-image: url(../logo.php?backdrop);
+    -webkit-background-size: cover;
+       -moz-background-size: cover;
+         -o-background-size: cover;
+            background-size: cover;
+    background-attachment: fixed;
+    mix-blend-mode: normal;
+    filter:"progid:DXImageTransform.Microsoft.Blur(PixelRadius='6')";
+    -ms-filter:"progid:DXImageTransform.Microsoft.Blur(PixelRadius='6')";
+}
+
+#blur {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    -webkit-filter: blur(7px);
+    filter: url("data:image/svg+xml;utf9,<svg%20version='1.1'%20xmlns='http://www.w3.org/2000/svg'><filter%20id='blur'><feGaussianBlur%20stdDeviation='5'%20/></filter></svg>#blur");
+    filter: blur(7px);
+    overflow: hidden;
+    z-index: -1;
+}
+
 body, input {
     font-family: helvetica, arial, sans-serif;
     color: #000;
 }
 
-input[type=reset], input[type=submit], input[type=button] {
-    display: inline-block;
-    -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
-    -moz-tap-highlight-color: rgba(0, 0, 0, 0);
-    -o-tap-highlight-color: rgba(0, 0, 0, 0);
-    tap-highlight-color: rgba(0, 0, 0, 0);
-}
-
 #loginBox {
-    border:1px solid #2a67ac;
-    border-right:2px solid #2a67ac;
-    border-bottom:3px solid #2a67ac;
-    box-shadow: 2px 2px 8px rgba(42, 103, 172, 0.5);
-    background:#fff;
+    box-shadow:0 0 50px 5px rgba(0,0,0,0.3), 0 0 5px -1px white;
+    mix-blend-mode: normal;
+    isolation: isolate;
     width:400px;
-    margin:10% auto 0 auto;
     padding:1em;
     padding-bottom: 1em;
     text-align:center;
     -moz-box-sizing: border-box;
     -webkit-box-sizing: border-box;
     box-sizing: border-box;
+    position: fixed;
+    left: 50%;
+    top: 15%;
+    margin-left: -200px;
+    border-radius: 5px;
+}
+
+#loginBox:after {
+    content: " ";
+    position: absolute;
+    left: 0;
+    right: 0;
+    top: 0;
+    bottom: 0;
+    background-color: rgba(255,255,255,0.6);
+    border-radius: 5px;
+    z-index: -1;
+    mix-blend-mode: normal;
+    border: 1px solid rgba(0,0,0,0.3);
+}
+/* IE 9-10 */
+@media screen\0 {
+    #loginBox:after {
+        background-color: white;
+    }
 }
 
 h1 {
@@ -99,7 +164,6 @@ h3 {
 form {
     width:220px;
     margin:0 auto;
-    overflow:hidden;
 }
 
 fieldset {
@@ -113,9 +177,10 @@ fieldset input {
     margin-bottom:1em;
     border:1px solid #ccc;
     border:1px solid rgba(0,0,0,0.3);
-    background:#fff;
+    background: white;
+    background: rgba(255, 255, 255, 0.5);
     padding:2px 4px;
-    width: 96%;
+    width: 100%;
 }
 
 hr {
@@ -133,32 +198,116 @@ div.banner:not(:empty) {
     margin-bottom: 1em;
 }
 
-input.submit {
-    border-radius: 4px;
+* {
+    box-sizing: border-box;
+}
+
+input[type="submit"],
+input[type="reset"],
+input[type="button"],
+.action-button,
+.button {
+    cursor: pointer;
     display:inline-block;
-    margin:0.25em;
+    vertical-align:bottom;
     height:24px;
-    line-height:24px;
-    font-weight:bold;
-    border:1px solid #666666;
-    padding:0 30px;
-    background: url('../images/grey_btn_bg.png?1312910883') top left repeat-x;
-    color: #333;
+    line-height: 22px;
+    border: none;
+    box-shadow: 0 0 0 1px rgba(0,0,0,0.25) inset;
+    padding:2px 11px;
+    color: #555;
+    background-color: #f0f0f0;
+    background-color: rgba(0,0,0,0.02);
+    -webkit-border-radius: 3px;
+    -moz-border-radius: 3px;
+    border-radius: 3px;
+    font-family: inherit;
+    font-size: 0.8em;
+    text-decoration: none;
+-webkit-user-select: none;
+   -moz-user-select: none;
+    -ms-user-select: none;
+        user-select: none;
+-webkit-transition: opacity 0.1s ease, background-color 0.1s ease, box-shadow 0.1s ease, color 0.1s ease, background 0.1s ease;
+        transition: opacity 0.1s ease, background-color 0.1s ease, box-shadow 0.1s ease, color 0.1s ease, background 0.1s ease;
+}
+input[type="submit"] i,
+input[type="reset"] i,
+input[type="button"] i,
+.action-button i,
+.button i {
+  margin-right: 0.1em;
+}
+
+.button:hover {
+  text-decoration: none;
+}
+
+button[type=submit], input[type="submit"], .primary.button {
+    font-weight: bold;
+    box-shadow: 0 0 0 1px rgba(0,0,0,0.45) inset;
+    background-color: rgba(0,0,0,0.07);
+}
+
+button[type=submit]:hover, input[type=submit]:hover, input[type=submit]:active {
+  color: white;
+  box-shadow: 0 0 0 2px rgba(0,0,0,0.7) inset;
+  background-color: #888;
+  background-color: rgba(0, 0, 0, 0.5);
 }
 
 input.submit:hover, input.submit:active {
     background-position:bottom left;
 }
 
-#copyRights {
-    font-size:0.7em;
-    color:#888;
-    padding:1em;
+#poweredBy {
+    font-size:0.8em;
+    color:#ccc;
+    padding:5px 15px;
     text-align:center;
+    position: fixed;
+    bottom: 10px;
+    right: 10px;
+    box-shadow: inset 0 0 3px white, 0 0 2px black, inset 0 0 4px black;
+    text-shadow: 0 0 2px black;
+    border-radius: 5px;
+    background-color: rgba(0,0,0,0.3)
 }
 
-#copyRights a {
-    color:#888;
+#poweredBy a {
+    color:inherit;
+}
+
+#poweredBy .osticket-logo {
+    height: auto;
+    width: 55px;
+    vertical-align: baseline;
+    border: none;
+    -webkit-filter: drop-shadow(0 0 4px black);
+    filter: drop-shadow(0 0 4px black);
+    ms-filter: progid:DXImageTransform.Microsoft.Dropshadow(OffX=0, OffY=0, Color='#444');
+    filter: progid:DXImageTransform.Microsoft.Dropshadow(OffX=0, OffY=0, Color='#444');
+}
+
+#company {
+    position:absolute;
+    left: 50%;
+    width: 400px;
+    margin-left: -200px;
+    bottom: -40px;
+    font-size: 0.8em;
+    color: #ccc;
+    text-align: center;
+}
+
+#company .content {
+    border-radius: 10px;
+    background-color: rgba(0,0,0,0.3);
+    box-shadow: 0 0 6px rgba(0,0,0,0.4);
+    text-shadow: 0 0 2px black;
+    display: inline-block;
+    padding: 0 15px;
+    color: white;
 }
 
 .external-auth {
@@ -202,4 +351,5 @@ input[type=text],
 input[type=password] {
     border-radius: 4px;
     padding: 5px;
+    font-size: 0.75em;
 }
diff --git a/scp/css/scp.css b/scp/css/scp.css
index 6d9780787f9ceee27992d3eee9c41133240b096c..2b12d668b35e890300911594ede78abbd0fc131c 100644
--- a/scp/css/scp.css
+++ b/scp/css/scp.css
@@ -1,20 +1,28 @@
 body {
+  font-family: "Lato", "Helvetica Neue", arial, helvetica, sans-serif;
+  font-weight: 400;
+  letter-spacing: 0.15px;
+  -webkit-font-smoothing:antialiased;
+          font-smoothing:antialiased;
+}
+body, html {
     background:#eee;
-    font-family:arial, helvetica, sans-serif;
-    font-size:10pt;
     color:#000;
+    font-size:14px;
     margin:0;
     padding:0;
 }
 
+.link,
 a {
     color:#184E81;
     text-decoration:none;
     display: inline-block;
 }
 
-a:hover {
+a:hover, .link:hover {
     text-decoration: underline;
+    cursor: pointer;
 }
 
 #nav a:hover,
@@ -28,10 +36,6 @@ div#header a {
     color:#E65524;
 }
 
-.form_table a:hover {
-    text-decoration: underline;
-}
-
 .centered {
     text-align:center;
 }
@@ -39,17 +43,46 @@ div#header a {
 .full-width {
     width: 100%;
 }
-
-.search-input {
-    height: 20px;
+.headline {
+    margin-bottom: 15px;
 }
 
 .clear {
     clear:both;
 }
 
+.big {
+    font-size: 110%;
+}
+
 .faded {
-    color:#666;
+    color: #666;
+    color: rgba(0,0,0,0.5);
+}
+.faded b {
+    color: #333;
+    color: rgba(0,0,0,0.75);
+}
+.faded strong {
+    color: #444;
+    color: rgba(0,0,0,0.6);
+}
+.faded-more {
+    color: #aaa;
+    color: rgba(0,0,0,0.35);
+}
+time[title]:hover {
+    text-decoration: underline;
+}
+a time {
+    color: initial;
+}
+
+.small[class^="icon-"],
+.small[class*=" icon-"] {
+    vertical-align: baseline;
+    padding-left: 2px;
+    font-size: 80%;
 }
 
 .strike { text-decoration:line-through; color:red; }
@@ -61,16 +94,21 @@ div#header a {
 #breadcrumbs {
     color: #333;
     margin-bottom: 15px;
+    background-color:#F4FAFF;
+    padding:8px;
 }
 
 #breadcrumbs a {
     color: #555;
 }
 
+.banner { margin: 0; padding: 5px 5px 11px; margin-bottom: 10px; color: #444; border: 1px solid #444;  background-color: #ddd; border-radius: 4px; }
+
 #msg_info,
 .info-banner { margin: 0; padding: 5px; margin-bottom: 10px; color: #3a87ad; border: 1px solid #bce8f1;  background-color: #d9edf7; }
 
 #msg_notice,
+.success-banner,
 .notice-banner { margin: 0; padding: 5px 10px 5px 36px; margin-bottom: 10px; border: 1px solid #0a0; background: url('../images/icons/ok.png') 10px 50% no-repeat #e0ffe0; }
 
 #msg_warning,
@@ -167,7 +205,6 @@ div#header a {
     height:26px;
     color:#555;
     text-align:center;
-    font-weight:bold;
     position:relative;
 
     border-radius:5px 5px 0 0;
@@ -183,6 +220,7 @@ div#header a {
 
 #nav .active a {
     color:#004a80;
+    font-weight:bold;
 }
 
 #nav > li + li {
@@ -204,7 +242,7 @@ div#header a {
     width:230px;
     background:#fbfbfb;
     margin:0;
-    padding:0;
+    padding:5px 0;
     position:absolute;
     left: -1px;
     z-index:500;
@@ -212,9 +250,7 @@ div#header a {
     border-left:1px solid #ccc;
     border-right:1px solid #ccc;
     border-radius: 0 0 5px 5px;
-
     display:block;
-    padding-left: 5px;
     -moz-box-shadow: 3px 3px 3px #ccc;
     -webkit-box-shadow: 3px 3px 3px #ccc;
     box-shadow: 3px 3px 3px #ccc;
@@ -228,7 +264,7 @@ div#header a {
 #nav .inactive li {
     display:block;
     margin:0;
-    padding:0 5px;
+    padding:0;
     list-style:none;
     text-align:left;
 }
@@ -252,19 +288,21 @@ div#header a {
 }
 
 #nav .inactive li a {
-    padding-left:24px;
-    background-position:0 50%;
-    background-repeat:no-repeat;
-    font-weight:normal;
+    background-position: 10px 50%;
+    background-repeat: no-repeat;
+    padding: 0 10px 0 34px;
 }
 
 #nav .inactive li a:hover {
     color:#E65524;
+    background-color: #fbfbfb;
+    background-color: rgba(0,0,0,0.05);
 }
 
 #sub_nav {
     background:#f7f7f7;
     border-bottom:1px solid #bebebe;
+    padding: 2px 20px;
 }
 
 #sub_nav a {
@@ -314,7 +352,6 @@ a.userPref { background:url(../images/icons/user_preferences.gif) }
 a.userPasswd { background:url(../images/icons/change_password.gif) }
 
 a.preferences { background:url(../images/icons/settings.gif) }
-a.attachment { background:url(../images/icons/attachment.gif ) }
 a.api { background:url(../images/icons/api.png) }
 a.newapi { background:url(../images/icons/new_api.png) }
 
@@ -372,6 +409,7 @@ a.lists { background:url(../images/icons/icon-list.png); background-size: 16px 1
     background-repeat: no-repeat;
     min-height: 16px;
     display: inline-block;
+    vertical-align: middle;
 }
 
 
@@ -436,7 +474,7 @@ a.Icon:hover {
     background:#fff;
 }
 
-#content a:not(.re-icon) {
+a {
     color:#184E81;
 }
 
@@ -444,7 +482,34 @@ a.Icon:hover {
     clear:both;
     padding:10px;
     text-align:center;
-    font-size:9pt;
+    font-size:0.9em;
+}
+table.dashboard-stats {
+    text-align:right;
+    border-bottom: 2px solid #ddd;
+}
+table.dashboard-stats tbody:first-child th {
+    border-bottom:1px dashed #ddd;
+    padding:0 4px 8px;
+}
+table.dashboard-stats tbody:nth-child(2) tr:nth-child(odd) {
+    background-color:#f0faff;
+}
+
+table.dashboard-stats tbody:nth-child(2) th {
+    padding:5px 8px;
+    border-right: 1px solid #ccc;
+    color:#999;
+}
+table.dashboard-stats tbody:nth-child(2) td {
+    padding:5px 4px;
+    border-right: 1px solid #ccc;
+}
+table.dashboard-stats tbody:nth-child(2) tr:hover {
+    background-color:#FFFFDD;
+}
+table.dashboard-stats tbody:nth-child(2) tr:hover th {
+    color:#000;
 }
 
 table { vertical-align:top; }
@@ -454,8 +519,6 @@ table.list {
     background:#ccc;
     margin: 2px 0;
     border-bottom: 1px solid #ccc;
-    font-family:arial, helvetica, sans-serif;
-    font-size:10pt;
 }
 
 table.list caption {
@@ -471,16 +534,17 @@ table.list thead th {
     color:#000;
     text-align:left;
     vertical-align:top;
-    padding: 0 4px;
+    padding: 4px 5px;
 }
 
 table.list th a {
-
     text-decoration:none;
     color:#000;
+    margin: -4px -5px;
+    padding: 4px 5px;
 }
 
-table.list thead th a { padding: 3px; padding-right: 15px; display: block; white-space: nowrap; color: #000; background: url('../images/asc_desc.gif') 100% 50% no-repeat; }
+table.list thead th a { padding-right: 15px; display: block; white-space: nowrap; color: #000; background: url('../images/asc_desc.gif') 100% 50% no-repeat; }
 
 table.list thead th a.asc { background: url('../images/asc.gif') 100% 50% no-repeat #cfe6ff; }
 table.list thead th a.desc { background: url('../images/desc.gif') 100% 50% no-repeat #cfe6ff; }
@@ -491,7 +555,7 @@ table.list tbody td {
     vertical-align:top;
 }
 
-table.list tbody td { background: #fff; padding: 1px 3px; vertical-align: top; }
+table.list tbody td { background: #fff; padding: 4px 3px; vertical-align: top; }
 table.list tbody tr:nth-child(2n+1) td { background-color: #f0faff; }
 table.list tbody tr:hover td { background: #ffe; }
 table.list tbody tr:nth-child(2n+1):hover td { background: #ffd; }
@@ -501,8 +565,12 @@ table.list tbody tr:hover td, table.list tbody tr.highlight td {  background: #F
 table.list tbody tr:hover td.nohover, table.list tbody tr.highlight td.nohover {}
 
 
-table.list tfoot td {
+table tfoot td {
     background:#eee;
+    padding: 1px;
+}
+
+table.list tfoot td {
     padding: 2px;
 }
 
@@ -561,28 +629,15 @@ a.print {
     background-image:url(../images/icons/printer.gif);
 }
 
-.btn {
-    padding:3px 10px;
-    background:url(../images/btn_bg.png) top left repeat-x #ccc;
-    border:1px solid #777;
-    color:#000;
-}
-
-#actions button, .button { padding:2px 5px 3px; margin-right:10px;  color:#777;}
-
 .btn_sm {
     padding:2px 5px;
-    font-size:9pt;
+    font-size:0.9em;
     background:url(../images/btn_sm_bg.png) top left repeat-x #f90;
     border:1px solid #777;
     color:#fff;
     font-weight:bold;
 }
 
-.btn:hover, .btn_sm:hover {
-    background-position: bottom left;
-}
-
 .search label {
     display:block;
     line-height:25px;
@@ -597,6 +652,43 @@ a.print {
     padding:2px;
 }
 
+.table {
+    width: 100%;
+    border-collapse: collapse;
+    margin-top:3px;
+}
+
+.table tr.header td,
+.table tr.header th,
+.table > thead th {
+    font-weight: 400;
+    font-size: 1.3em;
+    text-align: left;
+    min-height: 24px;
+}
+.table tbody:not(:first-child) th {
+  padding-top: 1.4em;
+}
+
+.table tr:not(:last-child):not(.header) {
+    border-bottom:1px dotted #ddd;
+}
+.table tr.header {
+    border-bottom: 1px dotted #777;
+}
+.table td:not(:empty) {
+    padding: 5px;
+}
+.table.two-column tbody tr td:first-child {
+    width: 25%;
+}
+.table > tbody > tr.header + tr td {
+  padding-top: 10px;
+}
+.table td .pull-right {
+  margin-right: 15px;
+}
+
 .form_table {
     margin-top:3px;
     border-left:1px solid #ddd;
@@ -605,6 +697,7 @@ a.print {
 
 .form_table td {
     border-bottom:1px solid #ddd;
+    padding: 4px;
 }
 .form_table td:not(:empty) {
     height: 20px;
@@ -612,13 +705,18 @@ a.print {
 
 table.fixed {
     table-layout: fixed;
-    border-collapse: collapse;
     width: 100%;
 }
-table.fixed td {
+table.fixed > thead > tr > th:not([width]),
+table.fixed > thead > tr > td:not([width]),
+table.fixed > tbody > tr > td:not([width]),
+table.fixed > tr > td:not([width]) {
     width: 180px;
 }
-table.fixed td + td {
+table.fixed > thead > tr > th + th:not([width]),
+table.fixed > thead > tr > td + td:not([width]),
+table.fixed > tbody > tr > td + td:not([width]),
+table.fixed > tr > td + td:not([width]) {
     width: auto;
 }
 
@@ -628,9 +726,15 @@ td.multi-line {
     padding-bottom: 0.4em;
 }
 
-.form_table input[type=text], .form_table input[type=password], .form_table textarea {
+input[type=text], input[type=password], textarea, input {
+    padding: 3px 5px;
+    font-size: 0.95em;
+    font-family: inherit;
     background:#fff;
     border:1px solid #aaa;
+    border-radius:4px;
+   -webkit-border-radius: 4px;
+   -moz-border-radius: 4px;
 }
 
 .form_table input[type=radio], .form_table input[type=checkbox] {
@@ -643,7 +747,7 @@ td.multi-line {
 }
 
 .form_table em {
-    font-weight:normal;
+    font-weight:400;
     color:#666;
 }
 
@@ -704,8 +808,8 @@ div.section-break h3 {
 }
 
 .settings_table h4 a span {
-    font-size:12pt;
-    line-height:14px;
+    font-size:1.2em;
+    line-height:1.15em;
     display:inline-block;
     width:14px;
     height:14px;
@@ -718,9 +822,9 @@ div.section-break h3 {
 }
 
 h2 {
-    margin:0;
+    margin:0 0 0.7em;
     padding:0;
-    font-size:12pt;
+    font-size:1.4em;
     color:#0A568E;
 }
 
@@ -731,14 +835,26 @@ h2 i {
     color:#0a0;
 }
 
-h2 span { color:#000; }
+h2 small {
+    font-size:.8em;
+}
+/*h2 span { color:#000; }*/
 
 h3 {
     margin:10px 0 0 0;
     padding:5px 0;
-    font-size:10pt;
     color:#444;
 }
+.tixTitle {
+   padding:0 5px 0px;
+}
+.tixTitle h3 {
+   color:#444;
+   padding:0;
+   margin:0;
+   font-size:1.4em;
+   font-weight:300;
+}
 
 .has_bottom_border {
     padding-bottom:5px;
@@ -753,6 +869,17 @@ h3 {
     background:#F4FAFF;
 }
 
+.ticket_info.custom-data thead th {
+    border-bottom: 2px solid #ccc;
+    background-color: white;
+}
+.custom-data th, .custom-data td {
+    padding: 3px;
+}
+table.custom-data {
+    margin-bottom: 1em;
+}
+
 .right_align { text-align:right; }
 
 h2 .reload {
@@ -771,9 +898,6 @@ h2 .reload {
     border:1px solid #f90;
 }
 
-
-
-
 #ticket_actions {
     padding:5px;
     background:#eee;
@@ -781,102 +905,173 @@ h2 .reload {
     border-bottom:none;
     margin:0;
 }
-
-#threads {
-    margin:0;
-    padding:5px 10px 0 10px;
-    border:1px solid #aaa;
-    background:#F4FAFF;
-    height:30px;
+/***** top page ticket response buttons *****/
+a#post-note:hover {
+    background-color:#fff9e2;
+    color:#555!IMPORTANT;
 }
 
-#threads li {
-    list-style:none;
-    margin:0;
-    padding:0;
-    display:inline;
+.thread-entry {
+    margin-bottom: 15px;
+    z-index: 0;
 }
-
-#threads li a {
-    display:inline-block;
-    width:auto;
-    height:30px;
-    line-height:30px;
-    border-top:1px solid #F4FAFF;
-    padding:0 10px 0 32px;
-    margin-right:10px;
+.thread-entry::after {
+  content: "";
+  border-bottom: 2px solid white;
+  display: block;
 }
-
-#threads li a.active {
-    height:29px;
-    background-color:#fff;
-    border:1px solid #aaa;
-    border-bottom:none;
-    border-top:2px solid #ed9100;
-    font-weight:bold;
+.thread-entry::before {
+  content: "";
+  display: block;
+  border-top: 2px solid white;
 }
-
-#toggle_ticket_thread {
-    background:url(../images/icons/open.gif) 10px 50% no-repeat;
+.thread-entry.avatar {
+    margin-left: 60px;
 }
-
-#toggle_notes {
-    background:url(../images/icons/note.gif) 10px 50% no-repeat;
+.thread-entry.message.avatar {
+    margin-right: 60px;
+    margin-left: 0;
 }
-
-#ticket_thread table.message,
-#ticket_thread table.response,
-#ticket_thread table.note {
-    margin-top:10px;
-    border:1px solid #aaa;
-    border-bottom:2px solid #aaa;
+.thread-entry > .avatar {
+    margin-left: -60px;
+    display:inline-block;
+    width:48px;
+    height:auto;
 }
-
-#ticket_notes table {
-    margin-top:10px;
-    border:1px solid #ddd;
-    border-bottom:2px solid #ddd;
+.avatar {
+    border-radius: 12%;
 }
-
-#ticket_thread table th, #ticket_notes table th {
-    text-align:left;
-    border-bottom:1px solid #aaa;
-    font-size:10pt;
-    padding:5px;
+.thread-entry.message > .avatar {
+    margin-left: initial;
+    margin-right: -60px;
 }
-
-#ticket_notes table th {
-    text-align:left;
-    border-bottom:1px solid #ddd;
-    font-size:10pt;
-    padding:5px;
-    background:#F4FAFF;
+img.avatar {
+    border-radius: inherit;
+    vertical-align: middle;
+    margin-right: 5px;
 }
-
-#ticket_notes table th em {
-    font-weight:normal;
-    font-size:10pt;
-    color:#666;
+.avatar > img.avatar {
+    width: 100%;
+    height: auto;
 }
-
-#ticket_notes .date {
-    font-weight:normal;
-    font-size:10pt;
-    color:#888;
-    text-align:right;
+.thread-entry .header {
+    padding: 8px 0.9em;
+    border: 1px solid #ccc;
+    border-color: rgba(0,0,0,0.2);
+    border-radius: 5px 5px 0 0;
+}
+.thread-entry.avatar .header:before {
+  position: absolute;
+  top: 7px;
+  right: -8px;
+  content: '';
+  border-top: 8px solid transparent;
+  border-bottom: 8px solid transparent;
+  border-left: 8px solid #9cadcc;
+  display: inline-block;
+}
+.thread-entry.avatar .header:after {
+  position: absolute;
+  top: 7px;
+  right: -8px;
+  content: '';
+  border-top: 7px solid transparent;
+  border-bottom: 7px solid transparent;
+  display: inline-block;
+  margin-top: 1px;
+}
+
+.thread-entry.avatar .header {
+    position: relative;
 }
 
-#ticket_thread > .message th {
+.thread-entry.message .header {
     background:#C3D9FF;
 }
+.thread-entry.avatar.message .header:after {
+    border-left: 7px solid #C3D9FF;
+    margin-right: 1px;
+}
 
-#ticket_thread > .response th {
+.thread-entry.response .header {
     background:#FFE0B3;
 }
+.thread-entry.avatar.response .header:before,
+.thread-entry.avatar.note .header:before {
+    top: 7px;
+    left: -8px;
+    right: initial;
+    border-left: none;
+    border-right: 8px solid #CCC;
+}
+.thread-entry.system .header {
+    background-color: #f4f4f4;
+}
+
+.thread-entry.avatar.response .header:before {
+    border-right-color: #ccb3af;
+}
+.thread-entry.avatar.note .header:before {
+    border-right-color: #ccccb0;
+}
+.thread-entry.avatar.response .header:after,
+.thread-entry.avatar.note .header:after {
+    top: 7px;
+    left: -8px;
+    right: initial;
+    border-left: none;
+    border-right: 7px solid #FFE0B3;
+    margin-left: 1px;
+}
 
-#ticket_thread > .note th {
+.thread-entry.note .header {
     background:#FFE;
 }
+.thread-entry.avatar.note .header:after {
+    border-right-color: #FFE;
+}
+.thread-entry .header .title {
+    max-width: 500px;
+    vertical-align: bottom;
+    display: inline-block;
+    margin-left: 15px;
+}
+.thread-entry .header .button {
+    margin-top: -4px;
+}
+
+.thread-entry .thread-body {
+    border: 1px solid #ddd;
+    border-top: none;
+    border-bottom:2px solid #aaa;
+    border-radius: 0 0 5px 5px;
+}
+.thread-body .attachments {
+  background-color: #f4faff;
+  margin: 0 -0.9em;
+  position: relative;
+  top: 0.9em;
+  padding: 0.3em 0.9em;
+  border-top: 1px dotted #ccc;
+  border-top-color: rgba(0,0,0,0.2);
+  border-radius: 0 0 6px 6px;
+}
+.thread-body .attachments:empty {
+    display: none;
+}
+.thread-body .attachments .filesize {
+  margin-left: 0.5em;
+  line-height: 1em;
+}
+.thread-body .attachment-info {
+    margin-right: 10px;
+    display: inline-block;
+    width: 48%;
+}
+.thread-body .attachment-info .filename {
+  max-width: 80%;
+  max-width: calc(100% - 70px);
+}
 
 #ticket_notes table td {
     padding:5px;
@@ -886,7 +1081,7 @@ h2 .reload {
     background:#f9f9f9;
 }
 
-#ticket_thread .info, #ticket_notes .info {
+.thread-entry .info, #ticket_notes .info {
     padding:5px;
     background:#F4FAFF;
     height:16px;
@@ -897,10 +1092,6 @@ h2 .reload {
      background:#f9f9f9;
 }
 
-#response_options {
-    margin-top:30px;
-}
-
 #response_options > form {
     padding:0 10px;
 }
@@ -908,30 +1099,26 @@ h2 .reload {
 ul.tabs {
     padding:4px 0 0 20px;
     margin:0;
-    margin-bottom: 5px;
     text-align:left;
     height:29px;
     border-bottom:1px solid #aaa;
     background:#eef3f8;
+    position: relative;
+    box-shadow: inset 0 -5px 10px -9px rgba(0,0,0,0.3);
 }
 
 #response_options ul.tabs {
-    padding-left:190px;
 }
 
-
 ul.tabs li {
     margin:0;
     padding:0;
-    display:inline;
+    display:inline-block;
     list-style:none;
-}
-
-ul.tabs li a {
+    text-align:center;
     min-width:130px;
     font-weight:bold;
-    padding:5px;
-    height:18px;
+    height:28px;
     line-height:20px;
     color:#444;
     display:inline-block;
@@ -939,9 +1126,145 @@ ul.tabs li a {
     position:relative;
     bottom:1px;
     background:#fbfbfb;
-    border:1px solid #eee;
+    background-color: rgba(251, 251, 251, 0.5);
+    border:1px solid #ccc;
+    border:1px solid rgba(204, 204, 204, 0.5);
+    border-bottom:none;
+    position: relative;
+    bottom: 1px;
+    border-top-left-radius: 5px;
+    border-top-right-radius: 5px;
+    font-size: 95%;
+}
+ul.tabs li.active {
+    color:#184E81;
+    background-color:#f9f9f9;
+    border:1px solid #aaa;
     border-bottom:none;
     text-align: center;
+    border-top:2px solid #81a9d7;
+    bottom: 0;
+    box-shadow: 4px -1px 6px -3px rgba(0,0,0,0.2);
+}
+li.error {
+  border-top: 2px solid rgba(255, 0, 0, 0.3) !important;
+}
+li.error.active {
+  border-top-color: rgba(255, 0, 0, 0.7) !important;
+
+}
+li.error a:before {
+  background-color: rgba(255,0,0,0.06);
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  content: "";
+  position: absolute;
+}
+ul.tabs li:not(.active) {
+    box-shadow: inset 0 -5px 10px -9px rgba(0,0,0,0.2);
+}
+ul.tabs.clean li.active {
+    background-color: white;
+}
+
+ul.tabs li a {
+    font-weight: 400;
+    line-height: 20px;
+    color: #444;
+    color: rgba(0,0,0,0.6);
+    display: block;
+    outline: none;
+    padding: 5px 10px;
+}
+ul.tabs li a:hover {
+    text-decoration: none;
+}
+
+ul.tabs li.active a {
+    font-weight: bold;
+    color: #222;
+    color: rgba(0,0,0,0.8);
+}
+
+ul.tabs li.empty {
+    padding: 5px;
+    border: none !important;
+}
+
+ul.tabs.vertical {
+    display: inline-block;
+    height: auto;
+    border-bottom: initial;
+    border-right: 1px solid #aaa;
+    padding-left: 0;
+    padding-bottom: 40px;
+    padding-top: 10px;
+    background: transparent;
+    box-shadow: inset -5px 0 10px -9px rgba(0,0,0,0.3);
+}
+ul.tabs.vertical.left {
+    float: left;
+    margin-right: 9px;
+}
+
+ul.tabs.vertical li {
+    border:1px solid #ccc;
+    border:1px solid rgba(204, 204, 204, 0.5);
+    border-right: none;
+    min-width: 0;
+    display: block;
+    border-top-right-radius: 0;
+    border-bottom-left-radius: 5px;
+    right: 0;
+    height: auto;
+}
+ul.tabs.vertical li:not(.active) {
+    box-shadow: inset -5px 0 10px -9px rgba(0,0,0,0.3);
+}
+
+ul.tabs.vertical li + li {
+    margin-top: 5px;
+}
+
+ul.tabs.vertical li.active {
+    border: 1px solid #aaa;
+    border-left: 2px solid #81a9d7;
+    border-right: none;
+    right: -1px;
+    box-shadow: -1px 4px 6px -3px rgba(0,0,0,0.3);
+}
+
+ul.tabs.vertical.left li {
+    text-align: right;
+}
+
+ul.tabs.vertical li a {
+    padding: 5px;
+}
+
+ul.tabs.alt {
+  background-color:initial;
+  border-bottom:2px solid #ccc;
+  border-bottom-color: rgba(0,0,0,0.1);
+  box-shadow:none;
+}
+
+ul.tabs.alt li {
+  width:auto;
+  border:none;
+  min-width:0;
+  box-shadow:none;
+  bottom: 1px;
+  height: auto;
+}
+
+ul.tabs.alt li.active {
+  border:none;
+  box-shadow:none;
+  background-color: transparent;
+  border-bottom:2px solid #81a9d7;
 }
 
 #response_options .reply_tab.tell {
@@ -951,16 +1274,6 @@ ul.tabs li a {
     background-repeat:no-repeat;
 }
 
-ul.tabs li a.active {
-    height:18px;
-    color:#184E81;
-    background-color:#f9f9f9;
-    border:1px solid #aaa;
-    border-top:2px solid #81a9d7;
-    border-bottom:none;
-    bottom: 0;
-}
-
 #response_options > form {
     padding:10px 5px;
     background:#f9f9f9;
@@ -976,13 +1289,12 @@ ul.tabs li a.active {
     vertical-align:top;
 }
 
-#response_options textarea {
-    width:760px !important;
-}
-
 #response_options input[type=text], #response_options textarea:not(.richtext) {
     border:1px solid #aaa;
     background:#fff;
+    border-radius:4px;
+   -webkit-border-radius: 4px;
+   -moz-border-radius: 4px;
 }
 
 .attachments .uploads div {
@@ -1016,7 +1328,7 @@ ul.tabs li a.active {
     display:block;
     height:30px;
     position:absolute;
-    z-index:5;
+    z-index:10;
 }
 
 .tip_arrow {
@@ -1025,7 +1337,7 @@ ul.tabs li a.active {
     top:5px;
     left:-12px;
     width:12px;
-    z-index:102;
+    z-index:1;
 }
 
 .tip_box.right .tip_arrow {
@@ -1053,15 +1365,15 @@ ul.tabs li a.active {
     -moz-border-radius:5px;
     -webkit-border-radius:5px;
     border-radius:5px;
-    -moz-box-shadow: 3px 3px 3px #666;
-    -webkit-box-shadow: 3px 3px 3px #666;
-    box-shadow: 3px 3px 3px #666;
+    -moz-box-shadow: 5px 5px 10px -2px rgba(0,0,0,0.5);
+    -webkit-box-shadow: 5px 5px 10px -2px rgba(0,0,0,0.5);
+    box-shadow: 5px 5px 10px -2px rgba(0,0,0,0.5);
     z-index:3;
     position:absolute;
     top:0;
     left:-1px;
     min-width:400px;
-    line-height: 1.15rem;
+    line-height: 1.45rem;
 }
 
 .tip_content .links {
@@ -1111,7 +1423,7 @@ ul.tabs li a.active {
     padding:5px 0;
     border-top:1px solid #aaa;
     height:16px;
-    font-size:9pt;
+    font-size:0.9em;
 }
 
 .tip_menu li {
@@ -1154,14 +1466,14 @@ ul.tabs li a.active {
     width:auto !important;
     width:295px;
     text-align:right;
-    line-height:24px;
+    line-height:1.5em;
 }
 
 .tip_content h1 {
-    font-size: 13pt;
+    font-size: 1.3em;
     margin-top: 0;
-    margin-bottom: 0.5em;
-    padding-bottom: 0.2em;
+    margin-bottom: 0.4em;
+    padding-bottom: 0.5em;
     border-bottom: 1px solid #ddd;
     padding-right: 1.5em;
 }
@@ -1170,6 +1482,7 @@ i.help-tip {
     vertical-align: inherit;
     color: #aaa;
     opacity: 0.8;
+    text-indent: initial;
 }
 i.help-tip:hover {
     color: orange !important;
@@ -1188,8 +1501,8 @@ caption:hover > i.help-tip {
 }
 
 h2 > i.help-tip {
-    vertical-align: baseline;
-    font-size: 11pt;
+    vertical-align: middle;
+    font-size: .7em;
 }
 .form_table th h4 i.help-tip {
     color: white;
@@ -1213,7 +1526,9 @@ h2 > i.help-tip {
   background-repeat:no-repeat, repeat-x;
   border-bottom:1px solid #ddd;
 }
-
+#kb li:last-child {
+    border-bottom:none;
+}
 
 #kb li h4 {
     padding-bottom:3px;
@@ -1230,65 +1545,34 @@ h2 > i.help-tip {
 }
 
 #kbSearch {
-    padding:10px 0;
-    overflow:hidden;
-}
-
-#kbSearch div {
-    clear:both;
-    overflow:hidden;
-    padding:5px 0 2px 3px;
+    margin-bottom: 1em;
 }
 
 #kbSearch #query {
-    margin:1px 5px 0 0;
-    display:inline-block;
-    float:left;
     width:200px;
 }
 
-#kbSearch #cid {
-    margin:0;
-    display:inline-block;
-    float:left;
-    width:200px;
-    margin-right:5px;
-    position:relative;
-    top:2px;
-}
-
-#kbSearch #topic-id {
-    margin:0;
-    display:inline-block;
-    float:left;
-    width:410px;
-}
-
-#kbSearch #searchSubmit {
-    margin:0;
-    display:inline-block;
-    float:left;
-    position:relative;
-    top:2px;
-}
-
 #faq {
   clear: both;
   margin: 0;
-  padding: 5px 0 10px 5px;
+  padding: 0px 0 10px 0px;
 }
 #faq ol {
   font-size: 15px;
   margin-left: 0;
   padding-left: 0;
+  margin:0!IMPORTANT;
 }
 #faq ol li {
   list-style: none;
-  margin: 0;
-  padding:5px 0;
+  margin: 0 0;
+  padding:10px 0 10px;
   color: #999;
   border-bottom:1px solid #ddd;
 }
+#faq ol li:last-child {
+  border-bottom:none;
+}
 
 #faq ol li a {
   display: inline;
@@ -1308,15 +1592,14 @@ h2 > i.help-tip {
   background-color:#e9f5ff;
 }
 
-time {
+time.faq {
     display:inline-block;
-    float:right;
     color:#777;
 }
 
 .cat-desc {
     padding-top:5px;
-    padding-bottom:25px;
+    padding-bottom:15px;
 }
 
 .cat-manage-bar {
@@ -1354,6 +1637,18 @@ time {
     overflow-y: auto;
 }
 
+.dialog#popup {
+    width:650px;
+}
+
+.dialog.size-normal {
+    width:650px !important;
+}
+
+.dialog.size-large {
+    width:750px !important;
+}
+
 .dialog #popup-loading {
     position:absolute;
     text-align:center;
@@ -1362,11 +1657,11 @@ time {
     bottom:0;
     left:0;
     right:0;
-    z-index:1;
+    z-index:11;
 }
 
-.redactor_editor {
-    font-size: 11pt;
+.redactor-editor {
+    font-size: 1.1em;
 }
 
 .dialog#advanced-search {
@@ -1414,22 +1709,16 @@ time {
     width:100%;
 }
 
-#advanced-search div.closed_by, #advanced-search span.spinner {
-    display:none;
-}
-
 .dialog fieldset {
     margin:0;
     padding:0 0;
     border:none;
-    overflow:hidden;
 }
 
-.dialog .custom-field .field-label {
-    margin-left: 3px;
-    margin-right: 3px;
+.custom-field .field-label {
+    margin: 0 3px 4px;
 }
-.dialog .custom-field + .custom-field {
+.custom-field + .custom-field {
     margin-top: 8px;
 }
 .dialog label.fixed-size {
@@ -1439,9 +1728,10 @@ time {
     padding:10px;
 }
 
-.dialog fieldset input {
+.dialog fieldset input:not([type=checkbox]) {
     border:1px solid #ccc;
     background:#fff;
+    padding: 3px;
 }
 
 .dialog fieldset span.between {
@@ -1458,79 +1748,235 @@ time {
     -webkit-box-sizing: content-box;
 }
 
-.dialog.draggable h3:hover {
+.dialog.draggable h3.drag-handle:hover {
     cursor: move;
 }
 
-#advanced-search fieldset.span6 {
-    display: inline-block;
-    width: 49%;
-    margin-bottom: 5px;
+.row {
+    display: table-row;
 }
-#advanced-search fieldset label {
-    display: block;
+
+.row .span6 {
+    display: table-cell;
+    width: 48%;
+    padding: 5px 10px;
+    vertical-align: top;
 }
-#advanced-search fieldset.span6 select,
-#advanced-search fieldset.span6 input {
-    max-width: 100%;
-    min-width: 75%;
+
+.search-dropdown {
+    padding-left: 19px;
 }
 
-#advanced-search .query input {
-    width:100%;
-    padding: 4px;
-    margin-bottom: 10px;
+.adv-search-field {
+    margin-top: 5px !important;
 }
 
-#advanced-search .date_range {
-    margin-bottom: 5px;
+#advanced-search fieldset {
+  margin-top: 3px;
+  position: relative;
+}
+#advanced-search .adv-search-method:before,
+#advanced-search .adv-search-val:before {
+  content: "";
+  border-left: 2px dotted #ccc;
+  border-bottom: 2px dotted #ccc;
+  border-color: rgba(0,0,0,0.15);
+  width: 10px;
+  height: 10px;
+  display: inline-block;
+  position: absolute;
+  left: -16px;
 }
-#advanced-search .date_range input {
-    width:227px;
-    width: calc(49% - 73px);
+#advanced-search .adv-search-method {
+  margin-left: 24px;
+}
+#advanced-search .adv-search-val {
+  margin-left: 45px;
 }
 
-#advanced-search .date_range i {
+input[type="submit"],
+input[type="reset"],
+input[type="button"],
+.action-button,
+.button {
+    cursor: pointer;
+    box-sizing: content-box;
     display:inline-block;
-    margin-left:3px;
-    position:relative;
-    top:5px;
-    width:16px;
-    height:16px;
-    background:url(../images/cal.png) bottom left no-repeat;
+    vertical-align:bottom;
+    margin:0 4px;
+    height:22px;
+    line-height: 22px;
+    border: none;
+    box-shadow: 0 0 0 1px rgba(0,0,0,0.25) inset;
+    padding:2px 11px;
+    color: #555;
+    background-color: #f0f0f0;
+    background-color: rgba(0,0,0,0.02);
+    -webkit-border-radius: 3px;
+    -moz-border-radius: 3px;
+    border-radius: 3px;
+    font-family: inherit;
+    font-size: 0.95em;
+    font-weight: normal;
+-webkit-user-select: none;
+   -moz-user-select: none;
+    -ms-user-select: none;
+        user-select: none;
+-webkit-transition: opacity 0.1s ease, background-color 0.1s ease, box-shadow 0.1s ease, color 0.1s ease, background 0.1s ease;
+        transition: opacity 0.1s ease, background-color 0.1s ease, box-shadow 0.1s ease, color 0.1s ease, background 0.1s ease;
+}
+input[type="submit"] i,
+input[type="reset"] i,
+input[type="button"] i,
+.action-button i,
+.button i {
+  margin-right: 0.1em;
+}
+select + .action-button,
+select + .button {
+  vertical-align: middle;
+}
+.dark.button {
+    background-color: rgba(0,0,0,0.5);
+    box-shadow: 0 0 0 1px rgba(255,255,255,0.5) inset;
+    color: white;
+}
+.dark.button:hover {
+    background-color: rgba(0,0,0,0.8);
+    box-shadow: 0 0 0 2px rgba(255,255,255,0.7) inset;
+    color: white;
+}
+.link.button, .link.button:hover, .link.button:active {
+    border: none;
+    box-shadow: none;
+    background-color: transparent;
+    color:#184E81;
+    padding: 0;
+    font-size: inherit;
 }
 
-#advanced-search fieldset.sorting select {
-    width:130px;
+.light .button:hover,
+.white.button {
+  background-color: rgba(255,255,255,0.7);
+  border-color: #555;
+}
+.light .button:hover,
+.white.button:hover {
+  background-color: rgba(255,255,255,0.9);
+  border-color: black;
 }
 
-#advanced-search p {
-    text-align:center;
+.button.attached {
+  margin-left: -4px;
+  margin-right: -4px;
+  box-shadow: none !important;
+  border-top-left-radius: 0;
+  border-bottom-left-radius: 0;
+  border: 1px solid #999;
+  border-left: none;
+  padding: 0 9px;
+}
+.input.attached {
+  height: 24px;
+  box-sizing: border-box;
+  display: inline-block;
+  margin-right: 5px;
+}
+.input.attached.focus {
+  outline-offset: -2px;
+  outline-style: auto;
+  outline-width: 5px;
+  outline-color: -webkit-focus-ring-color;
+}
+.input.attached input:focus {
+  outline-style: none;
+}
+.input.attached input {
+  height: 100%;
+  box-sizing: border-box;
+  margin-right:0;
+  border: 1px solid #999;
+  border-right:none;
+  border-top-right-radius: 0;
+  border-bottom-right-radius: 0;
+}
+.input.attached .button.attached {
+  height: 100%;
+  box-sizing: border-box;
 }
 
-.dialog input[type="submit"],
-.dialog input[type="reset"],
-.dialog input[type="button"] {
-    display:inline-block;
-    margin:0;
-    height:24px;
-    line-height:24px;
-    font-weight:bold;
-    border:1px solid #666666;
-    padding:0 10px;
-    background: url('../images/grey_btn_bg.png?1312910883') top left repeat-x;
-    color: #333;
+.green.button:hover {
+  background-color: inherit;
+  box-shadow: 0 0 0 2px #16ab39 inset;
+  color: #16ab39;
+}
+
+.red.button:hover {
+  background-color: inherit;
+  box-shadow: 0 0 0 2px #d01919 inset;
+  color: #d01919;
 }
 
-.dialog input[type="reset"],
-.dialog input[type="button"] {
-    opacity:0.7;
+.button:hover {
+  text-decoration: none;
 }
 
-.dialog input[type=submit]:hover, .dialog input[type=submit]:active,
-.dialog input[type=button]:hover, .dialog input[type=button]:active,
-.dialog input[type=reset]:hover, .dialog input[type=reset]:active {
-    background-position:bottom left;
+button[type=submit], input[type="submit"], .primary.button {
+    font-weight: normal;
+    box-shadow: 0 0 0 1px rgba(0,0,0,0.45) inset;
+    background-color: rgba(0,0,0,0.07);
+}
+
+.save.pending {
+  background-color: rgba(255, 174, 0, 0.63);
+  box-shadow: 0 0 0 2px rgba(255, 174, 0, 1) inset;
+}
+
+.button:hover, .button:active,
+.action-button:hover, .action-button:active,
+input[type=button]:hover, input[type=button]:active,
+input[type=reset]:hover, input[type=reset]:active {
+    color: black;
+    box-shadow: 0 0 0 2px rgba(0,0,0,0.5) inset;
+    background-color: #ddd;
+    background-color: rgba(0, 0, 0, 0.08);
+}
+
+button[type=submit]:hover, input[type=submit]:hover, input[type=submit]:active {
+  color: white;
+  box-shadow: 0 0 0 2px rgba(0,0,0,0.7) inset;
+  background-color: #888;
+  background-color: rgba(0, 0, 0, 0.5);
+}
+
+.button:disabled, .action-button:disabled,
+button[type=submit]:disabled, input[type=submit]:disabled {
+  opacity: 0.6;
+}
+
+.save.pending:hover {
+  box-shadow: 0 0 0 2px rgba(242, 165, 0, 1) inset;
+  background-color: rgba(255, 174, 0, 0.79);
+  color: black;
+}
+
+input[type=button].small, .small.button, input[type=submit].small {
+  font-size: 0.8em;
+  height: 18px;
+  line-height: 100%;
+  font-weight: normal;
+}
+
+.action-button.muted {
+  box-shadow: 0 0 0 1px rgba(0,0,0,0.08) inset;
+}
+
+.action-button.muted i.icon-caret-down {
+  border: none;
+}
+
+.action-button.inline, .button.inline {
+    vertical-align: middle;
 }
 
 /* Dynamic forms in dialogs */
@@ -1578,13 +2024,13 @@ time {
 
 /* Upgrader */
 #upgrader { width: 100%; height: auto; clear: both;}
-#upgrader #sidebar { width: 220px; padding: 10px; border: 1px solid #C8DDFA; float: right; background: #F7FBFE; }
-#upgrader #sidebar h3 { font-size: 10pt; margin: 0 0 5px 0; padding: 0; text-indent: 32px; background: url('../images/cog.png?1312913866') top left no-repeat; line-height: 24px; color: #2a67ac; }
+.sidebar { width: 220px; padding: 10px; border: 1px solid #C8DDFA; float: right; background: #F7FBFE; }
+.sidebar h3 { margin: 0 0 5px 0; padding: 0; text-indent: 32px; background: url('../images/cog.png?1312913866') top left no-repeat; line-height: 24px; color: #2a67ac; }
 
 #upgrader #main { width: 680px; float: left;}
-#upgrader #main h1 { margin: 0; padding: 0; font-size: 21pt; font-weight: normal; }
-#upgrader #main h2 { font-size: 12pt; margin: 0; padding: 0; color:#E65524; }
-#upgrader #main h3 { font-size: 10pt; margin: 0; padding: 0; }
+#upgrader #main h1 { margin: 0; padding: 0; font-size: 1.6em; font-weight: normal; }
+#upgrader #main h2 { font-size: 1.2em; margin: 0; padding: 0; color:#E65524; }
+#upgrader #main h3 { margin: 0; padding: 0; }
 #upgrader #main div#intro { padding-bottom: 5px; margin-bottom:10px; border-bottom: 1px solid #aaaaaa; }
 #upgrader #main  { padding-bottom: 20px; }
 
@@ -1597,8 +2043,6 @@ ul.progress li.yes small {color:green; }
 ul.progress li.no small {color:red;}
 
 #bar { clear: both; padding-top: 10px; height: 24px; line-height: 24px; text-align: center; border-top: 1px solid #aaaaaa; }
-#bar a, #bar .btn { display: inline-block; margin: 0; height: 24px; line-height: 24px; font-weight: bold; border: 1px solid #666666; text-decoration: none; padding: 0 10px; background: url('../images/grey_btn_bg.png?1312910883') top left repeat-x; color: #333; }
-#bar a:hover, #bar .btn:hover, #bar .btnover { background-position: bottom left; }
 #bar a.unstyled, #bar a.unstyled:hover { font-weight: normal; background: none; border: none; text-decoration: underline; color: #2a67ac; }
 
 #bar.error { background: #ffd; text-align: center; color: #a00; font-weight: bold; }
@@ -1681,6 +2125,8 @@ div.selected-signature {
     overflow-y: hidden;
     font-size: 15px;
     line-height: 1.25rem;
+    background-color: white;
+    background-color: rgba(255, 255, 255, 0.9);
 }
 div.selected-signature .inner {
     opacity: 0.5;
@@ -1714,6 +2160,23 @@ div.selected-signature .inner {
     top: 4px;
     right: 5px;
 }
+.muted-button:hover {
+    border: 1px solid #aaa;
+    border: 1px solid rgba(0,0,0,0.3);
+    cursor: pointer;
+    background: rgba(255,255,255,0.1);
+    color: black;
+}
+.muted-button {
+  border-radius: 5px;
+  padding: 1px 5px;
+  margin: -1px 0 -1px 5px;
+  border: 1px solid rgba(0,0,0,0.15);
+  color: #666;
+  color: rgba(0,0,0,0.5);
+  background-color: rgba(0,0,0,0.1);
+  background: linear-gradient(0, rgba(0,0,0,0.1), rgba(255,255,255,0.1));
+}
 
 .sortable-rows tr td:hover {
     cursor: move;
@@ -1764,15 +2227,22 @@ div.selected-signature .inner {
 }
 
 .row-item .button-group div {
-    padding: 9px;
-    padding-left: 12px;
     display: inline-block;
 }
+.row-item .button-group div a {
+    padding: 9px 12px 8px;
+}
 .row-item .management {
     margin-top: 10px;
     border-top: 1px dashed black;
 }
 
+.row-item:first-child .delete {
+    border-top-right-radius: 5px;
+}
+.row-item:last-child .delete {
+    border-bottom-right-radius: 5px;
+}
 .row-item .delete:hover {
     background: #fc9f41; /* Old browsers */
     color: rgba(255,255,255,0.8) !important;
@@ -1852,18 +2322,27 @@ tr.disabled th {
 }
 
 .label {
-  font-size: 11px;
-  padding: 1px 4px 2px;
-  -webkit-border-radius: 3px;
-  -moz-border-radius: 3px;
-  border-radius: 3px;
-  font-weight: bold;
-  line-height: 14px;
-  color: #ffffff;
-  vertical-align: baseline;
-  white-space: nowrap;
-  text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
-  background-color: #999999;
+    float: right;
+    margin-bottom: 4px;
+    font-size: 11px;
+    padding: 0px 7px;
+    -webkit-border-radius: 3px;
+    -moz-border-radius: 3px;
+    border-radius: 3px;
+    font-weight: bold;
+    line-height: 18px;
+    color: #ffffff;
+    vertical-align: baseline;
+    white-space: nowrap;
+    text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
+    background-color: #999999;
+}
+.label-bare {
+  background-color: transparent;
+  background-color: rgba(0,0,0,0);
+  border: 1px solid #999999;
+  color: #999999;
+  text-shadow: none;
 }
 .label-info {
   background-color: #3a87ad;
@@ -1886,6 +2365,12 @@ tr.disabled th {
 .tab_content {
     position: relative;
 }
+.tab_content:not(.left) {
+    padding: 12px 0;
+}
+.left-tabs {
+    margin-left: 48px;
+}
 .floating-options {
     display: inline-block;
     position: absolute;
@@ -1914,6 +2399,39 @@ table.custom-info td {
     border-bottom: 1px dotted rgba(0,0,0,0.3);
 }
 
+div.faq-status {
+   padding-top:6px;
+
+}
+
+.faq-title {
+    font-size: 170%;
+    font-weight: 600;
+    margin-right:10px;
+}
+.faq-content {
+    width: 670px;
+    margin: 0 15px;
+}
+.faq-category {
+    margin:0 15px;
+}
+.faq-meta section + section {
+    margin-top: 15px;
+}
+
+button a {
+    color: ButtonText !important;
+    text-decoration: none;
+}
+button a:hover {
+    text-decoration: none;
+}
+
+.bleed {
+    padding: 0;
+    margin: 0;
+}
 .doc-desc-title {
     font-weight: bold;
     text-transform: capitalize;
@@ -1922,8 +2440,9 @@ table.custom-info td {
     font-style: italic;
 }
 
-.form_table tr:hover i.help-tip {
-    opacity: 1;
+tr:hover i.help-tip,
+tr i.help-tip.warning {
+    opacity: 0.8 !important;
     color: #ffc20f;
 }
 
@@ -1968,7 +2487,439 @@ table.custom-info td {
 .ltr {
     direction: ltr;
     unicode-bidi: embed;
+
 }
 .required {
     font-weight: bold;
 }
+.truncate {
+    width: auto;
+    display: inline-block;
+    max-width: 100%;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    vertical-align: bottom;
+}
+.truncate.bleed {
+    text-overflow: hidden;
+}
+td.indented {
+    padding-left: 20px;
+}
+.secondary_lang {
+    padding:3px 0;
+    margin: 3px 0;
+    border-bottom: 1px dotted #ccc;
+}
+.saved-search {
+    padding: 5px;
+}
+
+.saved-search + .saved-search {
+    border-top: 1px dotted #ccc;
+}
+
+.accordian {
+  margin-bottom: 10px;
+}
+.accordian dt {
+    border-radius: 4px;
+    border: 1px solid #ccc;
+}
+.accordian dt.active {
+    border-bottom-left-radius: 0;
+    border-bottom-right-radius: 0;
+}
+.accordian dt, dd {
+  padding: 5px;
+}
+.accordian dt a {
+  color: black;
+  font-weight: 500;
+  display: block;
+}
+.accordian dt.active a {
+  color: #184E81;
+  text-decoration: none;
+}
+.accordian dt:not(.active) a i {
+  display: none;
+}
+.accordian dd {
+  border-top: 0;
+  font-size: 12px;
+  margin-left: 0;
+  border: 1px solid #ccc;
+  border-top: none;
+  box-shadow: inset 0px 10px 5px -10px rgba(0,0,0,0.1);
+  background-color:rgba(42,103,172,0.1);
+}
+.accordian dt ~ dt {
+  margin-top: 5px;
+}
+.accordian dd:last-of-type {
+   position: relative;
+   top: -1px;
+}
+
+#topic-forms tbody + tbody td.handle {
+  padding-top: 15px;
+}
+
+#dynamic-actions > tr > td {
+    padding: 5px;
+}
+
+.no-margin {
+    margin: 0 !important;
+}
+
+.form-simple select, .form-simple input, .form-simple textarea {
+    margin-left: 0;
+}
+.sticky.bar.fixed {
+  position: fixed;
+  top: 0;
+  left: 0;
+  z-index: 6;
+  width: 100%;
+  background-color: white;
+  background-color: rgba(255,255,255,0.95);
+  padding: 10px 20px;
+  box-sizing: border-box;
+  box-shadow: 0 3px 10px rgba(0,0,0,0.3);
+}
+.sticky.bar .content {
+  margin: auto;
+}
+.sticky.bar.fixed .notsticky {
+  display: none !important;
+}
+
+.sticky.bar.fixed .inline {
+    float:left;
+    display:inline;
+    margin:5px 10px 0 0;
+}
+
+.sticky.bar.opaque {
+  background-color: white;
+}
+.sticky.bar.fixed h2 {
+  margin: 0;
+}
+.sticky.bar:not(.fixed) .sticky.only {
+    display:none;
+}
+.scroll-up {
+  display:none;
+}
+@media screen and (min-width: 1040px) {
+  .scroll-up {
+    display: inline;
+    background-color: #eee;
+    background-color: rgba(0,0,0,0.1);
+    position: absolute;
+    top: 0px;
+    right: 20px;
+    padding: 8px 8px 5px;
+    border-radius: 0 0 5px 5px;
+    border: 1px dotted #888;
+    border-top: none;
+    color: #888 !important;
+    box-shadow: 0 3px 8px -6px rgba(0,0,0,0.9);
+  }
+  .scroll-up:hover {
+    background-color: #444;
+    background-color: rgba(0,0,0,0.7);
+    color: #ddd !important;
+    color: rgba(255,255,255,0.8) !important;
+    border-color:transparent;
+  }
+}
+
+.message.bar {
+  position: fixed;
+  top: 0;
+  left: 0;
+  right: 0;
+  padding: 9px 15px;
+  z-index: 10;
+  background-color: white;
+  box-shadow: 0 3px 10px rgba(0,0,0,0.2);
+  opacity: 0.95;
+}
+.message.bar.bottom {
+  bottom: 0;
+  top: auto;
+  box-shadow: 0 -3px 10px rgba(0,0,0,0.2);
+}
+.message.bar .avatar[class*=" oscar-"] {
+  display: inline-block;
+  width: 36px;
+  height: 36px;
+  margin-right: 10px;
+  background-image: url(../images/oscar-avatars.png);
+  background-repeat: no-repeat;
+  background-size: 180px 72px;
+}
+.avatar.oscar-boy {
+  background-position: -72px 0;
+}
+.avatar.oscar-borg {
+  background-position: 0 -36px;
+}
+.message.bar .title {
+  font-weight: bold;
+  font-size: 1.1em;
+}
+.message.bar .body {
+  margin-left: 42px;
+}
+.message.bar.warning {
+  border-bottom: 3px solid orange;
+}
+.message.bar.bottom.warning {
+  border-bottom: none;
+  border-top: 3px solid orange;
+}
+.message.bar.danger {
+  border-bottom: 3px solid red;
+}
+.message.bar.bottom.danger {
+  border-bottom: none;
+  border-top: 3px solid red;
+}
+.message.bar .title .avatar {
+    width: auto;
+    max-height: 20px;
+    border-radius: 3px;
+    margin: -4px 0.3em 0;
+    vertical-align: middle;
+}
+
+#thread-items::before {
+  border-left: 2px dotted #ddd;
+  border-bottom-color: rgba(0,0,0,0.1);
+  position: absolute;
+  margin-left: 74px;
+  z-index: -1;
+  content: "";
+  top: 0;
+  bottom: 0;
+  right: 0;
+  left: 0;
+}
+#thread-items {
+  z-index: 0;
+  position: relative;
+  padding-top: 0;
+  padding-bottom: 15px;
+  margin-top: 5px;
+}
+.thread-event {
+    padding: 0 2px 15px;
+    margin-left: 60px;
+}
+.thread-event a {
+    color: inherit;
+}
+.type-icon {
+    border-radius: 8px;
+    background-color: #f4f4f4;
+    padding: 4px 6px;
+    margin-right: 5px;
+    text-align: center;
+    display: inline-block;
+    font-size: 1.1em;
+    border: 1px solid #eee;
+    vertical-align: top;
+    position: relative;
+}
+.thread-event .type-icon::after {
+  content: "";
+  border: 16px solid white;
+  position: absolute;
+  top: -3px;
+  bottom: 0;
+  left: -3px;
+  right: 0;
+  z-index: -1;
+}
+.type-icon.dark {
+    border-color: #666;
+    background-color: #949494;
+}
+.thread-event img.avatar {
+    vertical-align: middle;
+    border-radius: 3px;
+    width: auto;
+    max-height: 20px;
+    margin: -3px 3px 0;
+}
+.thread-event .description {
+    margin-left: -30px;
+    padding-top: 6px;
+    padding-left: 30px;
+    display: inline-block;
+    width: 772px;
+    width: calc(100% - 95px);
+    line-height: 1.4em;
+}
+
+.freetext-files {
+    padding: 10px;
+    margin-top: 10px;
+    border: 1px dotted #ddd;
+    border-radius: 4px;
+    background-color: #f5f5f5;
+}
+.freetext-files .file {
+    margin-right: 10px;
+    display: inline-block;
+    width: 48%;
+    padding-top: 0.2em;
+}
+.freetext-files .title {
+    font-weight: bold;
+    margin-bottom: 0.3em;
+    font-size: 1.1em;
+}
+
+/* Form simple grid sizing */
+.iblock {
+    display: inline-block;
+}
+form .inset {
+    padding: 10px;
+}
+.dialog form .quick-add {
+  min-height: 150px;
+}
+.span12 {
+    width: 100%;
+}
+.span6 {
+    width: 48%;
+    width: calc(50% - 10px);
+}
+.span6 + .span6 {
+    margin-left: 1%;
+    margin-left: calc(0 + 10px);
+}
+.form.footer {
+    margin-top: 50px;
+}
+label.checkbox {
+    display: block;
+    padding-left: 1.3em;
+    text-indent: -1.3em;
+}
+label.inline.checkbox {
+    display: inline-block;
+}
+label.checkbox + label.checkbox {
+    margin-top: 0.3em;
+}
+input[type=checkbox] {
+  width: 1em;
+  height: 1em;
+  box-sizing: content-box;
+  padding: 0;
+  margin:0;
+  margin-right: 0.1em;
+  vertical-align: bottom;
+  position: relative;
+  top: -0.05em;
+  *overflow: hidden;
+}
+.vertical-pad {
+  margin-top: 3px;
+}
+
+input, textarea {
+    padding: 3px 5px;
+    font-size: 0.95em;
+    font-family: inherit;
+    border-radius:4px;
+   -webkit-border-radius: 4px;
+   -moz-border-radius: 4px;
+    border: 1px solid #bbb;
+}
+
+small {
+    font-weight: normal;
+    letter-spacing: 0.01px;
+}
+
+/* Form layouts */
+table.grid.form {
+  width: 100%;
+  table-layout: fixed;
+}
+table.grid.form caption {
+  font-size: 1.3em;
+  font-weight: bold;
+  text-align: start;
+  padding: 0 9px;
+}
+.grid.form .cell {
+  vertical-align: top;
+}
+.grid.form .field {
+  padding: 5px;
+}
+.grid.form .field input:not([type=checkbox]),
+.grid.form .field textarea,
+.grid.form .field select {
+  width: 100%;
+  display: block;
+}
+.grid.form .field > label {
+  display: block;
+  margin-bottom: 5px;
+}
+
+#basic_search {
+  background-color: #f4f4f4;
+  margin: -10px 0;
+  margin-bottom: 5px;
+  padding: 8px;
+  box-shadow: inset 0 4px 12px -10px black;
+  border-bottom: 1px dotted #aaa;
+  border-radius: 0 0 5px 5px;
+}
+
+#basic-ticket-search {
+  border: 1px solid #999;
+  border-color: rgba(0,0,0,0.45);
+  border-top-left-radius: 3px;
+  border-bottom-left-radius: 3px;
+}
+select {
+    height:24px;
+    line-height:24px;
+    max-width:350px;
+    border:1px solid #bbb;
+    display:inline-block;
+    padding:0 4px;
+    font-size:13px;
+    border-radius:4px;
+   -webkit-border-radius: 4px;
+   -moz-border-radius: 4px;
+}
+
+a.attachment {
+    padding-left: 1.2em;
+    display: block;
+}
+.sidebar section header {
+    font-weight: bold;
+    margin-bottom: 0.3em;
+}
+
+/* FIXME: Drop this with select2 4.0.1
+ * Fixes a rendering issue on Safari
+ */
+.select2-search__field{-webkit-appearance: textfield;}
diff --git a/scp/css/tooltip.css b/scp/css/tooltip.css
new file mode 100644
index 0000000000000000000000000000000000000000..daecf02bccdecc1d1477b834066b8771723610b9
--- /dev/null
+++ b/scp/css/tooltip.css
@@ -0,0 +1,115 @@
+.tooltip {
+  position: absolute;
+  z-index: 9999999;
+  display: block;
+  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+  font-style: normal;
+  font-weight: normal;
+  letter-spacing: normal;
+  line-break: auto;
+  line-height: 1.428571429;
+  text-align: left;
+  text-align: start;
+  text-decoration: none;
+  text-shadow: none;
+  text-transform: none;
+  white-space: normal;
+  word-break: normal;
+  word-spacing: normal;
+  word-wrap: normal;
+  font-size: 12px;
+  opacity: 0;
+  filter: alpha(opacity=0);
+}
+.tooltip.in {
+  opacity: 0.9;
+  filter: alpha(opacity=90);
+}
+.tooltip.top {
+  margin-top: -3px;
+  padding: 5px 0;
+}
+.tooltip.right {
+  margin-left: 3px;
+  padding: 0 5px;
+}
+.tooltip.bottom {
+  margin-top: 3px;
+  padding: 5px 0;
+}
+.tooltip.left {
+  margin-left: -3px;
+  padding: 0 5px;
+}
+.tooltip-inner {
+  max-width: 200px;
+  padding: 3px 8px;
+  color: #ffffff;
+  text-align: center;
+  background-color: #000000;
+  border-radius: 4px;
+}
+.tooltip-arrow {
+  position: absolute;
+  width: 0;
+  height: 0;
+  border-color: transparent;
+  border-style: solid;
+}
+.tooltip.top .tooltip-arrow {
+  bottom: 0;
+  left: 50%;
+  margin-left: -5px;
+  border-width: 5px 5px 0;
+  border-top-color: #000000;
+}
+.tooltip.top-left .tooltip-arrow {
+  bottom: 0;
+  right: 5px;
+  margin-bottom: -5px;
+  border-width: 5px 5px 0;
+  border-top-color: #000000;
+}
+.tooltip.top-right .tooltip-arrow {
+  bottom: 0;
+  left: 5px;
+  margin-bottom: -5px;
+  border-width: 5px 5px 0;
+  border-top-color: #000000;
+}
+.tooltip.right .tooltip-arrow {
+  top: 50%;
+  left: 0;
+  margin-top: -5px;
+  border-width: 5px 5px 5px 0;
+  border-right-color: #000000;
+}
+.tooltip.left .tooltip-arrow {
+  top: 50%;
+  right: 0;
+  margin-top: -5px;
+  border-width: 5px 0 5px 5px;
+  border-left-color: #000000;
+}
+.tooltip.bottom .tooltip-arrow {
+  top: 0;
+  left: 50%;
+  margin-left: -5px;
+  border-width: 0 5px 5px;
+  border-bottom-color: #000000;
+}
+.tooltip.bottom-left .tooltip-arrow {
+  top: 0;
+  right: 5px;
+  margin-top: -5px;
+  border-width: 0 5px 5px;
+  border-bottom-color: #000000;
+}
+.tooltip.bottom-right .tooltip-arrow {
+  top: 0;
+  left: 5px;
+  margin-top: -5px;
+  border-width: 0 5px 5px;
+  border-bottom-color: #000000;
+}
+
diff --git a/scp/css/translatable.css b/scp/css/translatable.css
new file mode 100644
index 0000000000000000000000000000000000000000..6601345e90f328102be8a818974ea1d342ad63be
--- /dev/null
+++ b/scp/css/translatable.css
@@ -0,0 +1,162 @@
+
+div.add-translation {
+  padding: 5px;
+  border-top: 1px solid rgba(0, 0, 0, 0.3);
+  background-color:#eee;
+  border-radius:0 0 5px 5px;
+  box-shadow: inset 0 1px 1px rgba(0,0,0,0.08);
+}
+div.add-translation button {
+  margin-left: 5px;
+}
+
+div.translations {
+  min-height: 20px;
+  position: absolute;
+  top: 100%;
+  left: 0;
+  z-index: 1000;
+  float: left;
+  display: none;
+  min-width: 160px;
+  padding: 4px 0 0;
+  margin: 0;
+  list-style: none;
+  background-color: #ffffff;
+  border-color: #ccc;
+  border-color: rgba(0, 0, 0, 0.2);
+  border-style: solid;
+  border-width: 1px;
+  -webkit-border-radius: 0 0 5px 5px;
+  -moz-border-radius: 0 0 5px 5px;
+  border-radius: 0 0 5px 5px;
+  -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+  -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+  -webkit-background-clip: padding-box;
+  -moz-background-clip: padding;
+  background-clip: padding-box;
+  *border-right-width: 2px;
+  *border-bottom-width: 2px;
+}
+div.translations .close {
+  position:absolute;
+  right:3px;
+  top:3px;
+  cursor: pointer;
+}
+ul.translations {
+  padding-left: 0;
+  min-width: 300px;
+  max-height: 150px;
+  overflow-y: auto;
+  padding: 5px 8px 8px;
+  margin: 0;
+}
+ul.translations li {
+  list-style: none;
+  padding: 0 10px;
+  box-sizing: border-box;
+  display: block;
+}
+ul.translations li + li {
+  margin-top: 10px;
+}
+ul.translations li label.language {
+  color: black;
+  font-weight: 400;
+  letter-spacing: 0;
+}
+ul.translations li label.language .flag {
+  margin-right: 6px;
+}
+ul.translations li input {
+  width: 100%;
+  box-sizing: border-box;
+  box-shadow: inset 0 1px 1px rgba(0,0,0,0.08);
+  padding: 2px 4px;
+  border-radius: 3px;
+  border: 1px solid #bbb;
+  font-family: sans-serif;
+  font-size: 12px;
+  margin-top: 4px;
+}
+.language-commit {
+  text-align: right;
+  padding: 5px 10px;
+  background-color: cyan;
+  background: repeating-linear-gradient(
+    45deg,
+    rgba(255, 255, 255, 0.05),
+    rgba(255, 255, 255, 0.05) 10px,
+    rgba(255, 255, 255, 0.3) 10px,
+    rgba(255, 255, 255, 0.3) 20px
+  ), #E65524;
+}
+
+
+div.translatable {
+  border: 1px solid #bbb;
+  box-shadow: inset 0 1px 1px rgba(0,0,0,0.05);
+  display: inline-block;
+  white-space: nowrap;
+  border-right: none;
+  background-color: white;
+  line-height: 16px;
+}
+div.translatable.textarea {
+  border: 1px solid #bbb;
+  box-shadow: inset 0 1px 1px rgba(0,0,0,0.05);
+  border-radius: 4px;
+}
+div.translatable.focus {
+  outline-offset: -2px;
+  outline-style: auto;
+  outline-width: 5px;
+  outline-color: -webkit-focus-ring-color;
+}
+div.translatable .flag {
+  margin-right: 4px;
+}
+div.translatable.textarea .flag {
+  vertical-align: top;
+}
+
+textarea.translatable,
+input.translatable {
+  border: none !important;
+  padding: 2px 5px !important;
+  margin: 0 !important;
+  background: none;
+}
+textarea.translatable,
+input.translatable:focus {
+  outline-style: none;
+}
+
+button.translatable {
+  margin: -1px 0;
+  padding: 4px 5px 5px;
+  background-color: #444;
+  background:linear-gradient(0deg, #444 0, #888 100%);
+  color: white;
+  border: none;
+  border-radius: 0 2px 2px 0;
+  cursor: pointer;
+  vertical-align: top;
+}
+
+div.translatable.textarea + button.translatable {
+  position: absolute;
+  top: 18px;
+  right: 4px;
+  border: none;
+  background-color: transparent;
+  background: transparent;
+  color: #aaa;
+  color: rgba(0, 0, 0, 0.5);
+  padding-top: 0;
+}
+div.translatable.textarea + button.translatable:hover {
+  color: black;
+}
diff --git a/scp/css/typeahead.css b/scp/css/typeahead.css
index 981923ab1f4c200b7121f5563171101dfea0a4d2..2e4e4d6ccd71853ecbf21973f302272476fe8dc3 100644
--- a/scp/css/typeahead.css
+++ b/scp/css/typeahead.css
@@ -7,7 +7,7 @@
   float: left;
   display: none;
   min-width: 160px;
-  padding: 4px 0;
+  padding: 4px 0 2px;
   margin: 0;
   list-style: none;
   background-color: #ffffff;
@@ -20,17 +20,24 @@
   border-radius: 0 0 5px 5px;
   -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
   -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
-  box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+  box-shadow: 0 10px 15px -5px rgba(0, 0, 0, 0.5);
   -webkit-background-clip: padding-box;
   -moz-background-clip: padding;
   background-clip: padding-box;
   *border-right-width: 2px;
   *border-bottom-width: 2px;
+  opacity: 0.95;
 }
 .dropdown-menu.pull-right {
   right: 0;
   left: auto;
 }
+.dropdown-menu.scroll {
+  max-height: 180px;
+  height: auto;
+  overflow-y: auto;
+  padding: 0;
+}
 .dropdown-menu .divider {
   height: 1px;
   margin: 8px 1px;
@@ -42,7 +49,7 @@
 }
 .dropdown-menu a {
   display: block;
-  padding: 3px 15px;
+  padding: 4px 15px;
   clear: both;
   font-weight: normal;
   line-height: 18px;
@@ -56,3 +63,12 @@
   text-decoration: none;
   background-color: #0088cc;
 }
+.dropdown-menu li > a:hover .faded,
+.dropdown-menu .active > a .faded,
+.dropdown-menu .active > a:hover .faded {
+  color: rgba(255,255,255,0.6);
+}
+
+.dropdown-menu li + li {
+    border-top: 1px solid rgba(0,0,0,0.15);
+}
diff --git a/scp/dashboard.php b/scp/dashboard.php
index 8e48056787a4c86402040162f362f86a440cda95..3bece49a338f21326711f13d84e941e603fa7e1e 100644
--- a/scp/dashboard.php
+++ b/scp/dashboard.php
@@ -14,58 +14,39 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 require('staff.inc.php');
+
+require_once INCLUDE_DIR . 'class.report.php';
+
+if ($_POST['export']) {
+    $report = new OverviewReport($_POST['start'], $_POST['period']);
+    switch (true) {
+    case ($data = $report->getTabularData($_POST['export'])):
+        $ts = strftime('%Y%m%d');
+        $group = Format::slugify($_POST['export']);
+        $delimiter = ',';
+        if (class_exists('NumberFormatter')) {
+            $nf = NumberFormatter::create(Internationalization::getCurrentLocale(),
+                NumberFormatter::DECIMAL);
+            $s = $nf->getSymbol(NumberFormatter::DECIMAL_SEPARATOR_SYMBOL);
+            if ($s == ',')
+                $delimiter = ';';
+        }
+
+        Http::download("stats-$group-$ts.csv", 'text/csv');
+        $output = fopen('php://output', 'w');
+        fputs($output, chr(0xEF) . chr(0xBB) . chr(0xBF));
+        fputcsv($output, $data['columns'], $delimiter);
+        foreach ($data['data'] as $row)
+            fputcsv($output, $row, $delimiter);
+        exit;
+    }
+}
+
 $nav->setTabActive('dashboard');
 $ost->addExtraHeader('<meta name="tip-namespace" content="dashboard.dashboard" />',
     "$('#content').data('tipNamespace', 'dashboard.dashboard');");
-require(STAFFINC_DIR.'header.inc.php');
-?>
-
-<script type="text/javascript" src="js/raphael-min.js"></script>
-<script type="text/javascript" src="js/g.raphael.js"></script>
-<script type="text/javascript" src="js/g.line-min.js"></script>
-<script type="text/javascript" src="js/g.dot-min.js"></script>
-<script type="text/javascript" src="js/bootstrap-tab.js"></script>
-<script type="text/javascript" src="js/dashboard.inc.js"></script>
-
-<link rel="stylesheet" type="text/css" href="css/bootstrap.css"/>
-<link rel="stylesheet" type="text/css" href="css/dashboard.css"/>
 
-<h2><?php echo __('Ticket Activity');
-?>&nbsp;<i class="help-tip icon-question-sign" href="#ticket_activity"></i></h2>
-<p><?php echo __('Select the starting time and period for the system activity graph');?></p>
-<form class="well form-inline" id="timeframe-form">
-    <label>
-        <i class="help-tip icon-question-sign" href="#report_timeframe"></i>&nbsp;&nbsp;<?php
-            echo __('Report timeframe'); ?>:
-        <input type="text" class="dp input-medium search-query"
-            name="start" placeholder="<?php echo __('Last month');?>"/>
-    </label>
-    <label>
-        <?php echo __('period');?>:
-        <select name="period">
-            <option value="now" selected="selected"><?php echo __('Up to today');?></option>
-            <option value="+7 days"><?php echo __('One Week');?></option>
-            <option value="+14 days"><?php echo __('Two Weeks');?></option>
-            <option value="+1 month"><?php echo __('One Month');?></option>
-            <option value="+3 months"><?php echo __('One Quarter');?></option>
-        </select>
-    </label>
-    <button class="btn" type="submit"><?php echo __('Refresh');?></button>
-</form>
-
-<!-- Create a graph and fetch some data to create pretty dashboard -->
-<div style="position:relative">
-    <div id="line-chart-here" style="height:300px"></div>
-    <div style="position:absolute;right:0;top:0" id="line-chart-legend"></div>
-</div>
-
-<hr/>
-<h2><?php echo __('Statistics'); ?>&nbsp;<i class="help-tip icon-question-sign" href="#statistics"></i></h2>
-<p><?php echo __('Statistics of tickets organized by department, help topic, and agent.');?></p>
-<ul class="nav nav-tabs" id="tabular-navigation"></ul>
-
-<div id="table-here"></div>
-
-<?php
+require(STAFFINC_DIR.'header.inc.php');
+require_once(STAFFINC_DIR.'dashboard.inc.php');
 include(STAFFINC_DIR.'footer.inc.php');
 ?>
diff --git a/scp/departments.php b/scp/departments.php
index 999380219ca49a9293089fb51a6a7272d6af93d5..d90a1953e439fd5308f870b4336349ff0fd07e48 100644
--- a/scp/departments.php
+++ b/scp/departments.php
@@ -33,7 +33,8 @@ if($_POST){
             }
             break;
         case 'create':
-            if(($id=Dept::create($_POST,$errors))){
+            $_dept = Dept::create();
+            if(($_dept->update($_POST,$errors))){
                 $msg=sprintf(__('Successfully added "%s"'),Format::htmlchars($_POST['name']));
                 $_REQUEST['a']=null;
             }elseif(!$errors['err']){
diff --git a/scp/emails.php b/scp/emails.php
index 4e8bf4befa5db5e0b136000acb2d4d170dabf572..9ff1c70a771ad9942977a415e8b2cae8d4794ffa 100644
--- a/scp/emails.php
+++ b/scp/emails.php
@@ -33,7 +33,9 @@ if($_POST){
             }
             break;
         case 'create':
-            if(($id=Email::create($_POST,$errors))){
+            $box = Email::create();
+            if ($box->update($_POST, $errors)) {
+                $id = $box->getId();
                 $msg=sprintf(__('Successfully added %s'), Format::htmlchars($_POST['name']));
                 $_REQUEST['a']=null;
             }elseif(!$errors['err']){
@@ -48,14 +50,8 @@ if($_POST){
             } else {
                 $count=count($_POST['ids']);
 
-                $sql='SELECT count(dept_id) FROM '.DEPT_TABLE.' dept '
-                    .' WHERE email_id IN ('.implode(',', db_input($_POST['ids'])).') '
-                    .' OR autoresp_email_id IN ('.implode(',', db_input($_POST['ids'])).')';
-
-                list($depts)=db_fetch_row(db_query($sql));
-                if($depts>0) {
-                    $errors['err'] = __('One or more of the selected emails is being used by a department. Remove association first!');
-                } elseif(!strcasecmp($_POST['a'], 'delete')) {
+                switch (strtolower($_POST['a'])) {
+                case 'delete':
                     $i=0;
                     foreach($_POST['ids'] as $k=>$v) {
                         if($v!=$cfg->getDefaultEmailId() && ($e=Email::lookup($v)) && $e->delete())
@@ -71,8 +67,9 @@ if($_POST){
                     elseif(!$errors['err'])
                         $errors['err'] = sprintf(__('Unable to delete %s'),
                             _N('selected email', 'selected emails', $count));
+                    break;
 
-                } else {
+                default:
                     $errors['err'] = __('Unknown action - get technical help.');
                 }
             }
diff --git a/scp/emailsettings.php b/scp/emailsettings.php
new file mode 100644
index 0000000000000000000000000000000000000000..6f871b4bf7b62e072689f4672b9df64f15bb5e06
--- /dev/null
+++ b/scp/emailsettings.php
@@ -0,0 +1,39 @@
+<?php
+/*********************************************************************
+    emailsettings.php
+
+    Handles settings for the email channel
+
+    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('admin.inc.php');
+
+$errors = array();
+$tip_namespace = 'settings.email';
+$inc = 'settings-emails.inc.php';
+
+if ($_POST && !$errors) {
+    if($cfg && $cfg->updateSettings($_POST,$errors)) {
+        $msg=sprintf(__('Successfully updated %s'), Format::htmlchars($page[0]));
+    } elseif(!$errors['err']) {
+        $errors['err']=__('Unable to update settings - correct errors below and try again');
+    }
+}
+
+$config=($errors && $_POST)?Format::input($_POST):Format::htmlchars($cfg->getConfigInfo());
+$ost->addExtraHeader('<meta name="tip-namespace" content="'.$tip_namespace.'" />',
+    "$('#content').data('tipNamespace', '".$tip_namespace."');");
+
+$nav->setTabActive('emails', 'emailsettings.php');
+require_once(STAFFINC_DIR.'header.inc.php');
+include_once(STAFFINC_DIR.$inc);
+include_once(STAFFINC_DIR.'footer.inc.php');
+
+?>
diff --git a/scp/emailtest.php b/scp/emailtest.php
index f4eb564c214828f64bd812044a7d1d0b6108643e..5bd65e3199bc17506116ac31717b251692b158fe 100644
--- a/scp/emailtest.php
+++ b/scp/emailtest.php
@@ -99,7 +99,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <?php echo __('To');?>:
             </td>
             <td>
-                <input type="text" size="60" name="email" value="<?php echo $info['email']; ?>">
+                <input type="text" size="60" name="email" value="<?php echo $info['email']; ?>"
+                    autofocus>
                 &nbsp;<span class="error">*&nbsp;<?php echo $errors['email']; ?></span>
             </td>
         </tr>
@@ -117,13 +118,15 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <div style="padding-top:0.5em;padding-bottom:0.5em">
                 <em><strong><?php echo __('Message');?></strong>: <?php echo __('email message to send.');?></em>&nbsp;<span class="error">*&nbsp;<?php echo $errors['message']; ?></span></div>
                 <textarea class="richtext draft draft-delete" name="message" cols="21"
-                    data-draft-namespace="email.diag"
-                    rows="10" style="width: 90%;"><?php echo $info['message']; ?></textarea>
+                    rows="10" style="width: 90%;" <?php
+    list($draft, $attrs) = Draft::getDraftAndDataAttrs('email.diag', false, $info['message']);
+    echo $attrs; ?>><?php echo $draft ?: $info['message'];
+                 ?></textarea>
             </td>
         </tr>
     </tbody>
 </table>
-<p style="padding-left:225px;">
+<p style="text-align:center;">
     <input type="submit" name="submit" value="<?php echo __('Send Message');?>">
     <input type="reset"  name="reset"  value="<?php echo __('Reset');?>">
     <input type="button" name="cancel" value="<?php echo __('Cancel');?>" onclick='window.location.href="emails.php"'>
diff --git a/scp/faq.php b/scp/faq.php
index 2a3c2e95116e2b6da074df0e0655366d581ddec3..358a2afec6093d96776a80ccbc03bb33a07ed8eb 100644
--- a/scp/faq.php
+++ b/scp/faq.php
@@ -23,20 +23,47 @@ if($_REQUEST['id'] && !($faq=FAQ::lookup($_REQUEST['id'])))
 if($_REQUEST['cid'] && !$faq && !($category=Category::lookup($_REQUEST['cid'])))
     $errors['err']=sprintf(__('%s: Unknown or invalid'), __('FAQ category'));
 
-$faq_form = new Form(array(
+$form_fields = array(
+    // Attachments for all languages — that is, attachments not specific to
+    // a particular language
     'attachments' => new FileUploadField(array('id'=>'attach',
         'configuration'=>array('extensions'=>false,
             'size'=>$cfg->getMaxFileSize())
-   )),
-));
+    )),
+);
 
-if($_POST):
+// Build attachment lists for language-specific attachment fields
+if ($langs = $cfg->getSecondaryLanguages()) {
+    // Primary-language specific files
+    $langs[] = $cfg->getPrimaryLanguage();
+    // Secondary-language specific files
+    foreach ($langs as $l) {
+        $form_fields['attachments.'.$l] = new FileUploadField(array(
+            'id'=>'attach','name'=>'attach:'.$l,
+            'configuration'=>array('extensions'=>false,
+                'size'=>$cfg->getMaxFileSize())
+        ));
+    }
+}
+
+$faq_form = new SimpleForm($form_fields, $_POST);
+
+if ($_POST) {
     $errors=array();
+    // General attachments
     $_POST['files'] = $faq_form->getField('attachments')->getClean();
+    // Language-specific attachments
+    if ($langs) {
+        $langs[] = $cfg->getPrimaryLanguage();
+        foreach ($langs as $lang) {
+            $_POST['files_'.$lang] = $faq_form->getField('attachments.'.$lang)->getClean();
+        }
+    }
     switch(strtolower($_POST['do'])) {
         case 'create':
         case 'add':
-            if(($faq=FAQ::add($_POST,$errors))) {
+            $faq = FAQ::create();
+            if($faq->update($_POST,$errors)) {
                 $msg=sprintf(__('Successfully added %s'), Format::htmlchars($faq->getQuestion()));
                 // Delete draft for this new faq
                 Draft::deleteForNamespace('faq', $thisstaff->getId());
@@ -45,13 +72,12 @@ if($_POST):
                      __('this FAQ article'));
         break;
         case 'update':
-        case 'edit';
+        case 'edit':
             if(!$faq)
                 $errors['err'] = sprintf(__('%s: Invalid or unknown'), __('FAQ article'));
             elseif($faq->update($_POST,$errors)) {
                 $msg=sprintf(__('Successfully updated %s'), __('this FAQ article'));
                 $_REQUEST['a']=null; //Go back to view
-                $faq->reload();
                 // Delete pending draft updates for this faq (for ALL users)
                 Draft::deleteForNamespace('faq.'.$faq->getId());
             } elseif(!$errors['err'])
@@ -97,15 +123,33 @@ if($_POST):
             $errors['err']=__('Unknown action');
 
     }
-endif;
-
+}
+else {
+    // Not a POST — load database-backed attachments to attachment fields
+    if ($langs && $faq) {
+        // Multi-lingual system
+        foreach ($langs as $lang) {
+            $attachments = $faq_form->getField('attachments.'.$lang);
+            $attachments->setAttachments($faq->getAttachments($lang)->window(array('inline' => false)));
+        }
+    }
+    if ($faq) {
+        // Common attachments
+        $attachments = $faq_form->getField('attachments');
+        $attachments->setAttachments($faq->getAttachments()->window(array('inline' => false)));
+    }
+}
 
 $inc='faq-categories.inc.php'; //FAQs landing page.
 if($faq) {
     $inc='faq-view.inc.php';
-    if($_REQUEST['a']=='edit' && $thisstaff->canManageFAQ())
+    if ($_REQUEST['a']=='edit'
+            && $thisstaff->hasPerm(FAQ::PERM_MANAGE))
         $inc='faq.inc.php';
-}elseif($_REQUEST['a']=='add' && $thisstaff->canManageFAQ()) {
+    elseif ($_REQUEST['a'] == 'print')
+        return $faq->printPdf();
+}elseif($_REQUEST['a']=='add'
+        && $thisstaff->hasPerm(FAQ::PERM_MANAGE)) {
     $inc='faq.inc.php';
 } elseif($category && $_REQUEST['a']!='search') {
     $inc='faq-category.inc.php';
diff --git a/scp/forms.php b/scp/forms.php
index 9721c2f0055d5d29013965c0cf88170164e3fc1f..59084775c088e9bd5e845c7b651e3542879e05a3 100644
--- a/scp/forms.php
+++ b/scp/forms.php
@@ -113,7 +113,7 @@ if($_POST) {
                 'name'=>trim($_POST["name-new-$i"]),
             ));
             $field->setRequirementMode($_POST["visibility-new-$i"]);
-            $field->setForm($form);
+            $form->fields->add($field);
             if (in_array($field->get('name'), $names))
                 $field->addError(__('Field variable name is not unique'), 'name');
             if ($field->isValid()) {
@@ -124,9 +124,7 @@ if($_POST) {
             else
                 $errors["new-$i"] = $field->errors();
         }
-        // XXX: Move to an instrumented list that can handle this better
         if (!$errors) {
-            $form->_dfields = $form->_fields = null;
             $form->save(true);
             foreach ($form_fields as $field) {
                 $field->form = $form;
diff --git a/scp/groups.php b/scp/groups.php
deleted file mode 100644
index c3f17f9c22e7f66b3e239bdf5943e623ccafb5e5..0000000000000000000000000000000000000000
--- a/scp/groups.php
+++ /dev/null
@@ -1,122 +0,0 @@
-<?php
-/*********************************************************************
-    groups.php
-
-    User Groups.
-
-    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('admin.inc.php');
-
-$group=null;
-if($_REQUEST['id'] && !($group=Group::lookup($_REQUEST['id'])))
-    $errors['err']=sprintf(__('%s: Unknown or invalid ID.'), __('group'));
-
-if($_POST){
-    switch(strtolower($_POST['do'])){
-        case 'update':
-            if(!$group){
-                $errors['err']=sprintf(__('%s: Unknown or invalid'), __('group'));
-            }elseif($group->update($_POST,$errors)){
-                $msg=sprintf(__('Successfully updated %s'),
-                    __('this group'));
-            }elseif(!$errors['err']){
-                $errors['err']=sprintf(__('Unable to update %s. Correct error(s) below and try again!'),
-                    __('this group'));
-            }
-            break;
-        case 'create':
-            if(($id=Group::create($_POST,$errors))){
-                $msg=sprintf(__('Successfully added %s'),Format::htmlchars($_POST['name']));
-                $_REQUEST['a']=null;
-            }elseif(!$errors['err']){
-                $errors['err']=sprintf(__('Unable to add %s. Correct error(s) below and try again.'),
-                    __('this group'));
-            }
-            break;
-        case 'mass_process':
-            if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) {
-                $errors['err'] = sprintf(__('You must select at least %s.'), __('one group'));
-            } elseif(in_array($thisstaff->getGroupId(), $_POST['ids'])) {
-                $errors['err'] = __("As an admin, you cannot disable/delete a group you belong to - you might lockout all admins!");
-            } else {
-                $count=count($_POST['ids']);
-                switch(strtolower($_POST['a'])) {
-                    case 'enable':
-                        $sql='UPDATE '.GROUP_TABLE.' SET group_enabled=1, updated=NOW() '
-                            .' WHERE group_id IN ('.implode(',', db_input($_POST['ids'])).')';
-
-                        if(db_query($sql) && ($num=db_affected_rows())){
-                            if($num==$count)
-                                $msg = sprintf(__('Successfully activated %s'),
-                                    _N('selected group', 'selected groups', $count));
-                            else
-                                $warn = sprintf(__('%1$d of %2$d %3$s activated'), $num, $count,
-                                    _N('selected group', 'selected groups', $count));
-                        } else {
-                            $errors['err'] = sprintf(__('Unable to activate %s'),
-                                _N('selected group', 'selected groups', $count));
-                        }
-                        break;
-                    case 'disable':
-                        $sql='UPDATE '.GROUP_TABLE.' SET group_enabled=0, updated=NOW() '
-                            .' WHERE group_id IN ('.implode(',', db_input($_POST['ids'])).')';
-                        if(db_query($sql) && ($num=db_affected_rows())) {
-                            if($num==$count)
-                                $msg = sprintf(__('Successfully disabled %s'),
-                                    _N('selected group', 'selected groups', $count));
-                            else
-                                $warn = sprintf(__('%1$d of %2$d %3$s disabled'), $num, $count,
-                                    _N('selected group', 'selected groups', $count));
-                        } else {
-                            $errors['err'] = sprintf(__('Unable to disable %s'),
-                                _N('selected group', 'selected groups', $count));
-                        }
-                        break;
-                    case 'delete':
-                        foreach($_POST['ids'] as $k=>$v) {
-                            if(($g=Group::lookup($v)) && $g->delete())
-                                $i++;
-                        }
-
-                        if($i && $i==$count)
-                            $msg = sprintf(__('Successfully deleted %s'),
-                                _N('selected group', 'selected groups', $count));
-                        elseif($i>0)
-                            $warn = sprintf(__('%1$d of %2$d %3$s deleted'), $i, $count,
-                                _N('selected group', 'selected groups', $count));
-                        elseif(!$errors['err'])
-                            $errors['err'] = sprintf(__('Unable to delete %s'),
-                                _N('selected group', 'selected groups', $count));
-                        break;
-                    default:
-                        $errors['err']  = __('Unknown action - get technical help.');
-                }
-            }
-            break;
-        default:
-            $errors['err']=__('Unknown action');
-            break;
-    }
-}
-
-$page='groups.inc.php';
-$tip_namespace = 'staff.groups';
-if($group || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) {
-    $page='group.inc.php';
-}
-
-$nav->setTabActive('staff');
-$ost->addExtraHeader('<meta name="tip-namespace" content="' . $tip_namespace . '" />',
-    "$('#content').data('tipNamespace', '".$tip_namespace."');");
-require(STAFFINC_DIR.'header.inc.php');
-require(STAFFINC_DIR.$page);
-include(STAFFINC_DIR.'footer.inc.php');
-?>
diff --git a/scp/helptopics.php b/scp/helptopics.php
index 1ae0568928701a0b8a54d59d89288e9a73fd0593..035b04da810e8ebf2dc5187cc180c64c857e9730 100644
--- a/scp/helptopics.php
+++ b/scp/helptopics.php
@@ -15,6 +15,7 @@
 **********************************************************************/
 require('admin.inc.php');
 include_once(INCLUDE_DIR.'class.topic.php');
+include_once(INCLUDE_DIR.'class.faq.php');
 require_once(INCLUDE_DIR.'class.dynamic_forms.php');
 
 $topic=null;
@@ -35,7 +36,9 @@ if($_POST){
             }
             break;
         case 'create':
-            if(($id=Topic::create($_POST,$errors))){
+            $_topic = Topic::create();
+            if ($_topic->update($_POST, $errors)) {
+                $topic = $_topic;
                 $msg=sprintf(__('Successfully added %s'), Format::htmlchars($_POST['topic']));
                 $_REQUEST['a']=null;
             }elseif(!$errors['err']){
@@ -58,10 +61,13 @@ if($_POST){
 
                 switch(strtolower($_POST['a'])) {
                     case 'enable':
-                        $sql='UPDATE '.TOPIC_TABLE.' SET isactive=1 '
-                            .' WHERE topic_id IN ('.implode(',', db_input($_POST['ids'])).')';
+                        $num = Topic::objects()->filter(array(
+                            'topic_id__in' => $_POST['ids'],
+                        ))->update(array(
+                            'isactive' => true,
+                        ));
 
-                        if(db_query($sql) && ($num=db_affected_rows())) {
+                        if ($num > 0) {
                             if($num==$count)
                                 $msg = sprintf(__('Successfully enabled %s'),
                                     _N('selected help topic', 'selected help topics', $count));
@@ -74,10 +80,14 @@ if($_POST){
                         }
                         break;
                     case 'disable':
-                        $sql='UPDATE '.TOPIC_TABLE.' SET isactive=0 '
-                            .' WHERE topic_id IN ('.implode(',', db_input($_POST['ids'])).')'
-                            .' AND topic_id <> '.db_input($cfg->getDefaultTopicId());
-                        if(db_query($sql) && ($num=db_affected_rows())) {
+                        $num = Topic::objects()->filter(array(
+                            'topic_id__in'=>$_POST['ids'],
+                        ))->exclude(array(
+                            'topic_id'=>$cfg->getDefaultTopicId(),
+                        ))->update(array(
+                            'isactive' => false,
+                        ));
+                        if ($num > 0) {
                             if($num==$count)
                                 $msg = sprintf(__('Successfully disabled %s'),
                                     _N('selected help topic', 'selected help topics', $count));
@@ -90,11 +100,9 @@ if($_POST){
                         }
                         break;
                     case 'delete':
-                        $i=0;
-                        foreach($_POST['ids'] as $k=>$v) {
-                            if(($t=Topic::lookup($v)) && $t->delete())
-                                $i++;
-                        }
+                        $i = Topic::objects()->filter(array(
+                            'topic_id__in'=>$_POST['ids']
+                        ))->delete();
 
                         if($i && $i==$count)
                             $msg = sprintf(__('Successfully deleted %s'),
diff --git a/scp/images/login-background.jpg b/scp/images/login-background.jpg
deleted file mode 100644
index eebfa40b8f24cf1a088e6d4eb1a6b279e958ad7c..0000000000000000000000000000000000000000
Binary files a/scp/images/login-background.jpg and /dev/null differ
diff --git a/scp/images/login-headquarters.jpg b/scp/images/login-headquarters.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..5e9c3846b34f49dd31e4bf06b849becc5a7d72b8
Binary files /dev/null and b/scp/images/login-headquarters.jpg differ
diff --git a/scp/images/oscar-avatars.png b/scp/images/oscar-avatars.png
new file mode 100644
index 0000000000000000000000000000000000000000..624c0420ab336fb6391b7288b869526bf9bc55df
Binary files /dev/null and b/scp/images/oscar-avatars.png differ
diff --git a/scp/images/oscar-wall.jpg b/scp/images/oscar-wall.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..598a909ff8f12c0f7581d59b944adf9ed1dd2470
Binary files /dev/null and b/scp/images/oscar-wall.jpg differ
diff --git a/scp/images/osticket-grey.png b/scp/images/osticket-grey.png
new file mode 100755
index 0000000000000000000000000000000000000000..df6ad5f9f0ee1bd60728fdc65f8bd9aac08365b5
Binary files /dev/null and b/scp/images/osticket-grey.png differ
diff --git a/scp/js/bootstrap-tooltip.js b/scp/js/bootstrap-tooltip.js
new file mode 100644
index 0000000000000000000000000000000000000000..0779f139d6ccd8349f5ac1a2a8610a4a2515047e
--- /dev/null
+++ b/scp/js/bootstrap-tooltip.js
@@ -0,0 +1,514 @@
+/* ========================================================================
+ * Bootstrap: tooltip.js v3.3.4
+ * http://getbootstrap.com/javascript/#tooltip
+ * Inspired by the original jQuery.tipsy by Jason Frame
+ * ========================================================================
+ * Copyright 2011-2015 Twitter, Inc.
+ * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
+ * ======================================================================== */
+
+
++function ($) {
+  'use strict';
+
+  // TOOLTIP PUBLIC CLASS DEFINITION
+  // ===============================
+
+  var Tooltip = function (element, options) {
+    this.type       = null
+    this.options    = null
+    this.enabled    = null
+    this.timeout    = null
+    this.hoverState = null
+    this.$element   = null
+    this.inState    = null
+
+    this.init('tooltip', element, options)
+  }
+
+  Tooltip.VERSION  = '3.3.4'
+
+  Tooltip.TRANSITION_DURATION = 150
+
+  Tooltip.DEFAULTS = {
+    animation: true,
+    placement: 'top',
+    selector: false,
+    template: '<div class="tooltip" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>',
+    trigger: 'hover focus',
+    title: '',
+    delay: 0,
+    html: false,
+    container: false,
+    viewport: {
+      selector: 'body',
+      padding: 0
+    }
+  }
+
+  Tooltip.prototype.init = function (type, element, options) {
+    this.enabled   = true
+    this.type      = type
+    this.$element  = $(element)
+    this.options   = this.getOptions(options)
+    this.$viewport = this.options.viewport && $($.isFunction(this.options.viewport) ? this.options.viewport.call(this, this.$element) : (this.options.viewport.selector || this.options.viewport))
+    this.inState   = { click: false, hover: false, focus: false }
+
+    if (this.$element[0] instanceof document.constructor && !this.options.selector) {
+      throw new Error('`selector` option must be specified when initializing ' + this.type + ' on the window.document object!')
+    }
+
+    var triggers = this.options.trigger.split(' ')
+
+    for (var i = triggers.length; i--;) {
+      var trigger = triggers[i]
+
+      if (trigger == 'click') {
+        this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
+      } else if (trigger != 'manual') {
+        var eventIn  = trigger == 'hover' ? 'mouseenter' : 'focusin'
+        var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout'
+
+        this.$element.on(eventIn  + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
+        this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
+      }
+    }
+
+    this.options.selector ?
+      (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
+      this.fixTitle()
+  }
+
+  Tooltip.prototype.getDefaults = function () {
+    return Tooltip.DEFAULTS
+  }
+
+  Tooltip.prototype.getOptions = function (options) {
+    options = $.extend({}, this.getDefaults(), this.$element.data(), options)
+
+    if (options.delay && typeof options.delay == 'number') {
+      options.delay = {
+        show: options.delay,
+        hide: options.delay
+      }
+    }
+
+    return options
+  }
+
+  Tooltip.prototype.getDelegateOptions = function () {
+    var options  = {}
+    var defaults = this.getDefaults()
+
+    this._options && $.each(this._options, function (key, value) {
+      if (defaults[key] != value) options[key] = value
+    })
+
+    return options
+  }
+
+  Tooltip.prototype.enter = function (obj) {
+    var self = obj instanceof this.constructor ?
+      obj : $(obj.currentTarget).data('bs.' + this.type)
+
+    if (!self) {
+      self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
+      $(obj.currentTarget).data('bs.' + this.type, self)
+    }
+
+    if (obj instanceof $.Event) {
+      self.inState[obj.type == 'focusin' ? 'focus' : 'hover'] = true
+    }
+
+    if (self.tip().hasClass('in') || self.hoverState == 'in') {
+      self.hoverState = 'in'
+      return
+    }
+
+    clearTimeout(self.timeout)
+
+    self.hoverState = 'in'
+
+    if (!self.options.delay || !self.options.delay.show) return self.show()
+
+    self.timeout = setTimeout(function () {
+      if (self.hoverState == 'in') self.show()
+    }, self.options.delay.show)
+  }
+
+  Tooltip.prototype.isInStateTrue = function () {
+    for (var key in this.inState) {
+      if (this.inState[key]) return true
+    }
+
+    return false
+  }
+
+  Tooltip.prototype.leave = function (obj) {
+    var self = obj instanceof this.constructor ?
+      obj : $(obj.currentTarget).data('bs.' + this.type)
+
+    if (!self) {
+      self = new this.constructor(obj.currentTarget, this.getDelegateOptions())
+      $(obj.currentTarget).data('bs.' + this.type, self)
+    }
+
+    if (obj instanceof $.Event) {
+      self.inState[obj.type == 'focusout' ? 'focus' : 'hover'] = false
+    }
+
+    if (self.isInStateTrue()) return
+
+    clearTimeout(self.timeout)
+
+    self.hoverState = 'out'
+
+    if (!self.options.delay || !self.options.delay.hide) return self.hide()
+
+    self.timeout = setTimeout(function () {
+      if (self.hoverState == 'out') self.hide()
+    }, self.options.delay.hide)
+  }
+
+  Tooltip.prototype.show = function () {
+    var e = $.Event('show.bs.' + this.type)
+
+    if (this.hasContent() && this.enabled) {
+      this.$element.trigger(e)
+
+      var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0])
+      if (e.isDefaultPrevented() || !inDom) return
+      var that = this
+
+      var $tip = this.tip()
+
+      var tipId = this.getUID(this.type)
+
+      this.setContent()
+      $tip.attr('id', tipId)
+      this.$element.attr('aria-describedby', tipId)
+
+      if (this.options.animation) $tip.addClass('fade')
+
+      var placement = typeof this.options.placement == 'function' ?
+        this.options.placement.call(this, $tip[0], this.$element[0]) :
+        this.options.placement
+
+      var autoToken = /\s?auto?\s?/i
+      var autoPlace = autoToken.test(placement)
+      if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
+
+      $tip
+        .detach()
+        .css({ top: 0, left: 0, display: 'block' })
+        .addClass(placement)
+        .data('bs.' + this.type, this)
+
+      this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
+      this.$element.trigger('inserted.bs.' + this.type)
+
+      var pos          = this.getPosition()
+      var actualWidth  = $tip[0].offsetWidth
+      var actualHeight = $tip[0].offsetHeight
+
+      if (autoPlace) {
+        var orgPlacement = placement
+        var viewportDim = this.getPosition(this.$viewport)
+
+        placement = placement == 'bottom' && pos.bottom + actualHeight > viewportDim.bottom ? 'top'    :
+                    placement == 'top'    && pos.top    - actualHeight < viewportDim.top    ? 'bottom' :
+                    placement == 'right'  && pos.right  + actualWidth  > viewportDim.width  ? 'left'   :
+                    placement == 'left'   && pos.left   - actualWidth  < viewportDim.left   ? 'right'  :
+                    placement
+
+        $tip
+          .removeClass(orgPlacement)
+          .addClass(placement)
+      }
+
+      var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
+
+      this.applyPlacement(calculatedOffset, placement)
+
+      var complete = function () {
+        var prevHoverState = that.hoverState
+        that.$element.trigger('shown.bs.' + that.type)
+        that.hoverState = null
+
+        if (prevHoverState == 'out') that.leave(that)
+      }
+
+      $.support.transition && this.$tip.hasClass('fade') ?
+        $tip
+          .one('bsTransitionEnd', complete)
+          .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
+        complete()
+    }
+  }
+
+  Tooltip.prototype.applyPlacement = function (offset, placement) {
+    var $tip   = this.tip()
+    var width  = $tip[0].offsetWidth
+    var height = $tip[0].offsetHeight
+
+    // manually read margins because getBoundingClientRect includes difference
+    var marginTop = parseInt($tip.css('margin-top'), 10)
+    var marginLeft = parseInt($tip.css('margin-left'), 10)
+
+    // we must check for NaN for ie 8/9
+    if (isNaN(marginTop))  marginTop  = 0
+    if (isNaN(marginLeft)) marginLeft = 0
+
+    offset.top  += marginTop
+    offset.left += marginLeft
+
+    // $.fn.offset doesn't round pixel values
+    // so we use setOffset directly with our own function B-0
+    $.offset.setOffset($tip[0], $.extend({
+      using: function (props) {
+        $tip.css({
+          top: Math.round(props.top),
+          left: Math.round(props.left)
+        })
+      }
+    }, offset), 0)
+
+    $tip.addClass('in')
+
+    // check to see if placing tip in new offset caused the tip to resize itself
+    var actualWidth  = $tip[0].offsetWidth
+    var actualHeight = $tip[0].offsetHeight
+
+    if (placement == 'top' && actualHeight != height) {
+      offset.top = offset.top + height - actualHeight
+    }
+
+    var delta = this.getViewportAdjustedDelta(placement, offset, actualWidth, actualHeight)
+
+    if (delta.left) offset.left += delta.left
+    else offset.top += delta.top
+
+    var isVertical          = /top|bottom/.test(placement)
+    var arrowDelta          = isVertical ? delta.left * 2 - width + actualWidth : delta.top * 2 - height + actualHeight
+    var arrowOffsetPosition = isVertical ? 'offsetWidth' : 'offsetHeight'
+
+    $tip.offset(offset)
+    this.replaceArrow(arrowDelta, $tip[0][arrowOffsetPosition], isVertical)
+  }
+
+  Tooltip.prototype.replaceArrow = function (delta, dimension, isVertical) {
+    this.arrow()
+      .css(isVertical ? 'left' : 'top', 50 * (1 - delta / dimension) + '%')
+      .css(isVertical ? 'top' : 'left', '')
+  }
+
+  Tooltip.prototype.setContent = function () {
+    var $tip  = this.tip()
+    var title = this.getTitle()
+
+    $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
+    $tip.removeClass('fade in top bottom left right')
+  }
+
+  Tooltip.prototype.hide = function (callback) {
+    var that = this
+    var $tip = $(this.$tip)
+    var e    = $.Event('hide.bs.' + this.type)
+
+    function complete() {
+      if (that.hoverState != 'in') $tip.detach()
+      that.$element
+        .removeAttr('aria-describedby')
+        .trigger('hidden.bs.' + that.type)
+      callback && callback()
+    }
+
+    this.$element.trigger(e)
+
+    if (e.isDefaultPrevented()) return
+
+    $tip.removeClass('in')
+
+    $.support.transition && $tip.hasClass('fade') ?
+      $tip
+        .one('bsTransitionEnd', complete)
+        .emulateTransitionEnd(Tooltip.TRANSITION_DURATION) :
+      complete()
+
+    this.hoverState = null
+
+    return this
+  }
+
+  Tooltip.prototype.fixTitle = function () {
+    var $e = this.$element
+    if ($e.attr('title') || typeof $e.attr('data-original-title') != 'string') {
+      $e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
+    }
+  }
+
+  Tooltip.prototype.hasContent = function () {
+    return this.getTitle()
+  }
+
+  Tooltip.prototype.getPosition = function ($element) {
+    $element   = $element || this.$element
+
+    var el     = $element[0]
+    var isBody = el.tagName == 'BODY'
+
+    var elRect    = el.getBoundingClientRect()
+    if (elRect.width == null) {
+      // width and height are missing in IE8, so compute them manually; see https://github.com/twbs/bootstrap/issues/14093
+      elRect = $.extend({}, elRect, { width: elRect.right - elRect.left, height: elRect.bottom - elRect.top })
+    }
+    var elOffset  = isBody ? { top: 0, left: 0 } : $element.offset()
+    var scroll    = { scroll: isBody ? document.documentElement.scrollTop || document.body.scrollTop : $element.scrollTop() }
+    var outerDims = isBody ? { width: $(window).width(), height: $(window).height() } : null
+
+    return $.extend({}, elRect, scroll, outerDims, elOffset)
+  }
+
+  Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
+    return placement == 'bottom' ? { top: pos.top + pos.height,   left: pos.left + pos.width / 2 - actualWidth / 2 } :
+           placement == 'top'    ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } :
+           placement == 'left'   ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
+        /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }
+
+  }
+
+  Tooltip.prototype.getViewportAdjustedDelta = function (placement, pos, actualWidth, actualHeight) {
+    var delta = { top: 0, left: 0 }
+    if (!this.$viewport) return delta
+
+    var viewportPadding = this.options.viewport && this.options.viewport.padding || 0
+    var viewportDimensions = this.getPosition(this.$viewport)
+
+    if (/right|left/.test(placement)) {
+      var topEdgeOffset    = pos.top - viewportPadding - viewportDimensions.scroll
+      var bottomEdgeOffset = pos.top + viewportPadding - viewportDimensions.scroll + actualHeight
+      if (topEdgeOffset < viewportDimensions.top) { // top overflow
+        delta.top = viewportDimensions.top - topEdgeOffset
+      } else if (bottomEdgeOffset > viewportDimensions.top + viewportDimensions.height) { // bottom overflow
+        delta.top = viewportDimensions.top + viewportDimensions.height - bottomEdgeOffset
+      }
+    } else {
+      var leftEdgeOffset  = pos.left - viewportPadding
+      var rightEdgeOffset = pos.left + viewportPadding + actualWidth
+      if (leftEdgeOffset < viewportDimensions.left) { // left overflow
+        delta.left = viewportDimensions.left - leftEdgeOffset
+      } else if (rightEdgeOffset > viewportDimensions.right) { // right overflow
+        delta.left = viewportDimensions.left + viewportDimensions.width - rightEdgeOffset
+      }
+    }
+
+    return delta
+  }
+
+  Tooltip.prototype.getTitle = function () {
+    var title
+    var $e = this.$element
+    var o  = this.options
+
+    title = $e.attr('data-original-title')
+      || (typeof o.title == 'function' ? o.title.call($e[0]) :  o.title)
+
+    return title
+  }
+
+  Tooltip.prototype.getUID = function (prefix) {
+    do prefix += ~~(Math.random() * 1000000)
+    while (document.getElementById(prefix))
+    return prefix
+  }
+
+  Tooltip.prototype.tip = function () {
+    if (!this.$tip) {
+      this.$tip = $(this.options.template)
+      if (this.$tip.length != 1) {
+        throw new Error(this.type + ' `template` option must consist of exactly 1 top-level element!')
+      }
+    }
+    return this.$tip
+  }
+
+  Tooltip.prototype.arrow = function () {
+    return (this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow'))
+  }
+
+  Tooltip.prototype.enable = function () {
+    this.enabled = true
+  }
+
+  Tooltip.prototype.disable = function () {
+    this.enabled = false
+  }
+
+  Tooltip.prototype.toggleEnabled = function () {
+    this.enabled = !this.enabled
+  }
+
+  Tooltip.prototype.toggle = function (e) {
+    var self = this
+    if (e) {
+      self = $(e.currentTarget).data('bs.' + this.type)
+      if (!self) {
+        self = new this.constructor(e.currentTarget, this.getDelegateOptions())
+        $(e.currentTarget).data('bs.' + this.type, self)
+      }
+    }
+
+    if (e) {
+      self.inState.click = !self.inState.click
+      if (self.isInStateTrue()) self.enter(self)
+      else self.leave(self)
+    } else {
+      self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
+    }
+  }
+
+  Tooltip.prototype.destroy = function () {
+    var that = this
+    clearTimeout(this.timeout)
+    this.hide(function () {
+      that.$element.off('.' + that.type).removeData('bs.' + that.type)
+      if (that.$tip) {
+        that.$tip.detach()
+      }
+      that.$tip = null
+      that.$arrow = null
+      that.$viewport = null
+    })
+  }
+
+
+  // TOOLTIP PLUGIN DEFINITION
+  // =========================
+
+  function Plugin(option) {
+    return this.each(function () {
+      var $this   = $(this)
+      var data    = $this.data('bs.tooltip')
+      var options = typeof option == 'object' && option
+
+      if (!data && /destroy|hide/.test(option)) return
+      if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
+      if (typeof option == 'string') data[option]()
+    })
+  }
+
+  var old = $.fn.tooltip
+
+  $.fn.tooltip             = Plugin
+  $.fn.tooltip.Constructor = Tooltip
+
+
+  // TOOLTIP NO CONFLICT
+  // ===================
+
+  $.fn.tooltip.noConflict = function () {
+    $.fn.tooltip = old
+    return this
+  }
+
+}(jQuery);
diff --git a/scp/js/bootstrap-typeahead.js b/scp/js/bootstrap-typeahead.js
index 1e039f02ade3395bac99ae5ef35ae113ce837879..fffa189ecb1875a847f5c174b8fa676016d605bb 100644
--- a/scp/js/bootstrap-typeahead.js
+++ b/scp/js/bootstrap-typeahead.js
@@ -28,10 +28,13 @@
     this.sorter = this.options.sorter || this.sorter
     this.highlighter = this.options.highlighter || this.highlighter
     this.$menu = $(this.options.menu).appendTo('body')
+    if (this.options.scroll)
+      this.$menu.addClass('scroll');
     this.source = this.options.source
     this.onselect = this.options.onselect
     this.strings = true
     this.shown = false
+    this.deferred = null
     this.listen()
   }
 
@@ -39,7 +42,7 @@
 
     constructor: Typeahead
 
-  , select: function () {
+  , select: function (e) {
       var val = JSON.parse(this.$menu.find('.active').attr('data-value'))
         , text
 
@@ -49,7 +52,8 @@
       this.$element.val(text)
 
       if (typeof this.onselect == "function")
-          this.onselect(val)
+          if (false === this.onselect(val, e))
+              return;
 
       return this.hide()
     }
@@ -75,6 +79,11 @@
       return this
     }
 
+  , fetch: function() {
+      var value = this.source(this, this.query)
+      if (value) this.process(value)
+    }
+
   , lookup: function (event) {
       var that = this
         , items
@@ -84,8 +93,9 @@
       this.query = this.$element.val();
       /*Check if we have a match on the current source?? */
       if (typeof this.source == "function") {
-        value = this.source(this, this.query)
-        if (value) this.process(value)
+        if (!this.options.delay) return this.fetch()
+        if (this.deferred) clearTimeout(this.deferred)
+        this.deferred = setTimeout(this.fetch.bind(this), this.options.delay)
       } else {
         this.process(this.source)
       }
@@ -101,15 +111,15 @@
 
       this.query = this.$element.val()
 
-      if (!this.query) {
+      if (this.query.length < this.options.minLength) {
         return this.shown ? this.hide() : this
       }
-      
+
       items = $.grep(results, function (item) {
         if (!that.strings)
           item = item[that.options.property]
 
-        if (that.matcher(item)) 
+        if (that.matcher(item))
             return item
       })
 
@@ -146,15 +156,17 @@
     }
 
   , highlighter: function (item) {
-      return item.replace(new RegExp('(' + this.query + ')', 'ig'), function ($1, match) {
-        return '<strong>' + match + '</strong>'
-      })
+      if (!this.query)
+          return item;
+      var exp = this.query.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&").replace(' ', '|')
+      return item.replace(new RegExp(exp, 'ig'), '<strong>$&</strong>')
     }
 
   , render: function (items) {
       var that = this
 
       items = $(items).map(function (i, item) {
+        var orig = item;
         i = $(that.options.item).attr('data-value', JSON.stringify(item))
         if (!that.strings) {
             if(item[that.options.render])
@@ -162,7 +174,7 @@
             else
                 item = item[that.options.property];
         }
-        i.find('a').html(that.highlighter(item))
+        i.find('a').html(that.highlighter(item, orig))
         return i[0]
       })
 
@@ -171,6 +183,16 @@
       return this
     }
 
+  , adjustScroll: function(next) {
+      var top = this.$menu.scrollTop(),
+        bottom = top + this.$menu.height(),
+        pos = next.position();
+      if (pos.top < 0)
+        this.$menu.scrollTop(top + pos.top - 10);
+      else if (next.height() + top + pos.top > bottom)
+        this.$menu.scrollTop(top + pos.top - this.$menu.height() + next.height() + 10);
+  }
+
   , next: function (event) {
       var active = this.$menu.find('.active').removeClass('active')
         , next = active.next()
@@ -180,6 +202,10 @@
       }
 
       next.addClass('active')
+
+      if (this.options.scroll) {
+        this.adjustScroll(next);
+      }
     }
 
   , prev: function (event) {
@@ -191,6 +217,10 @@
       }
 
       prev.addClass('active')
+
+      if (this.options.scroll) {
+        this.adjustScroll(prev);
+      }
     }
 
   , listen: function () {
@@ -199,7 +229,7 @@
         .on('keypress', $.proxy(this.keypress, this))
         .on('keyup',    $.proxy(this.keyup, this))
 
-      if ($.browser.webkit || $.browser.msie) {
+      if (this.eventSupported('keydown')) {
         this.$element.on('keydown', $.proxy(this.keypress, this))
       }
 
@@ -208,6 +238,15 @@
         .on('mouseenter', 'li', $.proxy(this.mouseenter, this))
     }
 
+  , eventSupported: function(eventName) {
+      var isSupported = eventName in this.$element
+      if (!isSupported) {
+        this.$element.setAttribute(eventName, 'return;')
+        isSupported = typeof this.$element[eventName] === 'function'
+      }
+      return isSupported
+    }
+
   , keyup: function (e) {
       e.stopPropagation()
       e.preventDefault()
@@ -220,7 +259,7 @@
         case 9: // tab
         case 13: // enter
           if (!this.shown) return
-          this.select()
+          this.select(e)
           break
 
         case 27: // escape
@@ -266,7 +305,7 @@
   , click: function (e) {
       e.stopPropagation()
       e.preventDefault()
-      this.select()
+      this.select(e)
     }
 
   , mouseenter: function (e) {
@@ -274,6 +313,10 @@
       $(e.currentTarget).addClass('active')
     }
 
+  , visible: function() {
+      return this.shown;
+    }
+
   }
 
 
@@ -298,6 +341,9 @@
   , onselect: null
   , property: 'value'
   , render: 'info'
+  , minLength: 1
+  , scroll: false
+  , delay: 200
   }
 
   $.fn.typeahead.Constructor = Typeahead
diff --git a/scp/js/dashboard.inc.js b/scp/js/dashboard.inc.js
index 683135e62c05b10975e6b79c897f0d33dc5557ec..dd850da7b6c4b57edfa58030c330e2922aa92e08 100644
--- a/scp/js/dashboard.inc.js
+++ b/scp/js/dashboard.inc.js
@@ -1,275 +1,99 @@
 (function ($) {
-    var current_tab = null;
-    function refresh(e) {
+    $.drawPlots = function(json) {
         $('#line-chart-here').empty();
         $('#line-chart-legend').empty();
         var r = new Raphael('line-chart-here'),
             width = $('#line-chart-here').width(),
             height = $('#line-chart-here').height();
-        $.ajax({
-            method:     'GET',
-            url:        'ajax.php/report/overview/graph',
-            data:       $(this).serialize(),
-            dataType:   'json',
-            success:    function(json) {
-                var times = [],
-                    smtimes = Array.prototype.concat.apply([], json.times),
-                    plots = [],
-                    max = 0;
+        var times = [],
+            smtimes = Array.prototype.concat.apply([], json.times),
+            plots = [],
+            max = 0;
 
-                // Convert the timestamp to number of whole days after the
-                // unix epoch.
-                for (key in smtimes) {
-                    smtimes[key] = Math.floor(smtimes[key] / 86400);
-                }
-                for (key in json.events) {
-                    e = json.events[key];
-                    if (json.plots[e] === undefined) continue;
-                    $('<span>').append(e)
-                        .attr({'class':'label','style':'margin-left:0.5em'})
-                        .appendTo($('#line-chart-legend'));
-                    $('<br>').appendTo('#line-chart-legend');
-                    times.push(smtimes);
-                    plots.push(json.plots[e]);
-                    // Keep track of max value from any plot
-                    max = Math.max(max, Math.max.apply(Math, json.plots[e]));
-                }
-                m = r.linechart(20, 0, width - 70, height,
-                    times, plots, { 
-                    gutter: 20,
-                    width: 1.6,
-                    nostroke: false, 
-                    shade: false,
-                    axis: "0 0 1 1",
-                    axisxstep: 8,
-                    axisystep: Math.min(12, max),
-                    symbol: "circle",
-                    smooth: false
-                }).hoverColumn(function () {
-                    this.tags = r.set();
-                    var slots = [];
-
-                    for (var i = 0, ii = this.y.length; i < ii; i++) {
-                        if (this.values[i] === 0) continue;
-                        if (this.symbols[i].node.style.display == "none") continue;
-                        var angle = 160;
-                        for (var j = 0, jj = slots.length; j < jj; j++) {
-                            if (slots[j][0] == this.x
-                                    && Math.abs(slots[j][1] - this.y[i]) < 20) {
-                                angle = 20;
-                                break;
-                            }
-                        }
-                        slots.push([this.x, this.y[i]]);
-                        this.tags.push(r.tag(this.x, this.y[i],
-                            this.values[i], angle,
-                            10).insertBefore(this).attr([
-                                { fill: '#eee' },
-                                { fill: this.symbols[i].attr('fill') }]));
-                    }
-                }, function () {
-                    this.tags && this.tags.remove();
-                });
-                // Change axis labels from Unix epoch
-                $('tspan', $('#line-chart-here')).each(function(e) {
-                    var text = this.firstChild.textContent;
-                    if (parseInt(text) > 10000)
-                        this.firstChild.textContent =
-                            $.datepicker.formatDate('mm-dd-yy',
-                            new Date(parseInt(text) * 86400000));
-                });
-                $('span.label').each(function(i, e) {
-                    e = $(e);
-                    e.click(function() {
-                        e.toggleClass('disabled');
-                        if (e.hasClass('disabled')) {
-                            m.symbols[i].hide();
-                            m.lines[i].hide();
-                        } else {
-                            m.symbols[i].show();
-                            m.lines[i].show();
-                        }
-                    });
-                });
-                // Dear aspiring API writers, please consider making [easy]
-                // things simpler than this...
-                $('span.label', '#line-chart-legend').css(
-                    'background-color', function(i) {
-                        return Raphael.color(m.symbols[i][0].attr('fill')).hex; 
-                });
-            }
-        });
-        if (this.start) build_table.apply(this);
-        return false;
-    }
-    $(function() { $('tabular-navigation').tab(); });
-
-    // Add tabs for the tabular display
-    $(function() {
-        $.ajax({
-            url:        'ajax.php/report/overview/table/groups',
-            dataType:   'json',
-            success:    function(json) {
-                var first=true;
-                for (key in json) {
-                    $('#tabular-navigation')
-                        .append($('<li>').attr(first ? {'class':'active'} : {})
-                        .append($('<a>')
-                            .click(build_table)
-                            .attr({'table-group':key,'href':'#'})
-                            .append(json[key])));
-                    first=false;
-                }
-                build_table.apply($('#tabular-navigation li:first-child a')[0])
-            }
-        });
-    });
-
-    var start, stop;
-    function build_table() {
-        if (this.tagName == 'A') {
-            current_tab = $(this).tab('show');
+        // Convert the timestamp to number of whole days after the
+        // unix epoch.
+        for (key in smtimes) {
+            smtimes[key] = Math.floor(smtimes[key] / 86400);
         }
-        else if (this.start) {
-            start = this.start.value || 'last month';
-            stop = this.period.value || 'now';
+        for (key in json.events) {
+            e = json.events[key];
+            if (json.plots[e] === undefined) continue;
+            $('<span>').append(e)
+                .attr({'class':'label','style':'margin-left:0.5em'})
+                .appendTo($('#line-chart-legend'));
+            $('<br>').appendTo('#line-chart-legend');
+            times.push(smtimes);
+            plots.push(json.plots[e]);
+            // Keep track of max value from any plot
+            max = Math.max(max, Math.max.apply(Math, json.plots[e]));
         }
+        m = r.linechart(20, 0, width - 70, height,
+            times, plots, {
+            gutter: 20,
+            width: 1.6,
+            nostroke: false,
+            shade: false,
+            axis: "0 0 1 1",
+            axisxstep: 8,
+            axisystep: Math.min(12, max),
+            symbol: "circle",
+            smooth: false
+        }).hoverColumn(function () {
+            this.tags = r.set();
+            var slots = [];
 
-        if (!current_tab)
-            current_tab = $('#tabular-navigation li:first-child a');
-
-        var group = current_tab.attr('table-group');
-        var pagesize = 25;
-        getConfig().then(function(c) { if (c.page_size) pagesize = c.page_size; });
-        $.ajax({
-            method:     'GET',
-            dataType:   'json',
-            url:        'ajax.php/report/overview/table',
-            data:       {group: group, start: start, period: stop},
-            success:    function(json) {
-                var q = $('<table>').attr({'class':'table table-condensed table-striped'}),
-                    h = $('<tr>').appendTo($('<thead>').appendTo(q)),
-                    max = [];
-                for (var c in json.columns) {
-                    h.append($('<th>').append(json.columns[c]));
-                    max.push(0);
-                }
-                for (y in json.data) {
-                    row = json.data[y];
-                    for (x in row) {
-                        max[x] = Math.max(max[x], parseFloat(row[x]||0));
-                    }
-                }
-                for (var i in json.data) {
-                    if (i % pagesize === 0)
-                        b = $('<tbody>').attr({'page':i/pagesize+1}).addClass('hidden').appendTo(q);
-                    row = json.data[i];
-                    tr = $('<tr>').appendTo(b);
-                    for (var j in row) {
-                        if (j == 0) 
-                            tr.append($('<th>').append(row[j]));
-                        else {
-                            val = parseFloat(row[j])||0;
-                            color = 'black';
-                            size = 0;
-                            if (val && max[j] && json.data.length > 1) {
-                                scale = val / max[j];
-                                color = Raphael.hsb(
-                                    Math.min((1 - scale) * .4, 1),
-                                    .75, .75);
-                                size = 16 * scale;
-                            }
-                            tr.append($('<td>')
-                                .append($('<div>').append(
-                                    $('<div>').css(val ? {
-                                        'background-color': color,
-                                        'width': size,
-                                        'height': size,
-                                        'top': 9 - (size / 2),
-                                        'right': 10 - (size / 2)
-                                    } : {})
-                                    .append("&nbsp;")))
-                                .append(row[j]));
-                        }
+            for (var i = 0, ii = this.y.length; i < ii; i++) {
+                if (this.values[i] === 0) continue;
+                if (this.symbols[i].node.style.display == "none") continue;
+                var angle = 160;
+                for (var j = 0, jj = slots.length; j < jj; j++) {
+                    if (slots[j][0] == this.x
+                            && Math.abs(slots[j][1] - this.y[i]) < 20) {
+                        angle = 20;
+                        break;
                     }
                 }
-                if (json.data.length == 0) {
-                    $('<tbody>').attr('page','1').append($('<tr>').append(
-                        $('<td>').attr('colspan','8').append(
-                            'No data for this timeframe found'))).appendTo(q);
-                }
-                $('tbody[page=1]', q).removeClass('hidden');
-                $('#table-here').empty().append(q);
-
-                // ----------------------> Pagination <---------------------
-                function goabs(e) {
-                    $('tbody', q).addClass('hidden');
-                    if (e.target) {
-                        page = e.target.text;
-                        $('tbody[page='+page+']', q).removeClass('hidden');
-                    } else {
-                        e.removeClass('hidden');
-                        page = e.attr('page')
-                    }
-                    return enable_next_prev(page);
-                }
-                function goprev() {
-                    current = $('tbody:not(.hidden)', q).attr('page');
-                    page = Math.max(1, parseInt(current) - 1);
-                    return goabs($('tbody[page='+page+']', q));
-                }
-                function gonext() {
-                    current = $('tbody:not(.hidden)', q).attr('page');
-                    page = Math.min(Math.floor(json.data.length / pagesize) + 1,
-                        parseInt(current) + 1);
-                    return goabs($('tbody[page='+page+']', q));
-                }
-                function enable_next_prev(page) {
-                    $('#table-here div.pagination li[page]').removeClass('active');
-                    $('#table-here div.pagination li[page='+page+']').addClass('active');
-
-                    if (page == 1)  $('#report-page-prev').addClass('disabled');
-                    else            $('#report-page-prev').removeClass('disabled');
-
-                    if (page == Math.floor(json.data.length / pagesize) + 1)
-                                    $('#report-page-next').addClass('disabled');
-                    else            $('#report-page-next').removeClass('disabled');
-                    return false;
-                }
-
-                var p = $('<ul>')
-                    .appendTo($('<div>').attr({'class':'pagination'})
-                    .appendTo($('#table-here')));
-                $('<a>').click(goprev).attr({'href':'#'})
-                    .append('&laquo;').appendTo($('<li>').attr({'id':'report-page-prev'})
-                    .appendTo(p));
-                $('tbody', q).each(function() {
-                    page = $(this).attr('page');
-                    $('<a>').click(goabs).attr({'href':'#'}).append(page)
-                        .appendTo($('<li>').attr({'page':page})
-                        .appendTo(p));
-                });
-                $('<a>').click(gonext).attr({'href':'#'})
-                    .append('&raquo;').appendTo($('<li>').attr({'id':'report-page-next'})
-                    .appendTo(p));
-
-                // ------------------------> Export <-----------------------
-                $('<a>').attr({'href':'ajax.php/report/overview/table/export?group='
-                        +group+'&start='+start+'&stop='+stop}).append('Export')
-                    .addClass('no-pjax')
-                    .appendTo($('<li>')
-                    .appendTo(p));
-
-                goprev();
+                slots.push([this.x, this.y[i]]);
+                this.tags.push(r.tag(this.x, this.y[i],
+                    this.values[i], angle,
+                    10).insertBefore(this).attr([
+                        { fill: '#eee' },
+                        { fill: this.symbols[i].attr('fill') }]));
             }
+        }, function () {
+            this.tags && this.tags.remove();
         });
-        return false;
-    }
-
-    $(function() {
-        var form = $('#timeframe-form');
-        form.submit(refresh);
-        //Trigger submit now...init.
-        form.submit();
-    });
+        // Change axis labels from Unix epoch
+        var qq = setInterval(function() {
+            if ($.datepicker === undefined)
+                return;
+            clearInterval(qq);
+            $('tspan', $('#line-chart-here')).each(function(e) {
+                var text = this.firstChild.textContent;
+                if (parseInt(text) > 10000)
+                    this.firstChild.textContent =
+                        $.datepicker.formatDate('mm-dd-yy',
+                        new Date(parseInt(text) * 86400000));
+            });
+        }, 50);
+        $('span.label').each(function(i, e) {
+            e = $(e);
+            e.click(function() {
+                e.toggleClass('disabled');
+                if (e.hasClass('disabled')) {
+                    m.symbols[i].hide();
+                    m.lines[i].hide();
+                } else {
+                    m.symbols[i].show();
+                    m.lines[i].show();
+                }
+            });
+        });
+        // Dear aspiring API writers, please consider making [easy]
+        // things simpler than this...
+        $('span.label', '#line-chart-legend').css(
+            'background-color', function(i) {
+                return Raphael.color(m.symbols[i][0].attr('fill')).hex;
+        });
+    };
 })(window.jQuery);
diff --git a/scp/js/jquery.dropdown.js b/scp/js/jquery.dropdown.js
index b885042086efee07eeb228c60abefd86ec278c0b..84a843dfd0e120660bfa1a3376a94d1c5ebb46f0 100644
--- a/scp/js/jquery.dropdown.js
+++ b/scp/js/jquery.dropdown.js
@@ -37,7 +37,11 @@ if(jQuery) (function($) {
 		var trigger = $(this),
 			dropdown = $( $(this).attr('data-dropdown') ),
 			isOpen = trigger.hasClass('dropdown-open'),
-            rtl = $('html').hasClass('rtl');
+            rtl = $('html').hasClass('rtl'),
+            relative = trigger.offsetParent(),
+            offset = relative.offset();
+        if (relative.get(0) !== document.body)
+            offset.top -= relative.scrollTop();
 
 		event.preventDefault();
 		event.stopPropagation();
@@ -50,9 +54,9 @@ if(jQuery) (function($) {
             dropdown.removeClass('anchor-right');
 
 		dropdown.css({
-				left: dropdown.hasClass('anchor-right') ?
-				trigger.offset().left - (dropdown.outerWidth() - trigger.outerWidth() - 4) : trigger.offset().left,
-				top: trigger.offset().top + trigger.outerHeight()
+				left: -offset.left + (dropdown.hasClass('anchor-right') ?
+				trigger.offset().left - (dropdown.outerWidth() - trigger.outerWidth() - 4) : trigger.offset().left),
+				top: -offset.top + trigger.offset().top + trigger.outerHeight()
 			}).show();
 		trigger.addClass('dropdown-open');
 	}
@@ -70,7 +74,7 @@ if(jQuery) (function($) {
 	$(function () {
 		$('body').on('click.dropdown', '[data-dropdown]', showMenu);
 		$('html').on('click.dropdown', hideDropdowns);
-		if( !$.browser.msie || ($.browser.msie && $.browser.version >= 9) ) {
+		if(document.addEventListener) {
 			$(window).on('resize.dropdown', hideDropdowns);
 		}
 	});
diff --git a/scp/js/jquery.translatable.js b/scp/js/jquery.translatable.js
new file mode 100644
index 0000000000000000000000000000000000000000..41b28a16972f0272a9053dcce82a529e76426671
--- /dev/null
+++ b/scp/js/jquery.translatable.js
@@ -0,0 +1,231 @@
+
+!function( $ ){
+
+  "use strict";
+
+  var Translatable = function( element, options ) {
+    this.$element = $(element);
+    this.options = $.extend({}, $.fn.translatable.defaults, options);
+    if (!this.$element.data('translateTag'))
+        return;
+
+    this.shown = false;
+    this.populated = false;
+
+    this.fetch('ajax.php/i18n/langs').then($.proxy(function(json) {
+      this.langs = json;
+      if (Object.keys(this.langs).length) this.decorate();
+    }, this));
+  },
+  // Class-static variables
+  urlcache = {};
+
+  Translatable.prototype = {
+
+    constructor: Translatable,
+
+    fetch: function( url, data, callback ) {
+      if ( !urlcache[ url ] ) {
+        urlcache[ url ] = $.Deferred(function( defer ) {
+          $.ajax( url, { data: data, dataType: 'json' } )
+            .then( defer.resolve, defer.reject );
+        }).promise();
+      }
+      return urlcache[ url ].done( callback );
+    },
+
+    decorate: function() {
+      this.$translations = $('<ul class="translations"></ul>');
+      this.$status = $('<li class="status"><i class="icon-spinner icon-spin"></i> '+__('Loading')+' ...</li>')
+        .appendTo(this.$translations);
+      this.$footer = $('<div class="add-translation"></div>');
+      this.$select = $('<select name="locale"></select>');
+      this.$menu = $(this.options.menu).appendTo('body');
+      this.$container = $('<div class="translatable"></div>')
+          .prependTo(this.$element.parent())
+          .append(this.$element);
+      if (this.$element.width() > 100)
+          this.$element.width(this.$element.width()-35);
+      this.$container.wrap('<div style="display:inline-block;position:relative;width:auto;white-space:nowrap;"></div>');
+      this.$button = $(this.options.button).appendTo(this.$container);
+      this.$menu.append($('<span class="close"><i class="icon-remove"></i></span>')
+          .on('click', $.proxy(this.hide, this)));
+      if (this.$element.is('textarea')) {
+          this.$container.addClass('textarea');
+      }
+      this.$menu.append(this.$translations).append(this.$footer);
+
+      this.$button.on('click', $.proxy(this.toggle, this));
+
+      this.$element
+        .addClass('translatable')
+        .focus($.proxy(function() { this.addClass('focus'); }, this.$container))
+        .blur($.proxy(function() { this.removeClass('focus'); }, this.$container));
+      getConfig().then($.proxy(function(c) {
+        this.attr({'spellcheck': 'true', 'lang': c.primary_language})
+        $('<span class="flag"></span>')
+          .addClass('flag-' + c.primary_lang_flag)
+          .insertAfter(this);
+        }, this.$element));
+    },
+
+    buildAdd: function() {
+      var self=this;
+      this.$footer
+        .append($('<form method="post"></form>')
+          .append(this.$select)
+          .append($('<button type="button"><i class="icon-plus-sign"></i> '+__('Add')+'</button>')
+            .on('click', $.proxy(this.define, this))
+          )
+        );
+      this.fetch('ajax.php/i18n/langs').then(function(langs) {
+        $.each(langs, function(k, v) {
+          self.$select.append($('<option>').val(k).text(v.name));
+        });
+      });
+    },
+
+    populate: function() {
+      var self=this;
+      if (this.populated)
+        return;
+      this.buildAdd();
+      this.fetch('ajax.php/i18n/translate/' + this.$element.data('translateTag'))
+      .then(function(json) {
+        $.each(json, function(k,v) {
+          self.add(k, v);
+        });
+        if (!Object.keys(json).length) {
+          self.$status.text(__('Not currently translated'));
+        }
+        else
+          self.$status.remove();
+      });
+      self.populated = true;
+    },
+
+    define: function(e) {
+      this.add($('option:selected', this.$select).val());
+    },
+
+    add: function(lang, text) {
+      var info = this.langs[lang];
+      this.$translations.append(
+        $('<li>')
+        .append($('<label class="language">')
+          .text(info.name)
+          .prepend($('<span>').addClass('flag flag-'+info.flag))
+          .append($('<input type="text" data-lang="'+lang+'">')
+            .attr('lang', lang)
+            .attr('spellcheck', 'true')
+            .attr('dir', info.direction || 'ltr')
+            .on('change keydown', $.proxy(this.showCommit, this))
+            .val(text)
+          )
+        )
+        .effect('highlight')
+      );
+      $('option[value='+lang+']', this.$select).remove();
+      if (!$('option', this.$select).length)
+        this.$footer.hide();
+      this.$status.remove();
+    },
+
+    showCommit: function(e) {
+      if (this.$commit) {
+          this.$commit.find('button').empty().text(' '+__('Save'))
+              .prepend($('<i>').addClass('fa icon-save'));
+          return !this.$commit.is(':visible')
+              ? this.$commit.slideDown() : true;
+      }
+      return this.$commit = $('<div class="language-commit"></div>')
+        .hide()
+        .insertAfter(this.$translations)
+        .append($('<button type="button" class="white button commit"><i class="fa fa-save icon-save"></i> '+__('Save')+'</button>')
+          .on('click', $.proxy(this.commit, this))
+        )
+        .slideDown();
+    },
+
+    commit: function(e) {
+      var changes = {}, self = this;
+      $('input[type=text]', this.$translations).each(function() {
+        var trans = $(this).val();
+        if (!trans)
+            $(this).closest('li').slideUp();
+        changes[$(this).data('lang')] = trans;
+      });
+      this.$commit.prop('disabled', true);
+      this.$commit.find('button').empty().text(' '+__('Saving'))
+          .prepend($('<i>').addClass('fa icon-spin icon-spinner'));
+      $.ajax('ajax.php/i18n/translate/' + this.$element.data('translateTag'), {
+        type: 'post',
+        data: changes,
+        success: function() {
+          self.$commit.slideUp();
+        }
+      });
+    },
+
+    toggle: function(e) {
+      e.stopPropagation();
+      e.preventDefault();
+
+      if (this.shown)
+        this.hide();
+      else
+        this.show();
+    },
+
+    show: function() {
+      if (this.shown)
+          return this;
+
+      var pos = $.extend({}, this.$container.offset(), {
+        height: this.$container[0].offsetHeight
+      })
+
+      this.$menu.css({
+        top: pos.top + pos.height
+      , left: pos.left
+      });
+
+      this.populate();
+
+      this.$menu.show();
+      this.shown = true;
+      return this;
+    },
+
+    hide: function() {
+      if (this.shown) {
+        this.$menu.hide();
+        this.shown = false;
+      }
+      return this;
+    }
+
+
+  };
+
+  /* PLUGIN DEFINITION
+   * =========================== */
+
+  $.fn.translatable = function ( option ) {
+    return this.each(function () {
+      var $this = $(this),
+        data = $this.data('translatable'),
+        options = typeof option == 'object' && option;
+      if (!data) $this.data('translatable', (data = new Translatable(this, options)));
+      if (typeof option == 'string') data[option]();
+    });
+  };
+
+  $.fn.translatable.defaults = {
+    menu: '<div class="translations"></div>',
+    button: '<button type="button" class="translatable"><i class="fa fa-globe icon-globe"></i></button>'
+  };
+
+  $.fn.translatable.Constructor = Translatable;
+
+}( window.jQuery );
diff --git a/scp/js/scp.js b/scp/js/scp.js
index 67bdca721289a744fd2a8de69340afd122579ce4..2e6a116be5b998e44cf4ae812e2314c095cce14b 100644
--- a/scp/js/scp.js
+++ b/scp/js/scp.js
@@ -37,7 +37,10 @@ function checkbox_checker(formObj, min, max) {
 
 var scp_prep = function() {
 
-    $("input:not(.dp):visible:enabled:first").focus();
+    $("input[autofocus]:visible:enabled:first").each(function() {
+      if ($(this).val())
+        $(this).blur();
+    });
     $('table.list input:checkbox').bind('click, change', function() {
         $(this)
             .parents("tr:first")
@@ -81,14 +84,18 @@ var scp_prep = function() {
         return false;
      });
 
-    $('#actions :submit.button:not(.no-confirm)').bind('click', function(e) {
+    $('#actions:submit, #actions :submit.button:not(.no-confirm), #actions .confirm').bind('click', function(e) {
 
-        var formObj = $(this).closest('form');
-        e.preventDefault();
-        if($('.dialog#confirm-action p#'+this.name+'-confirm').length == 0) {
-            alert('Unknown action '+this.name+' - get technical help.');
+        var formObj,
+            name = this.name || $(this).data('name');
+        if ($(this).data('formId'))
+            formObj = $('#' + $(this).data('formId'));
+        else
+            formObj = $(this).closest('form');
+        if($('.dialog#confirm-action p#'+name+'-confirm').length === 0) {
+            alert('Unknown action '+name+' - get technical help.');
         } else if(checkbox_checker(formObj, 1)) {
-            var action = this.name;
+            var action = name;
             $('.dialog#confirm-action').undelegate('.confirm');
             $('.dialog#confirm-action').delegate('input.confirm', 'click.confirm', function(e) {
                 e.preventDefault();
@@ -100,11 +107,11 @@ var scp_prep = function() {
              });
             $.toggleOverlay(true);
             $('.dialog#confirm-action .confirm-action').hide();
-            $('.dialog#confirm-action p#'+this.name+'-confirm')
+            $('.dialog#confirm-action p#'+name+'-confirm')
             .show()
             .parent('div').show().trigger('click');
         }
-
+        e.preventDefault();
         return false;
      });
 
@@ -125,24 +132,11 @@ var scp_prep = function() {
         }
      });
 
-
-    if($.browser.msie) {
-        $('.inactive').mouseenter(function() {
-            var elem = $(this);
-            var ie_shadow = $('<div>').addClass('ieshadow').css({
-                height:$('ul', elem).height()
-            });
-            elem.append(ie_shadow);
-        }).mouseleave(function() {
-            $('.ieshadow').remove();
-        });
-    }
-
     var warnOnLeave = function (el) {
         var fObj = el.closest('form');
         if(!fObj.data('changed')){
             fObj.data('changed', true);
-            $('input[type=submit]', fObj).css('color', 'red');
+            $('input[type=submit], button[type=submit]', fObj).addClass('save pending');
             $(window).bind('beforeunload', function(e) {
                 return __('Are you sure you want to leave? Any changes or info you\'ve entered will be discarded!');
             });
@@ -152,14 +146,14 @@ var scp_prep = function() {
         }
     };
 
-    $("form#save :input").change(function() {
-        warnOnLeave($(this));
+    $("form#save").on('change', ':input[name], :button[name]', function() {
+        if (!$(this).is('.nowarn')) warnOnLeave($(this));
     });
 
-    $("form#save :input[type=reset]").click(function() {
+    $("form#save").on('click', ':input[type=reset], :button[type=reset]', function() {
         var fObj = $(this).closest('form');
         if(fObj.data('changed')){
-            $('input[type=submit]', fObj).removeAttr('style');
+            $('input[type=submit], button[type=submit]', fObj).removeClass('save pending');
             $('label', fObj).removeAttr('style');
             $('label', fObj).removeClass('strike');
             fObj.data('changed', false);
@@ -182,7 +176,7 @@ var scp_prep = function() {
         form.submit();
      });
 
-    $(".clearrule").live('click',function() {
+    $(document).on('click', ".clearrule",function() {
         $(this).closest("tr").find(":input").val('');
         return false;
      });
@@ -221,12 +215,12 @@ var scp_prep = function() {
                         redactor = box.data('redactor');
                     if(canned.response) {
                         if (redactor)
-                            redactor.insertHtml(canned.response);
+                            redactor.insert.html(canned.response);
                         else
                             box.val(box.val() + canned.response);
 
                         if (redactor)
-                            redactor.observeStart();
+                            redactor.observe.load();
                     }
                     //Canned attachments.
                     var ca = $('.attachments', fObj);
@@ -258,11 +252,13 @@ var scp_prep = function() {
 
     /* Typeahead tickets lookup */
     var last_req;
-    $('#basic-ticket-search').typeahead({
+    $('input.basic-search').typeahead({
         source: function (typeahead, query) {
             if (last_req) last_req.abort();
+            var $el = this.$element;
+            var url = $el.data('url')+'?q='+encodeURIComponent(query);
             last_req = $.ajax({
-                url: "ajax.php/tickets/lookup?q="+query,
+                url: url,
                 dataType: 'json',
                 success: function (data) {
                     typeahead.process(data);
@@ -270,8 +266,14 @@ var scp_prep = function() {
             });
         },
         onselect: function (obj) {
-            $('#basic-ticket-search').val(obj.value);
-            $('#basic-ticket-search').closest('form').submit();
+            var $el = this.$element;
+            var form = $el.closest('form');
+            form.find('input[name=search-type]').val('typeahead');
+            $el.val(obj.value);
+            if (obj.id) {
+                form.append($('<input type="hidden" name="number">').val(obj.id))
+            }
+            form.submit();
         },
         property: "matches"
     });
@@ -321,109 +323,40 @@ var scp_prep = function() {
     });
 
     //Dialog
-    $('.dialog').each(function() {
+    $('.dialog').resize(function() {
         var w = $(window), $this=$(this);
         $this.css({
             top : (w.innerHeight() / 7),
             left : (w.width() - $this.outerWidth()) / 2
         });
-        $this.hasClass('draggable') && $this.draggable({handle:'h3'});
+        $this.hasClass('draggable') && $this.draggable({handle:'.drag-handle'});
+    });
+
+
+    $('.dialog').each(function() {
+        $this=$(this);
+        $this.resize();
+        $this.hasClass('draggable') && $this.draggable({handle:'.drag-handle'});
     });
 
     $('.dialog').delegate('input.close, a.close', 'click', function(e) {
         e.preventDefault();
-        $(this).parents('div.dialog').hide()
+        var $dialog = $(this).parents('div.dialog');
+        $dialog.off('blur.redactor');
+        $dialog
+        .hide()
+        .removeAttr('style');
         $.toggleOverlay(false);
 
         return false;
     });
 
-    /* advanced search */
-    $('.dialog#advanced-search').css({
-        top  : ($(window).height() / 6),
-        left : ($(window).width() / 2 - 300)
-    });
-
     /* loading ... */
     $("#loading").css({
         top  : ($(window).height() / 3),
         left : ($(window).width() - $("#loading").outerWidth()) / 2
     });
 
-    $('#go-advanced').click(function(e) {
-        e.preventDefault();
-        $('#result-count').html('');
-        $.toggleOverlay(true);
-        $('#advanced-search').show();
-    });
-
-
-    $('#advanced-search').delegate('#statusId, #flag', 'change', function() {
-        switch($(this).children('option:selected').data('state')) {
-            case 'closed':
-                $('select#assignee')
-                .attr('disabled','disabled')
-                .find('option:first')
-                .attr('selected', 'selected');
-                $('select#flag')
-                .attr('disabled','disabled')
-                .find('option:first')
-                .attr('selected', 'selected');
-                $('select#staffId').removeAttr('disabled');
-                break;
-            case 'open':
-                $('select#staffId')
-                .attr('disabled','disabled')
-                .find('option:first')
-                .attr('selected', 'selected');
-                $('select#assignee').removeAttr('disabled');
-                $('select#flag').removeAttr('disabled');
-                break;
-            default:
-                $('select#staffId').removeAttr('disabled');
-                $('select#assignee').removeAttr('disabled');
-                $('select#flag').removeAttr('disabled');
-        }
-    });
-
-    $('#advanced-search form#search').submit(function(e) {
-        e.preventDefault();
-        var fObj = $(this);
-        var elem = $('#advanced-search');
-        $('#result-count').html('');
-        fixupDatePickers.call(this);
-        $.ajax({
-                url: "ajax.php/tickets/search",
-                data: fObj.serialize(),
-                dataType: 'json',
-                beforeSend: function ( xhr ) {
-                   $('.buttons', elem).hide();
-                   $('.spinner', elem).show();
-                   return true;
-                },
-                success: function (resp) {
-
-                    if(resp.success) {
-                        $('#result-count').html('<div class="success">' + resp.success +'</div>');
-                    } else if (resp.fail) {
-                        $('#result-count').html('<div class="fail">' + resp.fail +'</div>');
-                    } else {
-                        $('#result-count').html('<div class="fail">Unknown error</div>');
-                    }
-                }
-            })
-            .done( function () {
-             })
-            .fail( function () {
-                $('#result-count').html('<div class="fail">'
-                    + __('Advanced search failed - try again!') + '</div>');
-            })
-            .always( function () {
-                $('.spinner', elem).hide();
-                $('.buttons', elem).show();
-             });
-    });
-
    // Return a helper with preserved width of cells
    var fixHelper = function(e, ui) {
       ui.children().each(function() {
@@ -437,13 +370,111 @@ var scp_prep = function() {
        'helper': fixHelper,
        'cursor': 'move',
        'stop': function(e, ui) {
-           var attr = ui.item.parent('tbody').data('sort');
+           var attr = ui.item.parent('tbody').data('sort'),
+               offset = parseInt($('#sort-offset').val(), 10) || 0;
            warnOnLeave(ui.item);
            $('input[name^='+attr+']', ui.item.parent('tbody')).each(function(i, el) {
-               $(el).val(i+1);
+               $(el).val(i + 1 + offset);
            });
        }
    });
+
+    // Scroll to a stop or top on scroll-up click
+     $(document).off('click.scroll-up');
+     $(document).on('click.scroll-up', 'a.scroll-up', function() {
+        $stop = $(this).data('stop');
+        $('html, body').animate({scrollTop: ($stop ? $stop : 0)}, 'fast');
+        return false;
+      });
+
+
+   // Make translatable fields translatable
+   $('input[data-translate-tag]').translatable();
+
+   if (window.location.hash) {
+     $('ul.tabs li a[href="' + window.location.hash + '"]').trigger('click');
+   }
+
+   // Make sticky bars float on scroll
+   // Thanks, https://stackoverflow.com/a/17166225/1025836
+   $('div.sticky.bar:not(.stop)').each(function() {
+     var $that = $(this),
+         placeholder = $('<div class="sticky placeholder">').insertBefore($that),
+         offset = $that.offset(),
+         top = offset.top - parseFloat($that.css('marginTop').replace(/auto/, 100)),
+         stop = $('div.sticky.bar.stop').filter(':visible'),
+         stopAt,
+         visible = false;
+
+     // Append scroll-up icon and set stop point for this sticky
+     $('.content', $that)
+     .append($('<a class="only sticky scroll-up" href="#" data-stop='
+             + (placeholder.offset().top-75) +' ><i class="icon-chevron-up icon-large"></i></a>'));
+
+     if (stop.length) {
+       var onmove = function() {
+         // Recalc when pictures pop in
+         stopAt = stop.offset().top;
+       };
+       $('#ticket_thread .thread-body img').each(function() {
+         this.onload = onmove;
+       });
+       onmove();
+     }
+
+     // Drop the sticky bar on PJAX navigation
+     $(document).on('pjax:start', function() {
+         placeholder.removeAttr('style');
+         $that.stop().removeClass('fixed');
+         $(window).off('.sticky');
+     });
+
+     $that.find('.content').width($that.width());
+     $(window).on('scroll.sticky', function (event) {
+       // what the y position of the scroll is
+       var y = $(this).scrollTop();
+
+       // whether that's below the form
+       if (y >= top && (!stopAt || stopAt > y)) {
+         // if so, add the fixed class
+         if (!visible) {
+           visible = true;
+           setTimeout(function() {
+             $that.addClass('fixed').css('top', '-'+$that.height()+'px')
+                .animate({top:0}, {easing: 'swing', duration:'fast'});
+             placeholder.height($that.height());
+             $that.find('[data-dropdown]').dropdown('hide');
+           }, 1);
+         }
+       } else {
+         // otherwise remove it
+         if (visible) {
+           visible = false;
+           setTimeout(function() {
+             placeholder.removeAttr('style');
+             $that.find('[data-dropdown]').dropdown('hide');
+             $that.stop().removeClass('fixed');
+           }, 1);
+         }
+       }
+    });
+  });
+
+  $('div.tab_content[id] div.error:not(:empty)').each(function() {
+    var div = $(this).closest('.tab_content');
+    $('a[href^=#'+div.attr('id')+']').parent().addClass('error');
+  });
+
+  $('[data-toggle="tooltip"]').tooltip()
+
+  $('[data-toggle="tooltip"]').on('click', function() {
+        $(this).tooltip('hide');
+  });
+
+  $('.attached.input input[autofocus]').parent().addClass('focus')
+  $('.attached.input input')
+    .on('focus', function() { $(this).parent().addClass('focus'); })
+    .on('blur', function() { $(this).parent().removeClass('focus'); })
 };
 
 $(document).ready(scp_prep);
@@ -498,13 +529,20 @@ jQuery.fn.exists = function() { return this.length>0; };
 
 $.translate_format = function(str) {
     var translation = {
-        'd':'dd',
-        'j':'d',
-        'z':'o',
-        'm':'mm',
-        'F':'MM',
-        'n':'m',
-        'Y':'yy'
+        'DD':   'oo',
+        'D':    'o',
+        'EEEE': 'DD',
+        'EEE':  'D',
+        'MMMM': '||',   // Double replace necessary
+        'MMM':  '|',
+        'MM':   'mm',
+        'M':    'm',
+        '||':   'MM',
+        '|':    'M',
+        'yyyy': '`',
+        'yyy':  '`',
+        'yy':   'y',
+        '`':    'yy'
     };
     // Change PHP formats to datepicker ones
     $.each(translation, function(php, jqdp) {
@@ -524,6 +562,25 @@ $(document).keydown(function(e) {
     }
 });
 
+
+$(document).on('focus', 'form.spellcheck textarea, form.spellcheck input[type=text]', function() {
+  var $this = $(this);
+  if ($this.attr('lang') !== undefined)
+    return;
+  var lang = $(this).closest('[lang]').attr('lang');
+  if (lang)
+    $(this).attr({'spellcheck':'true', 'lang': lang});
+});
+
+$(document).on('click', '.thread-entry-group a', function() {
+    var inner = $(this).parent().find('.thread-entry-group-inner');
+    if (inner.is(':visible'))
+      inner.slideUp();
+    else
+      inner.slideDown();
+    return false;
+});
+
 $.toggleOverlay = function (show) {
   if (typeof(show) === 'undefined') {
     return $.toggleOverlay(!$('#overlay').is(':visible'));
@@ -537,7 +594,7 @@ $.toggleOverlay = function (show) {
     $('body').css('overflow', 'auto');
   }
 };
-
+//modal---------//
 $.dialog = function (url, codes, cb, options) {
     options = options||{};
 
@@ -546,28 +603,48 @@ $.dialog = function (url, codes, cb, options) {
 
     var $popup = $('.dialog#popup');
 
+    $popup.attr('class',
+        function(pos, classes) {
+            return classes.replace(/\bsize-\S+/g, '');
+    });
+
+    $popup.addClass(options.size ? ('size-'+options.size) : 'size-normal');
+
     $.toggleOverlay(true);
     $('div.body', $popup).empty().hide();
     $('div#popup-loading', $popup).show()
         .find('h1').css({'margin-top':function() { return $popup.height()/3-$(this).height()/3}});
-    $popup.show();
-    $('div.body', $popup).load(url, function () {
+    $popup.resize().show();
+    $('div.body', $popup).load(url, options.data, function () {
         $('div#popup-loading', $popup).hide();
         $('div.body', $popup).slideDown({
             duration: 300,
             queue: false,
-            complete: function() { if (options.onshow) options.onshow(); }
+            complete: function() {
+                if (options.onshow) options.onshow();
+                $(this).removeAttr('style');
+            }
         });
+        $("input[autofocus]:visible:enabled:first", $popup).focus();
+        var submit_button = null;
         $(document).off('.dialog');
+        $(document).on('click.dialog',
+            '#popup input[type=submit], #popup button[type=submit]',
+            function(e) { submit_button = $(this); });
         $(document).on('submit.dialog', '.dialog#popup form', function(e) {
             e.preventDefault();
-            var $form = $(this);
+            var $form = $(this),
+                data = $form.serialize();
+            if (submit_button) {
+                data += '&' + escape(submit_button.attr('name')) + '='
+                    + escape(submit_button.attr('value'));
+            }
             $('div#popup-loading', $popup).show()
                 .find('h1').css({'margin-top':function() { return $popup.height()/3-$(this).height()/3}});
             $.ajax({
                 type:  $form.attr('method'),
                 url: 'ajax.php/'+$form.attr('action').substr(1),
-                data: $form.serialize(),
+                data: data,
                 cache: false,
                 success: function(resp, status, xhr) {
                     if (xhr && xhr.status && codes
@@ -581,9 +658,18 @@ $.dialog = function (url, codes, cb, options) {
                         var done = $.Event('dialog:close');
                         $popup.trigger(done, [resp, status, xhr]);
                     } else {
+                        try {
+                            var json = $.parseJSON(resp);
+                            if (json.redirect) return window.location.href = json.redirect;
+                        }
+                        catch (e) { }
                         $('div.body', $popup).html(resp);
                         $popup.effect('shake');
                         $('#msg_notice, #msg_error', $popup).delay(5000).slideUp();
+                        $('div.tab_content[id] div.error:not(:empty)', $popup).each(function() {
+                          var div = $(this).closest('.tab_content');
+                          $('a[href^=#'+div.attr('id')+']').parent().addClass('error');
+                        });
                     }
                 }
             })
@@ -596,6 +682,25 @@ $.dialog = function (url, codes, cb, options) {
      });
     if (options.onload) { options.onload(); }
  };
+$(document).on('click', 'a[data-dialog]', function(event) {
+    event.preventDefault();
+    event.stopImmediatePropagation();
+    var link = $(this);
+    $.dialog($(this).data('dialog'), 201, function(xhr, json) {
+      try {
+        json = JSON.parse(json);
+      } catch (e) {}
+      if (link.attr('href').length > 1) {
+        // Replace {xx} expressions with data from JSON
+        if (typeof json === 'object')
+            link.attr('href',
+              link.attr('href').replace(/\{([^}]+)\}/, function($0, $1) { return json[$1]; }));
+        $.pjax.click(event, '#pjax-container');
+      }
+      else $.pjax.reload('#pjax-container');
+    });
+    return false;
+});
 
 $.sysAlert = function (title, msg, cb) {
     var $dialog =  $('.dialog#alert');
@@ -603,14 +708,17 @@ $.sysAlert = function (title, msg, cb) {
         $.toggleOverlay(true);
         $('#title', $dialog).html(title);
         $('#body', $dialog).html(msg);
-        $dialog.show();
+        $dialog.resize().show();
+        if (cb)
+            $dialog.find('input.ok.close').click(cb);
     } else {
         alert(msg);
     }
 };
 
-$.confirm = function(message, title) {
+$.confirm = function(message, title, options) {
     title = title || __('Please Confirm');
+    options = options || {};
     var D = $.Deferred(),
       $popup = $('.dialog#popup'),
       hide = function() {
@@ -618,7 +726,7 @@ $.confirm = function(message, title) {
           $popup.hide();
       };
       $('div#popup-loading', $popup).hide();
-      $('div.body', $popup).empty()
+      var body = $('div.body', $popup).empty()
         .append($('<h3></h3>').text(title))
         .append($('<a class="close" href="#"><i class="icon-remove-circle"></i></a>'))
         .append($('<hr/>'))
@@ -626,7 +734,20 @@ $.confirm = function(message, title) {
             .text(message)
         ).append($('<div></div>')
             .append($('<b>').text(__('Please confirm to continue.')))
-        ).append($('<hr style="margin-top:1em"/>'))
+        );
+
+      if (Object.keys(options).length)
+          body.append('<hr>');
+      $.each(options, function(k, v) {
+        body.append($('<div>')
+          .html('&nbsp;'+v)
+          .prepend($('<input type="checkbox">')
+            .attr('name', k)
+          )
+        );
+      });
+
+      body.append($('<hr style="margin-top:1em"/>'))
         .append($('<p class="full-width"></p>')
             .append($('<span class="buttons pull-left"></span>')
                 .append($('<input type="button" class="close"/>')
@@ -635,10 +756,10 @@ $.confirm = function(message, title) {
             )).append($('<span class="buttons pull-right"></span>')
                 .append($('<input type="button"/>')
                     .attr('value', __('OK'))
-                    .click(function() {  hide(); D.resolve(); })
+                    .click(function() {  hide(); D.resolve(body.find('input').serializeArray()); })
         ))).append($('<div class="clear"></div>'));
     $.toggleOverlay(true);
-    $popup.show();
+    $popup.resize().show();
     return D.promise();
 };
 
@@ -662,16 +783,202 @@ $.orgLookup = function (url, cb) {
 
 $.uid = 1;
 
-//Tabs
-$(document).on('click.tab', 'ul.tabs li a', function(e) {
++function($) {
+  var MessageBar = function() {
+    this.defaults = {
+      avatar: 'oscar-boy',
+      bar: '<div class="message bar"></div>',
+      button: '<button type="button" class="inline button"></button>',
+      buttonClass: '',
+      buttonText: __('OK'),
+      classes: '',
+      dismissible: true,
+      html: false,
+      onok: null,
+      position: 'top',
+    };
+
+    this.show = function(title, message, options) {
+      this.hide();
+      options = $.extend({}, this.defaults, options);
+      var bar = this.bar = $(options.bar).addClass(options.classes)
+        .append($('<div class="title"></div>').html(title))
+        .append($('<div class="body"></div>').html(message))
+        .addClass(options.position);
+      if (options.avatar)
+        bar.prepend($('<div class="avatar pull-left" title="Oscar"></div>')
+            .addClass(options.avatar));
+
+      if (options.onok || options.dismissible) {
+        bar
+          .prepend($('<div><div class="valign-helper"></div></div>')
+            // FIXME: This is not compatible with .rtl
+            .css({position: 'absolute', top: 0, bottom: 0, right: 0, margin: '0 15px'})
+            .append($(options.button)
+              .text(options.buttonText)
+              .click(this.dismiss.bind(this))
+              .addClass(options.buttonClass)
+            )
+          );
+      }
+      this.visible = true;
+      this.options = options;
+
+      $('body').append(bar);
+      this.height = bar.height();
+
+      // Slight slide in
+      if (options.position == 'bottom') {
+        bar.css('bottom', -this.height/2).animate({'bottom': 0});
+      }
+      // Otherwise assume TOP positioning
+      else {
+        var hovering = false,
+            y = $(window).scrollTop(),
+            targetY = (y < this.height) ? -this.height - 10 + y : 0;
+        bar.css('top', -this.height/2).animate({'top': targetY});
+
+        // Plop out on mouse hover
+        bar.hover(function() {
+          if (!hovering && this.visible && bar.css('top') != '0') {
+            bar.stop().animate({'margin-top': -parseInt(bar.css('top'), 10)}, 400, 'easeOutBounce');
+            hovering = true;
+          }
+        }.bind(this), function() {
+          if (this.visible && hovering) {
+            bar.stop().animate({'margin-top': 0});
+            hovering = false;
+          }
+        }.bind(this));
+      }
+
+      return bar;
+    };
+
+    this.scroll = function(event) {
+      // Shade on scroll to top
+      if (!this.visible || this.options.position != 'top')
+        return;
+      var y = $(window).scrollTop();
+      if (y < this.height) {
+        this.bar.css({top: -this.height -10 + y});
+        this.shading = true;
+      }
+      else if (this.bar.css('top') != '0') {
+        if (this.shading) {
+          this.bar.stop().animate({top: 0});
+          this.shading = false;
+        }
+      }
+    };
+
+    this.dismiss = function(event) {
+      if (this.options.onok) {
+        this.bar.find('button').replaceWith(
+          $('<i class="icon-spinner icon-spin icon-large"></i>')
+        );
+        if (this.options.onok(event) === false)
+          return;
+      }
+      this.hide();
+    };
+
+    this.hide = function() {
+      if (!this.bar || !this.visible)
+        return;
+      var bar = this.bar.removeAttr('style');
+      var dir = this.options.position == 'bottom' ? 'down' : 'up';
+      // NOTE: destroy() is not called here because a new bar might be
+      //       created before the animation finishes
+      bar.hide("slide", { direction: dir }, 400, function() { bar.remove(); });
+      this.visible = false;
+    };
+
+    this.destroy = function() {
+      if (!this.bar || !this.visible)
+        return;
+      this.bar.remove();
+      this.visible = false;
+    };
+
+    // Destroy on away navigation
+    $(document).on('pjax:start.messageBar', this.destroy.bind(this));
+    $(window).on('scroll.messageBar', this.scroll.bind(this));
+  };
+
+  $.messageBar = new MessageBar();
+}(window.jQuery);
+
+// Tabs
+$(document).on('click.tab', 'ul.tabs > li > a', function(e) {
     e.preventDefault();
-    if ($('.tab_content'+$(this).attr('href')).length) {
-        var ul = $(this).closest('ul');
-        $('ul.tabs li a', ul.parent()).removeClass('active');
-        $(this).addClass('active');
-        $('.tab_content', ul.parent()).hide();
-        $('.tab_content'+$(this).attr('href')).show();
+    var $this = $(this),
+        $ul = $(this).closest('ul'),
+        $container = $('#'+$ul.attr('id')+'_container');
+    if (!$container.length)
+        $container = $ul.parent();
+
+    var $tab = $($this.attr('href'), $container);
+    if (!$tab.length && $(this).data('url').length > 1) {
+        var url = $this.data('url');
+        if (url.charAt(0) == '#')
+            url = 'ajax.php/' + url.substr(1);
+        $tab = $('<div>')
+            .addClass('tab_content')
+            .attr('id', $this.attr('href').substr(1)).hide();
+        $container.append(
+            $tab.load(url, function () {
+                // TODO: Add / hide loading spinner
+            })
+         );
+        $this.removeData('url');
+    }
+    else {
+        $tab.addClass('tab_content');
+        // Don't change the URL hash for nested tabs or in dialogs
+        if ($(this).closest('.tab_content, .dialog').length == 0)
+            $.changeHash($(this).attr('href'), true);
+    }
+
+    if ($tab.length) {
+        $ul.children('li.active').removeClass('active');
+        $(this).closest('li').addClass('active');
+        $container.children('.tab_content').hide();
+        $tab.fadeIn('fast');
+        return false;
     }
+
+});
+$.changeHash = function(hash, quiet) {
+  if (quiet) {
+    hash = hash.replace( /^#/, '' );
+    var fx, node = $( '#' + hash );
+    if ( node.length ) {
+      node.attr( 'id', '' );
+      fx = $( '<div></div>' )
+              .css({
+                  position:'absolute',
+                  visibility:'hidden',
+                  top: $(document).scrollTop() + 'px'
+              })
+              .attr( 'id', hash )
+              .appendTo( document.body );
+    }
+    document.location.hash = hash;
+    if ( node.length ) {
+      fx.remove();
+      node.attr( 'id', hash );
+    }
+  }
+  else {
+    document.location.hash = hash;
+  }
+};
+
+// Forms — submit, stay on same tab
+$(document).on('submit', 'form', function() {
+    if (!!$(this).attr('action') && $(this).attr('action').indexOf('#') == -1)
+        $(this).attr('action', $(this).attr('action') + window.location.hash);
 });
 
 //Collaborators
@@ -691,37 +998,22 @@ $(document).on('click', 'a.collaborator, a.collaborators', function(e) {
 // NOTE: getConfig should be global
 getConfig = (function() {
     var dfd = $.Deferred(),
-        requested = null;
+        requested = false;
     return function() {
-        if (dfd.state() != 'resolved' && !requested)
-            requested = $.ajax({
-                url: "ajax.php/config/scp",
-                dataType: 'json',
-                success: function (json_config) {
-                    dfd.resolve(json_config);
-                },
-                error: function() {
-                    requested = null;
-                }
-            });
         return dfd;
-    }
+    };
 })();
 
 $(document).on('pjax:click', function(options) {
-    clearTimeout(window.ticket_refresh);
-    // Release ticket lock (maybe)
-    if ($.autoLock !== undefined)
-        $.autoLock.releaseLock();
     // Stop all animations
     $(document).stop(false, true);
 
     // Remove tips and clear any pending timer
-    $('.tip, .help-tips, .userPreview, .ticketPreview, .previewfaq').each(function() {
+    $('.tip, .help-tips, .previewfaq, .preview').each(function() {
         if ($(this).data('timer'))
             clearTimeout($(this).data('timer'));
     });
-    $('.tip_box').remove();
+    $('.tip_box, .typeahead.dropdown-menu').remove();
 });
 
 $(document).on('pjax:start', function() {
@@ -759,8 +1051,50 @@ $(document).on('pjax:complete', function() {
     $('#overlay').removeAttr('style');
 });
 
+// Enable PJAX for the staff interface
+if ($.support.pjax) {
+  $(document).on('click', 'a', function(event) {
+    var $this = $(this);
+    if (!$this.hasClass('no-pjax')
+        && !$this.closest('.no-pjax').length
+        && $this.attr('href').charAt(0) != '#')
+      $.pjax.click(event, {container: $this.data('pjaxContainer') || $('#pjax-container'), timeout: 2000});
+  })
+}
+
+$(document).on('click', '.link:not(a):not(.button)', function(event) {
+  var $e = $(event.currentTarget);
+  $('<a>').attr({href: $e.attr('href'), 'class': $e.attr('class')})
+    .hide()
+    .insertBefore($e)
+    .get(0).click(event);
+});
+
+// Quick-Add dialogs
+$(document).on('change', 'select[data-quick-add]', function() {
+    var $select = $(this),
+        selected = $select.find('option:selected'),
+        type = selected.parent().closest('[data-quick-add]').data('quickAdd');
+    if (!type || (selected.data('quickAdd') === undefined && selected.val() !== ':new:'))
+        return;
+    $.dialog('ajax.php/admin/quick-add/' + type, 201,
+    function(xhr, data) {
+        data = JSON.parse(data);
+        if (data && data.id && data.name) {
+          var id = data.id;
+          if (selected.data('idPrefix'))
+            id = selected.data('idPrefix') + id;
+          $('<option>')
+            .attr('value', id)
+            .text(data.name)
+            .insertBefore(selected)
+          $select.val(id);
+        }
+    });
+});
+
 // Quick note interface
-$('.quicknote .action.edit-note').live('click.note', function() {
+$(document).on('click.note', '.quicknote .action.edit-note', function() {
     var note = $(this).closest('.quicknote'),
         body = note.find('.body'),
         T = $('<textarea>').text(body.html());
@@ -768,19 +1102,19 @@ $('.quicknote .action.edit-note').live('click.note', function() {
         T.addClass('no-bar small');
     body.replaceWith(T);
     $.redact(T);
-    $(T).redactor('focus');
+    $(T).redactor('focus.setStart');
     note.find('.action.edit-note').hide();
     note.find('.action.save-note').show();
     note.find('.action.cancel-edit').show();
     $('#new-note-box').hide();
     return false;
 });
-$('.quicknote .action.cancel-edit').live('click.note', function() {
+$(document).on('click.note', '.quicknote .action.cancel-edit', function() {
     var note = $(this).closest('.quicknote'),
         T = note.find('textarea'),
         body = $('<div class="body">');
     body.load('ajax.php/note/' + note.data('id'), function() {
-      try { T.redactor('destroy'); } catch (e) {}
+      try { T.redactor('core.destroy'); } catch (e) {}
       T.replaceWith(body);
       note.find('.action.save-note').hide();
       note.find('.action.cancel-edit').hide();
@@ -789,14 +1123,14 @@ $('.quicknote .action.cancel-edit').live('click.note', function() {
     });
     return false;
 });
-$('.quicknote .action.save-note').live('click.note', function() {
+$(document).on('click.note', '.quicknote .action.save-note', function() {
     var note = $(this).closest('.quicknote'),
         T = note.find('textarea');
     $.post('ajax.php/note/' + note.data('id'),
-      { note: T.redactor('get') },
+      { note: T.redactor('code.get') },
       function(html) {
         var body = $('<div class="body">').html(html);
-        try { T.redactor('destroy'); } catch (e) {}
+        try { T.redactor('core.destroy'); } catch (e) {}
         T.replaceWith(body);
         note.find('.action.save-note').hide();
         note.find('.action.cancel-edit').hide();
@@ -807,7 +1141,7 @@ $('.quicknote .action.save-note').live('click.note', function() {
     );
     return false;
 });
-$('.quicknote .delete').live('click.note', function() {
+$(document).on('click.note', '.quicknote .delete', function() {
   var that = $(this),
       id = $(this).closest('.quicknote').data('id');
   $.ajax('ajax.php/note/' + id, {
@@ -821,15 +1155,15 @@ $('.quicknote .delete').live('click.note', function() {
   });
   return false;
 });
-$('#new-note').live('click', function() {
+$(document).on('click', '#new-note', function() {
   var note = $(this).closest('.quicknote'),
     T = $('<textarea>'),
     button = $('<input type="button">').val(__('Create'));
     button.click(function() {
       $.post('ajax.php/' + note.data('url'),
-        { note: T.redactor('get'), no_options: note.hasClass('no-options') },
+        { note: T.redactor('code.get'), no_options: note.hasClass('no-options') },
         function(response) {
-          $(T).redactor('destroy').replaceWith(note);
+          $(T).redactor('core.destroy').replaceWith(note);
           $(response).show('highlight').insertBefore(note.parent());
           $('.submit', note.parent()).remove();
         },
@@ -842,7 +1176,7 @@ $('#new-note').live('click', function() {
     $('<p>').addClass('submit').css('text-align', 'center')
         .append(button).appendTo(T.parent());
     $.redact(T);
-    $(T).redactor('focus');
+    $(T).redactor('focus.setStart');
     return false;
 });
 
@@ -851,3 +1185,44 @@ function __(s) {
     return $.oststrings[s];
   return s;
 }
+
+// Thanks, http://stackoverflow.com/a/487049
+function addSearchParam(data) {
+    var kvp = document.location.search.substr(1).replace('+', ' ').split('&');
+    var i=kvp.length, x, params = {};
+    while (i--) {
+        x = kvp[i].split('=');
+        params[decodeURIComponent(x[0])] = decodeURIComponent(x[1]);
+    }
+
+    //this will reload the page, it's likely better to store this until finished
+    return $.param($.extend(params, data));
+}
+
+// Periodically adjust relative times
+window.relativeAdjust = setInterval(function() {
+  // Thanks, http://stackoverflow.com/a/7641822/1025836
+  var prettyDate = function(time) {
+    var date = new Date((time || "").replace(/-/g, "/").replace(/[TZ]/g, " ")),
+        diff = (((new Date()).getTime() - date.getTime()) / 1000),
+        day_diff = Math.floor(diff / 86400);
+
+    if (isNaN(day_diff) || day_diff < 0 || day_diff >= 31) return;
+
+    return day_diff == 0 && (
+         diff < 60 && __("just now")
+      || diff < 120 && __("about a minute ago")
+      || diff < 3600 && __("%d minutes ago").replace('%d', Math.floor(diff/60))
+      || diff < 7200 && __("about an hour ago")
+      || diff < 86400 &&  __("%d hours ago").replace('%d', Math.floor(diff/3600))
+    )
+    || day_diff == 1 && __("yesterday")
+    || day_diff < 7 && __("%d days ago").replace('%d', day_diff);
+    // Longer dates don't need to change dynamically
+  };
+  $('time.relative[datetime]').each(function() {
+    var rel = prettyDate($(this).attr('datetime'));
+    if (rel) $(this).text(rel);
+  });
+}, 20000);
+
diff --git a/scp/js/thread.js b/scp/js/thread.js
new file mode 100644
index 0000000000000000000000000000000000000000..0b4ebe50213763d035bd4df033856d6c51fe2241
--- /dev/null
+++ b/scp/js/thread.js
@@ -0,0 +1,174 @@
+/*********************************************************************
+    thread.js
+
+    Thread JS untils
+    Copyright (c) 2014 osTicket
+    http://www.osticket.com
+
+    Released under the GNU General Public License WITHOUT ANY WARRANTY.
+
+    vim: expandtab sw=4 ts=4 sts=4:
+**********************************************************************/
+
+var thread = {
+
+    options: {
+        autoScroll: true,
+        showimages: false
+    },
+
+    scrollTo: function (entry) {
+
+       if (!entry) return;
+
+       var frame = 0;
+       $('html, body').animate({
+            scrollTop: entry.offset().top - 50,
+       }, {
+            duration: 400,
+            step: function(now, fx) {
+                // Recalc end target every few frames
+                if (++frame % 6 == 0)
+                    fx.end = entry.offset().top - 50;
+            }
+        });
+    },
+
+    showExternalImage: function(div) {
+        var $div = $(div),
+            $img = $div.append($('<img>')
+              .attr('src', $div.data('src'))
+              .attr('alt', $div.attr('alt'))
+              .attr('title', $div.attr('title'))
+              .attr('style', $div.data('style'))
+            );
+        if ($div.attr('width'))
+            $img.width($div.attr('width'));
+        if ($div.attr('height'))
+            $img.height($div.attr('height'));
+    },
+
+    externalImages: function()  {
+
+        // Optionally show external images
+        $('.thread-entry', this.options.container).each(function(i, te) {
+            var extra = $(te).find('.textra'),
+                imgs = $(te).find('.non-local-image[data-src]');
+
+            if (!extra || !imgs.length)
+                return;
+
+            // Add Show Images buttons
+            extra.append($('<a>')
+              .addClass("white button action-button show-images")
+              .css({'font-weight':'normal'})
+              .text(' ' + __('Show Images'))
+              .click(function(ev) {
+                imgs.each(function(i, img) {
+                  thread.showExternalImage(img);
+                  $(img).removeClass('non-local-image')
+                    // Remove placeholder sizing
+                    .css({'display':'inline-block'})
+                    .width('auto')
+                    .height('auto')
+                    .removeAttr('width')
+                    .removeAttr('height');
+                  extra.find('.show-images').hide();
+                });
+              })
+              .prepend($('<i>')
+                .addClass('icon-picture')
+              )
+            );
+
+            // Show placeholders
+            imgs.each(function(i, img) {
+                var $img = $(img);
+                // Save a copy of the original styling
+                $img.data('style', $img.attr('style'));
+                $img.removeAttr('style');
+                // If the image has a 'height' attribute, use it, otherwise, use
+                // 40px
+                $img.height(($img.attr('height') || '40') + 'px');
+                // Ensure the image placeholder is visible width-wise
+                if (!$img.width())
+                    $img.width(($img.attr('width') || '80') + 'px');
+                // TODO: Add a hover-button to show just one image
+            });
+        });
+    },
+
+    inlineImages: function (entry_id) {
+        // TODO: use entry selector or object instead of ID
+        var selector = (entry_id == undefined)
+            ? '.thread-body img[data-cid]'
+            : '.thread-body#thread-id-'+entry_id+' img[data-cid]';
+
+        // Get urls
+        if (!(urls=this.options.container.data('imageUrls')))
+            return;
+
+        $(selector, this.options.container).each(function(i, el) {
+            var e = $(el),
+                cid = e.data('cid').toLowerCase(),
+                info = urls[cid];
+            if (info && !e.data('wrapped')) {
+                // Add a hover effect with the filename
+                var timeout, caption = $('<div class="image-hover">')
+                    .css({'float':e.css('float')});
+                e.wrap(caption).parent()
+                    .append($('<div class="caption">')
+                        .append($('<a href="'+info.download_url+'" class="dark button pull-right no-pjax"><i class="icon-download-alt"></i></a>')
+                          .attr('download', info.filename)
+                          .attr('title', __('Download'))
+                          .tooltip()
+                        )
+                    );
+                e.data('wrapped', true);
+            }
+        });
+    },
+
+    prepImages: function() {
+
+        // TODO: Check config options
+        this.externalImages();
+        this.inlineImages();
+    },
+
+    onLoad: function (container, options) {
+
+        // See if thread container is valid
+        $container = $('#'+container);
+        if (!$container || !$container.length)
+            return;
+
+        // set options
+        this.options.container = $container;
+        $.extend(this.options, options);
+
+        // Prep images
+        this.prepImages();
+
+        // Auto scroll to the last entry if autoScroll is enabled.
+        if (this.options.autoScroll === true) {
+            // Find the last entry to scroll to.
+            var e = $('.thread-entry', $container).filter(':visible').last();
+            if (e.length)
+                this.scrollTo(e);
+        }
+
+        // Open thread body links in a new tab/window
+        $('div.thread-body a', $container).each(function() {
+            $(this).attr('target', '_blank');
+        });
+
+        // Open first response option tab if not already active
+        if (!document.location.hash)
+            $('.actions .tabs li:visible:first:not(.active) a', $container.parent()).trigger('click');
+    }
+};
+
+// Set thread as JQuery object
+$.thread = thread;
+
diff --git a/scp/js/ticket.js b/scp/js/ticket.js
index bce07d154ae1d7df7208e5a256c67bd7644baece..b16cc8ea386dca8cf6931a9bd1e1b6b81aabeaf9 100644
--- a/scp/js/ticket.js
+++ b/scp/js/ticket.js
@@ -13,279 +13,259 @@
 
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
-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);
-            return true;
-        }else if(elm.attachEvent) {
-            return elm.attachEvent('on' + evType, fn);
-        }else{
-            elm['on' + evType] = fn;
-        }
++function( $ ) {
+  var Lock = function(element, options) {
+    this.$element = $(element);
+    this.options = $.extend({}, $.fn.exclusive.defaults, options);
+    if (!this.$element.data('lockObjectId'))
+      return;
+    this.objectId = this.$element.data('lockObjectId');
+    this.fails = 0;
+    this.disabled = false;
+    getConfig().then(function(c) {
+      if (c.lock_time)
+        this.setup(options.lockId || this.$element.data('lockId') || undefined);
+    }.bind(this));
+  }
+
+  Lock.prototype = {
+    constructor: Lock,
+    registry: [],
+
+    setup: function(lockId) {
+      // When something inside changes or is clicked which requires a lock,
+      // attempt to fetch one (lazily)
+      $(':input', this.$element).on('keyup, change', this.acquire.bind(this));
+      $(':submit', this.$element).click(this.ensureLocked.bind(this));
+
+      // If lock already held, assume full time of lock remains, but warn
+      // user about pending expiration
+      if (lockId) {
+        getConfig().then(function(c) {
+          this.update({id: lockId, time: c.lock_time - 10});
+        }.bind(this));
+      }
     },
 
-    removeEvent: function(elm, evType, fn, useCapture) {
-        if(elm.removeEventListener) {
-            elm.removeEventListener(evType, fn, useCapture);
-            return true;
-        }else if(elm.detachEvent) {
-            return elm.detachEvent('on' + evType, fn);
-        }else {
-            elm['on' + evType] = null;
-        }
-    },
-
-    //Incoming event...
-    handleEvent: function(e) {
-        if(!autoLock.lockId) {
-            var now = new Date().getTime();
-            //Retry every 5 seconds??
-            if(autoLock.retry && (!autoLock.lastattemptTime || (now-autoLock.lastattemptTime)>5000)) {
-                autoLock.acquireLock(e,autoLock.warn);
-                autoLock.lastattemptTime=new Date().getTime();
-            }
-        }else{
-            autoLock.renewLock(e);
-        }
-
-        if(autoLock.lockId && !autoLock.lasteventTime) { //I hate nav away warnings..but
-            $(document).on('pjax:beforeSend.changed', function(e) {
-                return confirm(__("Any changes or info you've entered will be discarded!"));
-            });
-            $(window).bind('beforeunload', function(e) {
-                return __("Any changes or info you've entered will be discarded!");
-             });
-        }
-
-        autoLock.lasteventTime=new Date().getTime();
+    acquire: function() {
+      if (this.lockId)
+        return this.renew();
+      if (this.nextRenew && new Date().getTime() < this.nextRenew)
+        return this.locked;
+      if (this.disabled || this.ajaxActive)
+        return this.locked;
+
+      this.ajaxActive = $.ajax({
+        type: "POST",
+        url: 'ajax.php/lock/'+this.objectId,
+        dataType: 'json',
+        cache: false,
+        success: $.proxy(this.update, this),
+        error: $.proxy(this.retry, this, this.acquire),
+        complete: $.proxy(function() { this.ajaxActive = false; }, this)
+      });
+      return this.locked = $.Deferred();
     },
 
-    //Watch activity on individual form.
-    watchForm: function(fObj,fn) {
-        if(!fObj || !fObj.length)
-            return;
-
-        //Watch onSubmit event on the form.
-        autoLock.addEvent(fObj,'submit',autoLock.onSubmit,true);
-        //Watch activity on text + textareas + select fields.
-        for (var i=0; i<fObj.length; i++) {
-            switch(fObj[i].type) {
-                case 'textarea':
-                case 'text':
-                    autoLock.addEvent(fObj[i],'keyup',autoLock.handleEvent,true);
-                    break;
-                case 'select-one':
-                case 'select-multiple':
-                    if(fObj.name!='reply') //Bug on double ajax call since select make it's own ajax call. TODO: fix it
-                        autoLock.addEvent(fObj[i],'change',autoLock.handleEvent,true);
-                    break;
-                default:
-            }
-        }
+    renew: function() {
+      if (!this.lockId)
+        return;
+      if (this.nextRenew && new Date().getTime() < this.nextRenew)
+        return this.locked;
+      if (this.disabled || this.ajaxActive)
+        return this.locked;
+
+      this.ajaxActive = $.ajax({
+        type: "POST",
+        url: 'ajax.php/lock/{0}/{1}/renew'.replace('{0}',this.lockId).replace('{1}',this.objectId),
+        dataType: 'json',
+        cache: false,
+        success: $.proxy(this.update, this),
+        error: $.proxy(this.retry, this, this.renew),
+        complete: $.proxy(function() { this.ajaxActive = false; }, this)
+      });
+      return this.locked = $.Deferred();
     },
 
-    //Watch all the forms on the document.
-    watchDocument: function() {
-
-        //Watch forms of interest only.
-        for (var i=0; i<document.forms.length; i++) {
-            if(!document.forms[i].id.value || parseInt(document.forms[i].id.value)!=autoLock.tid)
-                continue;
-            autoLock.watchForm(document.forms[i],autoLock.checkLock);
-        }
+    wakeup: function(e) {
+      // Click handler from message bar. Bar will be manually hidden when
+      // lock is re-acquired
+      this.renew();
+      return false;
     },
 
-    Init: function(config) {
-
-        //make sure we are on ticket view page & locking is enabled!
-        var fObj=$('form#note');
-        if(!fObj
-                || !$(':input[name=id]',fObj).length
-                || !$(':input[name=locktime]',fObj).length
-                || $(':input[name=locktime]',fObj).val()==0) {
-            return;
-        }
-
-        void(autoLock.tid=parseInt($(':input[name=id]',fObj).val()));
-        void(autoLock.lockTime=parseInt($(':input[name=locktime]',fObj).val()));
-
-        autoLock.watchDocument();
-        autoLock.resetTimer();
-        autoLock.addEvent(window,'unload',autoLock.releaseLock,true); //Release lock regardless of any activity.
+    retry: function(func, xhr, textStatus, response) {
+      var json = xhr ? xhr.responseJSON : response;
+
+      if (xhr.status == 418) {
+          this.disabled = true;
+          return this.destroy();
+      }
+
+      if ((typeof json == 'object' && !json.retry) || !this.options.retry)
+        return this.fail(json.msg);
+      if (typeof json == 'object' && json.retry == 'acquire') {
+        // Lock no longer exists server-side
+        this.destroy();
+        setTimeout(this.acquire.bind(this), 2);
+      }
+      if (++this.fails > this.options.maxRetries)
+        // Attempt to acquire a new lock ?
+        return this.fail(json ? json.msg : null);
+      this.retryTimer = setTimeout($.proxy(func, this), this.options.retryInterval * 1000);
     },
 
-
-    onSubmit: function(e) {
-        if(e.type=='submit') { //Submit. double check!
-            //remove nav away warning if any.
-            $(window).unbind('beforeunload');
-            //Only warn if we had a failed lock attempt.
-            if(autoLock.warn && !autoLock.lockId && autoLock.lasteventTime) {
-                var answer=confirm(__('Unable to acquire a lock on the ticket. Someone else could be working on the same ticket.  Please confirm if you wish to continue anyways.'));
-                if(!answer) {
-                    e.returnValue=false;
-                    e.cancelBubble=true;
-                    if(e.preventDefault) {
-                        e.preventDefault();
-                    }
-                    return false;
-                }
-            }
-        }
-        return true;
+    release: function() {
+      if (!this.lockId)
+        return false;
+      if (this.ajaxActive)
+        this.ajaxActive.abort();
+
+      $.ajax({
+        type: 'POST',
+        url: 'ajax.php/lock/{0}/release'.replace('{0}', this.lockId),
+        data: 'delete',
+        cache: false,
+        success: this.clearAll.bind(this),
+        complete: this.destroy.bind(this)
+      });
     },
 
-    acquireLock: function(e,warn) {
-
-        if(!autoLock.tid) { return false; }
-
-        var warn = warn || false;
-
-        if(autoLock.lockId) {
-            autoLock.renewLock(e);
-        } else {
-            $.ajax({
-                type: "POST",
-                url: 'ajax.php/tickets/'+autoLock.tid+'/lock',
-                dataType: 'json',
-                cache: false,
-                success: function(lock){
-                    autoLock.setLock(lock,'acquire',warn);
-                }
-            })
-            .done(function() { })
-            .fail(function() { });
-        }
-
-        return autoLock.lockId;
+    clearAll: function() {
+      // Clear all other current locks with the same ID as this
+      $.each(Lock.prototype.registry, function(i, l) {
+        if (l.lockId && l.lockId == this.lockId)
+          l.shutdown();
+      }.bind(this));
     },
 
-    //Renewal only happens on form activity..
-    renewLock: function(e) {
-
-        if(!autoLock.lockId) { return false; }
-
-        var now= new Date().getTime();
-        if(!autoLock.lastcheckTime || (now-autoLock.lastcheckTime)>=(autoLock.renewFreq*1000)){
-            $.ajax({
-                type: 'POST',
-                url: 'ajax.php/tickets/'+autoLock.tid+'/lock/'+autoLock.lockId+'/renew',
-                dataType: 'json',
-                cache: false,
-                success: function(lock){
-                    autoLock.setLock(lock,'renew',autoLock.warn);
-                }
-            })
-            .done(function() {  })
-            .fail(function() { });
-        }
+    shutdown: function() {
+      clearTimeout(this.warning);
+      clearTimeout(this.retryTimer);
+      $(document).off('.exclusive');
     },
 
-    releaseLock: function(e) {
-        if (!autoLock.tid || !autoLock.lockId) { return false; }
-
-        $.ajax({
-            type: 'POST',
-            url: 'ajax.php/tickets/'+autoLock.tid+'/lock/'+autoLock.lockId+'/release',
-            data: 'delete',
-            async: false,
-            cache: false,
-            success: function() {
-                autoLock.lockId = 0;
-            }
-        })
-        .done(function() { })
-        .fail(function() { });
+    destroy: function() {
+      this.shutdown();
+      delete this.lockId;
+      $(this.options.lockInput, this.$element).val('');
+      if (this.locked)
+        this.locked.reject();
     },
 
-    setLock: function(lock, action, warn) {
-        var warn = warn || false;
-
-        if(!lock) return false;
-
-        if(lock.id) {
-            autoLock.renewFreq=lock.time?(lock.time/2):30;
-            autoLock.lastcheckTime=new Date().getTime();
-        }
-        autoLock.lockId=lock.id; //override the lockid.
-
-        switch(action){
-            case 'renew':
-                if(!lock.id && lock.retry) {
-                    autoLock.lockAttempts=1; //reset retries.
-                    autoLock.acquireLock(e,false); //We lost the lock?? ..try to re acquire now.
-                }
-                break;
-            case 'acquire':
-                if(!lock.id) {
-                    autoLock.lockAttempts++;
-                    if(warn && (!lock.retry || autoLock.lockAttempts>=autoLock.maxattempts)) {
-                        autoLock.retry=false;
-                        alert(__('Unable to lock the ticket. Someone else could be working on the same ticket.'));
-                    }
-                }
-                break;
-        }
+    update: function(lock) {
+      if (typeof lock != 'object' || lock.retry === true) {
+        // Non-json response, or retry requested server-side
+        return this.retry(this.renew, this.activeAjax, false, lock);
+      }
+      if (!lock.id) {
+        // Response did not include a lock id number
+        return this.fail(lock.msg);
+      }
+      if (!this.lockId) {
+        // Set up release on away navigation
+        $(document).off('.exclusive');
+        $(document).on('pjax:click.exclusive', $.proxy(this.release, this));
+        Lock.prototype.registry.push(this);
+      }
+
+      this.lockId = lock.id;
+      this.fails = 0;
+      $.messageBar.hide();
+      this.errorBar = false;
+
+      // If there is an input with the name 'lockCode', then set the value
+      // to the lock.code retrieved (if any)
+      if (lock.code)
+        $(this.options.lockInput, this.$element).val(lock.code);
+
+      // Deadband renew to every 30 seconds
+      this.nextRenew = new Date().getTime() + 30000;
+
+      // Warn 10 seconds before expiration
+      this.lockTimeout(lock.time - 10);
+
+      if (this.locked)
+        this.locked.resolve(lock);
     },
 
-    discardWarning: function(e) {
-        e.returnValue=__("Any changes or info you've entered will be discarded!");
+    lockTimeout: function(time) {
+      if (this.warning)
+        clearTimeout(this.warning);
+      this.warning = setTimeout(this.warn.bind(this), time * 1000);
     },
 
-    //TODO: Monitor events and elapsed time and warn user when the lock is about to expire.
-    monitorEvents: function() {
-       // warn user when lock is about to expire??;
-        //autoLock.resetTimer();
+    ensureLocked: function(e) {
+      // Make sure a lock code has been fetched first
+      if (!$(this.options.lockInput, this.$element).val()) {
+        var $target = $(e.target),
+            text = $target.text() || $target.val();
+        $target.prop('disabled', true).text(__('Acquiring Lock')).val(__('Acquiring Lock'));
+        this.acquire().always(function(lock) {
+          $target.text(text).val(text).prop('disabled', false);
+          if (typeof lock == 'object' && lock.code)
+            $target.trigger(e.type, e);
+        }.bind(this));
+        return false;
+      }
     },
 
-    clearTimer: function() {
-        clearTimeout(autoLock.timerId);
+    warn: function() {
+      $.messageBar.show(
+        __('Your lock is expiring soon.'),
+        __('The lock you hold on this ticket will expire soon. Would you like to renew the lock?'),
+        {onok: this.wakeup.bind(this), buttonText: __("Renew")}
+      ).addClass('warning');
     },
 
-    resetTimer: function() {
-        clearTimeout(autoLock.timerId);
-        autoLock.timerId=setTimeout(function () { autoLock.monitorEvents() },30000);
+    fail: function(msg) {
+      // Don't retry for 5 seconds
+      this.nextRenew = new Date().getTime() + 5000;
+      // Resolve anything awaiting
+      if (this.locked)
+        this.locked.rejectWith(msg);
+      // No longer locked
+      this.destroy();
+      // Flash the error bar if it's already on the screen
+      if (this.errorBar && $.messageBar.visible)
+          return this.errorBar.effect('highlight');
+      // Add the error bar to the screen
+      this.errorBar = $.messageBar.show(
+        msg || __('Unable to lock the ticket.'),
+        __('Someone else could be working on the same ticket.'),
+        {avatar: 'oscar-borg', buttonClass: 'red', dismissible: true}
+      ).addClass('danger');
     }
-};
-$.autoLock = autoLock;
+  };
+
+  $.fn.exclusive = function ( option ) {
+    return this.each(function () {
+      var $this = $(this),
+        data = $this.data('exclusive'),
+        options = typeof option == 'object' && option;
+      if (!data) $this.data('exclusive', (data = new Lock(this, options)));
+      if (typeof option == 'string') data[option]();
+    });
+  };
+
+  $.fn.exclusive.defaults = {
+    lockInput: 'input[name=lockCode]',
+    maxRetries: 2,
+    retry: true,
+    retryInterval: 2
+  };
+
+  $.fn.exclusive.Constructor = Lock;
+
+}(window.jQuery);
 
 /*
    UI & form events
 */
-$.showNonLocalImage = function(div) {
-    var $div = $(div),
-        $img = $div.append($('<img>')
-          .attr('src', $div.data('src'))
-          .attr('alt', $div.attr('alt'))
-          .attr('title', $div.attr('title'))
-          .attr('style', $div.data('style'))
-        );
-    if ($div.attr('width'))
-        $img.width($div.attr('width'));
-    if ($div.attr('height'))
-        $img.height($div.attr('height'));
-};
-
 $.showImagesInline = function(urls, thread_id) {
     var selector = (thread_id == undefined)
         ? '.thread-body img[data-cid]'
-        : '.thread-body#thread-id-'+thread_id+' img[data-cid]';
+        : '.thread-body#thread-entry-'+thread_id+' img[data-cid]';
     $(selector).each(function(i, el) {
         var e = $(el),
             cid = e.data('cid').toLowerCase(),
@@ -308,79 +288,37 @@ $.showImagesInline = function(urls, thread_id) {
                     }
                 ).append($('<div class="caption">')
                     .append('<span class="filename">'+info.filename+'</span>')
-                    .append('<a href="'+info.download_url+'" class="action-button pull-right no-pjax"><i class="icon-download-alt"></i> '+__('Download')+'</a>')
+                    .append($('<a href="'+info.download_url+'" class="action-button pull-right no-pjax"><i class="icon-download-alt"></i> '+__('Download')+'</a>')
+                      .attr('download', info.filename)
+                    )
                 );
             e.data('wrapped', true);
         }
     });
 };
 
-$.refreshTicketView = function() {
-    if (0 === $('.dialog:visible').length)
-        $.pjax({url: document.location.href, container:'#pjax-container'});
-}
+$.refreshTicketView = function(interval) {
+    var refresh = setInterval(function() {
+      if ($('table.list input.ckb[name=tids\\[\\]]:checked').length)
+        // Skip the refresh b/c items are checked
+        return;
+      else if (0 < $('.dialog:visible').length)
+        // Dialog open — skip refresh
+        return;
+
+      clearInterval(refresh);
+      $.pjax({url: document.location.href, container:'#pjax-container'});
+    }, interval);
+    $(document).on('pjax:start', function() {
+        clearInterval(refresh);
+    });
+};
 
 var ticket_onload = function($) {
-    $('#response_options form').hide();
-    $('#ticket_notes').hide();
-    if(location.hash != "" && $('#response_options '+location.hash).length) {
-        $('#response_options '+location.hash+'_tab').addClass('active');
-        $('#response_options '+location.hash).show();
-    } else if(location.hash == "#notes" && $('#ticket_notes').length) {
-        $('#response_options #note_tab').addClass('active');
-        $('#response_options form').hide();
-        $('#response_options #note').show();
-        $('#ticket_thread').hide();
-        $('#ticket_notes').show();
-        $('#toggle_ticket_thread').removeClass('active');
-        $('#toggle_notes').addClass('active');
-    } else {
-        $('#response_options ul.tabs li:first a').addClass('active');
-        $('#response_options '+$('#response_options ul.tabs li:first a').attr('href')).show();
-    }
-
-    $('#reply_tab').click(function() {
-       $(this).removeClass('tell');
-     });
-
-    $('#note_tab').click(function() {
-        if($('#response').val() != '') {
-            $('#reply_tab').addClass('tell');
-        }
-     });
-
-    $('#response_options ul.tabs li a').click(function(e) {
-        e.preventDefault();
-        $('#response_options ul.tabs li a').removeClass('active');
-        $(this).addClass('active');
-        $('#response_options form').hide();
-        //window.location.hash = this.hash;
-        $('#response_options '+$(this).attr('href')).show();
-        $("#msg_error, #msg_notice, #msg_warning").fadeOut();
-     });
+    if (0 === $('#ticketThread').length)
+        return;
 
-    $('#toggle_ticket_thread, #toggle_notes, .show_notes').click(function(e) {
-        e.preventDefault();
-        $('#threads a').removeClass('active');
-
-        if($(this).attr('id') == 'toggle_ticket_thread') {
-            $('#ticket_notes').hide();
-            $('#ticket_thread').show();
-            $('#toggle_ticket_thread').addClass('active');
-            $('#reply_tab').removeClass('tell').click();
-        } else {
-            $('#ticket_thread').hide();
-            $('#ticket_notes').show();
-            $('#toggle_notes').addClass('active');
-            $('#note_tab').click();
-            if($('#response').val() != '') {
-                $('#reply_tab').addClass('tell');
-            }
-        }
-     });
-
-    //Start watching the form for activity.
-    autoLock.Init();
+    $(function(){$('.exclusive[data-lock-object-id]').exclusive();});
 
     /*** Ticket Actions **/
     //print options TODO: move to backend
@@ -398,69 +336,26 @@ var ticket_onload = function($) {
         var url = 'ajax.php/'
         +$(this).attr('href').substr(1)
         +'?_uid='+new Date().getTime();
-        var $redirect = $(this).data('href');
+        var $redirect = $(this).data('redirect');
+        var $options = $(this).data('dialog');
         $.dialog(url, [201], function (xhr) {
-            window.location.href = $redirect ? $redirect : window.location.href;
-        });
+            if (!!$redirect)
+                window.location.href = $redirect;
+            else
+                $.pjax.reload('#pjax-container');
+        }, $options);
 
         return false;
     });
 
-    $(document).on('change', 'form#reply select#emailreply', function(e) {
-         var $cc = $('form#reply tbody#cc_sec');
+    $(document).on('change', 'form[name=reply] select#emailreply', function(e) {
+         var $cc = $('form[name=reply] tbody#cc_sec');
         if($(this).val() == 0)
             $cc.hide();
         else
             $cc.show();
      });
 
-    // Optionally show external images
-    $('.thread-entry').each(function(i, te) {
-        var extra = $(te).find('.textra'),
-            imgs = $(te).find('.non-local-image[data-src]');
-        if (!extra) return;
-        if (!imgs.length) return;
-        extra.append($('<a>')
-          .addClass("action-button pull-right show-images")
-          .css({'font-weight':'normal'})
-          .text(' ' + __('Show Images'))
-          .click(function(ev) {
-            imgs.each(function(i, img) {
-              $.showNonLocalImage(img);
-              $(img).removeClass('non-local-image')
-                // Remove placeholder sizing
-                .css({'display':'inline-block'})
-                .width('auto')
-                .height('auto')
-                .removeAttr('width')
-                .removeAttr('height');
-              extra.find('.show-images').hide();
-            });
-          })
-          .prepend($('<i>')
-            .addClass('icon-picture')
-          )
-        );
-        imgs.each(function(i, img) {
-            var $img = $(img);
-            // Save a copy of the original styling
-            $img.data('style', $img.attr('style'));
-            $img.removeAttr('style');
-            // If the image has a 'height' attribute, use it, otherwise, use
-            // 40px
-            $img.height(($img.attr('height') || '40') + 'px');
-            // Ensure the image placeholder is visible width-wise
-            if (!$img.width())
-                $img.width(($img.attr('width') || '80') + 'px');
-            // TODO: Add a hover-button to show just one image
-        });
-    });
-
-    $('.thread-body').each(function() {
-        var urls = $(this).data('urls');
-        if (urls)
-            $.showImagesInline(urls, $(this).data('id'));
-    });
 };
 $(ticket_onload);
 $(document).on('pjax:success', function() { ticket_onload(jQuery); });
diff --git a/scp/js/tips.js b/scp/js/tips.js
index 93f4f14c75d49d1c151e27ffc787e1abb42f3078..ddf0051b651d2da3b186ec7ab8f01a5339064f4b 100644
--- a/scp/js/tips.js
+++ b/scp/js/tips.js
@@ -1,6 +1,10 @@
 jQuery(function() {
     var showtip = function (url, elem,xoffset) {
 
+            // If element is no longer visible
+            if (!elem.is(':visible'))
+                return;
+
             var pos = elem.offset();
             var y_pos = pos.top - 12;
             var x_pos = pos.left + (xoffset || (elem.width() + 16));
@@ -9,20 +13,25 @@ jQuery(function() {
             var tip_box = $('<div>').addClass('tip_box');
             var tip_shadow = $('<div>').addClass('tip_shadow');
             var tip_content = $('<div>').addClass('tip_content').load(url, function() {
-                tip_content.prepend('<a href="#" class="tip_close"><i class="icon-remove-circle"></i></a>').append(tip_arrow);
-            var width = $(window).width(),
-                rtl = $('html').hasClass('rtl'),
-                size = tip_content.outerWidth(),
-                left = the_tip.position().left,
-                left_room = left - size,
-                right_room = width - size - left,
-                flip = rtl
-                    ? (left_room > 0 && left_room > right_room)
-                    : (right_room < 0 && left_room > right_room);
-                if (flip) {
-                    the_tip.css({'left':x_pos-tip_content.outerWidth()-elem.width()-32+'px'});
-                    tip_box.addClass('right');
-                    tip_arrow.addClass('flip-x');
+                if (elem.is(':visible')) {
+                    tip_content.prepend('<a href="#" class="tip_close"><i class="icon-remove-circle"></i></a>').append(tip_arrow);
+                    var width = $(window).width(),
+                    rtl = $('html').hasClass('rtl'),
+                    size = tip_content.outerWidth(),
+                    left = the_tip.position().left,
+                    left_room = left - size,
+                    right_room = width - size - left,
+                    flip = rtl
+                        ? (left_room > 0 && left_room > right_room)
+                        : (right_room < 0 && left_room > right_room);
+                    if (flip) {
+                        the_tip.css({'left':x_pos-tip_content.outerWidth()-elem.width()-32+'px'});
+                        tip_box.addClass('right');
+                        tip_arrow.addClass('flip-x');
+                    }
+                } else {
+                    // Self close  if the element is gone
+                    $('.tip_box').remove();
                 }
             });
 
@@ -31,16 +40,22 @@ jQuery(function() {
                 "top":y_pos + "px",
                 "left":x_pos + "px"
             }).addClass(elem.data('id'));
+
+            // Close any open tips
             $('.tip_box').remove();
-            $('body').append(the_tip.hide().fadeIn());
-            $('.' + elem.data('id') + ' .tip_shadow').css({
-                "height":$('.' + elem.data('id')).height() + 5
-            });
+            // Only show the tip if the element is still visible.
+            if (elem.is(':visible')) {
+                $('body').append(the_tip.hide().fadeIn());
+                $('.' + elem.data('id') + ' .tip_shadow').css({
+                    "height":$('.' + elem.data('id')).height() + 5
+                });
+            }
     },
     getHelpTips = (function() {
         var dfd, cache = {};
-        return function(namespace) {
-            var namespace = namespace
+        return function(elem) {
+            var namespace =
+                   $(elem).closest('[data-tip-namespace]').data('tipNamespace')
                 || $('#content').data('tipNamespace')
                 || $('meta[name=tip-namespace]').attr('content');
             if (!namespace)
@@ -62,8 +77,8 @@ jQuery(function() {
 
     var tip_id = 1;
     //Generic tip.
-    $('.tip')
-    .live('click mouseover', function(e) {
+    $(document)
+    .on('click mouseover', '.tip', function(e) {
         e.preventDefault();
         if (!this.rel)
             this.rel = 'tip-' + (tip_id++);
@@ -84,12 +99,12 @@ jQuery(function() {
             }
         }
     })
-    .live('mouseout', function(e) {
+    .on('mouseout', '.tip', function(e) {
         clearTimeout($(this).data('timer'));
     });
 
-    $('.help-tip')
-    .live('mouseover click', function(e) {
+    $(document)
+    .on('mouseover click', '.help-tip', function(e) {
         e.preventDefault();
 
         var elem = $(this),
@@ -129,11 +144,11 @@ jQuery(function() {
                 }
             }, 500);
 
-        elem.live('mouseout', function() {
+        elem.on('mouseout', function() {
             clearTimeout(tip_timer);
         });
 
-        getHelpTips().then(function(tips) {
+        getHelpTips(elem).then(function(tips) {
             var href = elem.attr('href');
             if (href) {
                 section = tips[elem.attr('href').substr(1)];
@@ -174,7 +189,8 @@ jQuery(function() {
     });
 
     //faq preview tip
-    $('.previewfaq').live('mouseover', function(e) {
+    $(document)
+    .on('mouseover', '.previewfaq', function(e) {
         e.preventDefault();
         var elem = $(this);
 
@@ -193,12 +209,12 @@ jQuery(function() {
                 showtip(url,elem,xoffset);
             }
         }
-    }).live('mouseout', function(e) {
+    }).on('mouseout', '.previewfaq', function(e) {
         clearTimeout($(this).data('timer'));
     });
 
 
-    $('a.collaborators.preview').live('mouseover', function(e) {
+    $(document).on('mouseover', 'a.collaborators.preview', function(e) {
         e.preventDefault();
         var elem = $(this);
 
@@ -211,50 +227,27 @@ jQuery(function() {
         } else {
             showtip(url,elem,xoffset);
         }
-    }).live('mouseout', function(e) {
+    }).on('mouseout', 'a.collaborators.preview', function(e) {
         clearTimeout($(this).data('timer'));
-    }).live('click', function(e) {
+    }).on('click', 'a.collaborators.preview', function(e) {
         clearTimeout($(this).data('timer'));
         $('.tip_box').remove();
     });
 
 
-    //Ticket preview
-    $('.ticketPreview').live('mouseover', function(e) {
-        e.preventDefault();
+    // Tooltip preview
+    $(document).on('mouseover', '.preview', function(e) {
         var elem = $(this);
-
+        if (!elem.attr('href'))
+            return;
         var vars = elem.attr('href').split('=');
-        var url = 'ajax.php/tickets/'+vars[1]+'/preview';
-        var id='t'+vars[1];
-        var xoffset = 80;
-
-        elem.data('timer', 0);
-        if(!elem.data('id')) {
-            elem.data('id', id);
-            if(e.type=='mouseover') {
-                 /* wait about 1 sec - before showing the tip - mouseout kills the timeout*/
-                 elem.data('timer',setTimeout(function() { showtip(url,elem,xoffset);},750))
-            }else{
-                clearTimeout(elem.data('timer'));
-                showtip(url,elem,xoffset);
-            }
-        }
-    }).live('mouseout', function(e) {
-        $(this).data('id', 0);
-        clearTimeout($(this).data('timer'));
-    });
-
-    //User preview
-    $('.userPreview').live('mouseover', function(e) {
+        if (!elem.data('preview'))
+            return;
         e.preventDefault();
-        var elem = $(this);
-
-        var vars = elem.attr('href').split('=');
-        var url = 'ajax.php/users/'+vars[1]+'/preview';
-        var id='u'+vars[1];
+        var url = 'ajax.php/'+elem.data('preview').substr(1);
+        // TODO - hash url to integer and use it as id.
+        var id= url.match(/\d/g).join("");
         var xoffset = 80;
-
         elem.data('timer', 0);
         if(!elem.data('id')) {
             elem.data('id', id);
@@ -263,10 +256,10 @@ jQuery(function() {
                  elem.data('timer',setTimeout(function() { showtip(url,elem,xoffset);},750))
             }else{
                 clearTimeout(elem.data('timer'));
-                showtip(url, elem, xoffset);
+                showtip(url,elem,xoffset);
             }
         }
-    }).live('mouseout', function(e) {
+    }).on('mouseout', '.preview', function(e) {
         $(this).data('id', 0);
         clearTimeout($(this).data('timer'));
     });
@@ -277,7 +270,7 @@ jQuery(function() {
         $(this).parent().parent().remove();
     });
 
-    $(document).live('mouseup', function (e) {
+    $(document).on('mouseup', function (e) {
         var container = $('.tip_box');
         if (!container.is(e.target)
             && container.has(e.target).length === 0) {
diff --git a/scp/lists.php b/scp/lists.php
index 7ea71f39334d027016bfcc8fa67c433bf13fbbe8..6ec0fdc9259b5710144f9dfa131f46293832ce44 100644
--- a/scp/lists.php
+++ b/scp/lists.php
@@ -5,6 +5,7 @@ require_once(INCLUDE_DIR.'class.list.php');
 
 $list=null;
 $criteria=array();
+$redirect = false;
 if ($_REQUEST['id'])
     $criteria['id'] = $_REQUEST['id'];
 elseif ($_REQUEST['type'])
@@ -12,7 +13,8 @@ elseif ($_REQUEST['type'])
 
 if ($criteria) {
     $list = DynamicList::lookup($criteria);
-
+    if ($list)
+        $list = CustomListHandler::forList($list);
     if ($list)
          $form = $list->getForm();
     else
@@ -21,54 +23,33 @@ if ($criteria) {
 }
 
 $errors = array();
-$max_isort = 0;
 
 if($_POST) {
-    switch(strtolower($_POST['do'])) {
+    switch(strtolower($_REQUEST['do'])) {
         case 'update':
             if (!$list)
                 $errors['err']=sprintf(__('%s: Unknown or invalid ID.'),
                     __('custom list'));
             elseif ($list->update($_POST, $errors)) {
-                // Update items
-                $items = array();
-                foreach ($list->getAllItems() as $item) {
-                    $id = $item->getId();
-                    if ($_POST["delete-item-$id"] == 'on' && $item->isDeletable()) {
-                        $item->delete();
-                        continue;
-                    }
-
-                    $ht = array(
-                            'value' => $_POST["value-$id"],
-                            'abbrev' => $_POST["abbrev-$id"],
-                            'sort' => $_POST["sort-$id"],
-                            );
-                    $value = mb_strtolower($ht['value']);
-                    if (!$value)
-                        $errors["value-$id"] = __('Value required');
-                    elseif (in_array($value, $items))
-                        $errors["value-$id"] = __('Value already in-use');
-                    elseif ($item->update($ht, $errors)) {
-                        if ($_POST["disable-$id"] == 'on')
-                            $item->disable();
-                        elseif(!$item->isEnabled() && $item->isEnableable())
-                            $item->enable();
-
-                        $item->save();
-                        $items[] = $value;
+                // Update item sorting
+                if ($list->getSortMode() == 'SortCol') {
+                    foreach ($list->getAllItems() as $item) {
+                        $id = $item->getId();
+                        if (isset($_POST["sort-{$id}"])) {
+                            $item->sort = $_POST["sort-$id"];
+                            $item->save();
+                        }
                     }
-
-                    $max_isort = max($max_isort, $_POST["sort-$id"]);
                 }
 
                 // Update properties
                 if (!$errors && ($form = $list->getForm())) {
                     $names = array();
-                    foreach ($form->getDynamicFields() as $field) {
+                    $fields = $form->getDynamicFields();
+                    foreach ($fields as $field) {
                         $id = $field->get('id');
                         if ($_POST["delete-prop-$id"] == 'on' && $field->isDeletable()) {
-                            $field->delete();
+                            $fields->remove($field);
                             // Don't bother updating the field
                             continue;
                         }
@@ -117,9 +98,10 @@ if($_POST) {
             break;
         case 'add':
             if ($list=DynamicList::add($_POST, $errors)) {
-                $form = $list->getForm();
-                 $msg = sprintf(__('Successfully added %s'),
-                    __('this custom list'));
+                 $form = $list->getForm(true);
+                 Messages::success(sprintf(__('Successfully added %s'), __('this custom list')));
+                 // Redirect to list page
+                 $redirect = "lists.php?id={$list->id}#items";
             } elseif ($errors) {
                 $errors['err']=sprintf(__('Unable to add %s. Correct error(s) below and try again.'),
                     __('this custom list'));
@@ -155,19 +137,21 @@ if($_POST) {
                 }
             }
             break;
-    }
-
-    if ($list && $list->allowAdd()) {
-        for ($i=0; isset($_POST["sort-new-$i"]); $i++) {
-            if (!$_POST["value-new-$i"])
-                continue;
 
-            $list->addItem(array(
-                        'value' => $_POST["value-new-$i"],
-                        'abbrev' =>$_POST["abbrev-new-$i"],
-                        'sort' => $_POST["sort-new-$i"] ?: ++$max_isort,
-                        ), $errors);
-        }
+        case 'import-items':
+            if (!$list) {
+                $errors['err']=sprintf(__('%s: Unknown or invalid ID.'),
+                    __('custom list'));
+            }
+            else {
+                $status = $list->importFromPost($_FILES['import'] ?: $_POST['pasted']);
+                if (is_numeric($status))
+                    $msg = sprintf(__('Successfully imported %1$d %2$s.'), $status,
+                        _N('list item', 'list items', $status));
+                else
+                    $errors['err'] = $status;
+            }
+            break;
     }
 
     if ($form) {
@@ -175,26 +159,35 @@ if($_POST) {
             if (!$_POST["prop-label-new-$i"])
                 continue;
             $field = DynamicFormField::create(array(
-                'form_id' => $form->get('id'),
                 'sort' => $_POST["prop-sort-new-$i"] ?: ++$max_sort,
                 'label' => $_POST["prop-label-new-$i"],
                 'type' => $_POST["type-new-$i"],
                 'name' => $_POST["name-new-$i"],
+                'flags' => DynamicFormField::FLAG_ENABLED
+                    | DynamicFormField::FLAG_AGENT_VIEW
+                    | DynamicFormField::FLAG_AGENT_EDIT,
             ));
             if ($field->isValid()) {
-                $field->form = $form;
+                $form->fields->add($field);
                 $field->save();
             }
             else
                 $errors["new-$i"] = $field->errors();
         }
-        // XXX: Move to an instrumented list that can handle this better
-        if (!$errors)
-            $form->_dfields = $form->_fields = null;
     }
 }
 
+if ($redirect)
+    Http::redirect($redirect);
+
 $page='dynamic-lists.inc.php';
+if($list && !strcasecmp(@$_REQUEST['a'],'items') && isset($_SERVER['HTTP_X_PJAX'])) {
+    $page='templates/list-items.tmpl.php';
+    $pjax_container = @$_SERVER['HTTP_X_PJAX_CONTAINER'];
+    require(STAFFINC_DIR.$page);
+    // Don't emit the header
+    return;
+}
 if($list || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'],'add'))) {
     $page='dynamic-list.inc.php';
     $ost->addExtraHeader('<meta name="tip-namespace" content="manage.custom_list" />',
diff --git a/scp/login.php b/scp/login.php
index bbf8dc7a31c5f156dd7c81718858689aecf91839..0fc0d0991410c3a2a7ee019aab7ee1c91dd9c4ad 100644
--- a/scp/login.php
+++ b/scp/login.php
@@ -23,11 +23,11 @@ TextDomain::configureForUser();
 require_once(INCLUDE_DIR.'class.staff.php');
 require_once(INCLUDE_DIR.'class.csrf.php');
 
-$content = Page::lookup(Page::getIdByType('banner-staff'));
+$content = Page::lookupByType('banner-staff');
 
 $dest = $_SESSION['_staff']['auth']['dest'];
 $msg = $_SESSION['_staff']['auth']['msg'];
-$msg = $msg ?: ($content ? $content->getName() : __('Authentication Required'));
+$msg = $msg ?: ($content ? $content->getLocalName() : __('Authentication Required'));
 $dest=($dest && (!strstr($dest,'login.php') && !strstr($dest,'ajax.php')))?$dest:'index.php';
 $show_reset = false;
 if($_POST) {
@@ -67,9 +67,12 @@ elseif ($_GET['do']) {
 elseif (!$thisstaff || !($thisstaff->getId() || $thisstaff->isValid())) {
     if (($user = StaffAuthenticationBackend::processSignOn($errors, false))
             && ($user instanceof StaffSession))
-       @header("Location: $dest");
+       Http::redirect($dest);
 }
 
+// Browsers shouldn't suggest saving that username/password
+Http::response(422);
+
 define("OSTSCPINC",TRUE); //Make includes happy!
 include_once(INCLUDE_DIR.'staff/login.tpl.php');
 ?>
diff --git a/scp/logo.php b/scp/logo.php
index 0acbf430882b3690134e4048bb7c22ffc1cd697f..a0fae8c697381728fb1ec88d77c0d149b00cc608 100644
--- a/scp/logo.php
+++ b/scp/logo.php
@@ -23,10 +23,21 @@ define('DISABLE_SESSION', true);
 
 require_once('../main.inc.php');
 
-if (($logo = $ost->getConfig()->getStaffLogo())) {
+if (isset($_GET['backdrop'])) {
+    if (($backdrop = $ost->getConfig()->getStaffLoginBackdrop())) {
+        $backdrop->display();
+        // ::display() will not return
+    }
+    header("Cache-Control: private, max-age=86400");
+    header('Pragma: private');
+    Http::redirect('images/login-headquarters.jpg');
+}
+elseif (($logo = $ost->getConfig()->getStaffLogo())) {
     $logo->display();
-} else {
-    header('Location: images/ost-logo.png');
 }
 
+header("Cache-Control: private, max-age=86400");
+header('Pragma: private');
+Http::redirect('images/ost-logo.png');
+
 ?>
diff --git a/scp/logout.php b/scp/logout.php
index 1007d985c0bb8509514f42d55d91103571b3f965..a40079026540b0e1886d37e4f23fb6b0d7faa44e 100644
--- a/scp/logout.php
+++ b/scp/logout.php
@@ -20,19 +20,22 @@ require('staff.inc.php');
 if(!$_GET['auth'] || !$ost->validateLinkToken($_GET['auth']))
     @header('Location: index.php');
 
-$thisstaff->logOut();
+try {
+    $thisstaff->logOut();
 
-//Clear any ticket locks the staff has.
-TicketLock::removeStaffLocks($thisstaff->getId());
+    //Destroy session on logout.
+    // TODO: Stop doing this starting with 1.9 - separate session data per
+    // app/panel.
+    session_unset();
+    session_destroy();
 
-//Destroy session on logout.
-// TODO: Stop doing this starting with 1.9 - separate session data per
-// app/panel.
-session_unset();
-session_destroy();
+    osTicketSession::destroyCookie();
 
-osTicketSession::destroyCookie();
+    //Clear any ticket locks the staff has.
+    Lock::removeStaffLocks($thisstaff->getId());
+}
+catch (Exception $x) {
+    // Lock::removeStaffLocks may throw InconsistentModel on upgrade
+}
 
-@header('Location: login.php');
-require('login.php');
-?>
+Http::redirect('login.php');
diff --git a/scp/orgs.php b/scp/orgs.php
index e1b48de31e70cadc99dc4ce3f86b23e7de70385b..bfd2210abf39db4d913398669c7c0418fc7fb02e 100644
--- a/scp/orgs.php
+++ b/scp/orgs.php
@@ -13,6 +13,7 @@
     vim: expandtab sw=4 ts=4 sts=4:
 **********************************************************************/
 require('staff.inc.php');
+require_once INCLUDE_DIR . 'class.organization.php';
 require_once INCLUDE_DIR . 'class.note.php';
 
 $org = null;
@@ -94,18 +95,35 @@ if ($_POST) {
     default:
         $errors['err'] = __('Unknown action');
     }
-} elseif ($_REQUEST['a'] == 'export') {
+} elseif (!$org && $_REQUEST['a'] == 'export') {
     require_once(INCLUDE_DIR.'class.export.php');
     $ts = strftime('%Y%m%d');
-    if (!($token=$_REQUEST['qh']))
-        $errors['err'] = __('Query token required');
-    elseif (!($query=$_SESSION['orgs_qs_'.$token]))
+    if (!($query=$_SESSION[':Q:orgs']))
         $errors['err'] = __('Query token not found');
     elseif (!Export::saveOrganizations($query, __('organizations')."-$ts.csv", 'csv'))
         $errors['err'] = __('Internal error: Unable to export results');
 }
 
-$page = $org? 'org-view.inc.php' : 'orgs.inc.php';
+$page = 'orgs.inc.php';
+if ($org) {
+    $page = 'org-view.inc.php';
+    switch (strtolower($_REQUEST['t'])) {
+    case 'tickets':
+        if (isset($_SERVER['HTTP_X_PJAX'])) {
+            $page='templates/tickets.tmpl.php';
+            $pjax_container = @$_SERVER['HTTP_X_PJAX_CONTAINER'];
+            require(STAFFINC_DIR.$page);
+            return;
+        } elseif ($_REQUEST['a'] == 'export' && ($query=$_SESSION[':O:tickets'])) {
+            $filename = sprintf('%s-tickets-%s.csv',
+                    $org->getName(), strftime('%Y%m%d'));
+            if (!Export::saveTickets($query, $filename, 'csv'))
+                $errors['err'] = __('Internal error: Unable to dump query results');
+        }
+        break;
+    }
+}
+
 $nav->setTabActive('users');
 require(STAFFINC_DIR.'header.inc.php');
 require(STAFFINC_DIR.$page);
diff --git a/scp/pages.php b/scp/pages.php
index c761d92e2e84a1fdcb57b5d02e134213711c81d5..773d38fd7dc3366256519d33fbecf8da74b3fad7 100644
--- a/scp/pages.php
+++ b/scp/pages.php
@@ -23,13 +23,11 @@ if($_REQUEST['id'] && !($page=Page::lookup($_REQUEST['id'])))
 if($_POST) {
     switch(strtolower($_POST['do'])) {
         case 'add':
-            if(($pageId=Page::create($_POST, $errors))) {
+            $page = Page::create();
+            if($page->update($_POST, $errors)) {
+                $pageId = $page->getId();
                 $_REQUEST['a'] = null;
                 $msg=sprintf(__('Successfully added %s'), Format::htmlchars($_POST['name']));
-                // Attach inline attachments from the editor
-                if ($page = Page::lookup($pageId))
-                    $page->attachments->upload(
-                        Draft::getAttachmentIds($_POST['body']), true);
                 Draft::deleteForNamespace('page');
             } elseif(!$errors['err'])
                 $errors['err'] = sprintf(__('Unable to add %s. Correct error(s) below and try again.'),
@@ -43,12 +41,7 @@ if($_POST) {
                 $msg=sprintf(__('Successfully updated %s'),
                     __('this site page'));
                 $_REQUEST['a']=null; //Go back to view
-                // Attach inline attachments from the editor
-                $page->attachments->deleteInlines();
-                $page->attachments->upload(
-                    Draft::getAttachmentIds($_POST['body']),
-                    true);
-                Draft::deleteForNamespace('page.'.$page->getId());
+                Draft::deleteForNamespace('page.'.$page->getId().'%');
             } elseif(!$errors['err'])
                 $errors['err'] = sprintf(__('Unable to update %s. Correct error(s) below and try again.'),
                     __('this site page'));
@@ -64,9 +57,10 @@ if($_POST) {
                 $count=count($_POST['ids']);
                 switch(strtolower($_POST['a'])) {
                     case 'enable':
-                        $sql='UPDATE '.PAGE_TABLE.' SET isactive=1 '
-                            .' WHERE id IN ('.implode(',', db_input($_POST['ids'])).')';
-                        if(db_query($sql) && ($num=db_affected_rows())) {
+                        $num = Page::objects()
+                            ->filter(array('id__in'=>$_POST['ids']))
+                            ->update(array('isactive'=>1));
+                        if ($num) {
                             if($num==$count)
                                 $msg = sprintf(__('Successfully enabled %s'),
                                     _N('selected site page', 'selected site pages', $count));
@@ -80,8 +74,8 @@ if($_POST) {
                         break;
                     case 'disable':
                         $i = 0;
-                        foreach($_POST['ids'] as $k=>$v) {
-                            if(($p=Page::lookup($v)) && $p->disable())
+                        foreach (Page::objects()->filter(array('id__in'=>$_POST['ids'])) as $p) {
+                            if ($p->disable())
                                 $i++;
                         }
 
@@ -96,11 +90,9 @@ if($_POST) {
                                 _N('selected site page', 'selected site pages', $count));
                         break;
                     case 'delete':
-                        $i=0;
-                        foreach($_POST['ids'] as $k=>$v) {
-                            if(($p=Page::lookup($v)) && $p->delete())
-                                $i++;
-                        }
+                        $i = Page::objects()
+                            ->filter(array('id__in'=>$_POST['ids']))
+                            ->delete();
 
                         if($i && $i==$count)
                             $msg = sprintf(__('Successfully deleted %s'),
diff --git a/scp/plugins.php b/scp/plugins.php
index ccc856e3a3f744b3060affde8a7dba95fd54650b..06b7cbcd2664471bc4de14ff12a45ba3343713fc 100644
--- a/scp/plugins.php
+++ b/scp/plugins.php
@@ -23,7 +23,10 @@ if($_POST) {
             case 'enable':
                 foreach ($_POST['ids'] as $id) {
                     if ($p = Plugin::lookup($id)) {
-                        $p->enable();
+                        if (!$p->enable())
+                            $errors['err'] = sprintf(
+                                __('Unable to enable %s.'),
+                                $p->getName());
                     }
                 }
                 break;
diff --git a/scp/profile.php b/scp/profile.php
index d25d545d45f374211756f638ba7ff8203b19bb56..f787324a4b2f7a34a59eccaa3cba728db780a6d0 100644
--- a/scp/profile.php
+++ b/scp/profile.php
@@ -27,17 +27,20 @@ if($_POST && $_POST['id']!=$thisstaff->getId()) { //Check dummy ID used on the f
         $errors['err']=sprintf(__('%s: Unknown or invalid'), __('agent'));
     elseif($staff->updateProfile($_POST,$errors)){
         $msg=__('Profile updated successfully');
-        $thisstaff->reload();
-        $staff->reload();
-        $_SESSION['TZ_OFFSET']=$thisstaff->getTZoffset();
-        $_SESSION['TZ_DST']=$thisstaff->observeDaylight();
     }elseif(!$errors['err'])
         $errors['err']=__('Profile update error. Try correcting the errors below and try again!');
 }
 
 //Forced password Change.
 if($thisstaff->forcePasswdChange() && !$errors['err'])
-    $errors['err']=sprintf(__('<b>Hi %s</b> - You must change your password to continue!'),$thisstaff->getFirstName());
+    $errors['err'] = str_replace(
+        '<a>',
+        sprintf('<a data-dialog="ajax.php/staff/%d/change-password" href="#">', $thisstaff->getId()),
+        sprintf(
+            __('<b>Hi %s</b> - You must <a>change your password to continue</a>!'),
+            $thisstaff->getFirstName()
+        )
+    );
 elseif($thisstaff->onVacation() && !$warn)
     $warn=sprintf(__("<b>Welcome back %s</b>! You are listed as 'on vacation' Please let your manager know that you are back."),$thisstaff->getFirstName());
 
diff --git a/scp/roles.php b/scp/roles.php
new file mode 100644
index 0000000000000000000000000000000000000000..749dee96fad1f00cf7d40535a3a242b7b4b93356
--- /dev/null
+++ b/scp/roles.php
@@ -0,0 +1,147 @@
+<?php
+/*********************************************************************
+    roles.php
+
+    Agent's roles
+
+    Peter Rotich <peter@osticket.com>
+    Copyright (c)  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 'admin.inc.php';
+include_once INCLUDE_DIR . 'class.user.php';
+include_once INCLUDE_DIR . 'class.organization.php';
+include_once INCLUDE_DIR . 'class.canned.php';
+include_once INCLUDE_DIR . 'class.faq.php';
+include_once INCLUDE_DIR . 'class.email.php';
+include_once INCLUDE_DIR . 'class.report.php';
+include_once INCLUDE_DIR . 'class.thread.php';
+
+$errors = array();
+$role=null;
+if ($_REQUEST['id'] && !($role = Role::lookup($_REQUEST['id'])))
+    $errors['err'] = sprintf(__('%s: Unknown or invalid ID.'),
+        __('Role'));
+
+if ($_POST) {
+    switch (strtolower($_POST['do'])) {
+    case 'update':
+        if (!$role) {
+            $errors['err'] = sprintf(__('%s: Unknown or invalid ID.'),
+                    __('Role'));
+        } elseif ($role->update($_POST, $errors)) {
+            $msg = __('Role updated successfully');
+        } elseif ($errors) {
+            $errors['err'] = $errors['err'] ?:
+                sprintf(__('Unable to update %s. Correct error(s) below and try again!'),
+                    __('this role'));
+        } else {
+            $errors['err'] = sprintf(__('Unable to update %s.'), __('this role'))
+                    .' '.__('Internal error occurred');
+        }
+        break;
+    case 'add':
+        $_role = Role::create();
+        if ($_role->update($_POST, $errors)) {
+            unset($_REQUEST['a']);
+            $msg = sprintf(__('Successfully added %s'),
+                    __('role'));
+        } elseif ($errors) {
+            $errors['err'] = $errors['err'] ?:
+                sprintf(__('Unable to add %s. Correct error(s) below and try again.'),
+                    __('role'));
+        } else {
+            $errors['err'] = sprintf(__('Unable to add %s.'), __('role'))
+                    .' '.__('Internal error occurred');
+        }
+        break;
+    case 'mass_process':
+        if (!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) {
+            $errors['err'] = sprintf(__('You must select at least %s'),
+                    __('one role'));
+        } else {
+            $count = count($_POST['ids']);
+            switch(strtolower($_POST['a'])) {
+            case 'enable':
+                $num = Role::objects()->filter(array(
+                    'id__in' => $_POST['ids']
+                ))->update(array(
+                    'flags'=> SqlExpression::bitor(
+                        new SqlField('flags'),
+                        Role::FLAG_ENABLED)
+                ));
+                if ($num) {
+                    if($num==$count)
+                        $msg = sprintf(__('Successfully enabled %s'),
+                            _N('selected role', 'selected roles', $count));
+                    else
+                        $warn = sprintf(__('%1$d of %2$d %3$s enabled'), $num, $count,
+                            _N('selected role', 'selected roles', $count));
+                } else {
+                    $errors['err'] = sprintf(__('Unable to enable %s'),
+                        _N('selected role', 'selected roles', $count));
+                }
+                break;
+            case 'disable':
+                $num = Role::objects()->filter(array(
+                    'id__in' => $_POST['ids']
+                ))->update(array(
+                    'flags'=> SqlExpression::bitand(
+                        new SqlField('flags'),
+                        (~Role::FLAG_ENABLED))
+                ));
+
+                if ($num) {
+                    if($num==$count)
+                        $msg = sprintf(__('Successfully disabled %s'),
+                            _N('selected role', 'selected roles', $count));
+                    else
+                        $warn = sprintf(__('%1$d of %2$d %3$s disabled'), $num, $count,
+                            _N('selected role', 'selected roles', $count));
+                } else {
+                    $errors['err'] = sprintf(__('Unable to disable %s'),
+                        _N('selected role', 'selected roles', $count));
+                }
+                break;
+            case 'delete':
+                $i=0;
+                foreach ($_POST['ids'] as $k=>$v) {
+                    if (($r=Role::lookup($v)) && $r->isDeleteable() && $r->delete())
+                        $i++;
+                }
+                if ($i && $i==$count)
+                    $msg = sprintf(__('Successfully deleted %s'),
+                            _N('selected role', 'selected roles', $count));
+                elseif ($i > 0)
+                    $warn = sprintf(__('%1$d of %2$d %3$s deleted'), $i, $count,
+                            _N('selected role', 'selected roles', $count));
+                elseif (!$errors['err'])
+                    $errors['err'] = sprintf(__('Unable to delete %s — they may be in use.'),
+                            _N('selected role', 'selected roles', $count));
+                break;
+            default:
+                $errors['err'] =  __('Unknown action');
+            }
+        }
+        break;
+    }
+}
+
+$page='roles.inc.php';
+if($role || ($_REQUEST['a'] && !strcasecmp($_REQUEST['a'], 'add'))) {
+    $page='role.inc.php';
+    $ost->addExtraHeader('<meta name="tip-namespace" content="agents.role" />',
+        "$('#content').data('tipNamespace', 'agents.role');");
+}
+
+$nav->setTabActive('staff');
+require(STAFFINC_DIR.'header.inc.php');
+require(STAFFINC_DIR.$page);
+include(STAFFINC_DIR.'footer.inc.php');
+?>
diff --git a/scp/settings.php b/scp/settings.php
index b824ef7cabbf0305fae1c56d4c199147ec59b473..4a52a6f0192f1ca969b753d13d09b3301d382b26 100644
--- a/scp/settings.php
+++ b/scp/settings.php
@@ -21,18 +21,16 @@ $settingOptions=array(
         array(__('System Settings'), 'settings.system'),
     'tickets' =>
         array(__('Ticket Settings and Options'), 'settings.ticket'),
-    'emails' =>
-        array(__('Email Settings'), 'settings.email'),
+    'tasks' =>
+        array(__('Task Settings and Options'), 'settings.tasks'),
+    'agents' =>
+        array(__('Agent Settings and Options'), 'settings.agents'),
+    'users' =>
+        array(__('User Settings and Options'), 'settings.users'),
     'pages' =>
         array(__('Site Pages'), 'settings.pages'),
-    'access' =>
-        array(__('Access Control'), 'settings.access'),
     'kb' =>
         array(__('Knowledgebase Settings'), 'settings.kb'),
-    'autoresp' =>
-        array(__('Autoresponder Settings'), 'settings.autoresponder'),
-    'alerts' =>
-        array(__('Alerts and Notices Settings'), 'settings.alerts'),
 );
 //Handle a POST.
 $target=($_REQUEST['t'] && $settingOptions[$_REQUEST['t']])?$_REQUEST['t']:'system';
diff --git a/scp/slas.php b/scp/slas.php
index b47c73092e21344b8b81dceba0f3f2036a1ad697..62ba66c9294424b7ee3c0d6be37d38c5121bd98b 100644
--- a/scp/slas.php
+++ b/scp/slas.php
@@ -36,11 +36,12 @@ if($_POST){
             }
             break;
         case 'add':
-            if(($id=SLA::create($_POST,$errors))){
+            $_sla = SLA::create();
+            if (($_sla->update($_POST, $errors))) {
                 $msg=sprintf(__('Successfully added %s'),
                     __('a SLA plan'));
                 $_REQUEST['a']=null;
-            }elseif(!$errors['err']){
+            } elseif (!$errors['err']) {
                 $errors['err']=sprintf(__('Unable to add %s. Correct error(s) below and try again.'),
                     __('this SLA plan'));
             }
@@ -53,10 +54,13 @@ if($_POST){
                 $count=count($_POST['ids']);
                 switch(strtolower($_POST['a'])) {
                     case 'enable':
-                        $sql='UPDATE '.SLA_TABLE.' SET isactive=1 '
-                            .' WHERE id IN ('.implode(',', db_input($_POST['ids'])).')';
-
-                        if(db_query($sql) && ($num=db_affected_rows())) {
+                        $num = SLA::objects()->filter(array(
+                            'id__in' => $_POST['ids']
+                        ))->update(array(
+                            'flags' => SqlExpression::bitor(
+                                new SqlField('flags'), SLA::FLAG_ACTIVE)
+                        ));
+                        if ($num) {
                             if($num==$count)
                                 $msg = sprintf(__('Successfully enabled %s'),
                                     _N('selected SLA plan', 'selected SLA plans', $count));
@@ -69,9 +73,14 @@ if($_POST){
                         }
                         break;
                     case 'disable':
-                        $sql='UPDATE '.SLA_TABLE.' SET isactive=0 '
-                            .' WHERE id IN ('.implode(',', db_input($_POST['ids'])).')';
-                        if(db_query($sql) && ($num=db_affected_rows())) {
+                        $num = SLA::objects()->filter(array(
+                            'id__in' => $_POST['ids']
+                        ))->update(array(
+                            'flags' => SqlExpression::bitand(
+                                new SqlField('flags'), ~SLA::FLAG_ACTIVE)
+                        ));
+
+                        if ($num) {
                             if($num==$count)
                                 $msg = sprintf(__('Successfully disabled %s'),
                                     _N('selected SLA plan', 'selected SLA plans', $count));
@@ -85,7 +94,7 @@ if($_POST){
                         break;
                     case 'delete':
                         $i=0;
-                        foreach($_POST['ids'] as $k=>$v) {
+                        foreach ($_POST['ids'] as $k => $v) {
                             if (($p=SLA::lookup($v))
                                 && $p->getId() != $cfg->getDefaultSLAId()
                                 && $p->delete())
diff --git a/scp/staff.inc.php b/scp/staff.inc.php
index d4399c74b8fa94f9e049c84bf4be855c1d44df66..e486cde88b72018c1464c3bc5b94240c482a4420 100644
--- a/scp/staff.inc.php
+++ b/scp/staff.inc.php
@@ -35,8 +35,6 @@ define('KB_PREMADE_TABLE',TABLE_PREFIX.'kb_premade');
 /* include what is needed on staff control panel */
 
 require_once(INCLUDE_DIR.'class.staff.php');
-require_once(INCLUDE_DIR.'class.group.php');
-require_once(INCLUDE_DIR.'class.nav.php');
 require_once(INCLUDE_DIR.'class.csrf.php');
 
 /* First order of the day is see if the user is logged in and with a valid session.
@@ -51,7 +49,9 @@ if(!function_exists('staffLoginPage')) { //Ajax interface can pre-declare the fu
         $_SESSION['_staff']['auth']['dest'] =
             '/' . ltrim($_SERVER['REQUEST_URI'], '/');
         $_SESSION['_staff']['auth']['msg']=$msg;
-        require(SCP_DIR.'login.php');
+
+        // Redirect here with full path for application-type plugins
+        Http::redirect(ROOT_PATH.'scp/login.php');
         exit;
     }
 }
@@ -78,7 +78,7 @@ if (!$thisstaff || !$thisstaff->getId() || !$thisstaff->isValid()) {
 //2) if not super admin..check system status and group status
 if(!$thisstaff->isAdmin()) {
     //Check for disabled staff or group!
-    if(!$thisstaff->isactive() || !$thisstaff->isGroupActive()) {
+    if (!$thisstaff->isactive()) {
         staffLoginPage(__('Access Denied. Contact Admin'));
         exit;
     }
@@ -103,12 +103,11 @@ if ($_POST  && !$ost->checkCSRFToken()) {
 //Add token to the header - used on ajax calls [DO NOT CHANGE THE NAME]
 $ost->addExtraHeader('<meta name="csrf_token" content="'.$ost->getCSRFToken().'" />');
 
-/******* SET STAFF DEFAULTS **********/
-//Set staff's timezone offset.
-$_SESSION['TZ_OFFSET']=$thisstaff->getTZoffset();
-$_SESSION['TZ_DST']=$thisstaff->observeDaylight();
+// Load the navigation after the user in case some things are hidden
+require_once(INCLUDE_DIR.'class.nav.php');
 
-define('PAGE_LIMIT', $thisstaff->getPageLimit()?$thisstaff->getPageLimit():DEFAULT_PAGE_LIMIT);
+/******* SET STAFF DEFAULTS **********/
+define('PAGE_LIMIT', $thisstaff->getPageLimit() ?: DEFAULT_PAGE_LIMIT);
 
 $tabs=array();
 $submenu=array();
diff --git a/scp/staff.php b/scp/staff.php
index 0ad72c2a1cc518be449f69902044dc860e712711..cac7f3fa4f4e44de7ad56e34382653d2c4f6a720 100644
--- a/scp/staff.php
+++ b/scp/staff.php
@@ -15,6 +15,9 @@
 **********************************************************************/
 require('admin.inc.php');
 
+// Included here for role permission registration
+require_once INCLUDE_DIR . 'class.report.php';
+
 $staff=null;
 if($_REQUEST['id'] && !($staff=Staff::lookup($_REQUEST['id'])))
     $errors['err']=sprintf(__('%s: Unknown or invalid ID.'), __('agent'));
@@ -33,7 +36,15 @@ if($_POST){
             }
             break;
         case 'create':
-            if(($id=Staff::create($_POST,$errors))){
+            $staff = Staff::create();
+            // Unpack the data from the set-password dialog (if used)
+            if (isset($_SESSION['new-agent-passwd'])) {
+                foreach ($_SESSION['new-agent-passwd'] as $k=>$v)
+                    if (!isset($_POST[$k]))
+                        $_POST[$k] = $v;
+            }
+            if ($staff->update($_POST,$errors)) {
+                unset($_SESSION['new-agent-passwd']);
                 $msg=sprintf(__('Successfully added %s'),Format::htmlchars($_POST['firstname']));
                 $_REQUEST['a']=null;
             }elseif(!$errors['err']){
@@ -45,16 +56,19 @@ if($_POST){
             if(!$_POST['ids'] || !is_array($_POST['ids']) || !count($_POST['ids'])) {
                 $errors['err'] = sprintf(__('You must select at least %s.'),
                     __('one agent'));
-            } elseif(in_array($thisstaff->getId(),$_POST['ids'])) {
+            } elseif(in_array($_POST['a'], array('disable', 'delete'))
+                && in_array($thisstaff->getId(),$_POST['ids'])
+            ) {
                 $errors['err'] = __('You can not disable/delete yourself - you could be the only admin!');
             } else {
-                $count=count($_POST['ids']);
+                $count = count($_POST['ids']);
+                $members = Staff::objects()->filter(array(
+                    'staff_id__in' => $_POST['ids']
+                ));
                 switch(strtolower($_POST['a'])) {
                     case 'enable':
-                        $sql='UPDATE '.STAFF_TABLE.' SET isactive=1 '
-                            .' WHERE staff_id IN ('.implode(',', db_input($_POST['ids'])).')';
-
-                        if(db_query($sql) && ($num=db_affected_rows())) {
+                        $num = $members->update(array('isactive' => 1));
+                        if ($num) {
                             if($num==$count)
                                 $msg = sprintf('Successfully activated %s',
                                     _N('selected agent', 'selected agents', $count));
@@ -66,11 +80,10 @@ if($_POST){
                                 _N('selected agent', 'selected agents', $count));
                         }
                         break;
-                    case 'disable':
-                        $sql='UPDATE '.STAFF_TABLE.' SET isactive=0 '
-                            .' WHERE staff_id IN ('.implode(',', db_input($_POST['ids'])).') AND staff_id!='.db_input($thisstaff->getId());
 
-                        if(db_query($sql) && ($num=db_affected_rows())) {
+                    case 'disable':
+                        $num = $members->update(array('isactive' => 0));
+                        if ($num) {
                             if($num==$count)
                                 $msg = sprintf('Successfully disabled %s',
                                     _N('selected agent', 'selected agents', $count));
@@ -82,9 +95,11 @@ if($_POST){
                                 _N('selected agent', 'selected agents', $count));
                         }
                         break;
+
                     case 'delete':
-                        foreach($_POST['ids'] as $k=>$v) {
-                            if($v!=$thisstaff->getId() && ($s=Staff::lookup($v)) && $s->delete())
+                        $i = 0;
+                        foreach($members as $s) {
+                            if ($s->staff_id != $thisstaff->getId() && $s->delete())
                                 $i++;
                         }
 
@@ -98,6 +113,48 @@ if($_POST){
                             $errors['err'] = sprintf(__('Unable to delete %s'),
                                 _N('selected agent', 'selected agents', $count));
                         break;
+
+                    case 'permissions':
+                        foreach ($members as $s)
+                            if ($s->updatePerms($_POST['perms'], $errors) && $s->save())
+                                $i++;
+
+                        if($i && $i==$count)
+                            $msg = sprintf(__('Successfully updated %s'),
+                                _N('selected agent', 'selected agents', $count));
+                        elseif($i>0)
+                            $warn = sprintf(__('%1$d of %2$d %3$s updated'), $i, $count,
+                                _N('selected agent', 'selected agents', $count));
+                        elseif(!$errors['err'])
+                            $errors['err'] = sprintf(__('Unable to update %s'),
+                                _N('selected agent', 'selected agents', $count));
+                        break;
+
+                    case 'department':
+                        if (!$_POST['dept_id'] || !$_POST['role_id']
+                            || !Dept::lookup($_POST['dept_id'])
+                            || !Role::lookup($_POST['role_id'])
+                        ) {
+                            $errors['err'] = 'Internal error.';
+                            break;
+                        }
+                        foreach ($members as $s) {
+                            $s->setDepartmentId((int) $_POST['dept_id'], $_POST['eavesdrop']);
+                            $s->role_id = (int) $_POST['role_id'];
+                            if ($s->save() && $s->dept_access->saveAll())
+                                $i++;
+                        }
+                        if($i && $i==$count)
+                            $msg = sprintf(__('Successfully updated %s'),
+                                _N('selected agent', 'selected agents', $count));
+                        elseif($i>0)
+                            $warn = sprintf(__('%1$d of %2$d %3$s updated'), $i, $count,
+                                _N('selected agent', 'selected agents', $count));
+                        elseif(!$errors['err'])
+                            $errors['err'] = sprintf(__('Unable to update %s'),
+                                _N('selected agent', 'selected agents', $count));
+                        break;
+
                     default:
                         $errors['err'] = __('Unknown action - get technical help.');
                 }
diff --git a/scp/tasks.php b/scp/tasks.php
new file mode 100644
index 0000000000000000000000000000000000000000..616a1720adff44c09e86b72c66d6864f4a3c7fb8
--- /dev/null
+++ b/scp/tasks.php
@@ -0,0 +1,254 @@
+<?php
+/*************************************************************************
+    tasks.php
+
+    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.task.php');
+require_once(INCLUDE_DIR.'class.export.php');
+
+$page = '';
+$task = null; //clean start.
+if ($_REQUEST['id']) {
+    if (!($task=Task::lookup($_REQUEST['id'])))
+         $errors['err'] = sprintf(__('%s: Unknown or invalid ID.'), __('task'));
+    elseif (!$task->checkStaffPerm($thisstaff)) {
+        $errors['err'] = __('Access denied. Contact admin if you believe this is in error');
+        $task = null;
+    }
+}
+
+// Configure form for file uploads
+$note_attachments_form = new SimpleForm(array(
+    'attachments' => new FileUploadField(array('id'=>'attach',
+        'name'=>'attach:note',
+        'configuration' => array('extensions'=>'')))
+));
+
+$reply_attachments_form = new SimpleForm(array(
+    'attachments' => new FileUploadField(array('id'=>'attach',
+        'name'=>'attach:reply',
+        'configuration' => array('extensions'=>'')))
+));
+
+//At this stage we know the access status. we can process the post.
+if($_POST && !$errors):
+
+    if ($task) {
+        //More coffee please.
+        $errors=array();
+        $role = $thisstaff->getRole($task->getDeptId());
+        switch(strtolower($_POST['a'])):
+        case 'postnote': /* Post Internal Note */
+            $vars = $_POST;
+            $attachments = $note_attachments_form->getField('attachments')->getClean();
+            $vars['cannedattachments'] = array_merge(
+                $vars['cannedattachments'] ?: array(), $attachments);
+
+            $wasOpen = ($task->isOpen());
+            if(($note=$task->postNote($vars, $errors, $thisstaff))) {
+
+                $msg=__('Internal note posted successfully');
+                // Clear attachment list
+                $note_attachments_form->setSource(array());
+                $note_attachments_form->getField('attachments')->reset();
+
+                if($wasOpen && $task->isClosed())
+                    $task = null; //Going back to main listing.
+                else
+                    // Task is still open -- clear draft for the note
+                    Draft::deleteForNamespace('task.note.'.$task->getId(),
+                        $thisstaff->getId());
+
+            } else {
+                if(!$errors['err'])
+                    $errors['err'] = __('Unable to post internal note - missing or invalid data.');
+
+                $errors['postnote'] = __('Unable to post the note. Correct the error(s) below and try again!');
+            }
+            break;
+        case 'postreply': /* Post an update */
+            $vars = $_POST;
+            $attachments = $reply_attachments_form->getField('attachments')->getClean();
+            $vars['cannedattachments'] = array_merge(
+                $vars['cannedattachments'] ?: array(), $attachments);
+
+            $wasOpen = ($task->isOpen());
+            if (($response=$task->postReply($vars, $errors))) {
+
+                $msg=__('Reply posted successfully');
+                // Clear attachment list
+                $reply_attachments_form->setSource(array());
+                $reply_attachments_form->getField('attachments')->reset();
+
+                if ($wasOpen && $task->isClosed())
+                    $task = null; //Going back to main listing.
+                else
+                    // Task is still open -- clear draft for the note
+                    Draft::deleteForNamespace('task.reply.'.$task->getId(),
+                        $thisstaff->getId());
+
+            } else {
+                if (!$errors['err'])
+                    $errors['err'] = __('Unable to post reply - missing or invalid data.');
+
+                $errors['postreply'] = __('Unable to post the reply. Correct the error(s) below and try again!');
+            }
+            break;
+        default:
+            $errors['err']=__('Unknown action');
+        endswitch;
+    }
+    if(!$errors)
+        $thisstaff->resetStats(); //We'll need to reflect any changes just made!
+endif;
+
+/*... Quick stats ...*/
+$stats= $thisstaff->getTasksStats();
+
+// Clear advanced search upon request
+if (isset($_GET['clear_filter']))
+    unset($_SESSION['advsearch:tasks']);
+
+
+if (!$task) {
+    $queue_key = sprintf('::Q:%s', ObjectModel::OBJECT_TYPE_TASK);
+    $queue_name = strtolower($_GET['status'] ?: $_GET['a']);
+    if (!$queue_name && isset($_SESSION[$queue_key]))
+        $queue_name = $_SESSION[$queue_key];
+
+    // Stash current queue view
+    $_SESSION[$queue_key] = $queue_name;
+
+    // Set queue as status
+    if (@!isset($_REQUEST['advanced'])
+            && @$_REQUEST['a'] != 'search'
+            && !isset($_GET['status'])
+            && $queue_name)
+        $_GET['status'] = $_REQUEST['status'] = $queue_name;
+}
+
+//Navigation
+$nav->setTabActive('tasks');
+$open_name = _P('queue-name',
+    /* This is the name of the open tasks queue */
+    'Open');
+
+$nav->addSubMenu(array('desc'=>$open_name.' ('.number_format($stats['open']).')',
+                       'title'=>__('Open Tasks'),
+                       'href'=>'tasks.php?status=open',
+                       'iconclass'=>'Ticket'),
+                    ((!$_REQUEST['status'] && !isset($_SESSION['advsearch:tasks'])) || $_REQUEST['status']=='open'));
+
+if ($stats['assigned']) {
+
+    $nav->addSubMenu(array('desc'=>__('My Tasks').' ('.number_format($stats['assigned']).')',
+                           'title'=>__('Assigned Tasks'),
+                           'href'=>'tasks.php?status=assigned',
+                           'iconclass'=>'assignedTickets'),
+                        ($_REQUEST['status']=='assigned'));
+}
+
+if ($stats['overdue']) {
+    $nav->addSubMenu(array('desc'=>__('Overdue').' ('.number_format($stats['overdue']).')',
+                           'title'=>__('Stale Tasks'),
+                           'href'=>'tasks.php?status=overdue',
+                           'iconclass'=>'overdueTickets'),
+                        ($_REQUEST['status']=='overdue'));
+
+    if(!$sysnotice && $stats['overdue']>10)
+        $sysnotice=sprintf(__('%d overdue tasks!'), $stats['overdue']);
+}
+
+if ($stats['closed']) {
+    $nav->addSubMenu(array('desc' => __('Completed').' ('.number_format($stats['closed']).')',
+                           'title'=>__('Completed Tasks'),
+                           'href'=>'tasks.php?status=closed',
+                           'iconclass'=>'closedTickets'),
+                        ($_REQUEST['status']=='closed'));
+}
+
+if (isset($_SESSION['advsearch:tasks'])) {
+    // XXX: De-duplicate and simplify this code
+    $search = SavedSearch::create();
+    $form = $search->getFormFromSession('advsearch:tasks');
+    $form->loadState($_SESSION['advsearch:tasks']);
+    $tasks = Task::objects();
+    $tasks = $search->mangleQuerySet($tasks, $form);
+    $count = $tasks->count();
+    $nav->addSubMenu(array('desc' => __('Search').' ('.number_format($count).')',
+                           'title'=>__('Advanced Task Search'),
+                           'href'=>'tasks.php?status=search',
+                           'iconclass'=>'Ticket'),
+                        (!$_REQUEST['status'] || $_REQUEST['status']=='search'));
+}
+
+if ($thisstaff->hasPerm(TaskModel::PERM_CREATE, false)) {
+    $nav->addSubMenu(array('desc'=>__('New Task'),
+                           'title'=> __('Open a New Task'),
+                           'href'=>'#tasks/add',
+                           'iconclass'=>'newTicket new-task',
+                           'id' => 'new-task',
+                           'attr' => array(
+                               'data-dialog-config' => '{"size":"large"}'
+                               )
+                           ),
+                        ($_REQUEST['a']=='open'));
+}
+
+
+$ost->addExtraHeader('<script type="text/javascript" src="js/ticket.js"></script>');
+$ost->addExtraHeader('<script type="text/javascript" src="js/thread.js"></script>');
+$ost->addExtraHeader('<meta name="tip-namespace" content="tasks.queue" />',
+    "$('#content').data('tipNamespace', 'tasks.queue');");
+
+if($task) {
+    $ost->setPageTitle(sprintf(__('Task #%s'),$task->getNumber()));
+    $nav->setActiveSubMenu(-1);
+    $inc = 'task-view.inc.php';
+    if ($_REQUEST['a']=='edit'
+            && $task->checkStaffPerm($thisstaff, TaskModel::PERM_EDIT)) {
+        $inc = 'task-edit.inc.php';
+        if (!$forms) $forms=DynamicFormEntry::forObject($task->getId(), 'A');
+        // Auto add new fields to the entries
+        foreach ($forms as $f) $f->addMissingFields();
+    } elseif($_REQUEST['a'] == 'print' && !$task->pdfExport($_REQUEST['psize']))
+        $errors['err'] = __('Internal error: Unable to export the task to PDF for print.');
+} else {
+	$inc = 'tasks.inc.php';
+    if ($_REQUEST['a']=='open' &&
+            $thisstaff->hasPerm(Task::PERM_CREATE, false))
+        $inc = 'task-open.inc.php';
+    elseif($_REQUEST['a'] == 'export') {
+        $ts = strftime('%Y%m%d');
+        if (!($query=$_SESSION[':Q:tasks']))
+            $errors['err'] = __('Query token not found');
+        elseif (!Export::saveTasks($query, "tasks-$ts.csv", 'csv'))
+            $errors['err'] = __('Internal error: Unable to dump query results');
+    }
+
+    //Clear active submenu on search with no status
+    if($_REQUEST['a']=='search' && !$_REQUEST['status'])
+        $nav->setActiveSubMenu(-1);
+
+    //set refresh rate if the user has it configured
+    if(!$_POST && !$_REQUEST['a'] && ($min=$thisstaff->getRefreshRate())) {
+        $js = "clearTimeout(window.task_refresh);
+               window.task_refresh = setTimeout($.refreshTaskView,"
+            .($min*60000).");";
+        $ost->addExtraHeader('<script type="text/javascript">'.$js.'</script>',
+            $js);
+    }
+}
+
+require_once(STAFFINC_DIR.'header.inc.php');
+require_once(STAFFINC_DIR.$inc);
+require_once(STAFFINC_DIR.'footer.inc.php');
diff --git a/scp/teams.php b/scp/teams.php
index 215e9fb8788154f34ed6947a05bbf051292e690a..670ef7cce5d33105d5d4a7cb2c0bc978f1489961 100644
--- a/scp/teams.php
+++ b/scp/teams.php
@@ -33,7 +33,8 @@ if($_POST){
             }
             break;
         case 'create':
-            if(($id=Team::create($_POST,$errors))){
+            $team = Team::create();
+            if (($team->update($_POST, $errors))){
                 $msg=sprintf(__('Successfully added %s'),Format::htmlchars($_POST['team']));
                 $_REQUEST['a']=null;
             }elseif(!$errors['err']){
@@ -48,10 +49,16 @@ if($_POST){
                 $count=count($_POST['ids']);
                 switch(strtolower($_POST['a'])) {
                     case 'enable':
-                        $sql='UPDATE '.TEAM_TABLE.' SET isenabled=1 '
-                            .' WHERE team_id IN ('.implode(',', db_input($_POST['ids'])).')';
+                        $num = Team::objects()->filter(array(
+                            'team_id__in' => $_POST['ids']
+                        ))->update(array(
+                            'flags' => SqlExpression::bitor(
+                                new SqlField('flags'),
+                                Team::FLAG_ENABLED
+                            )
+                        ));
 
-                        if(db_query($sql) && ($num=db_affected_rows())) {
+                        if ($num) {
                             if($num==$count)
                                 $msg = sprintf(__('Successfully activated %s'),
                                     _N('selected team', 'selected teams', $count));
@@ -64,10 +71,16 @@ if($_POST){
                         }
                         break;
                     case 'disable':
-                        $sql='UPDATE '.TEAM_TABLE.' SET isenabled=0 '
-                            .' WHERE team_id IN ('.implode(',', db_input($_POST['ids'])).')';
+                        $num = Team::objects()->filter(array(
+                            'team_id__in' => $_POST['ids']
+                        ))->update(array(
+                            'flags' => SqlExpression::bitand(
+                                new SqlField('flags'),
+                                ~Team::FLAG_ENABLED
+                            )
+                        ));
 
-                        if(db_query($sql) && ($num=db_affected_rows())) {
+                        if ($num) {
                             if($num==$count)
                                 $msg = sprintf(__('Successfully disabled %s'),
                                     _N('selected team', 'selected teams', $count));
diff --git a/scp/tickets.php b/scp/tickets.php
index 004dcc9ec97d86fb0d8337d4e2a27f5f625cb220..72a7c8a34486251f4d7605e8eec82d95662ec2a6 100644
--- a/scp/tickets.php
+++ b/scp/tickets.php
@@ -25,32 +25,46 @@ require_once(INCLUDE_DIR.'class.export.php');       // For paper sizes
 
 $page='';
 $ticket = $user = null; //clean start.
+$redirect = false;
 //LOCKDOWN...See if the id provided is actually valid and if the user has access.
-if($_REQUEST['id']) {
-    if(!($ticket=Ticket::lookup($_REQUEST['id'])))
+if($_REQUEST['id'] || $_REQUEST['number']) {
+    if($_REQUEST['id'] && !($ticket=Ticket::lookup($_REQUEST['id'])))
          $errors['err']=sprintf(__('%s: Unknown or invalid ID.'), __('ticket'));
-    elseif(!$ticket->checkStaffAccess($thisstaff)) {
+    elseif($_REQUEST['number'] && !($ticket=Ticket::lookup(['number' => $_REQUEST['number']])))
+         $errors['err']=sprintf(__('%s: Unknown or invalid number.'), __('ticket'));
+    elseif(!$ticket->checkStaffPerm($thisstaff)) {
         $errors['err']=__('Access denied. Contact admin if you believe this is in error');
         $ticket=null; //Clear ticket obj.
     }
 }
 
-//Lookup user if id is available.
 if ($_REQUEST['uid']) {
     $user = User::lookup($_REQUEST['uid']);
 }
-elseif (!isset($_REQUEST['advsid']) && @$_REQUEST['a'] != 'search'
-    && !isset($_REQUEST['status']) && isset($_SESSION['::Q'])
-) {
-    $_REQUEST['status'] = $_SESSION['::Q'];
+if (!$ticket) {
+    $queue_key = sprintf('::Q:%s', ObjectModel::OBJECT_TYPE_TICKET);
+    $queue_name = strtolower($_GET['a'] ?: $_GET['status']); //Status is overloaded
+    if (!$queue_name && isset($_SESSION[$queue_key]))
+        $queue_name = $_SESSION[$queue_key];
+
+    // Stash current queue view
+    $_SESSION[$queue_key] = $queue_name;
+
+    // Set queue as status
+    if (@!isset($_REQUEST['advanced'])
+            && @$_REQUEST['a'] != 'search'
+            && !isset($_GET['status'])
+            && $queue_name)
+        $_GET['status'] = $_REQUEST['status'] = $queue_name;
 }
+
 // Configure form for file uploads
-$response_form = new Form(array(
+$response_form = new SimpleForm(array(
     'attachments' => new FileUploadField(array('id'=>'attach',
         'name'=>'attach:response',
         'configuration' => array('extensions'=>'')))
 ));
-$note_form = new Form(array(
+$note_form = new SimpleForm(array(
     'attachments' => new FileUploadField(array('id'=>'attach',
         'name'=>'attach:note',
         'configuration' => array('extensions'=>'')))
@@ -62,23 +76,42 @@ if($_POST && !$errors):
     if($ticket && $ticket->getId()) {
         //More coffee please.
         $errors=array();
-        $lock=$ticket->getLock(); //Ticket lock if any
+        $lock = $ticket->getLock(); //Ticket lock if any
+        $role = $thisstaff->getRole($ticket->getDeptId());
         switch(strtolower($_POST['a'])):
         case 'reply':
-            if(!$thisstaff->canPostReply())
+            if (!$role || !$role->hasPerm(TicketModel::PERM_REPLY)) {
                 $errors['err'] = __('Action denied. Contact admin for access');
+            }
             else {
                 $vars = $_POST;
                 $vars['cannedattachments'] = $response_form->getField('attachments')->getClean();
+<<<<<<< HEAD
                 $vars['response'] = ThreadBody::clean($vars['response']);
+=======
+                $vars['response'] = ThreadEntryBody::clean($vars['response']);
+>>>>>>> upstream/develop-next
                 if(!$vars['response'])
                     $errors['response']=__('Response required');
-                //Use locks to avoid double replies
-                if($lock && $lock->getStaffId()!=$thisstaff->getId())
-                    $errors['err']=__('Action Denied. Ticket is locked by someone else!');
+
+                if ($cfg->getLockTime()) {
+                    if (!$lock) {
+                        $errors['err'] = __('This action requires a lock. Please try again');
+                    }
+                    // Use locks to avoid double replies
+                    elseif ($lock->getStaffId()!=$thisstaff->getId()) {
+                        $errors['err'] = __('Action Denied. Ticket is locked by someone else!');
+                    }
+                    // Attempt to renew the lock if possible
+                    elseif (($lock->isExpired() && !$lock->renew())
+                        ||($lock->getCode() != $_POST['lockCode'])
+                    ) {
+                        $errors['err'] = __('Your lock has expired. Please try again');
+                    }
+                }
 
                 //Make sure the email is not banned
-                if(!$errors['err'] && TicketFilter::isBanned($ticket->getEmail()))
+                if(!$errors['err'] && Banlist::isBanned($ticket->getEmail()))
                     $errors['err']=__('Email is in banlist. Must be removed to reply.');
             }
 
@@ -94,8 +127,7 @@ if($_POST && !$errors):
                 $response_form->getField('attachments')->reset();
 
                 // Remove staff's locks
-                TicketLock::removeStaffLocks($thisstaff->getId(),
-                        $ticket->getId());
+                $ticket->releaseLock($thisstaff->getId());
 
                 // Cleanup response draft for this user
                 Draft::deleteForNamespace(
@@ -104,101 +136,35 @@ if($_POST && !$errors):
 
                 // Go back to the ticket listing page on reply
                 $ticket = null;
+                $redirect = 'tickets.php';
 
             } elseif(!$errors['err']) {
                 $errors['err']=__('Unable to post the reply. Correct the errors below and try again!');
             }
             break;
-        case 'transfer': /** Transfer ticket **/
-            //Check permission
-            if(!$thisstaff->canTransferTickets())
-                $errors['err']=$errors['transfer'] = __('Action Denied. You are not allowed to transfer tickets.');
-            else {
-
-                //Check target dept.
-                if(!$_POST['deptId'])
-                    $errors['deptId'] = __('Select department');
-                elseif($_POST['deptId']==$ticket->getDeptId())
-                    $errors['deptId'] = __('Ticket already in the department');
-                elseif(!($dept=Dept::lookup($_POST['deptId'])))
-                    $errors['deptId'] = __('Unknown or invalid department');
-
-                //Transfer message - required.
-                if(!$_POST['transfer_comments'])
-                    $errors['transfer_comments'] = __('Transfer comments required');
-                elseif(strlen($_POST['transfer_comments'])<5)
-                    $errors['transfer_comments'] = __('Transfer comments too short!');
-
-                //If no errors - them attempt the transfer.
-                if(!$errors && $ticket->transfer($_POST['deptId'], $_POST['transfer_comments'])) {
-                    $msg = sprintf(__('Ticket transferred successfully to %s'),$ticket->getDeptName());
-                    //Check to make sure the staff still has access to the ticket
-                    if(!$ticket->checkStaffAccess($thisstaff))
-                        $ticket=null;
-
-                } elseif(!$errors['transfer']) {
-                    $errors['err'] = __('Unable to complete the ticket transfer');
-                    $errors['transfer']=__('Correct the error(s) below and try again!');
-                }
-            }
-            break;
-        case 'assign':
-
-             if(!$thisstaff->canAssignTickets())
-                 $errors['err']=$errors['assign'] = __('Action Denied. You are not allowed to assign/reassign tickets.');
-             else {
-
-                 $id = preg_replace("/[^0-9]/", "",$_POST['assignId']);
-                 $claim = (is_numeric($_POST['assignId']) && $_POST['assignId']==$thisstaff->getId());
-                 $dept = $ticket->getDept();
-
-                 if (!$_POST['assignId'] || !$id)
-                     $errors['assignId'] = __('Select assignee');
-                 elseif ($_POST['assignId'][0]!='s' && $_POST['assignId'][0]!='t' && !$claim)
-                     $errors['assignId']= sprintf('%s - %s',
-                             __('Invalid assignee'),
-                             __('get technical support'));
-                 elseif ($_POST['assignId'][0]=='s'
-                         && $dept->assignMembersOnly()
-                         && !$dept->isMember($id)) {
-                     $errors['assignId'] = sprintf('%s. %s',
-                             __('Invalid assignee'),
-                             __('Must be department member'));
-                 } elseif($ticket->isAssigned()) {
-                     if($_POST['assignId'][0]=='s' && $id==$ticket->getStaffId())
-                         $errors['assignId']=__('Ticket already assigned to the agent.');
-                     elseif($_POST['assignId'][0]=='t' && $id==$ticket->getTeamId())
-                         $errors['assignId']=__('Ticket already assigned to the team.');
-                 }
-
-                 //Comments are not required on self-assignment (claim)
-                 if($claim && !$_POST['assign_comments'])
-                     $_POST['assign_comments'] = sprintf(__('Ticket claimed by %s'),$thisstaff->getName());
-                 elseif(!$_POST['assign_comments'])
-                     $errors['assign_comments'] = __('Assignment comments required');
-                 elseif(strlen($_POST['assign_comments'])<5)
-                         $errors['assign_comments'] = __('Comment too short');
-
-                 if(!$errors && $ticket->assign($_POST['assignId'], $_POST['assign_comments'], !$claim)) {
-                     if($claim) {
-                         $msg = __('Ticket is NOW assigned to you!');
-                     } else {
-                         $msg=sprintf(__('Ticket assigned successfully to %s'), $ticket->getAssigned());
-                         TicketLock::removeStaffLocks($thisstaff->getId(), $ticket->getId());
-                         $ticket=null;
-                     }
-                 } elseif(!$errors['assign']) {
-                     $errors['err'] = __('Unable to complete the ticket assignment');
-                     $errors['assign'] = __('Correct the error(s) below and try again!');
-                 }
-             }
-            break;
         case 'postnote': /* Post Internal Note */
             $vars = $_POST;
             $attachments = $note_form->getField('attachments')->getClean();
             $vars['cannedattachments'] = array_merge(
                 $vars['cannedattachments'] ?: array(), $attachments);
+<<<<<<< HEAD
             $vars['note'] = ThreadBody::clean($vars['note']);
+=======
+            $vars['note'] = ThreadEntryBody::clean($vars['note']);
+
+            if ($cfg->getLockTime()) {
+                if (!$lock) {
+                    $errors['err'] = __('This action requires a lock. Please try again');
+                }
+                // Use locks to avoid double replies
+                elseif ($lock->getStaffId()!=$thisstaff->getId()) {
+                    $errors['err'] = __('Action Denied. Ticket is locked by someone else!');
+                }
+                elseif ($lock->getCode() != $_POST['lockCode']) {
+                    $errors['err'] = __('Your lock has expired. Please try again');
+                }
+            }
+>>>>>>> upstream/develop-next
 
             $wasOpen = ($ticket->isOpen());
             if(($note=$ticket->postNote($vars, $errors, $thisstaff))) {
@@ -208,6 +174,9 @@ if($_POST && !$errors):
                 $note_form->setSource(array());
                 $note_form->getField('attachments')->reset();
 
+                // Remove staff's locks
+                $ticket->releaseLock($thisstaff->getId());
+
                 if($wasOpen && $ticket->isClosed())
                     $ticket = null; //Going back to main listing.
                 else
@@ -215,6 +184,7 @@ if($_POST && !$errors):
                     Draft::deleteForNamespace('ticket.note.'.$ticket->getId(),
                         $thisstaff->getId());
 
+                 $redirect = 'tickets.php';
             } else {
 
                 if(!$errors['err'])
@@ -225,13 +195,14 @@ if($_POST && !$errors):
             break;
         case 'edit':
         case 'update':
-            if(!$ticket || !$thisstaff->canEditTickets())
+            if(!$ticket || !$role->hasPerm(TicketModel::PERM_EDIT))
                 $errors['err']=__('Permission Denied. You are not allowed to edit tickets');
             elseif($ticket->update($_POST,$errors)) {
                 $msg=__('Ticket updated successfully');
+                $redirect = 'tickets.php?id='.$ticket->getId();
                 $_REQUEST['a'] = null; //Clear edit action - going back to view.
                 //Check to make sure the staff STILL has access post-update (e.g dept change).
-                if(!$ticket->checkStaffAccess($thisstaff))
+                if(!$ticket->checkStaffPerm($thisstaff))
                     $ticket=null;
             } elseif(!$errors['err']) {
                 $errors['err']=__('Unable to update the ticket. Correct the errors below and try again!');
@@ -253,7 +224,7 @@ if($_POST && !$errors):
                     }
                     break;
                 case 'claim':
-                    if(!$thisstaff->canAssignTickets()) {
+                    if(!$role->hasPerm(TicketModel::PERM_EDIT)) {
                         $errors['err'] = __('Permission Denied. You are not allowed to assign/claim tickets.');
                     } elseif(!$ticket->isOpen()) {
                         $errors['err'] = __('Only open tickets can be assigned');
@@ -299,7 +270,7 @@ if($_POST && !$errors):
                     }
                     break;
                 case 'banemail':
-                    if(!$thisstaff->canBanEmails()) {
+                    if (!$thisstaff->hasPerm(Email::PERM_BANLIST)) {
                         $errors['err']=__('Permission Denied. You are not allowed to ban emails');
                     } elseif(BanList::includes($ticket->getEmail())) {
                         $errors['err']=__('Email already in banlist');
@@ -310,7 +281,7 @@ if($_POST && !$errors):
                     }
                     break;
                 case 'unbanemail':
-                    if(!$thisstaff->canBanEmails()) {
+                    if (!$thisstaff->hasPerm(Email::PERM_BANLIST)) {
                         $errors['err'] = __('Permission Denied. You are not allowed to remove emails from banlist.');
                     } elseif(Banlist::remove($ticket->getEmail())) {
                         $msg = __('Email removed from banlist');
@@ -321,7 +292,7 @@ if($_POST && !$errors):
                     }
                     break;
                 case 'changeuser':
-                    if (!$thisstaff->canEditTickets()) {
+                    if (!$role->hasPerm(TicketModel::PERM_EDIT)) {
                         $errors['err']=__('Permission Denied. You are not allowed to edit tickets');
                     } elseif (!$_POST['user_id'] || !($user=User::lookup($_POST['user_id']))) {
                         $errors['err'] = __('Unknown user selected');
@@ -339,14 +310,13 @@ if($_POST && !$errors):
         default:
             $errors['err']=__('Unknown action');
         endswitch;
-        if($ticket && is_object($ticket))
-            $ticket->reload();//Reload ticket info following post processing
     }elseif($_POST['a']) {
 
         switch($_POST['a']) {
             case 'open':
                 $ticket=null;
-                if(!$thisstaff || !$thisstaff->canCreateTickets()) {
+                if (!$thisstaff ||
+                        !$thisstaff->hasPerm(TicketModel::PERM_CREATE, false)) {
                      $errors['err'] = sprintf('%s %s',
                              sprintf(__('You do not have permission %s.'),
                                  __('to create tickets')),
@@ -360,7 +330,7 @@ if($_POST && !$errors):
                     if(($ticket=Ticket::open($vars, $errors))) {
                         $msg=__('Ticket created successfully');
                         $_REQUEST['a']=null;
-                        if (!$ticket->checkStaffAccess($thisstaff) || $ticket->isClosed())
+                        if (!$ticket->checkStaffPerm($thisstaff) || $ticket->isClosed())
                             $ticket=null;
                         Draft::deleteForNamespace('ticket.staff%', $thisstaff->getId());
                         // Drop files from the response attachments widget
@@ -378,9 +348,19 @@ if($_POST && !$errors):
         $thisstaff ->resetStats(); //We'll need to reflect any changes just made!
 endif;
 
+if ($redirect) {
+    if ($msg)
+        Messages::success($msg);
+    Http::redirect($redirect);
+}
+
 /*... Quick stats ...*/
 $stats= $thisstaff->getTicketsStats();
 
+// Clear advanced search upon request
+if (isset($_GET['clear_filter']))
+    unset($_SESSION['advsearch']);
+
 //Navigation
 $nav->setTabActive('tickets');
 $open_name = _P('queue-name',
@@ -391,7 +371,7 @@ if($cfg->showAnsweredTickets()) {
                             'title'=>__('Open Tickets'),
                             'href'=>'tickets.php?status=open',
                             'iconclass'=>'Ticket'),
-                        (!$_REQUEST['status'] || $_REQUEST['status']=='open'));
+                        ((!$_REQUEST['status'] && !isset($_SESSION['advsearch'])) || $_REQUEST['status']=='open'));
 } else {
 
     if ($stats) {
@@ -400,7 +380,7 @@ if($cfg->showAnsweredTickets()) {
                                'title'=>__('Open Tickets'),
                                'href'=>'tickets.php?status=open',
                                'iconclass'=>'Ticket'),
-                            (!$_REQUEST['status'] || $_REQUEST['status']=='open'));
+                            ((!$_REQUEST['status'] && !isset($_SESSION['advsearch'])) || $_REQUEST['status']=='open'));
     }
 
     if($stats['answered']) {
@@ -432,22 +412,28 @@ if($stats['overdue']) {
         $sysnotice=sprintf(__('%d overdue tickets!'),$stats['overdue']);
 }
 
-if($thisstaff->showAssignedOnly() && $stats['closed']) {
-    $nav->addSubMenu(array('desc'=>__('My Closed Tickets').' ('.number_format($stats['closed']).')',
-                           'title'=>__('My Closed Tickets'),
-                           'href'=>'tickets.php?status=closed',
-                           'iconclass'=>'closedTickets'),
-                        ($_REQUEST['status']=='closed'));
-} else {
-
-    $nav->addSubMenu(array('desc' => __('Closed').' ('.number_format($stats['closed']).')',
-                           'title'=>__('Closed Tickets'),
-                           'href'=>'tickets.php?status=closed',
-                           'iconclass'=>'closedTickets'),
-                        ($_REQUEST['status']=='closed'));
+if (isset($_SESSION['advsearch'])) {
+    // XXX: De-duplicate and simplify this code
+    TicketForm::ensureDynamicDataView();
+    $search = SavedSearch::create();
+    $form = $search->getFormFromSession('advsearch');
+    $tickets = TicketModel::objects();
+    $tickets = $search->mangleQuerySet($tickets, $form);
+    $count = $tickets->count();
+    $nav->addSubMenu(array('desc' => __('Search').' ('.number_format($count).')',
+                           'title'=>__('Advanced Ticket Search'),
+                           'href'=>'tickets.php?status=search',
+                           'iconclass'=>'Ticket'),
+                        (!$_REQUEST['status'] || $_REQUEST['status']=='search'));
 }
 
-if($thisstaff->canCreateTickets()) {
+$nav->addSubMenu(array('desc' => __('Closed'),
+                       'title'=>__('Closed Tickets'),
+                       'href'=>'tickets.php?status=closed',
+                       'iconclass'=>'closedTickets'),
+                    ($_REQUEST['status']=='closed'));
+
+if ($thisstaff->hasPerm(TicketModel::PERM_CREATE, false)) {
     $nav->addSubMenu(array('desc'=>__('New Ticket'),
                            'title'=> __('Open a New Ticket'),
                            'href'=>'tickets.php?a=open',
@@ -458,30 +444,33 @@ if($thisstaff->canCreateTickets()) {
 
 
 $ost->addExtraHeader('<script type="text/javascript" src="js/ticket.js"></script>');
+$ost->addExtraHeader('<script type="text/javascript" src="js/thread.js"></script>');
 $ost->addExtraHeader('<meta name="tip-namespace" content="tickets.queue" />',
     "$('#content').data('tipNamespace', 'tickets.queue');");
 
-$inc = 'tickets.inc.php';
 if($ticket) {
     $ost->setPageTitle(sprintf(__('Ticket #%s'),$ticket->getNumber()));
     $nav->setActiveSubMenu(-1);
     $inc = 'ticket-view.inc.php';
-    if($_REQUEST['a']=='edit' && $thisstaff->canEditTickets()) {
+    if ($_REQUEST['a']=='edit'
+            && $ticket->checkStaffPerm($thisstaff, TicketModel::PERM_EDIT)) {
         $inc = 'ticket-edit.inc.php';
         if (!$forms) $forms=DynamicFormEntry::forTicket($ticket->getId());
         // Auto add new fields to the entries
-        foreach ($forms as $f) $f->addMissingFields();
+        foreach ($forms as $f) {
+            $f->filterFields(function($f) { return !$f->isStorable(); });
+            $f->addMissingFields();
+        }
     } elseif($_REQUEST['a'] == 'print' && !$ticket->pdfExport($_REQUEST['psize'], $_REQUEST['notes']))
         $errors['err'] = __('Internal error: Unable to export the ticket to PDF for print.');
 } else {
 	$inc = 'tickets.inc.php';
-    if($_REQUEST['a']=='open' && $thisstaff->canCreateTickets())
+    if ($_REQUEST['a']=='open' &&
+            $thisstaff->hasPerm(TicketModel::PERM_CREATE, false))
         $inc = 'ticket-open.inc.php';
     elseif($_REQUEST['a'] == 'export') {
         $ts = strftime('%Y%m%d');
-        if (!($token=$_REQUEST['h']))
-            $errors['err'] = __('Query token required');
-        elseif (!($query=$_SESSION['search_'.$token]))
+        if (!($query=$_SESSION[':Q:tickets']))
             $errors['err'] = __('Query token not found');
         elseif (!Export::saveTickets($query, "tickets-$ts.csv", 'csv'))
             $errors['err'] = __('Internal error: Unable to dump query results');
@@ -492,10 +481,8 @@ if($ticket) {
         $nav->setActiveSubMenu(-1);
 
     //set refresh rate if the user has it configured
-    if(!$_POST && !$_REQUEST['a'] && ($min=$thisstaff->getRefreshRate())) {
-        $js = "clearTimeout(window.ticket_refresh);
-               window.ticket_refresh = setTimeout($.refreshTicketView,"
-            .($min*60000).");";
+    if(!$_POST && !$_REQUEST['a'] && ($min=(int)$thisstaff->getRefreshRate())) {
+        $js = "+function(){ var qq = setInterval(function() { if ($.refreshTicketView === undefined) return; clearInterval(qq); $.refreshTicketView({$min}*60000); }, 200); }();";
         $ost->addExtraHeader('<script type="text/javascript">'.$js.'</script>',
             $js);
     }
diff --git a/scp/users.php b/scp/users.php
index 9d60ebcef071cbaf141b4fe8f5f84cf878710e88..f665050384c88854bd93b6ae310ef91f11eb9cbb 100644
--- a/scp/users.php
+++ b/scp/users.php
@@ -14,6 +14,9 @@
 **********************************************************************/
 require('staff.inc.php');
 
+if (!$thisstaff->hasPerm(User::PERM_DIRECTORY))
+    Http::redirect('index.php');
+
 require_once INCLUDE_DIR.'class.note.php';
 
 $user = null;
@@ -25,6 +28,8 @@ if ($_POST) {
         case 'update':
             if (!$user) {
                 $errors['err']=sprintf(__('%s: Unknown or invalid'), _N('end user', 'end users', 1));
+            } elseif (!$thisstaff->hasPerm(User::PERM_EDIT)) {
+                $errors['err'] = __('Action denied. Contact admin for access');
             } elseif(($acct = $user->getAccount())
                     && !$acct->update($_POST, $errors)) {
                  $errors['err']=__('Unable to update user account information');
@@ -88,9 +93,15 @@ if ($_POST) {
                     break;
 
                 case 'delete':
-                    foreach ($users as $U)
+                    foreach ($users as $U) {
+                        if (@$_POST['deletetickets']) {
+                            if (!$U->deleteAllTickets())
+                                // XXX: This message is very unclear
+                                $errors['err'] = __('You do not have permission to delete a user with tickets!');
+                        }
                         if ($U->delete())
                             $count++;
+                    }
                     break;
 
                 case 'reset':
@@ -148,18 +159,34 @@ if ($_POST) {
             $errors['err'] = __('Unknown action');
             break;
     }
-} elseif($_REQUEST['a'] == 'export') {
+} elseif(!$user && $_REQUEST['a'] == 'export') {
     require_once(INCLUDE_DIR.'class.export.php');
     $ts = strftime('%Y%m%d');
-    if (!($token=$_REQUEST['qh']))
-        $errors['err'] = __('Query token required');
-    elseif (!($query=$_SESSION['users_qs_'.$token]))
+    if (!($query=$_SESSION[':Q:users']))
         $errors['err'] = __('Query token not found');
     elseif (!Export::saveUsers($query, __("users")."-$ts.csv", 'csv'))
         $errors['err'] = __('Internal error: Unable to dump query results');
 }
 
-$page = $user? 'user-view.inc.php' : 'users.inc.php';
+$page = 'users.inc.php';
+if ($user ) {
+    $page = 'user-view.inc.php';
+    switch (strtolower($_REQUEST['t'])) {
+    case 'tickets':
+        if (isset($_SERVER['HTTP_X_PJAX'])) {
+            $page='templates/tickets.tmpl.php';
+            $pjax_container = @$_SERVER['HTTP_X_PJAX_CONTAINER'];
+            require(STAFFINC_DIR.$page);
+            return;
+        } elseif ($_REQUEST['a'] == 'export' && ($query=$_SESSION[':U:tickets'])) {
+            $filename = sprintf('%s-tickets-%s.csv',
+                    $user->getName(), strftime('%Y%m%d'));
+            if (!Export::saveTickets($query, $filename, 'csv'))
+                $errors['err'] = __('Internal error: Unable to dump query results');
+        }
+        break;
+    }
+}
 
 $nav->setTabActive('users');
 require(STAFFINC_DIR.'header.inc.php');
diff --git a/setup/cli/manage.php b/setup/cli/manage.php
index 83d715ac12f87de14aee5aa66bc80140c9bb44f6..d2ae92b5a22eb59a16700e5dc5fac1692d9b200a 100755
--- a/setup/cli/manage.php
+++ b/setup/cli/manage.php
@@ -1,66 +1,3 @@
-#!/usr/bin/env php
 <?php
 
-require_once "modules/class.module.php";
-
-if (!function_exists('noop')) { function noop() {} }
-session_set_save_handler('noop','noop','noop','noop','noop','noop');
-
-class Manager extends Module {
-    var $prologue =
-        "Manage one or more osTicket installations";
-
-    var $arguments = array(
-        'action' => "Action to be managed"
-    );
-
-    var $usage = '$script action [options] [arguments]';
-
-    var $autohelp = false;
-
-    function showHelp() {
-        foreach (glob(dirname(__file__).'/modules/*.php') as $script)
-            include_once $script;
-
-        global $registered_modules;
-        $this->epilog =
-            "Currently available modules follow. Use 'manage.php <module>
-            --help' for usage regarding each respective module:";
-
-        parent::showHelp();
-
-        echo "\n";
-        foreach ($registered_modules as $name=>$mod)
-            echo str_pad($name, 20) . $mod->prologue . "\n";
-    }
-
-    function run($args, $options) {
-        if ($options['help'] && !$args['action'])
-            $this->showHelp();
-
-        else {
-            $action = $args['action'];
-
-            global $argv;
-            foreach ($argv as $idx=>$val)
-                if ($val == $action)
-                    unset($argv[$idx]);
-
-            require_once dirname(__file__)."/modules/{$args['action']}.php";
-            if (($module = Module::getInstance($action)))
-                return $module->_run($args['action']);
-
-            $this->stderr->write("Unknown action given\n");
-            $this->showHelp();
-        }
-    }
-}
-
-if (php_sapi_name() != "cli")
-    die("Management only supported from command-line\n");
-
-$manager = new Manager();
-$manager->parseOptions();
-$manager->_run(basename(__file__));
-
-?>
+include dirname(__file__) . '/../../manage.php';
diff --git a/setup/cli/modules/user.php b/setup/cli/modules/user.php
deleted file mode 100644
index 2d6d146a9950a95fda1bd4ad07c4b7a836148368..0000000000000000000000000000000000000000
--- a/setup/cli/modules/user.php
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-require_once dirname(__file__) . "/class.module.php";
-require_once dirname(__file__) . "/../cli.inc.php";
-
-class UserManager extends Module {
-    var $prologue = 'CLI user manager';
-
-    var $arguments = array(
-        'action' => array(
-            'help' => 'Action to be performed',
-            'options' => array(
-                'import' => 'Import users to the system',
-                'export' => 'Export users from the system',
-            ),
-        ),
-    );
-
-
-    var $options = array(
-        'file' => array('-f', '--file', 'metavar'=>'path',
-            'help' => 'File or stream to process'),
-        'org' => array('-O', '--org', 'metavar'=>'ORGID',
-            'help' => 'Set the organization ID on import'),
-        );
-
-    var $stream;
-
-    function run($args, $options) {
-
-        Bootstrap::connect();
-
-        switch ($args['action']) {
-            case 'import':
-                // Properly detect Macintosh style line endings
-                ini_set('auto_detect_line_endings', true);
-
-                if (!$options['file'])
-                    $this->fail('CSV file to import users from is required!');
-                elseif (!($this->stream = fopen($options['file'], 'rb')))
-                    $this->fail("Unable to open input file [{$options['file']}]");
-
-                $extras = array();
-                if ($options['org']) {
-                    if (!($org = Organization::lookup($options['org'])))
-                        $this->fail($options['org'].': Unknown organization ID');
-                    $extras['org_id'] = $options['org'];
-                }
-                $status = User::importCsv($this->stream, $extras);
-                if (is_numeric($status))
-                    $this->stderr->write("Successfully imported $status clients\n");
-                else
-                    $this->fail($status);
-                break;
-
-            case 'export':
-                $stream = $options['file'] ?: 'php://stdout';
-                if (!($this->stream = fopen($stream, 'c')))
-                    $this->fail("Unable to open output file [{$options['file']}]");
-
-                fputcsv($this->stream, array('Name', 'Email'));
-                foreach (User::objects() as $user)
-                    fputcsv($this->stream,
-                            array((string) $user->getName(), $user->getEmail()));
-                break;
-            default:
-                $this->stderr->write('Unknown action!');
-        }
-        @fclose($this->stream);
-    }
-}
-Module::register('user', 'UserManager');
-?>
diff --git a/setup/css/wizard.css b/setup/css/wizard.css
index a742c75cbbbf0cc2c91b72ffbabf6a99abef16a0..7897e047441001f20c58ba5bf8ca300b972e0532 100644
--- a/setup/css/wizard.css
+++ b/setup/css/wizard.css
@@ -11,8 +11,8 @@ a { color: #2a67ac; display: inline-block; }
 .hidden { display: none;}
 .error { color:#f00;}
 
-#header { height: 72px; margin-bottom: 20px; width: 100%; }
-#header #logo { width: 280px; height: 72px; display: block; float: left; }
+#header { min-height: 72px; margin-bottom: 20px; width: 100%; }
+#header #logo { width: auto; height: 72px; margin-right: 3em; float: left; }
 #header .info { font-size: 11pt; font-weight: bold; border-bottom: 1px solid #2a67ac; color: #444; text-align: right; float: right; }
 #header ul { margin: 0; padding: 0; text-align: right; }
 #header ul li { list-style: none; margin: 5px 0 0 0; padding: 0; }
@@ -194,7 +194,7 @@ a.flag { text-decoration: none; }
 .rtl ul.progress li { padding-left: 0; padding-right: 24px; }
 .rtl form h4.head { padding-right: 24px; background-position: right center; background-position: calc(100% - 5px); }
 .rtl form .subhead { padding-right: 24px; }
-.rtl #header #logo { float: right; }
+.rtl #header #logo { float: right; margin: 0; margin-left: 3em; }
 .rtl #header .info, .rtl #header ul, .rtl div.flags { text-align:left; }
 .rtl #header .info { float: left; }
 .rtl .ltr { text-align: right; }
diff --git a/setup/doc/forms.md b/setup/doc/forms.md
index c3545096c5e6daaee3ff394eb4b490f9a5a84daa..f1520cd3712cb8093fbb83787027bc15db3b051c 100644
--- a/setup/doc/forms.md
+++ b/setup/doc/forms.md
@@ -16,7 +16,7 @@ constructor.
 The simplest way to create forms is to instanciate the Form instance
 directly:
 
-    $form = new Form(array(
+    $form = new SimpleForm(array(
         'email' => new TextboxField(array('label'=>'Email Address')),
     );
 
@@ -34,7 +34,7 @@ the cleaned values from the form fields based on the data from the request.
 To create a class that defines the fields statically, one might write a
 trampoline constructor:
 
-    class UserForm extends Form {
+    class UserForm extends SimpleForm {
         function __construct() {
             $args = func_get_args();
             $fields = array(
diff --git a/setup/doc/i18n.md b/setup/doc/i18n.md
index 92409a0ded3d22ccd9c03050bb2d72e71c373a53..bc995667d8442b2ac8769ebf4bfb86ff9d1e204a 100644
--- a/setup/doc/i18n.md
+++ b/setup/doc/i18n.md
@@ -110,7 +110,7 @@ comments can be used. For instance:
 
 Use the command line to compile the POT file to standard out
 
-    php setup/cli/manage.php i18n make-pot > message.pot
+    php manage.php i18n make-pot > message.pot
 
 ### Building language packs
 
diff --git a/setup/doc/package.md b/setup/doc/package.md
index 5570bb60931b910e3281e29ee5d326c938f6eae7..c9c3f0eca350dfde0540f892ce6bea66d2d0d941 100644
--- a/setup/doc/package.md
+++ b/setup/doc/package.md
@@ -11,11 +11,11 @@ being added to the distribution.
 
 More information is available via the automated help output.
 
-    php setup/cli/manage.php package --help
+    php manage.php package --help
 
 Creating the ZIP file
 ---------------------
 To package the system using the defaults (as a ZIP file), just invoke the
 packager with no other options.
 
-    php setup/cli/manage.php package
+    php manage.php package
diff --git a/setup/images/logo-upgrade.png b/setup/images/logo-upgrade.png
deleted file mode 100644
index f8430d2488076b97f4ebd42d9e80d840e8f4773f..0000000000000000000000000000000000000000
Binary files a/setup/images/logo-upgrade.png and /dev/null differ
diff --git a/setup/images/logo.png b/setup/images/logo.png
index d34958ca04f08e78e5b67dfcce4c210183cb3255..1766ffa8862b2336a594046236001957734d39d9 100644
Binary files a/setup/images/logo.png and b/setup/images/logo.png differ
diff --git a/setup/inc/class.installer.php b/setup/inc/class.installer.php
index 0c7ea1f5b2a7029062e40969e3312cccd4a8be08..85441fedd2566d87dd384814d472695a2d98bdb1 100644
--- a/setup/inc/class.installer.php
+++ b/setup/inc/class.installer.php
@@ -21,7 +21,7 @@ class Installer extends SetupWizard {
 
     var $config;
 
-    function Installer($configfile) {
+    function __construct($configfile) {
         $this->config =$configfile;
         $this->errors=array();
     }
@@ -53,8 +53,8 @@ class Installer extends SetupWizard {
         $f['lname']         = array('type'=>'string',   'required'=>1, 'error'=>__('Last name required'));
         $f['admin_email']   = array('type'=>'email',    'required'=>1, 'error'=>__('Valid email required'));
         $f['username']      = array('type'=>'username', 'required'=>1, 'error'=>__('Username required'));
-        $f['passwd']        = array('type'=>'password', 'required'=>1, 'error'=>__('Password required'));
-        $f['passwd2']       = array('type'=>'password', 'required'=>1, 'error'=>__('Confirm Password'));
+        $f['passwd']        = array('type'=>'string', 'required'=>1, 'error'=>__('Password required'));
+        $f['passwd2']       = array('type'=>'string', 'required'=>1, 'error'=>__('Confirm Password'));
         $f['prefix']        = array('type'=>'string',   'required'=>1, 'error'=>__('Table prefix required'));
         $f['dbhost']        = array('type'=>'string',   'required'=>1, 'error'=>__('Host name required'));
         $f['dbname']        = array('type'=>'string',   'required'=>1, 'error'=>__('Database name required'));
@@ -73,6 +73,7 @@ class Installer extends SetupWizard {
         //Admin's pass confirmation.
         if(!$this->errors && strcasecmp($vars['passwd'],$vars['passwd2']))
             $this->errors['passwd2']=__('Password(s) do not match');
+
         //Check table prefix underscore required at the end!
         if($vars['prefix'] && substr($vars['prefix'], -1)!='_')
             $this->errors['prefix']=__('Bad prefix. Must have underscore (_) at the end. e.g \'ost_\'');
@@ -110,15 +111,25 @@ class Installer extends SetupWizard {
             }
         }
 
-        //bailout on errors.
-        if($this->errors) return false;
-
         /*************** We're ready to install ************************/
         define('ADMIN_EMAIL',$vars['admin_email']); //Needed to report SQL errors during install.
         define('TABLE_PREFIX',$vars['prefix']); //Table prefix
         Bootstrap::defineTables(TABLE_PREFIX);
         Bootstrap::loadCode();
 
+        // Check password against password policy (after loading code)
+        try {
+            PasswordPolicy::checkPassword($vars['passwd'], null);
+        }
+        catch (BadPassword $e) {
+            $this->errors['passwd'] = $e->getMessage();
+        }
+
+        // bailout on errors.
+        if ($this->errors)
+            return false;
+
+
         $debug = true; // Change it to false to squelch SQL errors.
 
         //Last minute checks.
@@ -151,108 +162,125 @@ class Installer extends SetupWizard {
             }
         }
 
-        if(!$this->errors) {
-
-            // TODO: Use language selected from install worksheet
-            $i18n = new Internationalization($vars['lang_id']);
-            $i18n->loadDefaultData();
-
-            Signal::send('system.install', $this);
-
-            $sql='SELECT `id` FROM '.TABLE_PREFIX.'sla ORDER BY `id` LIMIT 1';
-            $sla_id_1 = db_result(db_query($sql, false));
-
-            $sql='SELECT `dept_id` FROM '.TABLE_PREFIX.'department ORDER BY `dept_id` LIMIT 1';
-            $dept_id_1 = db_result(db_query($sql, false));
-
-            $sql='SELECT `tpl_id` FROM '.TABLE_PREFIX.'email_template_group ORDER BY `tpl_id` LIMIT 1';
-            $template_id_1 = db_result(db_query($sql, false));
-
-            $sql='SELECT `group_id` FROM '.TABLE_PREFIX.'groups ORDER BY `group_id` LIMIT 1';
-            $group_id_1 = db_result(db_query($sql, false));
-
-            $sql='SELECT `value` FROM '.TABLE_PREFIX.'config WHERE namespace=\'core\' and `key`=\'default_timezone_id\' LIMIT 1';
-            $default_timezone = db_result(db_query($sql, false));
+        if ($this->errors)
+            return false;
 
-            //Create admin user.
-            $sql='INSERT INTO '.TABLE_PREFIX.'staff SET created=NOW() '
-                .", isactive=1, isadmin=1, group_id='$group_id_1', dept_id='$dept_id_1'"
-                .", timezone_id='$default_timezone', max_page_size=25"
-                .', email='.db_input($vars['admin_email'])
-                .', firstname='.db_input($vars['fname'])
-                .', lastname='.db_input($vars['lname'])
-                .', username='.db_input($vars['username'])
-                .', passwd='.db_input(Passwd::hash($vars['passwd']));
-            if(!db_query($sql, false) || !($uid=db_insert_id()))
-                $this->errors['err']=__('Unable to create admin user (#6)');
+        // TODO: Use language selected from install worksheet
+        $i18n = new Internationalization($vars['lang_id']);
+        $i18n->loadDefaultData();
+
+        Signal::send('system.install', $this);
+
+        list($sla_id) = Sla::objects()->order_by('id')->values_flat('id')->first();
+        list($dept_id) = Dept::objects()->order_by('id')->values_flat('id')->first();
+        list($role_id) = Role::objects()->order_by('id')->values_flat('id')->first();
+
+        $sql='SELECT `tpl_id` FROM `'.TABLE_PREFIX.'email_template_group` ORDER BY `tpl_id` LIMIT 1';
+        $template_id_1 = db_result(db_query($sql, false));
+
+        // Create admin user.
+        $staff = Staff::create(array(
+            'isactive' => 1,
+            'isadmin' => 1,
+            'max_page_size' => 25,
+            'dept_id' => $dept_id,
+            'role_id' => $role_id,
+            'email' => $vars['admin_email'],
+            'firstname' => $vars['fname'],
+            'lastname' => $vars['lname'],
+            'username' => $vars['username'],
+        ));
+        $staff->updatePerms(array(
+            User::PERM_CREATE,
+            User::PERM_EDIT,
+            User::PERM_DELETE,
+            User::PERM_MANAGE,
+            User::PERM_DIRECTORY,
+            Organization::PERM_CREATE,
+            Organization::PERM_EDIT,
+            Organization::PERM_DELETE,
+            FAQ::PERM_MANAGE,
+            Email::PERM_BANLIST,
+        ));
+        $staff->setPassword($vars['passwd']);
+        if (!$staff->save()) {
+            $this->errors['err'] = __('Unable to create admin user (#6)');
+            return false;
         }
 
-        if(!$this->errors) {
-            //Create default emails!
-            $email = $vars['email'];
-            list(,$domain)=explode('@',$vars['email']);
-            $sql='INSERT INTO '.TABLE_PREFIX.'email (`name`,`email`,`created`,`updated`) VALUES '
-                    ." ('Support','$email',NOW(),NOW())"
-                    .",('osTicket Alerts','alerts@$domain',NOW(),NOW())"
-                    .",('','noreply@$domain',NOW(),NOW())";
-            $support_email_id = db_query($sql, false) ? db_insert_id() : 0;
-
-
-            $sql='SELECT `email_id` FROM '.TABLE_PREFIX."email WHERE `email`='alerts@$domain' LIMIT 1";
-            $alert_email_id = db_result(db_query($sql, false));
-
-            //Create config settings---default settings!
-            $defaults = array(
-                'default_email_id'=>$support_email_id,
-                'alert_email_id'=>$alert_email_id,
-                'default_dept_id'=>$dept_id_1, 'default_sla_id'=>$sla_id_1,
-                'default_template_id'=>$template_id_1,
-                'admin_email'=>$vars['admin_email'],
-                'schema_signature'=>$streams['core'],
-                'helpdesk_url'=>URL,
-                'helpdesk_title'=>$vars['name']);
-            $config = new Config('core');
-            if (!$config->updateAll($defaults))
-                $this->errors['err']=__('Unable to create config settings').' (#7)';
-
-            // Set company name
-            require_once(INCLUDE_DIR.'class.company.php');
-            $company = new Company();
-            $company->getForm()->setAnswer('name', $vars['name']);
-            $company->getForm()->save();
-
-			foreach ($streams as $stream=>$signature) {
-				if ($stream != 'core') {
-                    $config = new Config($stream);
-                    if (!$config->update('schema_signature', $signature))
-                        $this->errors['err']=__('Unable to create config settings').' (#8)';
-				}
-			}
+        // Create default emails!
+        $email = $vars['email'];
+        list(,$domain) = explode('@', $vars['email']);
+        foreach (array(
+            "Support" => $email,
+            "osTicket Alerts" => "alerts@$domain",
+            '' => "noreply@$domain",
+        ) as $name => $mailbox) {
+            $mb = Email::create(array(
+                'name' => $name,
+                'email' => $mailbox,
+                'dept_id' => $dept_id,
+            ));
+            $mb->save();
+            if ($mailbox == $email)
+                $support_email_id = $mb->email_id;
+            if ($mailbox == "alerts@$domain")
+                $alert_email_id = $mb->email_id;
         }
 
-        if($this->errors) return false; //Abort on internal errors.
+        //Create config settings---default settings!
+        $defaults = array(
+            'default_email_id'=>$support_email_id,
+            'alert_email_id'=>$alert_email_id,
+            'default_dept_id'=>$dept_id, 'default_sla_id'=>$sla_id,
+            'default_template_id'=>$template_id_1,
+            'default_timezone' => $vars['timezone'] ?: date_default_timezone_get(),
+            'admin_email'=>$vars['admin_email'],
+            'schema_signature'=>$streams['core'],
+            'helpdesk_url'=>URL,
+            'helpdesk_title'=>$vars['name']
+        );
+
+        $config = new Config('core');
+        if (!$config->updateAll($defaults))
+            $this->errors['err']=__('Unable to create config settings').' (#7)';
+
+        // Set company name
+        require_once(INCLUDE_DIR.'class.company.php');
+        $company = new Company();
+        $company->getForm()->setAnswer('name', $vars['name']);
+        $company->getForm()->save();
+
+        foreach ($streams as $stream => $signature) {
+            if ($stream != 'core') {
+                $config = new Config($stream);
+                if (!$config->update('schema_signature', $signature))
+                    $this->errors['err'] = __('Unable to create config settings').' (#8)';
+				    }
+			  }
+
+        if ($this->errors)
+            return false; //Abort on internal errors.
 
 
         //Rewrite the config file - MUST be done last to allow for installer recovery.
-        $configFile= str_replace("define('OSTINSTALLED',FALSE);","define('OSTINSTALLED',TRUE);",$configFile);
-        $configFile= str_replace('%ADMIN-EMAIL',$vars['admin_email'],$configFile);
-        $configFile= str_replace('%CONFIG-DBHOST',$vars['dbhost'],$configFile);
-        $configFile= str_replace('%CONFIG-DBNAME',$vars['dbname'],$configFile);
-        $configFile= str_replace('%CONFIG-DBUSER',$vars['dbuser'],$configFile);
-        $configFile= str_replace('%CONFIG-DBPASS',$vars['dbpass'],$configFile);
-        $configFile= str_replace('%CONFIG-PREFIX',$vars['prefix'],$configFile);
-        $configFile= str_replace('%CONFIG-SIRI',Misc::randCode(32),$configFile);
-        if(!$fp || !ftruncate($fp,0) || !fwrite($fp,$configFile)) {
+        $configFile = strtr($configFile, array(
+            "define('OSTINSTALLED',FALSE);" => "define('OSTINSTALLED',TRUE);",
+            '%ADMIN-EMAIL' => $vars['admin_email'],
+            '%CONFIG-DBHOST' => $vars['dbhost'],
+            '%CONFIG-DBNAME' => $vars['dbname'],
+            '%CONFIG-DBUSER' => $vars['dbuser'],
+            '%CONFIG-DBPASS' => $vars['dbpass'],
+            '%CONFIG-PREFIX' => $vars['prefix'],
+            '%CONFIG-SIRI' => Misc::randCode(32),
+        ));
+        if (!$fp || !ftruncate($fp,0) || !fwrite($fp,$configFile)) {
             $this->errors['err']=__('Unable to write to config file. Permission denied! (#5)');
             return false;
         }
         @fclose($fp);
 
         /************* Make the system happy ***********************/
-
-        $sql='UPDATE '.TABLE_PREFIX."email SET dept_id=$dept_id_1";
-        db_query($sql, false);
-
         global $cfg;
         $cfg = new OsticketConfig();
 
@@ -263,9 +291,9 @@ class Installer extends SetupWizard {
         $ticket = Ticket::create($ticket_vars, $errors, 'api', false, false);
 
         if ($ticket
-                && ($org = Organization::objects()->order_by('id')->one())) {
-
-            $user=User::lookup($ticket->getOwnerId());
+            && ($org = Organization::objects()->order_by('id')->one())
+        ) {
+            $user = User::lookup($ticket->getOwnerId());
             $user->setOrganization($org);
         }
 
diff --git a/setup/inc/footer.inc.php b/setup/inc/footer.inc.php
index 852062efeba285b4cc856fde61a2ffb8481c1abb..5bc03569e76a870ef0ded62abae2fe750393e765 100644
--- a/setup/inc/footer.inc.php
+++ b/setup/inc/footer.inc.php
@@ -3,5 +3,10 @@
         </div> <!-- content -->
     </div> <!-- wizard -->
     <div id="footer" class="centered">Copyright &copy; 2013 <a target="_blank" href="http://osticket.com">osTicket.com</a></div>
+
+    <script type="text/javascript" src="../js/jquery-1.11.2.min.js"></script>
+    <script type="text/javascript" src="../js/jstz.min.js"></script>
+    <script type="text/javascript" src="js/setup.js"></script>
+    <script type="text/javascript" src="js/tips.js"></script>
 </body>
 </html>
diff --git a/setup/inc/header.inc.php b/setup/inc/header.inc.php
index b7530d1ce8df63df7e5abe99b392629a075b1d27..4d59e4076b9f5ed93d2428b02f683048d607e58e 100644
--- a/setup/inc/header.inc.php
+++ b/setup/inc/header.inc.php
@@ -11,14 +11,11 @@ if (($lang = Internationalization::getCurrentLanguage())
     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
     <link rel="stylesheet" href="css/wizard.css">
     <link type="text/css" rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/flags.css">
-    <script type="text/javascript" src="../js/jquery-1.8.3.min.js"></script>
-    <script type="text/javascript" src="js/tips.js"></script>
-    <script type="text/javascript" src="js/setup.js"></script>
 </head>
 <body>
     <div id="wizard">
         <div id="header">
-            <img id="logo" src="./images/<?php echo $wizard['logo']?$wizard['logo']:'logo.png'; ?>" width="280" height="72" alt="osTicket">
+            <img id="logo" src="./images/<?php echo $wizard['logo'] ?: 'logo.png'; ?>" alt="osTicket">
             <div class="info"><?php echo $wizard['tagline']; ?></div>
             <br/>
             <ul class="links">
diff --git a/setup/inc/install-prereq.inc.php b/setup/inc/install-prereq.inc.php
index 12c97730a035cadf8aec02d677280f38b81c8e8a..bff091f6089e42e8c575b6f346b875f7f82dfe90 100644
--- a/setup/inc/install-prereq.inc.php
+++ b/setup/inc/install-prereq.inc.php
@@ -15,7 +15,7 @@ if(!defined('SETUPINC')) die('Kwaheri!');
             <?php echo __('These items are necessary in order to install and use osTicket.');?>
             <ul class="progress">
                 <li class="<?php echo $installer->check_php()?'yes':'no'; ?>">
-                <?php echo sprintf(__('%s or greater'), '<span class="ltr">PHP v5.3</span>');?> &mdash; <small class="ltr">(<b><?php echo PHP_VERSION; ?></b>)</small></li>
+                <?php echo sprintf(__('%s or greater'), '<span class="ltr">PHP v5.4</span>');?> &mdash; <small class="ltr">(<b><?php echo PHP_VERSION; ?></b>)</small></li>
                 <li class="<?php echo $installer->check_mysql()?'yes':'no'; ?>">
                 <?php echo __('MySQLi extension for PHP');?> &mdash; <small><b><?php
                     echo extension_loaded('mysqli')?__('module loaded'):__('missing!'); ?></b></small></li>
@@ -36,6 +36,12 @@ if(!defined('SETUPINC')) die('Kwaheri!');
                     echo __('recommended for all installations');?></li>
                 <li class="<?php echo extension_loaded('phar')?'yes':'no'; ?>">Phar <?php echo __('extension');?> &mdash; <?php
                     echo __('recommended for plugins and language packs');?></li>
+                <li class="<?php echo extension_loaded('intl')?'yes':'no'; ?>">Intl <?php echo __('extension');?> &mdash; <?php
+                    echo __('recommended for improved localization');?></li>
+                <li class="<?php echo extension_loaded('apcu')?'yes':'no'; ?>">APCu <?php echo __('extension');?> &mdash; <?php
+                    echo __('(faster performance)');?></li>
+                <li class="<?php echo extension_loaded('Zend OPcache')?'yes':'no'; ?>">Zend OPcache <?php echo __('extension');?> &mdash; <?php
+                    echo __('(faster performance)');?></li>
             </ul>
             <div id="bar">
                 <form method="post" action="install.php">
diff --git a/setup/inc/install.inc.php b/setup/inc/install.inc.php
index 807be16fda249a64a3550be5bfc39d891a25b1fd..15efe60615e4bf0b2b8ad5bd23d13a1f9ad77e07 100644
--- a/setup/inc/install.inc.php
+++ b/setup/inc/install.inc.php
@@ -115,6 +115,8 @@ $info=($_POST && $errors)?Format::htmlchars($_POST):array('prefix'=>'ost_','dbho
                 <div id="bar">
                     <input class="btn" type="submit" value="<?php echo __('Install Now');?>" tabindex="14">
                 </div>
+
+                <input type="hidden" name="timezone" id="timezone"/>
             </form>
     </div>
     <div>
diff --git a/setup/inc/streams/core/install-mysql.sql b/setup/inc/streams/core/install-mysql.sql
index 430a0b1ea44841ca299526a20fab8578bad4c8e0..105d0420a28ce9d3755328854fdbc2dc22e45c63 100644
--- a/setup/inc/streams/core/install-mysql.sql
+++ b/setup/inc/streams/core/install-mysql.sql
@@ -17,11 +17,15 @@ CREATE TABLE `%TABLE_PREFIX%api_key` (
 
 DROP TABLE IF EXISTS `%TABLE_PREFIX%attachment`;
 CREATE TABLE `%TABLE_PREFIX%attachment` (
+  `id` int(10) unsigned NOT NULL auto_increment,
   `object_id` int(11) unsigned NOT NULL,
   `type` char(1) NOT NULL,
   `file_id` int(11) unsigned NOT NULL,
+  `name` varchar(255) NULL default NULL,
   `inline` tinyint(1) unsigned NOT NULL DEFAULT '0',
-  PRIMARY KEY (`object_id`,`file_id`,`type`)
+  `lang` varchar(16),
+  PRIMARY KEY  (`id`),
+  UNIQUE KEY `file-type` (`object_id`,`file_id`,`type`)
 ) DEFAULT CHARSET=utf8;
 
 DROP TABLE IF EXISTS `%TABLE_PREFIX%faq`;
@@ -78,9 +82,7 @@ CREATE TABLE `%TABLE_PREFIX%sequence` (
 DROP TABLE IF EXISTS `%TABLE_PREFIX%sla`;
 CREATE TABLE `%TABLE_PREFIX%sla` (
   `id` int(11) unsigned NOT NULL auto_increment,
-  `isactive` tinyint(1) unsigned NOT NULL default '1',
-  `enable_priority_escalation` tinyint(1) unsigned NOT NULL default '1',
-  `disable_overdue_alerts` tinyint(1) unsigned NOT NULL default '0',
+  `flags` int(10) unsigned NOT NULL default 3,
   `grace_period` int(10) unsigned NOT NULL default '0',
   `name` varchar(64) NOT NULL default '',
   `notes` text,
@@ -110,10 +112,12 @@ INSERT INTO `%TABLE_PREFIX%config` (`namespace`, `key`, `value`) VALUES
 DROP TABLE IF EXISTS `%TABLE_PREFIX%form`;
 CREATE TABLE `%TABLE_PREFIX%form` (
     `id` int(11) unsigned NOT NULL auto_increment,
+    `pid` int(10) unsigned DEFAULT NULL,
     `type` varchar(8) NOT NULL DEFAULT 'G',
-    `deletable` tinyint(1) NOT NULL DEFAULT 1,
+    `flags` int(10) unsigned NOT NULL DEFAULT 1,
     `title` varchar(255) NOT NULL,
     `instructions` varchar(512),
+    `name` varchar(64) NOT NULL DEFAULT '',
     `notes` text,
     `created` datetime NOT NULL,
     `updated` datetime NOT NULL,
@@ -124,11 +128,9 @@ DROP TABLE IF EXISTS `%TABLE_PREFIX%form_field`;
 CREATE TABLE `%TABLE_PREFIX%form_field` (
     `id` int(11) unsigned NOT NULL auto_increment,
     `form_id` int(11) unsigned NOT NULL,
+    `flags` int(10) unsigned DEFAULT 1,
     `type` varchar(255) NOT NULL DEFAULT 'text',
     `label` varchar(255) NOT NULL,
-    `required` tinyint(1) NOT NULL DEFAULT 0,
-    `private` tinyint(1) NOT NULL DEFAULT 0,
-    `edit_mask` tinyint(1) NOT NULL DEFAULT 0,
     `name` varchar(64) NOT NULL,
     `configuration` text,
     `sort` int(11) unsigned NOT NULL,
@@ -145,6 +147,7 @@ CREATE TABLE `%TABLE_PREFIX%form_entry` (
     `object_id` int(11) unsigned,
     `object_type` char(1) NOT NULL DEFAULT 'T',
     `sort` int(11) unsigned NOT NULL DEFAULT 1,
+    `extra` text,
     `created` datetime NOT NULL,
     `updated` datetime NOT NULL,
     PRIMARY KEY (`id`),
@@ -169,6 +172,7 @@ CREATE TABLE `%TABLE_PREFIX%list` (
     `sort_mode` enum('Alpha', '-Alpha', 'SortCol') NOT NULL DEFAULT 'Alpha',
     `masks` int(11) unsigned NOT NULL DEFAULT 0,
     `type` VARCHAR( 16 ) NULL DEFAULT NULL,
+    `configuration` text NOT NULL DEFAULT '',
     `notes` text,
     `created` datetime NOT NULL,
     `updated` datetime NOT NULL,
@@ -193,22 +197,25 @@ CREATE TABLE `%TABLE_PREFIX%list_items` (
 
 DROP TABLE IF EXISTS `%TABLE_PREFIX%department`;
 CREATE TABLE `%TABLE_PREFIX%department` (
-  `dept_id` int(11) unsigned NOT NULL auto_increment,
+  `id` int(11) unsigned NOT NULL auto_increment,
+  `pid` int(11) unsigned default NULL,
   `tpl_id` int(10) unsigned NOT NULL default '0',
   `sla_id` int(10) unsigned NOT NULL default '0',
   `email_id` int(10) unsigned NOT NULL default '0',
   `autoresp_email_id` int(10) unsigned NOT NULL default '0',
   `manager_id` int(10) unsigned NOT NULL default '0',
-  `dept_name` varchar(128) NOT NULL default '',
-  `dept_signature` text NOT NULL,
+  `flags` int(10) unsigned NOT NULL default 0,
+  `name` varchar(128) NOT NULL default '',
+  `signature` text NOT NULL,
   `ispublic` tinyint(1) unsigned NOT NULL default '1',
   `group_membership` tinyint(1) NOT NULL default '0',
   `ticket_auto_response` tinyint(1) NOT NULL default '1',
   `message_auto_response` tinyint(1) NOT NULL default '0',
+  `path` varchar(128) NOT NULL default '/',
   `updated` datetime NOT NULL,
   `created` datetime NOT NULL,
-  PRIMARY KEY  (`dept_id`),
-  UNIQUE KEY `dept_name` (`dept_name`),
+  PRIMARY KEY  (`id`),
+  UNIQUE KEY `name` (`name`, `pid`),
   KEY `manager_id` (`manager_id`),
   KEY `autoresp_email_id` (`autoresp_email_id`),
   KEY `tpl_id` (`tpl_id`)
@@ -291,21 +298,8 @@ CREATE TABLE `%TABLE_PREFIX%filter` (
   `status` int(11) unsigned NOT NULL DEFAULT '0',
   `match_all_rules` tinyint(1) unsigned NOT NULL default '0',
   `stop_onmatch` tinyint(1) unsigned NOT NULL default '0',
-  `reject_ticket` tinyint(1) unsigned NOT NULL default '0',
-  `use_replyto_email` tinyint(1) unsigned NOT NULL default '0',
-  `disable_autoresponder` tinyint(1) unsigned NOT NULL default '0',
-  `canned_response_id` int(11) unsigned NOT NULL default '0',
-  `email_id` int(10) unsigned NOT NULL default '0',
-  `status_id` int(10) unsigned NOT NULL default '0',
-  `priority_id` int(10) unsigned NOT NULL default '0',
-  `dept_id` int(10) unsigned NOT NULL default '0',
-  `staff_id` int(10) unsigned NOT NULL default '0',
-  `team_id` int(10) unsigned NOT NULL default '0',
-  `sla_id` int(10) unsigned NOT NULL default '0',
-  `form_id` int(11) unsigned NOT NULL default '0',
-  `topic_id` int(11) unsigned NOT NULL default '0',
-  `ext_id` varchar(11),
   `target` ENUM(  'Any',  'Web',  'Email',  'API' ) NOT NULL DEFAULT  'Any',
+  `email_id` int(10) unsigned NOT NULL default '0',
   `name` varchar(32) NOT NULL default '',
   `notes` text,
   `created` datetime NOT NULL,
@@ -315,6 +309,18 @@ CREATE TABLE `%TABLE_PREFIX%filter` (
   KEY `email_id` (`email_id`)
 ) DEFAULT CHARSET=utf8;
 
+DROP TABLE IF EXISTS `%TABLE_PREFIX%filter_action`;
+CREATE TABLE `%TABLE_PREFIX%filter_action` (
+  `id` int(11) unsigned NOT NULL auto_increment,
+  `filter_id` int(10) unsigned NOT NULL,
+  `sort` int(10) unsigned NOT NULL default 0,
+  `type` varchar(24) NOT NULL,
+  `configuration` text,
+  `updated` datetime NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `filter_id` (`filter_id`)
+) DEFAULT CHARSET=utf8;
+
 DROP TABLE IF EXISTS `%TABLE_PREFIX%filter_rule`;
 CREATE TABLE `%TABLE_PREFIX%filter_rule` (
   `id` int(11) unsigned NOT NULL auto_increment,
@@ -384,35 +390,30 @@ CREATE TABLE `%TABLE_PREFIX%file_chunk` (
   PRIMARY KEY (`file_id`, `chunk_id`)
 ) DEFAULT CHARSET=utf8;
 
-DROP TABLE IF EXISTS `%TABLE_PREFIX%groups`;
-CREATE TABLE `%TABLE_PREFIX%groups` (
-  `group_id` int(10) unsigned NOT NULL auto_increment,
-  `group_enabled` tinyint(1) unsigned NOT NULL default '1',
-  `group_name` varchar(50) NOT NULL default '',
-  `can_create_tickets` tinyint(1) unsigned NOT NULL default '1',
-  `can_edit_tickets` tinyint(1) unsigned NOT NULL default '1',
-  `can_post_ticket_reply` tinyint( 1 ) unsigned NOT NULL DEFAULT  '1',
-  `can_delete_tickets` tinyint(1) unsigned NOT NULL default '0',
-  `can_close_tickets` tinyint(1) unsigned NOT NULL default '1',
-  `can_assign_tickets` tinyint(1) unsigned NOT NULL default '1',
-  `can_transfer_tickets` tinyint(1) unsigned NOT NULL default '1',
-  `can_ban_emails` tinyint(1) unsigned NOT NULL default '0',
-  `can_manage_premade` tinyint(1) unsigned NOT NULL default '0',
-  `can_manage_faq` tinyint(1) unsigned NOT NULL default '0',
-  `can_view_staff_stats` tinyint( 1 ) unsigned NOT NULL DEFAULT  '0',
+DROP TABLE IF EXISTS `%TABLE_PREFIX%group`;
+CREATE TABLE `%TABLE_PREFIX%group` (
+  `id` int(10) unsigned NOT NULL auto_increment,
+  `role_id` int(11) unsigned NOT NULL,
+  `flags` int(11) unsigned NOT NULL default '1',
+  `name` varchar(120) NOT NULL default '',
   `notes` text,
   `created` datetime NOT NULL,
   `updated` datetime NOT NULL,
-  PRIMARY KEY  (`group_id`),
-  KEY `group_active` (`group_enabled`)
+  PRIMARY KEY  (`id`),
+  KEY `role_id` (`role_id`)
 ) DEFAULT CHARSET=utf8;
 
-DROP TABLE IF EXISTS `%TABLE_PREFIX%group_dept_access`;
-CREATE TABLE `%TABLE_PREFIX%group_dept_access` (
-  `group_id` int(10) unsigned NOT NULL default '0',
-  `dept_id` int(10) unsigned NOT NULL default '0',
-  UNIQUE KEY `group_dept` (`group_id`,`dept_id`),
-  KEY `dept_id`  (`dept_id`)
+DROP TABLE IF EXISTS `%TABLE_PREFIX%role`;
+CREATE TABLE `%TABLE_PREFIX%role` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `flags` int(10) unsigned NOT NULL DEFAULT '1',
+  `name` varchar(64) DEFAULT NULL,
+  `permissions` text,
+  `notes` text,
+  `created` datetime NOT NULL,
+  `updated` datetime NOT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `name` (`name`)
 ) DEFAULT CHARSET=utf8;
 
 DROP TABLE IF EXISTS `%TABLE_PREFIX%help_topic`;
@@ -430,7 +431,6 @@ CREATE TABLE `%TABLE_PREFIX%help_topic` (
   `team_id` int(10) unsigned NOT NULL default '0',
   `sla_id` int(10) unsigned NOT NULL default '0',
   `page_id` int(10) unsigned NOT NULL default '0',
-  `form_id` int(10) unsigned NOT NULL default '0',
   `sequence_id` int(10) unsigned NOT NULL DEFAULT '0',
   `sort` int(10) unsigned NOT NULL default '0',
   `topic` varchar(32) NOT NULL default '',
@@ -448,6 +448,17 @@ CREATE TABLE `%TABLE_PREFIX%help_topic` (
   KEY `page_id` (`page_id`)
 ) DEFAULT CHARSET=utf8;
 
+DROP TABLE IF EXISTS `%TABLE_PREFIX%help_topic_form`;
+CREATE TABLE `%TABLE_PREFIX%help_topic_form` (
+  `id` int(11) unsigned NOT NULL auto_increment,
+  `topic_id` int(11) unsigned NOT NULL default 0,
+  `form_id` int(10) unsigned NOT NULL default 0,
+  `sort` int(10) unsigned NOT NULL default 1,
+  `extra` text,
+  PRIMARY KEY (`id`),
+  KEY `topic-form` (`topic_id`, `form_id`)
+) DEFAULT CHARSET=utf8;
+
 DROP TABLE IF EXISTS `%TABLE_PREFIX%organization`;
 CREATE TABLE `%TABLE_PREFIX%organization` (
   `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
@@ -510,9 +521,8 @@ CREATE TABLE `%TABLE_PREFIX%session` (
 DROP TABLE IF EXISTS `%TABLE_PREFIX%staff`;
 CREATE TABLE `%TABLE_PREFIX%staff` (
   `staff_id` int(11) unsigned NOT NULL auto_increment,
-  `group_id` int(10) unsigned NOT NULL default '0',
   `dept_id` int(10) unsigned NOT NULL default '0',
-  `timezone_id` int(10) unsigned NOT NULL default '0',
+  `role_id` int(10) unsigned NOT NULL default '0',
   `username` varchar(32) NOT NULL default '',
   `firstname` varchar(32) default NULL,
   `lastname` varchar(32) default NULL,
@@ -523,6 +533,9 @@ CREATE TABLE `%TABLE_PREFIX%staff` (
   `phone_ext` varchar(6) default NULL,
   `mobile` varchar(24) NOT NULL default '',
   `signature` text NOT NULL,
+  `lang` varchar(16) DEFAULT NULL,
+  `timezone` varchar(64) default NULL,
+  `locale` varchar(16) DEFAULT NULL,
   `notes` text,
   `isactive` tinyint(1) NOT NULL default '1',
   `isadmin` tinyint(1) NOT NULL default '0',
@@ -530,12 +543,13 @@ CREATE TABLE `%TABLE_PREFIX%staff` (
   `onvacation` tinyint(1) unsigned NOT NULL default '0',
   `assigned_only` tinyint(1) unsigned NOT NULL default '0',
   `show_assigned_tickets` tinyint(1) unsigned NOT NULL default '0',
-  `daylight_saving` tinyint(1) unsigned NOT NULL default '0',
   `change_passwd` tinyint(1) unsigned NOT NULL default '0',
   `max_page_size` int(11) unsigned NOT NULL default '0',
   `auto_refresh_rate` int(10) unsigned NOT NULL default '0',
   `default_signature_type` ENUM( 'none', 'mine', 'dept' ) NOT NULL DEFAULT 'none',
   `default_paper_size` ENUM( 'Letter', 'Legal', 'Ledger', 'A4', 'A3' ) NOT NULL DEFAULT 'Letter',
+  `extra` text,
+  `permissions` text,
   `created` datetime NOT NULL,
   `lastlogin` datetime default NULL,
   `passwdreset` datetime default NULL,
@@ -543,8 +557,17 @@ CREATE TABLE `%TABLE_PREFIX%staff` (
   PRIMARY KEY  (`staff_id`),
   UNIQUE KEY `username` (`username`),
   KEY `dept_id` (`dept_id`),
-  KEY `issuperuser` (`isadmin`),
-  KEY `group_id` (`group_id`,`staff_id`)
+  KEY `issuperuser` (`isadmin`)
+) DEFAULT CHARSET=utf8;
+
+DROP TABLE IF EXISTS `%TABLE_PREFIX%staff_dept_access`;
+CREATE TABLE `%TABLE_PREFIX%staff_dept_access` (
+  `staff_id` int(10) unsigned NOT NULL DEFAULT '0',
+  `dept_id` int(10) unsigned NOT NULL DEFAULT '0',
+  `role_id` int(10) unsigned NOT NULL DEFAULT '0',
+  `flags` int(10) unsigned NOT NULL DEFAULT '1',
+  PRIMARY KEY `staff_dept` (`staff_id`,`dept_id`),
+  KEY `dept_id` (`dept_id`)
 ) DEFAULT CHARSET=utf8;
 
 DROP TABLE IF EXISTS `%TABLE_PREFIX%syslog`;
@@ -565,15 +588,13 @@ DROP TABLE IF EXISTS `%TABLE_PREFIX%team`;
 CREATE TABLE `%TABLE_PREFIX%team` (
   `team_id` int(10) unsigned NOT NULL auto_increment,
   `lead_id` int(10) unsigned NOT NULL default '0',
-  `isenabled` tinyint(1) unsigned NOT NULL default '1',
-  `noalerts` tinyint(1) unsigned NOT NULL default '0',
+  `flags` int(10) unsigned NOT NULL default 1,
   `name` varchar(125) NOT NULL default '',
   `notes` text,
   `created` datetime NOT NULL,
   `updated` datetime NOT NULL,
   PRIMARY KEY  (`team_id`),
   UNIQUE KEY `name` (`name`),
-  KEY `isnabled` (`isenabled`),
   KEY `lead_id` (`lead_id`)
 ) DEFAULT CHARSET=utf8;
 
@@ -581,10 +602,61 @@ DROP TABLE IF EXISTS `%TABLE_PREFIX%team_member`;
 CREATE TABLE `%TABLE_PREFIX%team_member` (
   `team_id` int(10) unsigned NOT NULL default '0',
   `staff_id` int(10) unsigned NOT NULL,
-  `updated` datetime NOT NULL,
+  `flags` int(10) unsigned NOT NULL DEFAULT '0',
   PRIMARY KEY  (`team_id`,`staff_id`)
 ) DEFAULT CHARSET=utf8;
 
+DROP TABLE IF EXISTS `%TABLE_PREFIX%thread`;
+CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%thread` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `object_id` int(11) unsigned NOT NULL,
+  `object_type` char(1) NOT NULL,
+  `extra` text,
+  `lastresponse` datetime DEFAULT NULL,
+  `lastmessage` datetime DEFAULT NULL,
+  `created` datetime NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `object_id` (`object_id`),
+  KEY `object_type` (`object_type`)
+) DEFAULT CHARSET=utf8;
+
+DROP TABLE IF EXISTS `%TABLE_PREFIX%thread_entry`;
+CREATE TABLE `%TABLE_PREFIX%thread_entry` (
+  `id` int(11) unsigned NOT NULL auto_increment,
+  `pid` int(11) unsigned NOT NULL default '0',
+  `thread_id` int(11) unsigned NOT NULL default '0',
+  `staff_id` int(11) unsigned NOT NULL default '0',
+  `user_id` int(11) unsigned not null default 0,
+  `type` char(1) NOT NULL default '',
+  `flags` int(11) unsigned NOT NULL default '0',
+  `poster` varchar(128) NOT NULL default '',
+  `editor` int(10) unsigned NULL,
+  `editor_type` char(1) NULL,
+  `source` varchar(32) NOT NULL default '',
+  `title` varchar(255),
+  `body` text NOT NULL,
+  `format` varchar(16) NOT NULL default 'html',
+  `ip_address` varchar(64) NOT NULL default '',
+  `created` datetime NOT NULL,
+  `updated` datetime NOT NULL,
+  PRIMARY KEY  (`id`),
+  KEY `pid` (`pid`),
+  KEY `thread_id` (`thread_id`),
+  KEY `staff_id` (`staff_id`),
+  KEY `type` (`type`)
+) DEFAULT CHARSET=utf8;
+
+DROP TABLE IF EXISTS `%TABLE_PREFIX%thread_entry_email`;
+CREATE TABLE `%TABLE_PREFIX%thread_entry_email` (
+  `id` int(11) unsigned NOT NULL auto_increment,
+  `thread_entry_id` int(11) unsigned NOT NULL,
+  `mid` varchar(255) NOT NULL,
+  `headers` text,
+  PRIMARY KEY (`id`),
+  KEY `thread_entry_id` (`thread_entry_id`),
+  KEY `mid` (`mid`)
+) DEFAULT CHARSET=utf8;
+
 DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket`;
 CREATE TABLE `%TABLE_PREFIX%ticket` (
   `ticket_id` int(11) unsigned NOT NULL auto_increment,
@@ -598,16 +670,18 @@ CREATE TABLE `%TABLE_PREFIX%ticket` (
   `staff_id` int(10) unsigned NOT NULL default '0',
   `team_id` int(10) unsigned NOT NULL default '0',
   `email_id` int(11) unsigned NOT NULL default '0',
+  `lock_id` int(11) unsigned NOT NULL default '0',
   `flags` int(10) unsigned NOT NULL default '0',
   `ip_address` varchar(64) NOT NULL default '',
   `source` enum('Web','Email','Phone','API','Other') NOT NULL default 'Other',
+  `source_extra` varchar(40) NULL default NULL,
   `isoverdue` tinyint(1) unsigned NOT NULL default '0',
   `isanswered` tinyint(1) unsigned NOT NULL default '0',
   `duedate` datetime default NULL,
+  `est_duedate` datetime default NULL,
   `reopened` datetime default NULL,
   `closed` datetime default NULL,
-  `lastmessage` datetime default NULL,
-  `lastresponse` datetime default NULL,
+  `lastupdate` datetime default NULL,
   `created` datetime NOT NULL,
   `updated` datetime NOT NULL,
   PRIMARY KEY  (`ticket_id`),
@@ -623,54 +697,34 @@ CREATE TABLE `%TABLE_PREFIX%ticket` (
   KEY `sla_id` (`sla_id`)
 ) DEFAULT CHARSET=utf8;
 
-DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_attachment`;
-CREATE TABLE `%TABLE_PREFIX%ticket_attachment` (
-  `attach_id` int(11) unsigned NOT NULL auto_increment,
-  `ticket_id` int(11) unsigned NOT NULL default '0',
-  `file_id` int(10) unsigned NOT NULL default '0',
-  `ref_id` int(11) unsigned NOT NULL default '0',
-  `inline` tinyint(1) NOT NULL default  '0',
-  `created` datetime NOT NULL,
-  PRIMARY KEY  (`attach_id`),
-  KEY `ticket_id` (`ticket_id`),
-  KEY `ref_id` (`ref_id`),
-  KEY `file_id` (`file_id`)
-) DEFAULT CHARSET=utf8;
-
-DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_lock`;
-CREATE TABLE `%TABLE_PREFIX%ticket_lock` (
+DROP TABLE IF EXISTS `%TABLE_PREFIX%lock`;
+CREATE TABLE `%TABLE_PREFIX%lock` (
   `lock_id` int(11) unsigned NOT NULL auto_increment,
-  `ticket_id` int(11) unsigned NOT NULL default '0',
   `staff_id` int(10) unsigned NOT NULL default '0',
   `expire` datetime default NULL,
+  `code` varchar(20),
   `created` datetime NOT NULL,
   PRIMARY KEY  (`lock_id`),
-  UNIQUE KEY `ticket_id` (`ticket_id`),
   KEY `staff_id` (`staff_id`)
 ) DEFAULT CHARSET=utf8;
 
-DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_email_info`;
-CREATE TABLE `%TABLE_PREFIX%ticket_email_info` (
-  `id` int(11) unsigned NOT NULL auto_increment,
-  `thread_id` int(11) unsigned NOT NULL,
-  `email_mid` varchar(255) NOT NULL,
-  `headers` text,
-  PRIMARY KEY (`id`),
-  KEY `email_mid` (`email_mid`)
-) DEFAULT CHARSET=utf8;
-
-DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_event`;
-CREATE TABLE `%TABLE_PREFIX%ticket_event` (
-  `ticket_id` int(11) unsigned NOT NULL default '0',
+DROP TABLE IF EXISTS `%TABLE_PREFIX%thread_event`;
+CREATE TABLE `%TABLE_PREFIX%thread_event` (
+  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
+  `thread_id` int(11) unsigned NOT NULL default '0',
   `staff_id` int(11) unsigned NOT NULL,
   `team_id` int(11) unsigned NOT NULL,
   `dept_id` int(11) unsigned NOT NULL,
   `topic_id` int(11) unsigned NOT NULL,
-  `state` enum('created','closed','reopened','assigned','transferred','overdue') NOT NULL,
-  `staff` varchar(255) NOT NULL default 'SYSTEM',
+  `state` enum('created','closed','reopened','assigned','transferred','overdue','edited','viewed','error','collab','resent') NOT NULL,
+  `data` varchar(1024) DEFAULT NULL COMMENT 'Encoded differences',
+  `username` varchar(128) NOT NULL default 'SYSTEM',
+  `uid` int(11) unsigned DEFAULT NULL,
+  `uid_type` char(1) NOT NULL DEFAULT 'S',
   `annulled` tinyint(1) unsigned NOT NULL default '0',
   `timestamp` datetime NOT NULL,
-  KEY `ticket_state` (`ticket_id`, `state`, `timestamp`),
+  PRIMARY KEY (`id`),
+  KEY `ticket_state` (`thread_id`, `state`, `timestamp`),
   KEY `ticket_stats` (`timestamp`, `state`)
 ) DEFAULT CHARSET=utf8;
 
@@ -705,92 +759,50 @@ CREATE TABLE `%TABLE_PREFIX%ticket_priority` (
   KEY `ispublic` (`ispublic`)
 ) DEFAULT CHARSET=utf8;
 
-DROP TABLE IF EXISTS `%TABLE_PREFIX%ticket_thread`;
-CREATE TABLE `%TABLE_PREFIX%ticket_thread` (
-  `id` int(11) unsigned NOT NULL auto_increment,
-  `pid` int(11) unsigned NOT NULL default '0',
-  `ticket_id` int(11) unsigned NOT NULL default '0',
-  `staff_id` int(11) unsigned NOT NULL default '0',
-  `user_id` int(11) unsigned not null default 0,
-  `thread_type` enum('M','R','N') NOT NULL,
-  `poster` varchar(128) NOT NULL default '',
-  `source` varchar(32) NOT NULL default '',
-  `title` varchar(255),
-  `body` mediumtext NOT NULL,
-  `format` varchar(16) NOT NULL default 'html',
-  `ip_address` varchar(64) NOT NULL default '',
-  `created` datetime NOT NULL,
-  `updated` datetime NOT NULL,
-  PRIMARY KEY  (`id`),
-  KEY `ticket_id` (`ticket_id`),
-  KEY `staff_id` (`staff_id`),
-  KEY `pid` (`pid`)
-) DEFAULT CHARSET=utf8;
-
-CREATE TABLE `%TABLE_PREFIX%ticket_collaborator` (
+CREATE TABLE `%TABLE_PREFIX%thread_collaborator` (
   `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
   `isactive` tinyint(1) NOT NULL DEFAULT '1',
-  `ticket_id` int(11) unsigned NOT NULL DEFAULT '0',
+  `thread_id` int(11) unsigned NOT NULL DEFAULT '0',
   `user_id` int(11) unsigned NOT NULL DEFAULT '0',
   -- M => (message) clients, N => (note) 3rd-Party, R => (reply) external authority
   `role` char(1) NOT NULL DEFAULT 'M',
   `created` datetime NOT NULL,
   `updated` datetime NOT NULL,
   PRIMARY KEY (`id`),
-  UNIQUE KEY `collab` (`ticket_id`,`user_id`)
+  UNIQUE KEY `collab` (`thread_id`,`user_id`),
+  KEY `user_id` (`user_id`)
 ) DEFAULT CHARSET=utf8;
 
-
-
-DROP TABLE IF EXISTS `%TABLE_PREFIX%timezone`;
-CREATE TABLE `%TABLE_PREFIX%timezone` (
-  `id` int(11) unsigned NOT NULL auto_increment,
-  `offset` float(3,1) NOT NULL default '0.0',
-  `timezone` varchar(255) NOT NULL default '',
-  PRIMARY KEY  (`id`)
-) DEFAULT CHARSET=utf8;
-
-INSERT INTO `%TABLE_PREFIX%timezone` (`id`, `offset`, `timezone`) VALUES
-  (1, -12.0, 'Eniwetok, Kwajalein'),
-  (2, -11.0, 'Midway Island, Samoa'),
-  (3, -10.0, 'Hawaii'),
-  (4, -9.0, 'Alaska'),
-  (5, -8.0, 'Pacific Time (US & Canada)'),
-  (6, -7.0, 'Mountain Time (US & Canada)'),
-  (7, -6.0, 'Central Time (US & Canada), Mexico City'),
-  (8, -5.0, 'Eastern Time (US & Canada), Bogota, Lima'),
-  (9, -4.0, 'Atlantic Time (Canada), Caracas, La Paz'),
-  (10, -3.5, 'Newfoundland'),
-  (11, -3.0, 'Brazil, Buenos Aires, Georgetown'),
-  (12, -2.0, 'Mid-Atlantic'),
-  (13, -1.0, 'Azores, Cape Verde Islands'),
-  (14, 0.0, 'Western Europe Time, London, Lisbon, Casablanca'),
-  (15, 1.0, 'Brussels, Copenhagen, Madrid, Paris'),
-  (16, 2.0, 'Kaliningrad, South Africa'),
-  (17, 3.0, 'Baghdad, Riyadh, Moscow, St. Petersburg'),
-  (18, 3.5, 'Tehran'),
-  (19, 4.0, 'Abu Dhabi, Muscat, Baku, Tbilisi'),
-  (20, 4.5, 'Kabul'),
-  (21, 5.0, 'Ekaterinburg, Islamabad, Karachi, Tashkent'),
-  (22, 5.5, 'Bombay, Calcutta, Madras, New Delhi'),
-  (23, 6.0, 'Almaty, Dhaka, Colombo'),
-  (24, 7.0, 'Bangkok, Hanoi, Jakarta'),
-  (25, 8.0, 'Beijing, Perth, Singapore, Hong Kong'),
-  (26, 9.0, 'Tokyo, Seoul, Osaka, Sapporo, Yakutsk'),
-  (27, 9.5, 'Adelaide, Darwin'),
-  (28, 10.0, 'Eastern Australia, Guam, Vladivostok'),
-  (29, 11.0, 'Magadan, Solomon Islands, New Caledonia'),
-  (30, 12.0, 'Auckland, Wellington, Fiji, Kamchatka');
+DROP TABLE IF EXISTS `%TABLE_PREFIX%task`;
+CREATE TABLE `%TABLE_PREFIX%task` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `object_id` int(11) NOT NULL DEFAULT '0',
+  `object_type` char(1) NOT NULL,
+  `number` varchar(20) DEFAULT NULL,
+  `dept_id` int(10) unsigned NOT NULL DEFAULT '0',
+  `staff_id` int(10) unsigned NOT NULL DEFAULT '0',
+  `team_id` int(10) unsigned NOT NULL DEFAULT '0',
+  `lock_id` int(11) unsigned NOT NULL DEFAULT '0',
+  `flags` int(10) unsigned NOT NULL DEFAULT '0',
+  `duedate` datetime DEFAULT NULL,
+  `closed` datetime DEFAULT NULL,
+  `created` datetime NOT NULL,
+  `updated` datetime NOT NULL,
+  PRIMARY KEY (`id`),
+  KEY `dept_id` (`dept_id`),
+  KEY `staff_id` (`staff_id`),
+  KEY `team_id` (`team_id`),
+  KEY `created` (`created`),
+  KEY `object` (`object_id`,`object_type`)
+) DEFAULT CHARSET=utf8;
 
 -- pages
 CREATE TABLE IF NOT EXISTS `%TABLE_PREFIX%content` (
   `id` int(10) unsigned NOT NULL auto_increment,
-  `content_id` int(10) unsigned NOT NULL default '0',
   `isactive` tinyint(1) unsigned NOT NULL default '0',
   `type` varchar(32) NOT NULL default 'other',
   `name` varchar(255) NOT NULL,
   `body` text NOT NULL,
-  `lang` varchar(16) NOT NULL default 'en_US',
   `notes` text,
   `created` datetime NOT NULL,
   `updated` datetime NOT NULL,
@@ -811,6 +823,37 @@ CREATE TABLE `%TABLE_PREFIX%plugin` (
   primary key (`id`)
 ) DEFAULT CHARSET=utf8;
 
+DROP TABLE IF EXISTS `%TABLE_PREFIX%queue`;
+CREATE TABLE `%TABLE_PREFIX%queue` (
+  `id` int(11) unsigned not null auto_increment,
+  `parent_id` int(11) unsigned not null default 0,
+  `flags` int(11) unsigned not null default 0,
+  `staff_id` int(11) unsigned not null default 0,
+  `sort` int(11) unsigned not null default 0,
+  `title` varchar(60),
+  `config` text,
+  `created` datetime not null,
+  `updated` datetime not null,
+  primary key (`id`)
+) DEFAULT CHARSET=utf8;
+
+DROP TABLE IF EXISTS `%TABLE_PREFIX%translation`;
+CREATE TABLE `%TABLE_PREFIX%translation` (
+  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+  `object_hash` char(16) CHARACTER SET ascii DEFAULT NULL,
+  `type` enum('phrase','article','override') DEFAULT NULL,
+  `flags` int(10) unsigned NOT NULL DEFAULT '0',
+  `revision` int(11) unsigned DEFAULT NULL,
+  `agent_id` int(10) unsigned NOT NULL DEFAULT '0',
+  `lang` varchar(16) NOT NULL DEFAULT '',
+  `text` mediumtext NOT NULL,
+  `source_text` text,
+  `updated` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  KEY `type` (`type`,`lang`),
+  KEY `object_hash` (`object_hash`)
+) DEFAULT CHARSET=utf8;
+
 DROP TABLE IF EXISTS `%TABLE_PREFIX%user`;
 CREATE TABLE `%TABLE_PREFIX%user` (
   `id` int(10) unsigned NOT NULL auto_increment,
@@ -828,6 +871,7 @@ DROP TABLE IF EXISTS `%TABLE_PREFIX%user_email`;
 CREATE TABLE `%TABLE_PREFIX%user_email` (
   `id` int(10) unsigned NOT NULL auto_increment,
   `user_id` int(10) unsigned NOT NULL,
+  `flags` int(10) unsigned NOT NULL DEFAULT 0,
   `address` varchar(128) NOT NULL,
   PRIMARY KEY  (`id`),
   UNIQUE KEY `address` (`address`),
@@ -839,12 +883,12 @@ CREATE TABLE `%TABLE_PREFIX%user_account` (
   `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
   `user_id` int(10) unsigned NOT NULL,
   `status` int(11) unsigned NOT NULL DEFAULT '0',
-  `timezone_id` int(11) NOT NULL DEFAULT '0',
-  `dst` tinyint(1) NOT NULL DEFAULT '1',
+  `timezone` varchar(64) DEFAULT NULL,
   `lang` varchar(16) DEFAULT NULL,
   `username` varchar(64) DEFAULT NULL,
   `passwd` varchar(128) CHARACTER SET ascii COLLATE ascii_bin DEFAULT NULL,
   `backend` varchar(32) DEFAULT NULL,
+  `extra` text,
   `registered` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
   PRIMARY KEY (`id`),
   KEY `user_id` (`user_id`),
diff --git a/setup/js/setup.js b/setup/js/setup.js
index 7dd4c81aa5887c7388538e9dadd9f06927d1ac4b..981d21b4e50cff539b9860f28c10da3e41757193 100644
--- a/setup/js/setup.js
+++ b/setup/js/setup.js
@@ -1,5 +1,5 @@
 jQuery(function($) {
-            
+
     $("#overlay").css({
         opacity : 0.3,
         top     : 0,
@@ -12,10 +12,18 @@ jQuery(function($) {
         top  : ($(window).height() / 3),
         left : ($(window).width() / 2 - 160)
         });
-        
+
     $('form#install').submit(function(e) {
         $('input[type=submit]', this).attr('disabled', 'disabled');
         $('#overlay, #loading').show();
         return true;
         });
+
+    var recheck = setInterval(function() {
+        if (window.jstz !== undefined) {
+            clearInterval(recheck);
+            var zone = jstz.determine();
+            $('#timezone').val(zone.name());
+        }
+    }, 200);
 });
diff --git a/setup/js/tips.js b/setup/js/tips.js
index 1e35d976897cfc3cd121f3a8cb0ad195eb55fecf..a3e8b06463a22dfa94e8a00c85d7ef3df2c422a5 100644
--- a/setup/js/tips.js
+++ b/setup/js/tips.js
@@ -18,7 +18,7 @@ jQuery(function($) {
     .each(function(i, e) {
         e.rel = 'tip-' + i;
     })
-    .live('mouseover click', function(e) {
+    .on('mouseover click', function(e) {
         e.preventDefault();
 
         var elem = $(this),
@@ -51,7 +51,7 @@ jQuery(function($) {
                 }
             }, 500);
 
-        elem.live('mouseout', function() {
+        elem.on('mouseout', function() {
             clearTimeout(tip_timer);
         });
 
diff --git a/setup/setup.inc.php b/setup/setup.inc.php
index 035a8333d8786500f49e622a87763603b70ca52a..68bb943229213c84f3ef85f952498c93cc3218c5 100644
--- a/setup/setup.inc.php
+++ b/setup/setup.inc.php
@@ -68,7 +68,7 @@ require_once INCLUDE_DIR.'class.translation.php';
 
 // Support flags in the setup portal too
 if (isset($_GET['lang']) && $_GET['lang']) {
-    $_SESSION['client:lang'] = $_GET['lang'];
+    Internationalization::setCurrentLanguage($_GET['lang']);
 }
 TextDomain::configureForUser();
 
diff --git a/setup/test/run-tests.php b/setup/test/run-tests.php
index 2cf780068159380012cb28331b1a65c6b8aa0126..30d81e225735c50e21d2634cae790d00b22d1d4f 100644
--- a/setup/test/run-tests.php
+++ b/setup/test/run-tests.php
@@ -5,13 +5,10 @@ if (php_sapi_name() != 'cli') exit();
 //Allow user to select suite
 $selected_test = (isset($argv[1])) ? $argv[1] : false;
 
+require_once 'bootstrap.php';
 require_once "tests/class.test.php";
 
 $root = get_osticket_root_path();
-define('INCLUDE_DIR', "$root/include/");
-define('PEAR_DIR', INCLUDE_DIR."pear/");
-ini_set('include_path', './'.PATH_SEPARATOR.INCLUDE_DIR.PATH_SEPARATOR.PEAR_DIR);
-
 $fails = array();
 
 require_once INCLUDE_DIR . 'class.i18n.php';
diff --git a/setup/test/tests/mockdb.php b/setup/test/tests/mockdb.php
index b6bc348cf0ddf50be84cd9fe24e4237c74f9ec90..fec95feffc488e5deccb58b16543f00cca908cfe 100644
--- a/setup/test/tests/mockdb.php
+++ b/setup/test/tests/mockdb.php
@@ -1,5 +1,9 @@
 <?php
 
+define('TABLE_PREFIX', '%');
+
+Bootstrap::defineTables(TABLE_PREFIX);
+
 function db_connect($source) {
     global $__db;
     $__db = $source;
@@ -15,6 +19,11 @@ function db_query($sql) {
     return $__db->query($sql);
 }
 
+function db_prepare($sql) {
+    global $__db;
+    return $__db->prepare($sql);
+}
+
 function db_fetch_row($res) {
     return $res->fetch_row();
 }
@@ -54,6 +63,12 @@ class MockDbSource {
         return new MockDbCursor($this->data[$hash] ?: array());
     }
 
+    function prepare($sql) {
+        $cursor = $this->query($sql);
+        $cursor->param_count = preg_match_all('/ \? /', $sql);
+        return $cursor;
+    }
+
     function addRecordset($hash, &$data) {
         $this->data[$hash] = $data;
     }
@@ -79,4 +94,36 @@ class MockDbCursor {
     function num_rows() {
         return count($this->data);
     }
+
+    function fetch() {
+        return $this->fetch_array();
+    }
+
+    function execute() {
+        return $this;
+    }
+    function bind_result() {
+        return true;
+    }
+    function bind_param() {
+        return true;
+    }
+    function store_result() {
+        return true;
+    }
+    function result_metadata() {
+        return new DbMetaData();
+    }
+
+    function close() {
+        return true;
+    }
+}
+
+class DbMetaData {
+    function fetch_fields() {
+        return array();
+    }
+    function free_result() {
+    }
 }
diff --git a/setup/test/tests/stubs.php b/setup/test/tests/stubs.php
index c8aaa3f690e0c1f921a5374c5982608a0f5909f4..6bcbf3110d835657c99f9383807a3b6aa7b0a3a3 100644
--- a/setup/test/tests/stubs.php
+++ b/setup/test/tests/stubs.php
@@ -11,6 +11,7 @@ class mysqli {
     function select_db() {}
     function set_charset() {}
     function autocommit() {}
+    function rollback() {}
 }
 
 class mysqli_stmt {
@@ -30,11 +31,13 @@ class mysqli_stmt {
 class mysqli_result {
     function free() {}
     function free_result() {}
+    function fetch_fields() {}
 }
 
 class ReflectionClass {
     function getMethods() {}
     function getConstants() {}
+    function newInstanceArgs() {}
 }
 
 class DomNode {
@@ -124,6 +127,47 @@ class IntlBreakIterator {
 
 class SqlFunction {
     static function NOW() {}
+    static function LENGTH() {}
+    static function COALESCE() {}
+    static function DATEDIFF() {}
+}
+
+class SqlExpression {
+    static function plus() {}
+    static function times() {}
+    static function bitor() {}
+    static function bitand() {}
+}
+
+class SqlInterval {
+    static function MINUTE() {}
+    static function DAY() {}
+}
+
+class SqlAggregate {
+    static function COUNT() {}
+}
+
+class Q {
+    static function ANY() {}
+}
+
+class IntlDateFormatter {
+    function setPattern() {}
+    function getPattern() {}
+    function parse() {}
+}
+
+class ResourceBundle {
+    function getLocales() {}
+}
+
+class NumberFormatter {
+    function getSymbol() {}
+}
+
+class Collator {
+    function setStrength() {}
 }
 
 class Aws_Route53_Client {
diff --git a/setup/test/tests/test.header_functions.php b/setup/test/tests/test.header_functions.php
index 0b2328ce4f30cd42ea6ee09a3e51fd138ffffb91..852241b1b0af5e1da92a237fbc2899d892f11296 100644
--- a/setup/test/tests/test.header_functions.php
+++ b/setup/test/tests/test.header_functions.php
@@ -1,7 +1,5 @@
 <?php
 require_once "class.test.php";
-define('INCLUDE_DIR', realpath(dirname(__file__).'/../../../include').'/');
-define('PEAR_DIR', INCLUDE_DIR.'/pear/');
 require_once INCLUDE_DIR."class.mailparse.php";
 
 abstract class Priorities {
diff --git a/setup/test/tests/test.mail-parse.php b/setup/test/tests/test.mail-parse.php
index d1d80c225bc2ac846e62aa17b71d2a2f93128130..8d1350f0f178cc4d9d81f73b5561e25a4075f66a 100644
--- a/setup/test/tests/test.mail-parse.php
+++ b/setup/test/tests/test.mail-parse.php
@@ -1,5 +1,7 @@
 <?php
 
+require_once 'mockdb.php';
+
 require_once INCLUDE_DIR.'class.validator.php';
 require_once INCLUDE_DIR.'class.auth.php';
 require_once INCLUDE_DIR.'class.staff.php';
@@ -7,8 +9,6 @@ require_once INCLUDE_DIR.'class.email.php';
 require_once INCLUDE_DIR.'class.format.php';
 require_once INCLUDE_DIR.'class.thread.php';
 
-require_once 'mockdb.php';
-
 class TestMailParsing extends Test {
     var $name = "Mail parsing library tests";
 
diff --git a/tickets.php b/tickets.php
index 752a2416a8edfbb23a032de5f19bd1398781df93..8ba0bd644349edee72c61853278ff5b622af799b 100644
--- a/tickets.php
+++ b/tickets.php
@@ -35,41 +35,43 @@ if($_REQUEST['id']) {
 if (!$ticket && $thisclient->isGuest())
     Http::redirect('view.php');
 
-$tform = TicketForm::objects()->one();
+$tform = TicketForm::objects()->one()->getForm();
 $messageField = $tform->getField('message');
 $attachments = $messageField->getWidget()->getAttachments();
 
 //Process post...depends on $ticket object above.
-if($_POST && is_object($ticket) && $ticket->getId()):
+if ($_POST && is_object($ticket) && $ticket->getId()) {
     $errors=array();
     switch(strtolower($_POST['a'])){
     case 'edit':
         if(!$ticket->checkUserAccess($thisclient) //double check perm again!
                 || $thisclient->getId() != $ticket->getUserId())
             $errors['err']=__('Access Denied. Possibly invalid ticket ID');
-        elseif (!$cfg || !$cfg->allowClientUpdates())
-            $errors['err']=__('Access Denied. Client updates are currently disabled');
         else {
             $forms=DynamicFormEntry::forTicket($ticket->getId());
+            $changes = array();
             foreach ($forms as $form) {
+                $form->filterFields(function($f) { return !$f->isStorable(); });
                 $form->setSource($_POST);
                 if (!$form->isValid())
                     $errors = array_merge($errors, $form->errors());
             }
         }
         if (!$errors) {
-            foreach ($forms as $f) $f->save();
+            foreach ($forms as $f) {
+                $changes += $f->getChanges();
+                $f->save();
+            }
+            if ($changes)
+                $ticket->logEvent('edited', array('fields' => $changes));
             $_REQUEST['a'] = null; //Clear edit action - going back to view.
-            $ticket->logNote(__('Ticket details updated'), sprintf(
-                __('Ticket details were updated by client %s &lt;%s&gt;'),
-                $thisclient->getName(), $thisclient->getEmail()));
         }
         break;
     case 'reply':
         if(!$ticket->checkUserAccess($thisclient)) //double check perm again!
             $errors['err']=__('Access Denied. Possibly invalid ticket ID');
 
-        $_POST['message'] = ThreadBody::clean($_POST['message']);
+        $_POST['message'] = ThreadEntryBody::clean($_POST['message']);
         if (!$_POST['message'])
             $errors['message'] = __('Message required');
 
@@ -103,20 +105,31 @@ if($_POST && is_object($ticket) && $ticket->getId()):
     default:
         $errors['err']=__('Unknown action');
     }
-    $ticket->reload();
-endif;
+}
+elseif (is_object($ticket) && $ticket->getId()) {
+    switch(strtolower($_REQUEST['a'])) {
+    case 'print':
+        if (!$ticket || !$ticket->pdfExport($_REQUEST['psize']))
+            $errors['err'] = __('Internal error: Unable to export the ticket to PDF for print.');
+        break;
+    }
+}
+
 $nav->setActiveNav('tickets');
 if($ticket && $ticket->checkUserAccess($thisclient)) {
     if (isset($_REQUEST['a']) && $_REQUEST['a'] == 'edit'
-            && $cfg->allowClientUpdates()) {
+            && $ticket->hasClientEditableFields()) {
         $inc = 'edit.inc.php';
         if (!$forms) $forms=DynamicFormEntry::forTicket($ticket->getId());
         // Auto add new fields to the entries
-        foreach ($forms as $f) $f->addMissingFields();
+        foreach ($forms as $f) {
+            $f->filterFields(function($f) { return !$f->isStorable(); });
+            $f->addMissingFields();
+        }
     }
     else
         $inc='view.inc.php';
-} elseif($thisclient->getNumTickets()) {
+} elseif($thisclient->getNumTickets($thisclient->canSeeOrgTickets())) {
     $inc='tickets.inc.php';
 } else {
     $nav->setActiveNav('new');
@@ -124,8 +137,6 @@ if($ticket && $ticket->checkUserAccess($thisclient)) {
 }
 include(CLIENTINC_DIR.'header.inc.php');
 include(CLIENTINC_DIR.$inc);
-if ($tform instanceof DynamicFormEntry)
-    $tform = $tform->getForm();
 print $tform->getMedia();
 include(CLIENTINC_DIR.'footer.inc.php');
 ?>