diff --git a/include/ajax.tickets.php b/include/ajax.tickets.php
index 6e746fbfe0d1181b5d069737569c4eecdbcf00a1..e13128529cc4fa4ba26d8b3cc0f1d03c4d7bb489 100644
--- a/include/ajax.tickets.php
+++ b/include/ajax.tickets.php
@@ -150,23 +150,31 @@ class TicketsAjaxAPI extends AjaxController {
     function releaseLock($tid, $id=0) {
         global $thisstaff;
 
-        if($id && is_numeric($id)){ //Lock Id provided!
+        if (!($ticket = Ticket::lookup($tid))) {
+            return 0;
+        }
 
-            $lock = Lock::lookup($id);
-            //Already gone?
-            if(!$lock || !$lock->getStaffId() || $lock->isExpired()) //Said lock doesn't exist or is is expired
+        if ($id) {
+            // Fetch the lock from the ticket
+            if (!($lock = $ticket->getLock())) {
                 return 1;
-
-            //make sure the user actually owns the lock before releasing it.
-            return ($lock->getStaffId()==$thisstaff->getId() && $lock->release())?1:0;
-
-        } elseif ($tid) { //release all the locks the user owns on the ticket.
-            if (!($ticket = Ticket::lookup($tid)))
+            }
+            // Identify the lock by the ID number
+            if ($lock->getId() != $id) {
                 return 0;
-            return Lock::removeStaffLocks($thisstaff->getId(), $ticket)?1:0;
+            }
+            // You have to own the lock
+            if ($lock->getStaffId() != $thisstaff->getId()) {
+                return 0;
+            }
+            // Can't be expired
+            if ($lock->isExpired()) {
+                return 1;
+            }
+            return $lock->release() ? 1 : 0;
         }
 
-        return 0;
+        return Lock::removeStaffLocks($thisstaff->getId(), $ticket) ? 1 : 0;
     }
 
     function previewTicket ($tid) {
diff --git a/include/class.auth.php b/include/class.auth.php
index b115258952323bd069ece521c8643b7211f5fa45..192c3af7079265b5d99527390ca497db9bd45fe2 100644
--- a/include/class.auth.php
+++ b/include/class.auth.php
@@ -1050,8 +1050,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);
@@ -1102,8 +1104,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;
diff --git a/include/class.client.php b/include/class.client.php
index 4932f4c400adfd6ab4d528fe3bb69853171e0aff..25efc2ca60132537015107d13d273b913bc13ee7 100644
--- a/include/class.client.php
+++ b/include/class.client.php
@@ -89,7 +89,7 @@ implements EmailContact, ITicketUser {
         }
 
         if (!$user
-                || !$user instanceof TicketUser
+                || !$user instanceof ITicketUser
                 || strcasecmp($ticket->getAuthToken($user, $matches['algo']), $token))
             return false;
 
@@ -174,10 +174,13 @@ class  EndUser extends BaseAuthenticatedUser {
         $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() {
@@ -402,5 +405,7 @@ interface ITicketUser {
     function flagGuest();
     function isGuest();
     function getUserId();
+    function getTicketId();
+    function getTicket();
 }
 ?>
diff --git a/include/class.collaborator.php b/include/class.collaborator.php
index 35e5ea0287185080469a3f947bf046359c1be48d..c457f1cd3cf4feac927e6076a428bc6a90ca4766 100644
--- a/include/class.collaborator.php
+++ b/include/class.collaborator.php
@@ -73,15 +73,12 @@ implements EmailContact, ITicketUser {
     function getName() {
         return $this->user->getName();
     }
-    function sendAccessLink($ticket) {
-        return $this->user->sendAccessLink($ticket);
-    }
 
     // VariableReplacer interface
     function getVar($what) {
         global $cfg;
 
-        switch ($what) {
+        switch (strtolower($what)) {
         case 'ticket_link':
             return sprintf('%s/view.php?%s',
                 $cfg->getBaseUrl(),
diff --git a/include/class.ticket.php b/include/class.ticket.php
index b7560b8a15f8da4d8b2e475595cc76e4fc3ab6e5..5df9f9a82de862e52128542f5907323e852ee41b 100644
--- a/include/class.ticket.php
+++ b/include/class.ticket.php
@@ -1042,6 +1042,31 @@ implements RestrictedAccess, Threadable {
         return $authtoken;
     }
 
+    function sendAccessLink($user) {
+        global $ost;
+
+        if (!($email = $ost->getConfig()->getDefaultEmail())
+            || !($content = Page::lookupByType('access-link')))
+            return;
+
+        $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;
diff --git a/include/class.user.php b/include/class.user.php
index 80b51163e90a3f1c7255e2ebdac9bc2cddf3f597..3a5fd1c29e205db10434a235315117b808de74c2 100644
--- a/include/class.user.php
+++ b/include/class.user.php
@@ -246,27 +246,9 @@ class User extends UserModel {
         $form->save();
     }
 
-    function sendAccessLink($ticket) {
-        global $ost;
-
-        if (!($email = $ost->getConfig()->getDefaultEmail())
-            || !($content = Page::lookupByType('access-link')))
-            return;
-
-        $vars = array(
-            'url' => $ost->getConfig()->getBaseUrl(),
-            'ticket' => $ticket,
-            'user' => $this,
-            'recipient' => $this);
-
-        $lang = $this->getLanguage(UserAccount::LANG_MAILOUTS);
-        $msg = $ost->replaceTemplateVariables(array(
-            'subj' => $content->getLocalName($lang),
-            'body' => $content->getLocalBody($lang),
-        ), $vars);
-
-        $email->send($this->getEmail(), Format::striptags($msg['subj']),
-            $msg['body']);
+    function getLanguage($flags=false) {
+        if ($acct = $this->getAccount())
+            return $acct->getLanguage($flags);
     }
 
     function to_json() {
diff --git a/include/client/tickets.inc.php b/include/client/tickets.inc.php
index d88ec62d51a78227497452396abf93111a0a9a10..76412f7e3b49e8921a432cc4f65369fcb46d1007 100644
--- a/include/client/tickets.inc.php
+++ b/include/client/tickets.inc.php
@@ -45,7 +45,7 @@ $$x=' class="'.strtolower($_REQUEST['order'] ?: 'desc').'" ';
 // Add visibility constraints
 $tickets->filter(Q::any(array(
     'user_id' => $thisclient->getId(),
-    'collaborators__user_id' => $thisclient->getId(),
+    'thread__collaborators__user_id' => $thisclient->getId(),
 )));
 
 // Perform basic search
diff --git a/include/staff/templates/tickets.tmpl.php b/include/staff/templates/tickets.tmpl.php
index 6e6b6cf5c383bfceaca595fa135c478b887bb862..ce48d471b659a6ae2b0cb2fb95bc9a646bd90066 100644
--- a/include/staff/templates/tickets.tmpl.php
+++ b/include/staff/templates/tickets.tmpl.php
@@ -49,8 +49,8 @@ if ($results) {
      .' LEFT JOIN '.THREAD_ENTRY_TABLE.' entry ON (entry.thread_id=thread.id) '
      .' LEFT JOIN '.ATTACHMENT_TABLE.' attach
             ON (attach.object_id=entry.id AND attach.`type` = "H") '
-     .' LEFT JOIN '.TICKET_COLLABORATOR_TABLE.' collab
-            ON ( ticket.ticket_id=collab.ticket_id) '
+     .' LEFT JOIN '.THREAD_COLLABORATOR_TABLE.' collab
+            ON ( thread.id=collab.thread_id) '
      .' WHERE ticket.ticket_id IN ('.implode(',', db_input(array_keys($results))).')
         GROUP BY ticket.ticket_id';
     $ids_res = db_query($counts_sql);
diff --git a/login.php b/login.php
index d5df38be538ccb2641c2196a21e5a064cfb304f1..201f840c5b5cf6f4b31bf7e2922eb22d4263db78 100644
--- a/login.php
+++ b/login.php
@@ -72,11 +72,11 @@ elseif ($_POST && isset($_POST['lticket'])) {
             Http::redirect('tickets.php');
 
         // This will succeed as it is checked in the authentication backend
-        $ticket = Ticket::lookup($_POST['lticket']);
+        $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);
+        $ticket->sendAccessLink($user);
         $msg = sprintf(__("%s - access link sent to your email!"),
             Format::htmlchars($user->getName()->getFirst()));
         $_POST = null;