Prevents rare race condition during user login
authorAlexander Ebert <ebert@woltlab.com>
Sun, 25 May 2014 20:35:49 +0000 (22:35 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Sun, 25 May 2014 20:35:49 +0000 (22:35 +0200)
In some cases it might happen, that two logins for the same users occur almost synchronously (e.g. starting browsers with two or more tabs pointing to the same URL). In that case it is possible that two session would have been created since the creation of the session takes place after the check from the second request passed.

wcfsetup/install/files/lib/system/session/SessionHandler.class.php
wcfsetup/setup/db/install.sql

index 4f1151a1d6e7597ed67c7a8b68866476d101a3ee..176080ac6303d22ab356a975a47af924c445a8ab 100644 (file)
@@ -18,6 +18,7 @@ use wcf\util\HeaderUtil;
 use wcf\util\PasswordUtil;
 use wcf\util\StringUtil;
 use wcf\util\UserUtil;
+use wcf\system\database\DatabaseException;
 
 /**
  * Handles sessions.
@@ -561,7 +562,7 @@ class SessionHandler extends SingletonFactory {
                                WHERE           sessionID <> ?
                                                AND userID = ?";
                        $statement = WCF::getDB()->prepareStatement($sql);
-                       $statement->execute(array($this->sessionID, $user->userID));
+                       //$statement->execute(array($this->sessionID, $user->userID));
                        
                        // reset session variables
                        $this->variables = array();
@@ -574,9 +575,33 @@ class SessionHandler extends SingletonFactory {
                if (!$hideSession) {
                        // update session
                        $sessionEditor = new $this->sessionEditorClassName($this->session);
-                       $sessionEditor->update(array(
-                               'userID' => $this->user->userID
-                       ));
+                       
+                       try {
+                               $sessionEditor->update(array(
+                                       'userID' => $this->user->userID
+                               ));
+                       }
+                       catch (DatabaseException $e) {
+                               // MySQL error 23000 = unique key
+                               // do not check against the message itself, some weird systems localize them
+                               if ($e->getCode() == 23000) {
+                                       // user is not a guest, delete all other sessions of this user
+                                       $sql = "DELETE FROM     ".$sessionTable."
+                                               WHERE           sessionID <> ?
+                                                               AND userID = ?";
+                                       $statement = WCF::getDB()->prepareStatement($sql);
+                                       $statement->execute(array($this->sessionID, $user->userID));
+                                               
+                                       // update session
+                                       $sessionEditor->update(array(
+                                               'userID' => $user->userID
+                                       ));
+                               }
+                               else {
+                                       // not our business
+                                       throw $e;
+                               }
+                       }
                }
                
                // reset caches
@@ -646,9 +671,30 @@ class SessionHandler extends SingletonFactory {
                                if ($session === null) {
                                        // update session
                                        $sessionEditor = new $this->sessionEditorClassName($this->session);
-                                       $sessionEditor->update(array(
-                                               'userID' => $user->userID
-                                       ));
+                                       
+                                       try {
+                                               $sessionEditor->update(array(
+                                                       'userID' => $user->userID
+                                               ));
+                                       }
+                                       catch (DatabaseException $e) {
+                                               // MySQL error 23000 = unique key
+                                               // do not check against the message itself, some weird systems localize them
+                                               if ($e->getCode() == 23000) {
+                                                       // find existing session for this user
+                                                       $session = call_user_func(array($this->sessionClassName, 'getSessionByUserID'), $user->userID);
+                                                       
+                                                       // update session
+                                                       $sessionEditor = new $this->sessionEditorClassName($this->session);
+                                                       $sessionEditor->update(array(
+                                                                       'userID' => $user->userID
+                                                       ));
+                                               }
+                                               else {
+                                                       // not our business
+                                                       throw $e;
+                                               }
+                                       }
                                }
                                else {
                                        // delete guest session
index 9227c39d03aaabc4cafd3de4239ec13e72bc62c7..654ef966fead0476e701a952e486cfe103e6c6dd 100644 (file)
@@ -835,7 +835,8 @@ CREATE TABLE wcf1_session (
        objectID INT(10) NOT NULL DEFAULT 0,
        sessionVariables MEDIUMTEXT,
        spiderID INT(10),
-       KEY packageID (lastActivityTime, spiderID)
+       KEY packageID (lastActivityTime, spiderID),
+       UNIQUE KEY userID (userID)
 );
 
 DROP TABLE IF EXISTS wcf1_session_virtual;