2 namespace wcf\system\session
;
3 use wcf\data\user\User
;
4 use wcf\data\user\UserEditor
;
5 use wcf\page\ITrackablePage
;
6 use wcf\system\cache\builder\SpiderCacheBuilder
;
7 use wcf\system\cache\builder\UserGroupPermissionCacheBuilder
;
8 use wcf\system\exception\PermissionDeniedException
;
9 use wcf\system\request\RequestHandler
;
10 use wcf\system\user\authentication\UserAuthenticationFactory
;
11 use wcf\system\user\storage\UserStorageHandler
;
12 use wcf\system\SingletonFactory
;
14 use wcf\util\PasswordUtil
;
15 use wcf\util\StringUtil
;
16 use wcf\util\UserUtil
;
21 * @author Alexander Ebert
22 * @copyright 2001-2014 WoltLab GmbH
23 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
24 * @package com.woltlab.wcf
25 * @subpackage system.session
26 * @category Community Framework
28 class SessionHandler
extends SingletonFactory
{
30 * prevents update on shutdown
33 protected $doNotUpdate = false;
36 * various environment variables
39 protected $environment = array();
42 * group data and permissions
45 protected $groupData = null;
48 * language id for active user
51 protected $languageID = 0;
54 * language ids for active user
57 protected $languageIDs = null;
61 * @var \wcf\data\acp\session\ACPSession
63 protected $session = null;
69 protected $sessionClassName = '';
72 * session editor class name
75 protected $sessionEditorClassName = '';
81 protected $styleID = null;
84 * enable cookie support
87 protected $useCookies = false;
91 * @var \wcf\data\user\User
93 protected $user = null;
99 protected $variables = null;
102 * indicates if session variables changed and must be saved upon shutdown
105 protected $variablesChanged = false;
108 * Provides access to session data.
113 public function __get($key) {
114 if (isset($this->environment
[$key])) {
115 return $this->environment
[$key];
118 return $this->session
->{$key};
122 * Loads an existing session or creates a new one.
124 * @param string $sessionEditorClassName
125 * @param string $sessionID
127 public function load($sessionEditorClassName, $sessionID) {
128 $this->sessionEditorClassName
= $sessionEditorClassName;
129 $this->sessionClassName
= call_user_func(array($sessionEditorClassName, 'getBaseClass'));
131 // try to get existing session
132 if (!empty($sessionID)) {
133 $this->getExistingSession($sessionID);
136 // create new session
137 if ($this->session
=== null) {
143 * Initializes session system.
145 public function initSession() {
146 // init session environment
147 $this->loadVariables();
148 $this->initSecurityToken();
149 $this->defineConstants();
151 // assign language and style id
152 $this->languageID
= ($this->getVar('languageID') === null) ?
$this->user
->languageID
: $this->getVar('languageID');
153 $this->styleID
= ($this->getVar('styleID') === null) ?
$this->user
->styleID
: $this->getVar('styleID');
155 // init environment variables
156 $this->initEnvironment();
160 * Enables cookie support.
162 public function enableCookies() {
163 $this->useCookies
= true;
167 * Initializes environment variables.
169 protected function initEnvironment() {
170 $this->environment
= array(
171 'lastRequestURI' => $this->session
->requestURI
,
172 'lastRequestMethod' => $this->session
->requestMethod
,
173 'ipAddress' => UserUtil
::getIpAddress(),
174 'userAgent' => UserUtil
::getUserAgent(),
175 'requestURI' => UserUtil
::getRequestURI(),
176 'requestMethod' => (!empty($_SERVER['REQUEST_METHOD']) ?
substr($_SERVER['REQUEST_METHOD'], 0, 7) : '')
181 * Disables update on shutdown.
183 public function disableUpdate() {
184 $this->doNotUpdate
= true;
188 * Defines global wcf constants related to session.
190 protected function defineConstants() {
191 if ($this->useCookies ||
$this->session
->spiderID
) {
192 if (!defined('SID_ARG_1ST')) define('SID_ARG_1ST', '');
193 if (!defined('SID_ARG_2ND')) define('SID_ARG_2ND', '');
194 if (!defined('SID_ARG_2ND_NOT_ENCODED')) define('SID_ARG_2ND_NOT_ENCODED', '');
195 if (!defined('SID')) define('SID', '');
196 if (!defined('SID_INPUT_TAG')) define('SID_INPUT_TAG', '');
199 if (!defined('SID_ARG_1ST')) define('SID_ARG_1ST', '?s='.$this->sessionID
);
200 if (!defined('SID_ARG_2ND')) define('SID_ARG_2ND', '&s='.$this->sessionID
);
201 if (!defined('SID_ARG_2ND_NOT_ENCODED')) define('SID_ARG_2ND_NOT_ENCODED', '&s='.$this->sessionID
);
202 if (!defined('SID')) define('SID', $this->sessionID
);
203 if (!defined('SID_INPUT_TAG')) define('SID_INPUT_TAG', '<input type="hidden" name="s" value="'.$this->sessionID
.'" />');
207 if (!defined('SECURITY_TOKEN')) define('SECURITY_TOKEN', $this->getSecurityToken());
208 if (!defined('SECURITY_TOKEN_INPUT_TAG')) define('SECURITY_TOKEN_INPUT_TAG', '<input type="hidden" name="t" value="'.$this->getSecurityToken().'" />');
212 * Initializes security token.
214 protected function initSecurityToken() {
215 if ($this->getVar('__SECURITY_TOKEN') === null) {
216 $this->register('__SECURITY_TOKEN', StringUtil
::getRandomID());
221 * Returns security token.
225 public function getSecurityToken() {
226 return $this->getVar('__SECURITY_TOKEN');
230 * Validates the given security token, returns false if
231 * given token is invalid.
233 * @param string $token
236 public function checkSecurityToken($token) {
237 return PasswordUtil
::secureCompare($this->getSecurityToken(), $token);
241 * Registers a session variable.
244 * @param string $value
246 public function register($key, $value) {
247 $this->variables
[$key] = $value;
248 $this->variablesChanged
= true;
252 * Unsets a session variable.
256 public function unregister($key) {
257 unset($this->variables
[$key]);
258 $this->variablesChanged
= true;
262 * Returns the value of a session variable.
266 public function getVar($key) {
267 if (isset($this->variables
[$key])) {
268 return $this->variables
[$key];
275 * Initializes session variables.
277 protected function loadVariables() {
278 @$this->variables
= unserialize($this->session
->sessionVariables
);
279 if (!is_array($this->variables
)) {
280 $this->variables
= array();
285 * Returns the user object of this session.
287 * @return \wcf\data\user\User $user
289 public function getUser() {
294 * Tries to read existing session identified by the given session id.
296 * @param string $sessionID
298 protected function getExistingSession($sessionID) {
299 $this->session
= new $this->sessionClassName($sessionID);
300 if (!$this->session
->sessionID ||
!$this->validate()) {
301 $this->session
= null;
306 $this->user
= new User($this->session
->userID
);
310 * Validates the ip address and the user agent of this session.
314 protected function validate() {
315 if (SESSION_VALIDATE_IP_ADDRESS
) {
316 if ($this->session
->ipAddress
!= UserUtil
::getIpAddress()) {
320 if (SESSION_VALIDATE_USER_AGENT
) {
321 if ($this->session
->userAgent
!= UserUtil
::getUserAgent()) {
330 * Creates a new session.
332 protected function create() {
334 if ($this->sessionEditorClassName
== 'wcf\data\session\SessionEditor') {
335 // get spider information
336 $spiderID = $this->getSpiderID(UserUtil
::getUserAgent());
337 if ($spiderID !== null) {
338 // try to use existing session
339 if (($session = $this->getExistingSpiderSession($spiderID)) !== null) {
340 $this->user
= new User(null);
341 $this->session
= $session;
347 // create new session hash
348 $sessionID = StringUtil
::getRandomID();
350 // get user automatically
351 $this->user
= UserAuthenticationFactory
::getInstance()->getUserAuthentication()->loginAutomatically(call_user_func(array($this->sessionClassName
, 'supportsPersistentLogins')));
354 if ($this->user
=== null) {
355 // no valid user found
357 $this->user
= new User(null);
360 if ($this->user
->userID
!= 0) {
362 // delete all other sessions of this user
363 call_user_func(array($this->sessionEditorClassName
, 'deleteUserSessions'), array($this->user
->userID
));
367 $sessionData = array(
368 'sessionID' => $sessionID,
369 'userID' => $this->user
->userID
,
370 'ipAddress' => UserUtil
::getIpAddress(),
371 'userAgent' => UserUtil
::getUserAgent(),
372 'lastActivityTime' => TIME_NOW
,
373 'requestURI' => UserUtil
::getRequestURI(),
374 'requestMethod' => (!empty($_SERVER['REQUEST_METHOD']) ?
substr($_SERVER['REQUEST_METHOD'], 0, 7) : '')
376 if ($spiderID !== null) $sessionData['spiderID'] = $spiderID;
377 $this->session
= call_user_func(array($this->sessionEditorClassName
, 'create'), $sessionData);
381 * Returns the value of the permission with the given name.
383 * @param string $permission
384 * @return mixed permission value
386 public function getPermission($permission) {
387 $this->loadGroupData();
389 if (!isset($this->groupData
[$permission])) return false;
390 return $this->groupData
[$permission];
394 * Checks if the active user has the given permissions and throws a
395 * PermissionDeniedException if that isn't the case.
397 public function checkPermissions(array $permissions) {
398 foreach ($permissions as $permission) {
399 if (!$this->getPermission($permission)) {
400 throw new PermissionDeniedException();
406 * Loads group data from cache.
408 protected function loadGroupData() {
409 if ($this->groupData
!== null) return;
411 // work-around for setup process (package wcf does not exist yet)
414 $sql = "SELECT groupID
415 FROM wcf".WCF_N
."_user_to_group
417 $statement = WCF
::getDB()->prepareStatement($sql);
418 $statement->execute(array($this->user
->userID
));
419 while ($row = $statement->fetchArray()) {
420 $groupIDs[] = $row['groupID'];
424 $groupIDs = $this->user
->getGroupIDs();
427 // get group data from cache
428 $this->groupData
= UserGroupPermissionCacheBuilder
::getInstance()->getData($groupIDs);
429 if (isset($this->groupData
['groupIDs']) && $this->groupData
['groupIDs'] != $groupIDs) {
430 $this->groupData
= array();
435 * Returns language ids for active user.
437 * @return array<integer>
439 public function getLanguageIDs() {
440 $this->loadLanguageIDs();
442 return $this->languageIDs
;
446 * Loads language ids for active user.
448 protected function loadLanguageIDs() {
449 if ($this->languageIDs
!== null) return;
451 $this->languageIDs
= array();
453 if (!$this->user
->userID
) {
457 // work-around for setup process (package wcf does not exist yet)
459 $sql = "SELECT languageID
460 FROM wcf".WCF_N
."_user_to_language
462 $statement = WCF
::getDB()->prepareStatement($sql);
463 $statement->execute(array($this->user
->userID
));
464 while ($row = $statement->fetchArray()) {
465 $this->languageIDs
[] = $row['languageID'];
469 $this->languageIDs
= $this->user
->getLanguageIDs();
474 * Stores a new user object in this session, e.g. a user was guest because not
475 * logged in, after the login his old session is used to store his full data.
477 * @param \wcf\data\userUser $user
478 * @param boolean $hideSession if true, database won't be updated
480 public function changeUser(User
$user, $hideSession = false) {
481 $sessionTable = call_user_func(array($this->sessionClassName
, 'getDatabaseTableName'));
483 if ($user->userID
&& !$hideSession) {
484 // user is not a guest, delete all other sessions of this user
485 $sql = "DELETE FROM ".$sessionTable."
488 $statement = WCF
::getDB()->prepareStatement($sql);
489 $statement->execute(array($this->sessionID
, $user->userID
));
491 // reset session variables
492 $this->variables
= array();
493 $this->variablesChanged
= true;
496 // update user reference
501 $sessionEditor = new $this->sessionEditorClassName($this->session
);
502 $sessionEditor->update(array(
503 'userID' => $this->user
->userID
508 $this->groupData
= null;
509 $this->languageIDs
= null;
510 $this->languageID
= $this->user
->languageID
;
511 $this->styleID
= $this->user
->styleID
;
513 // truncate session variables
517 * Updates user session on shutdown.
519 public function update() {
520 if ($this->doNotUpdate
) return;
524 'ipAddress' => UserUtil
::getIpAddress(),
525 'userAgent' => $this->userAgent
,
526 'requestURI' => $this->requestURI
,
527 'requestMethod' => $this->requestMethod
,
528 'lastActivityTime' => TIME_NOW
530 if (!class_exists('wcf\system\CLIWCF', false) && PACKAGE_ID
&& RequestHandler
::getInstance()->getActiveRequest() && RequestHandler
::getInstance()->getActiveRequest()->getRequestObject() instanceof ITrackablePage
&& RequestHandler
::getInstance()->getActiveRequest()->getRequestObject()->isTracked()) {
531 $data['controller'] = RequestHandler
::getInstance()->getActiveRequest()->getRequestObject()->getController();
532 $data['parentObjectType'] = RequestHandler
::getInstance()->getActiveRequest()->getRequestObject()->getParentObjectType();
533 $data['parentObjectID'] = RequestHandler
::getInstance()->getActiveRequest()->getRequestObject()->getParentObjectID();
534 $data['objectType'] = RequestHandler
::getInstance()->getActiveRequest()->getRequestObject()->getObjectType();
535 $data['objectID'] = RequestHandler
::getInstance()->getActiveRequest()->getRequestObject()->getObjectID();
537 if ($this->variablesChanged
) {
538 $data['sessionVariables'] = serialize($this->variables
);
542 $sessionEditor = new $this->sessionEditorClassName($this->session
);
543 $sessionEditor->update($data);
547 * Updates last activity time to protect session from expiring.
549 public function keepAlive() {
550 $this->disableUpdate();
552 // update last activity time
553 $sessionEditor = new $this->sessionEditorClassName($this->session
);
554 $sessionEditor->update(array(
555 'lastActivityTime' => TIME_NOW
560 * Deletes this session and it's related data.
562 public function delete() {
564 if ($this->user
->userID
) {
565 self
::resetSessions(array($this->user
->userID
));
567 // update last activity time
568 if (!class_exists('\wcf\system\WCFACP', false)) {
569 $editor = new UserEditor($this->user
);
570 $editor->update(array('lastActivityTime' => TIME_NOW
));
575 $this->changeUser(new User(null), true);
578 $sessionEditor = new $this->sessionEditorClassName($this->session
);
579 $sessionEditor->delete();
582 $this->disableUpdate();
586 * Returns currently active language id.
590 public function getLanguageID() {
591 return $this->languageID
;
595 * Sets the currently active language id.
597 * @param integer $languageID
599 public function setLanguageID($languageID) {
600 $this->languageID
= $languageID;
601 $this->register('languageID', $this->languageID
);
605 * Returns currently active style id.
609 public function getStyleID() {
610 return $this->styleID
;
614 * Sets the currently active style id.
616 * @param integer $styleID
618 public function setStyleID($styleID) {
619 $this->styleID
= $styleID;
620 $this->register('styleID', $this->styleID
);
624 * Resets session-specific storage data.
626 * @param array<integer> $userIDs
628 public static function resetSessions(array $userIDs = array()) {
629 if (!empty($userIDs)) {
630 UserStorageHandler
::getInstance()->reset($userIDs, 'groupIDs', 1);
631 UserStorageHandler
::getInstance()->reset($userIDs, 'languageIDs', 1);
634 UserStorageHandler
::getInstance()->resetAll('groupIDs', 1);
635 UserStorageHandler
::getInstance()->resetAll('languageIDs', 1);
640 * Returns the spider id for given user agent.
642 * @param string $userAgent
645 protected function getSpiderID($userAgent) {
646 $spiderList = SpiderCacheBuilder
::getInstance()->getData();
647 $userAgent = strtolower($userAgent);
649 foreach ($spiderList as $spider) {
650 if (strpos($userAgent, $spider->spiderIdentifier
) !== false) {
651 return $spider->spiderID
;
659 * Searches for existing session of a search spider.
661 * @param integer $spiderID
662 * @return \wcf\data\session\Session
664 protected function getExistingSpiderSession($spiderID) {
666 FROM wcf".WCF_N
."_session
669 $statement = WCF
::getDB()->prepareStatement($sql);
670 $statement->execute(array($spiderID));
671 $row = $statement->fetchArray();
672 if ($row !== false) {
673 // fix session validation
674 $row['ipAddress'] = UserUtil
::getIpAddress();
675 $row['userAgent'] = UserUtil
::getUserAgent();
677 // return session object
678 return new $this->sessionClassName(null, $row);