diff --git a/api/api.inc.php b/api/api.inc.php
index 926a0e2a9ddb4b0464c5f1385e56665312339d8e..48836382022d6b8ddec422e0afc46ee074930567 100644
--- a/api/api.inc.php
+++ b/api/api.inc.php
@@ -42,7 +42,7 @@ function api_exit($code,$msg='') {
         //Error occured...
         $_SESSION['api']['errors']+=1;
         $_SESSION['api']['time']=time();
-        $ost->logWarning("API error - code #$code",$msg);
+        $ost->logWarning("API error - code #$code", $msg, ($_SESSION['api']['errors']>10));
         //echo "API Error:.$msg";
     }
     if($remotehost){
@@ -66,19 +66,20 @@ function api_exit($code,$msg='') {
 }
 
 //Remote hosts need authorization.
+$apikey = null;
 if($remotehost) {
-
-    $ip=$_SERVER['REMOTE_ADDR'];
-    $key=$_SERVER['HTTP_USER_AGENT']; //pulling all tricks.
-    //Upto 10 consecutive errors allowed...before a 5 minute timeout.
+    //Upto 10 consecutive errors allowed...before a 2 minute timeout.
     //One more error during timeout and timeout starts a new clock
-    if($_SESSION['api']['errors']>10 && (time()-$_SESSION['api']['time'])<=5*60) { // timeout!
-        api_exit(EX_NOPERM,"Remote host [$ip] in timeout - error #".$_SESSION['api']['errors']);
-    }
-    //Check API key & ip
-    if(!Validator::is_ip($ip) || !Api::validate($key,$ip)) { 
-        api_exit(EX_NOPERM,'Unknown remote host ['.$ip.'] or invalid API key ['.$key.']');
-    }
+    if($_SESSION['api']['errors']>10 && (time()-$_SESSION['api']['time'])<=2*60)  // timeout!
+        api_exit(EX_NOPERM, 'Remote host ['.$_SERVER['REMOTE_ADDR'].'] in timeout - error #'.$_SESSION['api']['errors']);
+        
+    if(!isset($_SERVER['HTTP_X_API_KEY']) || !isset($_SERVER['REMOTE_ADDR']))
+        api_exit(EX_NOPERM, 'API key required');
+    elseif(!($apikey=API::lookupByKey($_SERVER['HTTP_X_API_KEY'], $_SERVER['REMOTE_ADDR']))
+                || !$apikey->isActive()
+                || $apikey->getIPAddr()!=$_SERVER['REMOTE_ADDR'])
+        api_exit(EX_NOPERM, 'API key not found/active or source IP not authorized');
+    
     //At this point we know the remote host/IP is allowed.
     $_SESSION['api']['errors']=0; //clear errors for the session.
 }
diff --git a/api/pipe.php b/api/pipe.php
index 699e7400001c8f497a5bbd30b5d921ca353abe2b..ff23cfa1b56c0f6fa75824f8106337c7440f6cb9 100644
--- a/api/pipe.php
+++ b/api/pipe.php
@@ -16,6 +16,7 @@
 **********************************************************************/
 @chdir(realpath(dirname(__FILE__)).'/'); //Change dir.
 ini_set('memory_limit', '256M'); //The concern here is having enough mem for emails with attachments.
+$apikey = null;
 require('api.inc.php');
 require_once(INCLUDE_DIR.'class.mailparse.php');
 require_once(INCLUDE_DIR.'class.email.php');
@@ -23,6 +24,9 @@ require_once(INCLUDE_DIR.'class.email.php');
 //Make sure piping is enabled!
 if(!$cfg->isEmailPipingEnabled())
     api_exit(EX_UNAVAILABLE,'Email piping not enabled - check MTA settings.');
+elseif($apikey && !$apikey->canCreateTickets()) //apikey is ONLY set on remote post - local post don't need a key (for now).
+    api_exit(EX_NOPERM, 'API key not authorized');
+
 //Get the input
 $data=isset($_SERVER['HTTP_HOST'])?file_get_contents('php://input'):file_get_contents('php://stdin');
 if(empty($data)){
@@ -77,8 +81,8 @@ $name=trim($from->personal,'"');
 if($from->comment && $from->comment[0])
     $name.=' ('.$from->comment[0].')';
 $subj=utf8_encode($parser->getSubject());
-if(!($body=Format::stripEmptyLines($parser->getBody())) && $subj)
-    $body=$subj;
+if(!($body=Format::stripEmptyLines($parser->getBody())))
+    $body=$subj?$subj:'(EMPTY)';
 
 $var['mid']=$parser->getMessageId();
 $var['email']=$from->mailbox.'@'.$from->host;
diff --git a/include/ajax.reports.php b/include/ajax.reports.php
index 0ad0640a3d64270eda544011c2aa7ec28d101acb..bf86374055bd45b8e07a1860a157d77d3b53aa64 100644
--- a/include/ajax.reports.php
+++ b/include/ajax.reports.php
@@ -80,6 +80,8 @@ class OverviewReportAjaxAPI extends AjaxController {
                       (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())).')'):'')
                      .')'
                      ) 
             )
diff --git a/include/api.ticket.php b/include/api.ticket.php
index 51e129ad41e5cfe691517fbdb1a180326ecec4f7..7fd5ba713c8237aad483c4a44b08989cf9b17c3f 100644
--- a/include/api.ticket.php
+++ b/include/api.ticket.php
@@ -22,7 +22,9 @@ class TicketController extends ApiController {
     }
 
     function create($format) {
-        $this->requireApiKey();
+
+        if(!($key=$this->getApiKey()) || !$key->canCreateTickets())
+            Http::response(401, 'API key not authorized');
 
         # Parse request body
         $data = $this->getRequest($format);
diff --git a/include/class.api.php b/include/class.api.php
index f3d3094a5b695a1601cb0d91e5dafdfe6014918b..df8cfb0c8a9cb9f452b62de56c1e15181731d614 100644
--- a/include/class.api.php
+++ b/include/class.api.php
@@ -17,102 +17,115 @@ class API {
 
     var $id;
 
-    var $info;
+    var $ht;
 
-    function API($id){
-        $this->id=0;
+    function API($id) {
+        $this->id = 0;
         $this->load($id);
     }
 
-    function load($id) {
+    function load($id=0) {
+
+        if(!$id && !($id=$this->getId()))
+            return false;
 
         $sql='SELECT * FROM '.API_KEY_TABLE.' WHERE id='.db_input($id);
-        if(($res=db_query($sql)) && db_num_rows($res)) {
-            $info=db_fetch_array($res);
-            $this->id=$info['id'];
-            $this->info=$info;
-            return true;
-        }
-        return false;
+        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($this->getId());
+        return $this->load();
     }
 
-    function getId(){
+    function getId() {
         return $this->id;
     }
 
-    function getKey(){
-        return $this->info['apikey'];
+    function getKey() {
+        return $this->ht['apikey'];
     }
 
-    function getIPAddr(){
-        return $this->info['ipaddr'];
+    function getIPAddr() {
+        return $this->ht['ipaddr'];
     }
         
-    function getNotes(){
-        return $this->info['notes'];
+    function getNotes() {
+        return $this->ht['notes'];
     }
 
-    function isActive(){
-        return ($this->info['isactive']);
+    function getHashtable() {
+        return $this->ht;
     }
 
-    function update($vars,&$errors){
-        if(API::save($this->getId(),$vars,$errors)){
-            $this->reload();
-            return true;
-        }
+    function isActive() {
+        return ($this->ht['isactive']);
+    }
+
+    function canCreateTickets() {
+        return ($this->ht['can_create_tickets']);
+    }
+
+    function update($vars, &$errors) {
+
+        if(!API::save($this->getId(), $vars, $errors))
+            return false;
         
-        return false;
+        $this->reload();
+
+        return true;
     }
 
-    function delete(){
+    function delete() {
         $sql='DELETE FROM '.API_KEY_TABLE.' WHERE id='.db_input($this->getId()).' LIMIT 1';
         return (db_query($sql) && ($num=db_affected_rows()));
     }
 
     /** Static functions **/
-    function add($vars,&$errors){
-        return API::save(0,$vars,$errors);
+    function add($vars, &$errors) {
+        return API::save(0, $vars, $errors);
     }
 
-    function validate($key,$ip){
-
-        $sql='SELECT id FROM '.API_KEY_TABLE.' WHERE ipaddr='.db_input($ip).' AND apikey='.db_input($key);
-        return (($res=db_query($sql)) && db_num_rows($res));
+    function validate($key, $ip) {
+        return ($key && $ip && self::getIdByKey($key, $ip));
     }
 
-    function getKeyByIPAddr($ip){
+    function getIdByKey($key, $ip='') {
 
-        $sql='SELECT apikey FROM '.API_KEY_TABLE.' WHERE ipaddr='.db_input($ip);
+        $sql='SELECT id FROM '.API_KEY_TABLE.' WHERE apikey='.db_input($key);
+        if($ip)
+            $sql.=' AND ipaddr='.db_input($ip);
+        
         if(($res=db_query($sql)) && db_num_rows($res))
-            list($key)=db_fetch_row($res);
+            list($id) = db_fetch_row($res);
 
-        return $key;
+        return $id;
     }
 
-    function lookup($id){
-        return ($id && is_numeric($id) && ($k= new API($id)) && $k->getId()==$id)?$k:null;
+    function lookupByKey($key, $ip='') {
+        return self::lookup(self::getIdByKey($key, $ip));
     }
 
-    function save($id,$vars,&$errors){
+    function lookup($id) {
+        return ($id && is_numeric($id) && ($k= new API($id)) && $k->getId()==$id)?$k:null;
+    }
 
-        if(!$id) {
-            if(!$vars['ipaddr'] || !Validator::is_ip($vars['ipaddr']))
-                $errors['ipaddr']='Valid IP required';
-            elseif(API::getKeyByIPAddr($vars['ipaddr']))
-                $errors['ipaddr']='API key for the IP already exists';
-        }
+    function save($id, $vars, &$errors) {
 
+        if(!$id && (!$vars['ipaddr'] || !Validator::is_ip($vars['ipaddr'])))
+            $errors['ipaddr'] = 'Valid IP required';
+        
         if($errors) return false;
 
-
-        $sql=' updated=NOW() '.
-             ',isactive='.db_input($vars['isactive']).
-             ',notes='.db_input($vars['notes']);
+        $sql=' updated=NOW() '
+            .',isactive='.db_input($vars['isactive'])
+            .',can_create_tickets='.db_input($vars['can_create_tickets'])
+            .',notes='.db_input($vars['notes']);
 
         if($id) {
             $sql='UPDATE '.API_KEY_TABLE.' SET '.$sql.' WHERE id='.db_input($id);
@@ -120,14 +133,17 @@ class API {
                 return true;
 
             $errors['err']='Unable to update API key. Internal error occurred';
-        }else{
-            $sql='INSERT INTO '.API_KEY_TABLE.' SET '.$sql.',created=NOW() '.
-                 ',ipaddr='.db_input($vars['ipaddr']).
-                 ',apikey='.db_input(strtoupper(md5(time().$vars['ipaddr'].md5(Misc::randcode(16)))));
+
+        } else {
+            $sql='INSERT INTO '.API_KEY_TABLE.' SET '.$sql
+                .',created=NOW() '
+                .',ipaddr='.db_input($vars['ipaddr'])
+                .',apikey='.db_input(strtoupper(md5(time().$vars['ipaddr'].md5(Misc::randcode(16)))));
+
             if(db_query($sql) && ($id=db_insert_id()))
                 return $id;
 
-            $errors['err']='Unable to add API key. Internal error';
+            $errors['err']='Unable to add API key. Try again!';
         }
 
         return false;
@@ -141,16 +157,24 @@ class API {
  * API request.
  */
 class ApiController {
+
     function requireApiKey() {
         # Validate the API key -- required to be sent via the X-API-Key
         # header
-        if (!isset($_SERVER['HTTP_X_API_KEY']))
+        if (!isset($_SERVER['HTTP_X_API_KEY']) || !isset($_SERVER['REMOTE_ADDR']))
             Http::response(403, "API key required");
-        else if (!Api::validate($_SERVER['HTTP_X_API_KEY'],
-                $_SERVER['REMOTE_ADDR']))
-            Http::response(401,
-                "API key not found or source IP not authorized");
+        elseif (!($key=API::lookupByKey($_SERVER['HTTP_X_API_KEY'], $_SERVER['REMOTE_ADDR']))
+                || !$key->isActive() 
+                || $key->getIPAddr()!=$_SERVER['REMOTE_ADDR'])
+            Http::response(401, "API key not found/active or source IP not authorized");
+
+        return $key;
     }
+
+    function getApiKey() {
+        return $this->requireApiKey();
+    }
+
     /**
      * Retrieves the body of the API request and converts it to a common
      * hashtable. For JSON formats, this is mostly a noop, the conversion
@@ -248,7 +272,7 @@ class ApiJsonDataParser extends JsonDataParser {
             return $current;
         foreach ($current as $key=>&$value) {
             if ($key == "phone") {
-                list($value,$current["phone_ext"])
+                list($value, $current["phone_ext"])
                     = explode("X", strtoupper($value), 2); 
             } else if ($key == "alert") {
                 $value = (bool)$value;
diff --git a/include/class.filter.php b/include/class.filter.php
index b69a98987aeca7081a92f101e0dec00b50928bc1..0af8330b8022393162450c803031b000a7471ab0 100644
--- a/include/class.filter.php
+++ b/include/class.filter.php
@@ -233,7 +233,9 @@ class Filter {
             'equal'     => array('strcmp', 0),
             'not_equal' => array('strcmp', null, 0),
             'contains'  => array('strpos', null, false),
-            'dn_contain'=> array('strpos', false)
+            'dn_contain'=> array('strpos', false),
+            'starts'    => array('strpos', 0),
+            'ends'      => array('endsWith', true)
         );
 
         $match = false;
@@ -309,7 +311,9 @@ class Filter {
             'equal'=>       'Equal',
             'not_equal'=>   'Not Equal',
             'contains'=>    'Contains',
-            'dn_contain'=>  'Does Not Contain'
+            'dn_contain'=>  'Does Not Contain',
+            'starts'=>      'Starts With',
+            'ends'=>        'Ends With'
         );
     }
 
@@ -366,8 +370,8 @@ class Filter {
 
     function save_rules($id,$vars,&$errors) {
 
-        $matches=array('name','email','subject','body','header');
-        $types=array('equal','not_equal','contains','dn_contain');
+        $matches = array_keys(self::getSupportedMatches());
+        $types = array_keys(self::getSupportedMatchTypes());
 
         $rules=array();
         for($i=1; $i<=25; $i++) { //Expecting no more than 25 rules...
@@ -942,4 +946,20 @@ class TicketFilter {
         return $sources[strtolower($origin)];
     }
 }
+
+/**
+ * Function: endsWith
+ *
+ * Returns TRUE if the haystack ends with needle and FALSE otherwise.
+ * Thanks, http://stackoverflow.com/a/834355
+ */
+function endsWith($haystack, $needle)
+{
+    $length = strlen($needle);
+    if ($length == 0) {
+        return true;
+    }
+
+    return (substr($haystack, -$length) === $needle);
+}
 ?>
diff --git a/include/class.group.php b/include/class.group.php
index b13082be9f24301e12bf298cac0d043f86b7f8d1..10a5eadafe9525f0c044c46d918924c00e2451cc 100644
--- a/include/class.group.php
+++ b/include/class.group.php
@@ -209,6 +209,8 @@ class Group {
             .', 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($vars['notes']);
             
         if($id) {
diff --git a/include/class.staff.php b/include/class.staff.php
index 9c6078c7f148e3a01612c05cac9c796422871269..102e79ecda4ae642dc7a777f75143b3148078a22 100644
--- a/include/class.staff.php
+++ b/include/class.staff.php
@@ -302,7 +302,7 @@ class Staff {
     function canEditTickets() {
         return ($this->ht['can_edit_tickets']);
     }
-    
+
     function canDeleteTickets() {
         return ($this->ht['can_delete_tickets']);
     }
@@ -311,6 +311,14 @@ class Staff {
         return ($this->ht['can_close_tickets']);
     }
 
+    function canPostReply() {
+        return ($this->ht['can_post_ticket_reply']);
+    }
+
+    function canViewStaffStats() {
+        return ($this->ht['can_view_staff_stats']);
+    }
+
     function canAssignTickets() {
         return ($this->ht['can_assign_tickets']);
     }
diff --git a/include/class.topic.php b/include/class.topic.php
index 900ff76168363c47e9b4763fee6cf2f00334a0fd..ee579e45b2d74e292ff9fd118a5547e32aa2b553 100644
--- a/include/class.topic.php
+++ b/include/class.topic.php
@@ -165,10 +165,13 @@ class Topic {
         return self::getHelpTopics(true);
     }
 
-    function getIdByName($topic) {
-        $sql='SELECT topic_id FROM '.TOPIC_TABLE.' WHERE topic='.db_input($topic);
+    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);
+            list($id) = db_fetch_row($res);
 
         return $id;
     }
@@ -188,7 +191,7 @@ class Topic {
             $errors['topic']='Help topic required';
         elseif(strlen($vars['topic'])<5)
             $errors['topic']='Topic is too short. 5 chars minimum';
-        elseif(($tid=self::getIdByName($vars['topic'])) && $tid!=$id)
+        elseif(($tid=self::getIdByName($vars['topic'], $vars['pid'])) && $tid!=$id)
             $errors['topic']='Topic already exists';
 
         if(!$vars['dept_id'])
diff --git a/include/staff/apikey.inc.php b/include/staff/apikey.inc.php
index ff5592b46fd57b374bb0a2d0825f99d3156bceef..6e2ffe6e6d8fdc4d41134726e6853162f3bfe0da 100644
--- a/include/staff/apikey.inc.php
+++ b/include/staff/apikey.inc.php
@@ -6,9 +6,7 @@ if($api && $_REQUEST['a']!='add'){
     $title='Update API Key';
     $action='update';
     $submit_text='Save Changes';
-    $info['id']=$api->getId();
-    $info['isactive']=$api->isActive()?1:0;
-    $info['notes']=$api->getNotes();
+    $info=$api->getHashtable();
     $qstr.='&id='.$api->getId();
 }else {
     $title='Add New API Key';
@@ -30,13 +28,13 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
         <tr>
             <th colspan="2">
                 <h4><?php echo $title; ?></h4>
-                <em>API Key is autogenerated and unique per IP address.</em>
+                <em>API Key is auto-generated. Delete and re-add to change the key.</em>
             </th>
         </tr>
     </thead>
     <tbody>
         <tr>
-            <td width="180" class="required">
+            <td width="150" class="required">
                 Status:
             </td>
             <td>
@@ -47,7 +45,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
         </tr>
         <?php if($api){ ?>
         <tr>
-            <td width="180">
+            <td width="150">
                 IP Address:
             </td>
             <td>
@@ -55,14 +53,14 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
             </td>
         </tr>
         <tr>
-            <td width="180">
+            <td width="150">
                 API Key:
             </td>
-            <td><?php echo $api->getKey(); ?> &nbsp;&nbsp;<em>(Delete and re-add to change the key)</em></td>
+            <td><?php echo $api->getKey(); ?> &nbsp;</td>
         </tr>
         <?php }else{ ?>
         <tr>
-            <td width="180" class="required">
+            <td width="150" class="required">
                IP Address:
             </td>
             <td>
@@ -73,16 +71,13 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
         <?php } ?>
         <tr>
             <th colspan="2">
-                <em><strong>Enabled Services:</strong>: Check applicable API services.</em>
+                <em><strong>Enabled Services:</strong>: Check applicable API services. All active keys can make cron call.</em>
             </th>
         </tr>
         <tr>
-            <td width="180">
-                Email piping:
-            </td>
-            <td>
-                <input type="checkbox" name="email_piping" value="1" checked="checked" disabled="disabled" >
-                    <strong>Enable</strong> remote email piping.
+            <td colspan=2 style="padding-left:5px">
+                <input type="checkbox" name="can_create_tickets" value="1" <?php echo $info['can_create_tickets']?'checked="checked"':''; ?> >
+                Can Create Tickets. <em>(XML/JSON/PIPE)</em>
             </td>
         </tr>
         <tr>
diff --git a/include/staff/apikeys.inc.php b/include/staff/apikeys.inc.php
index b085329343d101fe69de9b4628ed13ace6d6586b..90b4e31eef10af37b71ba2ebef6edffc9dc63d7b 100644
--- a/include/staff/apikeys.inc.php
+++ b/include/staff/apikeys.inc.php
@@ -5,7 +5,7 @@ $qstr='';
 $sql='SELECT * FROM '.API_KEY_TABLE.' WHERE 1';
 $sortOptions=array('key'=>'apikey','status'=>'isactive','ip'=>'ipaddr','date'=>'created','created'=>'created','updated'=>'updated');
 $orderWays=array('DESC'=>'DESC','ASC'=>'ASC');
-$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'date';
+$sort=($_REQUEST['sort'] && $sortOptions[strtolower($_REQUEST['sort'])])?strtolower($_REQUEST['sort']):'key';
 //Sorting options...
 if($sort && $sortOptions[$sort]) {
     $order_column =$sortOptions[$sort];
@@ -54,10 +54,10 @@ else
     <thead>
         <tr>
             <th width="7">&nbsp;</th>        
-            <th width="150" nowrap><a  <?php echo $date_sort; ?>href="apikeys.php?<?php echo $qstr; ?>&sort=date">Date Added</a></th>
             <th width="320"><a <?php echo $key_sort; ?> href="apikeys.php?<?php echo $qstr; ?>&sort=key">API Key</a></th>
-            <th width="100"><a  <?php echo $status_sort; ?> href="apikeys.php?<?php echo $qstr; ?>&sort=status">Status</a></th>
             <th width="120"><a  <?php echo $ip_sort; ?> href="apikeys.php?<?php echo $qstr; ?>&sort=ip">IP Addr.</a></th>
+            <th width="100"><a  <?php echo $status_sort; ?> href="apikeys.php?<?php echo $qstr; ?>&sort=status">Status</a></th>
+            <th width="150" nowrap><a  <?php echo $date_sort; ?>href="apikeys.php?<?php echo $qstr; ?>&sort=date">Date Added</a></th>
             <th width="150" nowrap><a  <?php echo $updated_sort; ?>href="apikeys.php?<?php echo $qstr; ?>&sort=updated">Last Updated</a></th>
         </tr>
     </thead>
@@ -75,10 +75,10 @@ else
                 <td width=7px>
                   <input type="checkbox" class="ckb" name="ids[]" value="<?php echo $row['id']; ?>" 
                             <?php echo $sel?'checked="checked"':''; ?>> </td>
-                <td>&nbsp;<?php echo Format::db_date($row['created']); ?></td>
                 <td>&nbsp;<a href="apikeys.php?id=<?php echo $row['id']; ?>"><?php echo Format::htmlchars($row['apikey']); ?></a></td>
-                <td><?php echo $row['isactive']?'Active':'<b>Disabled</b>'; ?></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>
             </tr>
             <?php
diff --git a/include/staff/group.inc.php b/include/staff/group.inc.php
index f2935d72e56237ce35dbc701f563fdeaf475a829..bfcc2a596a54850211a71ff72ea95cb698ecdb57 100644
--- a/include/staff/group.inc.php
+++ b/include/staff/group.inc.php
@@ -76,6 +76,14 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 &nbsp;&nbsp;<i>Ability to edit tickets.</i>
             </td>
         </tr>
+        <tr><td>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"':''; ?> />Yes
+                &nbsp;&nbsp;
+                <input type="radio" name="can_post_ticket_reply"  value="0"   <?php echo !$info['can_post_ticket_reply']?'checked="checked"':''; ?> />No
+                &nbsp;&nbsp;<i>Ability to post a ticket reply.</i>
+            </td>
+        </tr>
         <tr><td>Can <b>Close</b> Tickets</td>
             <td>
                 <input type="radio" name="can_close_tickets"  value="1" <?php echo $info['can_close_tickets']?'checked="checked"':''; ?> />Yes
@@ -105,7 +113,7 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 <input type="radio" name="can_delete_tickets"  value="1"   <?php echo $info['can_delete_tickets']?'checked="checked"':''; ?> />Yes
                 &nbsp;&nbsp;
                 <input type="radio" name="can_delete_tickets"  value="0"   <?php echo !$info['can_delete_tickets']?'checked="checked"':''; ?> />No
-                &nbsp;&nbsp;<i>Deleted tickets can't be recovered!</i>
+                &nbsp;&nbsp;<i>Ability to delete tickets (Deleted tickets can't be recovered!)</i>
             </td>
         </tr>
         <tr><td>Can Ban Emails</td>
@@ -132,6 +140,14 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info);
                 &nbsp;&nbsp;<i>Ability to add/update/disable/delete knowledgebase categories and FAQs.</i>
             </td>
         </tr>
+        <tr><td>Can View Staff Stats.</td>
+            <td>
+                <input type="radio" name="can_view_staff_stats"  value="1" <?php echo $info['can_view_staff_stats']?'checked="checked"':''; ?> />Yes
+                &nbsp;&nbsp;
+                <input type="radio" name="can_view_staff_stats"  value="0" <?php echo !$info['can_view_staff_stats']?'checked="checked"':''; ?> />No
+                &nbsp;&nbsp;<i>Ability to view stats of other staff members in allowed departments.</i>
+            </td>
+        </tr>
         <tr>
             <th colspan="2">
                 <em><strong>Department Access</strong>: Check all departments the group members are allowed to access.&nbsp;&nbsp;&nbsp;<a id="selectAll" href="#deptckb">Select All</a>&nbsp;&nbsp;<a id="selectNone" href="#deptckb">Select None</a>&nbsp;&nbsp;</em>
diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php
index 53bdd909b901e9c61be7ba1da9579c5e5bb81aef..b14a4654af003d0c5dd6b85635d8d91a63048940 100644
--- a/include/staff/ticket-view.inc.php
+++ b/include/staff/ticket-view.inc.php
@@ -348,7 +348,11 @@ if(!$cfg->showNotesInline()) { ?>
 
 <div id="response_options">
     <ul>
+        <?php
+        if($thisstaff->canPostReply()) { ?>
         <li><a id="reply_tab" href="#reply">Post Reply</a></li>
+        <?php
+        } ?>
         <li><a id="note_tab" href="#note">Post Internal Note</a></li>
         <?php
         if($thisstaff->canTransferTickets()) { ?>
@@ -361,12 +365,12 @@ if(!$cfg->showNotesInline()) { ?>
         <?php
         } ?>
     </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">
         <?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="locktime" value="<?php echo $cfg->getLockTime(); ?>">
         <input type="hidden" name="a" value="reply">
         <span class="error"></span>
         <table border="0" cellspacing="0" cellpadding="3">
@@ -485,9 +489,12 @@ if(!$cfg->showNotesInline()) { ?>
             <input class="btn_sm" type="reset" value="Reset">
         </p>
     </form>
+    <?php
+    } ?>
     <form id="note" 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="a" value="postnote">
         <table border="0" cellspacing="0" cellpadding="3">
             <?php 
diff --git a/include/upgrader/sql/00ff231f-9f3b454c.patch.sql b/include/upgrader/sql/00ff231f-9f3b454c.patch.sql
new file mode 100644
index 0000000000000000000000000000000000000000..7c3e3bd3c681e1e75f07cb87fe5496c5116da859
--- /dev/null
+++ b/include/upgrader/sql/00ff231f-9f3b454c.patch.sql
@@ -0,0 +1,38 @@
+/**
+ * @version v1.7 RC4
+ * @signature 9f3b454c06dfd5ee96003eae5182ac13
+ *
+ *  - Supports starts- and ends-with in ticket filter rules
+ *  - Fix assigned template variable
+ *  - Allow nested templates to have duplicate names
+ *  - New permission settings for API key  & groups
+ */
+
+ALTER TABLE  `%TABLE_PREFIX%filter_rule` CHANGE  `how`  `how` ENUM(  'equal',
+    'not_equal',  'contains',  'dn_contain',  'starts',  'ends' );
+
+-- templates -> %message
+UPDATE `%TABLE_PREFIX%email_template`
+    SET `assigned_alert_body` = REPLACE(`assigned_alert_body`, '%message', '%{comments}');
+
+-- API Access.
+ALTER TABLE  `%TABLE_PREFIX%api_key`
+    CHANGE  `isactive`  `isactive` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT  '1',
+    ADD  `can_create_tickets` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT  '1' AFTER  `apikey`,
+    DROP INDEX  `ipaddr`,
+    ADD INDEX  `ipaddr` (  `ipaddr` );
+
+-- Help topics 
+ALTER TABLE  `%TABLE_PREFIX%help_topic` 
+    DROP INDEX  `topic` ,
+    ADD UNIQUE  `topic` (  `topic` ,  `topic_pid` );
+
+
+-- group settings.
+ALTER TABLE  `%TABLE_PREFIX%groups` 
+    ADD  `can_post_ticket_reply` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT  '1' AFTER  `can_transfer_tickets` ,
+    ADD  `can_view_staff_stats` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT  '0' AFTER  `can_post_ticket_reply`;
+
+-- update schema signature.
+UPDATE `%TABLE_PREFIX%config`
+    SET `schema_signature`='9f3b454c06dfd5ee96003eae5182ac13';
diff --git a/main.inc.php b/main.inc.php
index 5b5fc593165e2c627cf4bcbf75ce58125501b0d2..baf06e4ec7773b2d104b517e4bde2aa3e46d2e5f 100644
--- a/main.inc.php
+++ b/main.inc.php
@@ -63,7 +63,7 @@
 
     #Current version && schema signature (Changes from version to version)
     define('THIS_VERSION','1.7-RC3+'); //Shown on admin panel
-    define('SCHEMA_SIGNATURE','00ff231f2ade8797a0e7f2a7fccd52f4'); //MD5 signature of the db schema. (used to trigger upgrades)
+    define('SCHEMA_SIGNATURE','9f3b454c06dfd5ee96003eae5182ac13'); //MD5 signature of the db schema. (used to trigger upgrades)
     #load config info
     $configfile='';
     if(file_exists(ROOT_DIR.'ostconfig.php')) //Old installs prior to v 1.6 RC5
diff --git a/scp/js/ticket.js b/scp/js/ticket.js
index 65ee84aad0882649f8aec44daf447b45d38e64d9..8cb792d1c403661063ea564c3bff3b8938bc9d50 100644
--- a/scp/js/ticket.js
+++ b/scp/js/ticket.js
@@ -94,7 +94,7 @@ var autoLock = {
     Init: function(config) {
 
         //make sure we are on ticket view page & locking is enabled!
-        var fObj=$('form#reply');
+        var fObj=$('form#note');
         if(!fObj 
                 || !$(':input[name=id]',fObj).length 
                 || !$(':input[name=locktime]',fObj).length
diff --git a/scp/js/upgrader.js b/scp/js/upgrader.js
index 8fe378193842494d7bed4df290828e05e091a263..1631ac5cd5a7a156498f44e6f13020dae875dde2 100644
--- a/scp/js/upgrader.js
+++ b/scp/js/upgrader.js
@@ -8,7 +8,7 @@ jQuery(function($) {
         height  : $(window).height()
         });
 
-    $("#loading").css({
+    $("#upgrading").css({
         top  : ($(window).height() / 3),
         left : ($(window).width() / 2 - 160)
         });
diff --git a/scp/tickets.php b/scp/tickets.php
index 1eee98d0c6dec68a90fd26bd8804f9c3190dae26..17384ae50b627ab56bb1b8e9fe5c0df01034e062 100644
--- a/scp/tickets.php
+++ b/scp/tickets.php
@@ -42,18 +42,23 @@ if($_POST && !$errors):
         $statusKeys=array('open'=>'Open','Reopen'=>'Open','Close'=>'Closed');
         switch(strtolower($_POST['a'])):
         case 'reply':
+            if(!$thisstaff->canPostReply())
+                $errors['err'] = 'Action denied. Contact admin for access';
+            else {
 
-            if(!$_POST['msgId'])
-                $errors['err']='Missing message ID - Internal error';
-            if(!$_POST['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(!$_POST['msgId'])
+                    $errors['err']='Missing message ID - Internal error';
+                if(!$_POST['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!';
             
-            //Make sure the email is not banned
-            if(!$errors['err'] && TicketFilter::isBanned($ticket->getEmail()))
-                $errors['err']='Email is in banlist. Must be removed to reply.';
+                //Make sure the email is not banned
+                if(!$errors['err'] && TicketFilter::isBanned($ticket->getEmail()))
+                    $errors['err']='Email is in banlist. Must be removed to reply.';
+            }
 
             $wasOpen =($ticket->isOpen());
             //If no error...do the do.
@@ -470,7 +475,7 @@ if($cfg->showAnsweredTickets()) {
 }
 
 if($stats['assigned']) {
-    if(!$ost->getWarning() && $stats['assigned']>3)
+    if(!$ost->getWarning() && $stats['assigned']>10)
         $ost->setWarning($stats['assigned'].' tickets assigned to you! Do something about it!');
 
     $nav->addSubMenu(array('desc'=>'My Tickets ('.$stats['assigned'].')',
diff --git a/setup/inc/sql/osTicket-mysql.sql b/setup/inc/sql/osTicket-mysql.sql
index 5d2f4ceefdb5d912ef5cf7b84f9b1e3a91508d75..004b9934092948798fc84adbe2e987af6006cd2b 100644
--- a/setup/inc/sql/osTicket-mysql.sql
+++ b/setup/inc/sql/osTicket-mysql.sql
@@ -5,11 +5,12 @@ CREATE TABLE `%TABLE_PREFIX%api_key` (
   `isactive` tinyint(1) NOT NULL default '1',
   `ipaddr` varchar(64) NOT NULL,
   `apikey` varchar(255) NOT NULL,
+  `can_create_tickets` TINYINT( 1 ) UNSIGNED NOT NULL DEFAULT  '1',
   `notes` text,
   `updated` datetime NOT NULL,
   `created` datetime NOT NULL,
   PRIMARY KEY  (`id`),
-  UNIQUE KEY `ipaddr` (`ipaddr`),
+  KEY `ipaddr` (`ipaddr`),
   UNIQUE KEY `apikey` (`apikey`)
 ) ENGINE=MyISAM  DEFAULT CHARSET=utf8;
 
@@ -133,7 +134,7 @@ CREATE TABLE `%TABLE_PREFIX%config` (
   `assigned_alert_team_members` tinyint(1) unsigned NOT NULL default '0',
   `auto_assign_reopened_tickets` tinyint(1) unsigned NOT NULL default '1',
   `show_related_tickets` tinyint(1) unsigned NOT NULL default '1',
-  `show_assigned_tickets` tinyint(1) unsigned NOT NULL default '0',
+  `show_assigned_tickets` tinyint(1) unsigned NOT NULL default '1',
   `show_answered_tickets` tinyint(1) unsigned NOT NULL default '0',
   `show_notes_inline` tinyint(1) unsigned NOT NULL default '1',
   `hide_staff_name` tinyint(1) unsigned NOT NULL default '0',
@@ -265,7 +266,7 @@ CREATE TABLE `%TABLE_PREFIX%filter_rule` (
   `id` int(11) unsigned NOT NULL auto_increment,
   `filter_id` int(10) unsigned NOT NULL default '0',
   `what` enum('name','email','subject','body','header') NOT NULL,
-  `how` enum('equal','not_equal','contains','dn_contain') NOT NULL,
+  `how` enum('equal','not_equal','contains','dn_contain','starts','ends') NOT NULL,
   `val` varchar(255) NOT NULL,
   `isactive` tinyint(1) unsigned NOT NULL DEFAULT '1',
   `notes` tinytext NOT NULL,
@@ -355,6 +356,7 @@ CREATE TABLE `%TABLE_PREFIX%groups` (
   `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',
@@ -362,6 +364,7 @@ CREATE TABLE `%TABLE_PREFIX%groups` (
   `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',
   `notes` text,
   `created` datetime NOT NULL,
   `updated` datetime NOT NULL,
@@ -402,7 +405,7 @@ CREATE TABLE `%TABLE_PREFIX%help_topic` (
   `created` datetime NOT NULL,
   `updated` datetime NOT NULL,
   PRIMARY KEY  (`topic_id`),
-  UNIQUE KEY `topic` (`topic`),
+  UNIQUE KEY `topic` ( `topic` ,  `topic_pid` ),
   KEY `topic_pid` (`topic_pid`),
   KEY `priority_id` (`priority_id`),
   KEY `dept_id` (`dept_id`),
diff --git a/setup/inc/sql/osTicket-mysql.sql.md5 b/setup/inc/sql/osTicket-mysql.sql.md5
index 4aa4254e6af1e5496dc39cf89726de1a607bab14..49a6e93dd4e013257c9abf0f1117e12241ee381c 100644
--- a/setup/inc/sql/osTicket-mysql.sql.md5
+++ b/setup/inc/sql/osTicket-mysql.sql.md5
@@ -1 +1 @@
-00ff231f2ade8797a0e7f2a7fccd52f4
+9f3b454c06dfd5ee96003eae5182ac13
diff --git a/setup/install.php b/setup/install.php
index 5f590da1c915d1c2c794e8363b41abb8a5bb3afa..130983a692155869064ae08b8ad47e88f4a6b2f2 100644
--- a/setup/install.php
+++ b/setup/install.php
@@ -105,7 +105,12 @@ switch(strtolower($_SESSION['ost_installer']['s'])) {
         break;
     default:
         //Fail IF any of the old config files exists.
-        if(file_exists(INCLUDE_DIR.'settings.php') || file_exists(ROOT_DIR.'ostconfig.php'))
+        if(file_exists(INCLUDE_DIR.'settings.php') 
+                || file_exists(ROOT_DIR.'ostconfig.php')
+                || (file_exists(OSTICKET_CONFIGFILE) 
+                    && preg_match("/define\('OSTINSTALLED',TRUE\)\;/i", 
+                        file_get_contents(OSTICKET_CONFIGFILE)))
+                )
             $inc='file-unclean.inc.php';
         else
             $inc='install-prereq.inc.php';
diff --git a/setup/upgrade.php b/setup/upgrade.php
new file mode 100644
index 0000000000000000000000000000000000000000..2dfc5b475031ca9cf727962648d7c83db381ede5
--- /dev/null
+++ b/setup/upgrade.php
@@ -0,0 +1,4 @@
+<?php
+/* Simply redirect to Admin Panel - new upgrade home */
+header('Location: ../scp/upgrade.php');
+?>