2 namespace wcf\system\session
;
3 use wcf\data\session\virtual\SessionVirtual
;
4 use wcf\data\session\virtual\SessionVirtualAction
;
5 use wcf\data\session\virtual\SessionVirtualEditor
;
6 use wcf\data\user\User
;
7 use wcf\data\user\UserEditor
;
8 use wcf\page\ITrackablePage
;
9 use wcf\system\cache\builder\SpiderCacheBuilder
;
10 use wcf\system\cache\builder\UserGroupPermissionCacheBuilder
;
11 use wcf\system\exception\PermissionDeniedException
;
12 use wcf\system\request\RequestHandler
;
13 use wcf\system\user\authentication\UserAuthenticationFactory
;
14 use wcf\system\user\storage\UserStorageHandler
;
15 use wcf\system\SingletonFactory
;
17 use wcf\util\HeaderUtil
;
18 use wcf\util\PasswordUtil
;
19 use wcf\util\StringUtil
;
20 use wcf\util\UserUtil
;
25 * @author Alexander Ebert
26 * @copyright 2001-2014 WoltLab GmbH
27 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
28 * @package com.woltlab.wcf
29 * @subpackage system.session
30 * @category Community Framework
32 class SessionHandler
extends SingletonFactory
{
34 * prevents update on shutdown
37 protected $doNotUpdate = false;
40 * various environment variables
43 protected $environment = array();
46 * group data and permissions
49 protected $groupData = null;
52 * language id for active user
55 protected $languageID = 0;
58 * language ids for active user
61 protected $languageIDs = null;
65 * @var \wcf\data\acp\session\ACPSession
67 protected $session = null;
73 protected $sessionClassName = '';
76 * session editor class name
79 protected $sessionEditorClassName = '';
82 * virtual session support
85 protected $supportsVirtualSessions = false;
91 protected $styleID = null;
94 * enable cookie support
97 protected $useCookies = false;
101 * @var \wcf\data\user\User
103 protected $user = null;
109 protected $variables = null;
112 * indicates if session variables changed and must be saved upon shutdown
115 protected $variablesChanged = false;
118 * virtual session object, null for guests
119 * @var \wcf\data\session\virtual\SessionVirtual
121 protected $virtualSession = false;
124 * Provides access to session data.
129 public function __get($key) {
130 if (isset($this->environment
[$key])) {
131 return $this->environment
[$key];
134 return $this->session
->{$key};
138 * Loads an existing session or creates a new one.
140 * @param string $sessionEditorClassName
141 * @param string $sessionID
143 public function load($sessionEditorClassName, $sessionID) {
144 $this->sessionEditorClassName
= $sessionEditorClassName;
145 $this->sessionClassName
= call_user_func(array($sessionEditorClassName, 'getBaseClass'));
146 $this->supportsVirtualSessions
= call_user_func(array($this->sessionClassName
, 'supportsVirtualSessions'));
148 // try to get existing session
149 if (!empty($sessionID)) {
150 $this->getExistingSession($sessionID);
153 // create new session
154 if ($this->session
=== null) {
160 * Initializes session system.
162 public function initSession() {
163 // init session environment
164 $this->loadVariables();
165 $this->initSecurityToken();
166 $this->defineConstants();
168 // assign language and style id
169 $this->languageID
= ($this->getVar('languageID') === null) ?
$this->user
->languageID
: $this->getVar('languageID');
170 $this->styleID
= ($this->getVar('styleID') === null) ?
$this->user
->styleID
: $this->getVar('styleID');
172 // init environment variables
173 $this->initEnvironment();
177 * Enables cookie support.
179 public function enableCookies() {
180 $this->useCookies
= true;
184 * Initializes environment variables.
186 protected function initEnvironment() {
187 $this->environment
= array(
188 'lastRequestURI' => $this->session
->requestURI
,
189 'lastRequestMethod' => $this->session
->requestMethod
,
190 'ipAddress' => UserUtil
::getIpAddress(),
191 'userAgent' => UserUtil
::getUserAgent(),
192 'requestURI' => UserUtil
::getRequestURI(),
193 'requestMethod' => (!empty($_SERVER['REQUEST_METHOD']) ?
substr($_SERVER['REQUEST_METHOD'], 0, 7) : '')
198 * Disables update on shutdown.
200 public function disableUpdate() {
201 $this->doNotUpdate
= true;
205 * Defines global wcf constants related to session.
207 protected function defineConstants() {
208 if ($this->useCookies ||
$this->session
->spiderID
) {
209 if (!defined('SID_ARG_1ST')) define('SID_ARG_1ST', '');
210 if (!defined('SID_ARG_2ND')) define('SID_ARG_2ND', '');
211 if (!defined('SID_ARG_2ND_NOT_ENCODED')) define('SID_ARG_2ND_NOT_ENCODED', '');
212 if (!defined('SID')) define('SID', '');
213 if (!defined('SID_INPUT_TAG')) define('SID_INPUT_TAG', '');
216 if (!defined('SID_ARG_1ST')) define('SID_ARG_1ST', '?s='.$this->sessionID
);
217 if (!defined('SID_ARG_2ND')) define('SID_ARG_2ND', '&s='.$this->sessionID
);
218 if (!defined('SID_ARG_2ND_NOT_ENCODED')) define('SID_ARG_2ND_NOT_ENCODED', '&s='.$this->sessionID
);
219 if (!defined('SID')) define('SID', $this->sessionID
);
220 if (!defined('SID_INPUT_TAG')) define('SID_INPUT_TAG', '<input type="hidden" name="s" value="'.$this->sessionID
.'" />');
224 if (!defined('SECURITY_TOKEN')) define('SECURITY_TOKEN', $this->getSecurityToken());
225 if (!defined('SECURITY_TOKEN_INPUT_TAG')) define('SECURITY_TOKEN_INPUT_TAG', '<input type="hidden" name="t" value="'.$this->getSecurityToken().'" />');
229 * Initializes security token.
231 protected function initSecurityToken() {
232 if ($this->getVar('__SECURITY_TOKEN') === null) {
233 $this->register('__SECURITY_TOKEN', StringUtil
::getRandomID());
238 * Returns security token.
242 public function getSecurityToken() {
243 return $this->getVar('__SECURITY_TOKEN');
247 * Validates the given security token, returns false if
248 * given token is invalid.
250 * @param string $token
253 public function checkSecurityToken($token) {
254 return PasswordUtil
::secureCompare($this->getSecurityToken(), $token);
258 * Registers a session variable.
261 * @param string $value
263 public function register($key, $value) {
264 $this->variables
[$key] = $value;
265 $this->variablesChanged
= true;
269 * Unsets a session variable.
273 public function unregister($key) {
274 unset($this->variables
[$key]);
275 $this->variablesChanged
= true;
279 * Returns the value of a session variable.
283 public function getVar($key) {
284 if (isset($this->variables
[$key])) {
285 return $this->variables
[$key];
292 * Initializes session variables.
294 protected function loadVariables() {
295 @$this->variables
= unserialize($this->session
->sessionVariables
);
296 if (!is_array($this->variables
)) {
297 $this->variables
= array();
302 * Returns the user object of this session.
304 * @return \wcf\data\user\User $user
306 public function getUser() {
311 * Tries to read existing session identified by the given session id.
313 * @param string $sessionID
315 protected function getExistingSession($sessionID) {
316 $this->session
= new $this->sessionClassName($sessionID);
317 if (!$this->session
->sessionID
) {
318 $this->session
= null;
322 $this->user
= new User($this->session
->userID
);
323 $this->loadVirtualSession();
325 if (!$this->validate()) {
326 $this->session
= null;
328 $this->virtualSession
= false;
335 * Loads the virtual session object unless the user is not logged in or the session
336 * does not support virtual sessions. If there is no virtual session yet, it will be
337 * created on-the-fly.
339 * @param boolean $forceReload
341 protected function loadVirtualSession($forceReload = false) {
342 if ($this->virtualSession
=== false ||
$forceReload) {
343 $this->virtualSession
= null;
344 if ($this->user
->userID
&& $this->supportsVirtualSessions
) {
345 $virtualSessionAction = new SessionVirtualAction(array(), 'create', array('data' => array('sessionID' => $this->session
->sessionID
)));
346 $returnValues = $virtualSessionAction->executeAction();
347 $this->virtualSession
= $returnValues['returnValues'];
353 * Validates the ip address and the user agent of this session.
357 protected function validate() {
358 if (SESSION_VALIDATE_IP_ADDRESS
) {
359 if ($this->supportsVirtualSessions
&& ($this->virtualSession
instanceof SessionVirtual
)) {
360 if ($this->virtualSession
->ipAddress
!= UserUtil
::getIpAddress()) {
364 else if ($this->session
->ipAddress
!= UserUtil
::getIpAddress()) {
369 if (SESSION_VALIDATE_USER_AGENT
) {
370 if ($this->supportsVirtualSessions
&& ($this->virtualSession
instanceof SessionVirtual
)) {
371 if ($this->virtualSession
->userAgent
!= UserUtil
::getUserAgent()) {
375 else if ($this->session
->userAgent
!= UserUtil
::getUserAgent()) {
384 * Creates a new session.
386 protected function create() {
388 if ($this->sessionEditorClassName
== 'wcf\data\session\SessionEditor') {
389 // get spider information
390 $spiderID = $this->getSpiderID(UserUtil
::getUserAgent());
391 if ($spiderID !== null) {
392 // try to use existing session
393 if (($session = $this->getExistingSpiderSession($spiderID)) !== null) {
394 $this->user
= new User(null);
395 $this->session
= $session;
401 // create new session hash
402 $sessionID = StringUtil
::getRandomID();
404 // get user automatically
405 $this->user
= UserAuthenticationFactory
::getInstance()->getUserAuthentication()->loginAutomatically(call_user_func(array($this->sessionClassName
, 'supportsPersistentLogins')));
408 if ($this->user
=== null) {
409 // no valid user found
411 $this->user
= new User(null);
413 else if (!$this->supportsVirtualSessions
) {
414 // delete all other sessions of this user
415 call_user_func(array($this->sessionEditorClassName
, 'deleteUserSessions'), array($this->user
->userID
));
418 $createNewSession = true;
419 if ($this->supportsVirtualSessions
) {
420 // find existing session
421 $session = call_user_func(array($this->sessionClassName
, 'getSessionByUserID'), $this->user
->userID
);
423 if ($session !== null) {
424 // inherit existing session
425 $this->session
= $session;
426 $this->loadVirtualSession(true);
428 $createNewSession = false;
432 if ($createNewSession) {
434 $sessionData = array(
435 'sessionID' => $sessionID,
436 'userID' => $this->user
->userID
,
437 'ipAddress' => UserUtil
::getIpAddress(),
438 'userAgent' => UserUtil
::getUserAgent(),
439 'lastActivityTime' => TIME_NOW
,
440 'requestURI' => UserUtil
::getRequestURI(),
441 'requestMethod' => (!empty($_SERVER['REQUEST_METHOD']) ?
substr($_SERVER['REQUEST_METHOD'], 0, 7) : '')
444 if ($spiderID !== null) $sessionData['spiderID'] = $spiderID;
445 $this->session
= call_user_func(array($this->sessionEditorClassName
, 'create'), $sessionData);
446 $this->loadVirtualSession(true);
451 * Returns the value of the permission with the given name.
453 * @param string $permission
454 * @return mixed permission value
456 public function getPermission($permission) {
457 $this->loadGroupData();
459 if (!isset($this->groupData
[$permission])) return false;
460 return $this->groupData
[$permission];
464 * Checks if the active user has the given permissions and throws a
465 * PermissionDeniedException if that isn't the case.
467 public function checkPermissions(array $permissions) {
468 foreach ($permissions as $permission) {
469 if (!$this->getPermission($permission)) {
470 throw new PermissionDeniedException();
476 * Loads group data from cache.
478 protected function loadGroupData() {
479 if ($this->groupData
!== null) return;
481 // work-around for setup process (package wcf does not exist yet)
484 $sql = "SELECT groupID
485 FROM wcf".WCF_N
."_user_to_group
487 $statement = WCF
::getDB()->prepareStatement($sql);
488 $statement->execute(array($this->user
->userID
));
489 while ($row = $statement->fetchArray()) {
490 $groupIDs[] = $row['groupID'];
494 $groupIDs = $this->user
->getGroupIDs();
497 // get group data from cache
498 $this->groupData
= UserGroupPermissionCacheBuilder
::getInstance()->getData($groupIDs);
499 if (isset($this->groupData
['groupIDs']) && $this->groupData
['groupIDs'] != $groupIDs) {
500 $this->groupData
= array();
505 * Returns language ids for active user.
507 * @return array<integer>
509 public function getLanguageIDs() {
510 $this->loadLanguageIDs();
512 return $this->languageIDs
;
516 * Loads language ids for active user.
518 protected function loadLanguageIDs() {
519 if ($this->languageIDs
!== null) return;
521 $this->languageIDs
= array();
523 if (!$this->user
->userID
) {
527 // work-around for setup process (package wcf does not exist yet)
529 $sql = "SELECT languageID
530 FROM wcf".WCF_N
."_user_to_language
532 $statement = WCF
::getDB()->prepareStatement($sql);
533 $statement->execute(array($this->user
->userID
));
534 while ($row = $statement->fetchArray()) {
535 $this->languageIDs
[] = $row['languageID'];
539 $this->languageIDs
= $this->user
->getLanguageIDs();
544 * Stores a new user object in this session, e.g. a user was guest because not
545 * logged in, after the login his old session is used to store his full data.
547 * @param \wcf\data\userUser $user
548 * @param boolean $hideSession if true, database won't be updated
551 public function changeUser(User
$user, $hideSession = false) {
552 if ($this->supportsVirtualSessions
) {
553 return $this->changeUserVirtual($user);
556 $sessionTable = call_user_func(array($this->sessionClassName
, 'getDatabaseTableName'));
558 if ($user->userID
&& !$hideSession) {
559 // user is not a guest, delete all other sessions of this user
560 $sql = "DELETE FROM ".$sessionTable."
563 $statement = WCF
::getDB()->prepareStatement($sql);
564 $statement->execute(array($this->sessionID
, $user->userID
));
566 // reset session variables
567 $this->variables
= array();
568 $this->variablesChanged
= true;
571 // update user reference
576 $sessionEditor = new $this->sessionEditorClassName($this->session
);
577 $sessionEditor->update(array(
578 'userID' => $this->user
->userID
583 $this->groupData
= null;
584 $this->languageIDs
= null;
585 $this->languageID
= $this->user
->languageID
;
586 $this->styleID
= $this->user
->styleID
;
592 * Changes the user stored in the session, this method is different from changeUser() because it
593 * attempts to re-use sessions unless there are other virtual sessions for the same user (userID != 0).
594 * In reverse, logging out attempts to re-use the current session or spawns a new session depending
595 * on other virtual sessions.
597 * @param \wcf\data\user\User $user
599 protected function changeUserVirtual(User
$user) {
600 $sessionTable = call_user_func(array($this->sessionClassName
, 'getDatabaseTableName'));
602 switch ($user->userID
) {
604 // user -> guest (logout)
607 // delete virtual session
608 $virtualSessionEditor = new SessionVirtualEditor($this->virtualSession
);
609 $virtualSessionEditor->delete();
611 // there are still other virtual sessions, create a new session
612 if (SessionVirtual
::countVirtualSessions($this->session
->sessionID
)) {
614 $sessionData = array(
615 'sessionID' => StringUtil
::getRandomID(),
616 'userID' => $user->userID
,
617 'ipAddress' => UserUtil
::getIpAddress(),
618 'userAgent' => UserUtil
::getUserAgent(),
619 'lastActivityTime' => TIME_NOW
,
620 'requestURI' => UserUtil
::getRequestURI(),
621 'requestMethod' => (!empty($_SERVER['REQUEST_METHOD']) ?
substr($_SERVER['REQUEST_METHOD'], 0, 7) : '')
624 $this->session
= call_user_func(array($this->sessionEditorClassName
, 'create'), $sessionData);
626 HeaderUtil
::setCookie('cookieHash', $this->session
->sessionID
);
629 // this was the last virtual session, re-use current session
631 $sessionEditor = new $this->sessionEditorClassName($this->session
);
632 $sessionEditor->update(array(
633 'userID' => $user->userID
639 // guest -> user (login)
642 // find existing session for this user
643 $session = call_user_func(array($this->sessionClassName
, 'getSessionByUserID'), $user->userID
);
645 // no session exists, re-use current session
646 if ($session === null) {
648 $sessionEditor = new $this->sessionEditorClassName($this->session
);
649 $sessionEditor->update(array(
650 'userID' => $user->userID
654 // delete guest session
655 $sessionEditor = new $this->sessionEditorClassName($this->session
);
656 $sessionEditor->delete();
658 // inherit existing session
659 $this->session
= $session;
665 $this->loadVirtualSession(true);
668 $this->groupData
= null;
669 $this->languageIDs
= null;
670 $this->languageID
= $this->user
->languageID
;
671 $this->styleID
= $this->user
->styleID
;
677 * Updates user session on shutdown.
679 public function update() {
680 if ($this->doNotUpdate
) return;
684 'ipAddress' => UserUtil
::getIpAddress(),
685 'userAgent' => $this->userAgent
,
686 'requestURI' => $this->requestURI
,
687 'requestMethod' => $this->requestMethod
,
688 'lastActivityTime' => TIME_NOW
690 if (!class_exists('wcf\system\CLIWCF', false) && PACKAGE_ID
&& RequestHandler
::getInstance()->getActiveRequest() && RequestHandler
::getInstance()->getActiveRequest()->getRequestObject() instanceof ITrackablePage
&& RequestHandler
::getInstance()->getActiveRequest()->getRequestObject()->isTracked()) {
691 $data['controller'] = RequestHandler
::getInstance()->getActiveRequest()->getRequestObject()->getController();
692 $data['parentObjectType'] = RequestHandler
::getInstance()->getActiveRequest()->getRequestObject()->getParentObjectType();
693 $data['parentObjectID'] = RequestHandler
::getInstance()->getActiveRequest()->getRequestObject()->getParentObjectID();
694 $data['objectType'] = RequestHandler
::getInstance()->getActiveRequest()->getRequestObject()->getObjectType();
695 $data['objectID'] = RequestHandler
::getInstance()->getActiveRequest()->getRequestObject()->getObjectID();
697 if ($this->variablesChanged
) {
698 $data['sessionVariables'] = serialize($this->variables
);
702 $sessionEditor = new $this->sessionEditorClassName($this->session
);
703 $sessionEditor->update($data);
705 if ($this->virtualSession
instanceof SessionVirtual
) {
706 $virtualSessionEditor = new SessionVirtualEditor($this->virtualSession
);
707 $virtualSessionEditor->updateLastActivityTime();
712 * Updates last activity time to protect session from expiring.
714 public function keepAlive() {
715 $this->disableUpdate();
717 // update last activity time
718 $sessionEditor = new $this->sessionEditorClassName($this->session
);
719 $sessionEditor->update(array(
720 'lastActivityTime' => TIME_NOW
723 if ($this->virtualSession
instanceof SessionVirtual
) {
724 $virtualSessionEditor = new SessionVirtualEditor($this->virtualSession
);
725 $virtualSessionEditor->updateLastActivityTime();
730 * Deletes this session and it's related data.
732 public function delete() {
734 if ($this->user
->userID
) {
735 self
::resetSessions(array($this->user
->userID
));
737 // update last activity time
738 if (!class_exists('\wcf\system\WCFACP', false)) {
739 $editor = new UserEditor($this->user
);
740 $editor->update(array('lastActivityTime' => TIME_NOW
));
745 $deleteSession = $this->changeUser(new User(null), true);
748 if ($deleteSession !== false) {
749 $sessionEditor = new $this->sessionEditorClassName($this->session
);
750 $sessionEditor->delete();
754 $this->disableUpdate();
758 * Returns currently active language id.
762 public function getLanguageID() {
763 return $this->languageID
;
767 * Sets the currently active language id.
769 * @param integer $languageID
771 public function setLanguageID($languageID) {
772 $this->languageID
= $languageID;
773 $this->register('languageID', $this->languageID
);
777 * Returns currently active style id.
781 public function getStyleID() {
782 return $this->styleID
;
786 * Sets the currently active style id.
788 * @param integer $styleID
790 public function setStyleID($styleID) {
791 $this->styleID
= $styleID;
792 $this->register('styleID', $this->styleID
);
796 * Resets session-specific storage data.
798 * @param array<integer> $userIDs
800 public static function resetSessions(array $userIDs = array()) {
801 if (!empty($userIDs)) {
802 UserStorageHandler
::getInstance()->reset($userIDs, 'groupIDs', 1);
803 UserStorageHandler
::getInstance()->reset($userIDs, 'languageIDs', 1);
806 UserStorageHandler
::getInstance()->resetAll('groupIDs', 1);
807 UserStorageHandler
::getInstance()->resetAll('languageIDs', 1);
812 * Returns the spider id for given user agent.
814 * @param string $userAgent
817 protected function getSpiderID($userAgent) {
818 $spiderList = SpiderCacheBuilder
::getInstance()->getData();
819 $userAgent = strtolower($userAgent);
821 foreach ($spiderList as $spider) {
822 if (strpos($userAgent, $spider->spiderIdentifier
) !== false) {
823 return $spider->spiderID
;
831 * Searches for existing session of a search spider.
833 * @param integer $spiderID
834 * @return \wcf\data\session\Session
836 protected function getExistingSpiderSession($spiderID) {
838 FROM wcf".WCF_N
."_session
841 $statement = WCF
::getDB()->prepareStatement($sql);
842 $statement->execute(array($spiderID));
843 $row = $statement->fetchArray();
844 if ($row !== false) {
845 // fix session validation
846 $row['ipAddress'] = UserUtil
::getIpAddress();
847 $row['userAgent'] = UserUtil
::getUserAgent();
849 // return session object
850 return new $this->sessionClassName(null, $row);