Use \PDO::fetchAll() instead of PreparedStatement::fetchColumns()
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / session / SessionHandler.class.php
CommitLineData
11ade432
AE
1<?php
2namespace wcf\system\session;
e1622bfa
TD
3use wcf\data\acp\session\virtual\ACPSessionVirtual;
4use wcf\data\acp\session\virtual\ACPSessionVirtualAction;
5use wcf\data\acp\session\virtual\ACPSessionVirtualEditor;
5a05fde9
AE
6use wcf\data\session\virtual\SessionVirtual;
7use wcf\data\session\virtual\SessionVirtualAction;
8use wcf\data\session\virtual\SessionVirtualEditor;
11ade432 9use wcf\data\user\User;
235c040a 10use wcf\data\user\UserEditor;
8f3fc897 11use wcf\page\ITrackablePage;
e8d26212 12use wcf\system\cache\builder\SpiderCacheBuilder;
ae6b590f 13use wcf\system\cache\builder\UserGroupOptionCacheBuilder;
b401cd0d 14use wcf\system\cache\builder\UserGroupPermissionCacheBuilder;
a5a77025 15use wcf\system\database\DatabaseException;
e1dc298d 16use wcf\system\event\EventHandler;
11ade432 17use wcf\system\exception\PermissionDeniedException;
7aa1a486 18use wcf\system\request\RequestHandler;
75afb100 19use wcf\system\user\authentication\UserAuthenticationFactory;
c96ee721 20use wcf\system\user\storage\UserStorageHandler;
11ade432
AE
21use wcf\system\SingletonFactory;
22use wcf\system\WCF;
e1622bfa 23use wcf\system\WCFACP;
ac094463 24use wcf\util\HeaderUtil;
d4f5c98c 25use wcf\util\PasswordUtil;
11ade432
AE
26use wcf\util\StringUtil;
27use wcf\util\UserUtil;
28
29/**
a17de04e 30 * Handles sessions.
11ade432
AE
31 *
32 * @author Alexander Ebert
2b6cb5c2 33 * @copyright 2001-2015 WoltLab GmbH
11ade432
AE
34 * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
35 * @package com.woltlab.wcf
36 * @subpackage system.session
9f959ced 37 * @category Community Framework
11ade432
AE
38 */
39class SessionHandler extends SingletonFactory {
b1a3cc1e
AE
40 /**
41 * suffix used to tell ACP and frontend cookies apart
42 * @var string
43 */
44 protected $cookieSuffix = '';
45
11ade432
AE
46 /**
47 * prevents update on shutdown
11ade432 48 * @var boolean
a79013ef 49 */
11ade432
AE
50 protected $doNotUpdate = false;
51
52 /**
53 * various environment variables
11ade432 54 * @var array
a79013ef 55 */
11ade432
AE
56 protected $environment = array();
57
58 /**
59 * group data and permissions
7a23a706 60 * @var mixed[][]
a79013ef 61 */
11ade432
AE
62 protected $groupData = null;
63
91082aee
AE
64 /**
65 * true if client provided a valid session cookie
66 * @var boolean
67 */
68 protected $hasValidCookie = false;
69
47c269ea
AE
70 /**
71 * true if within ACP or WCFSetup
72 * @var boolean
73 */
74 protected $isACP = false;
75
11ade432 76 /**
acfe7efb 77 * language id for active user
11ade432 78 * @var integer
a79013ef 79 */
11ade432
AE
80 protected $languageID = 0;
81
82 /**
83 * language ids for active user
7a23a706 84 * @var integer[]
a79013ef 85 */
11ade432
AE
86 protected $languageIDs = null;
87
88 /**
89 * session object
0ad90fc3 90 * @var \wcf\data\acp\session\ACPSession
a79013ef 91 */
11ade432
AE
92 protected $session = null;
93
11ade432
AE
94 /**
95 * session class name
11ade432 96 * @var string
a79013ef 97 */
11ade432
AE
98 protected $sessionClassName = '';
99
100 /**
101 * session editor class name
11ade432
AE
102 * @var string
103 */
104 protected $sessionEditorClassName = '';
105
5a05fde9
AE
106 /**
107 * virtual session support
108 * @var boolean
109 */
110 protected $supportsVirtualSessions = false;
111
83736ee3
AE
112 /**
113 * style id
114 * @var integer
115 */
116 protected $styleID = null;
117
11ade432
AE
118 /**
119 * user object
0ad90fc3 120 * @var \wcf\data\user\User
a79013ef 121 */
11ade432
AE
122 protected $user = null;
123
124 /**
125 * session variables
11ade432 126 * @var array
a79013ef 127 */
11ade432
AE
128 protected $variables = null;
129
130 /**
131 * indicates if session variables changed and must be saved upon shutdown
11ade432 132 * @var boolean
a79013ef 133 */
11ade432
AE
134 protected $variablesChanged = false;
135
5a05fde9
AE
136 /**
137 * virtual session object, null for guests
138 * @var \wcf\data\session\virtual\SessionVirtual
139 */
140 protected $virtualSession = false;
141
3d7fee9e 142 /**
0f0590c2
MS
143 * true if this is a new session
144 * @var boolean
3d7fee9e
MW
145 */
146 protected $firstVisit = false;
147
ae6b590f
MS
148 /**
149 * list of names of permissions only available for users
7a23a706 150 * @var string[]
ae6b590f
MS
151 */
152 protected $usersOnlyPermissions = array();
153
11ade432
AE
154 /**
155 * Provides access to session data.
156 *
157 * @param string $key
158 * @return mixed
a79013ef 159 */
11ade432
AE
160 public function __get($key) {
161 if (isset($this->environment[$key])) {
162 return $this->environment[$key];
163 }
164
165 return $this->session->{$key};
166 }
167
ae6b590f
MS
168 /**
169 * @see \wcf\system\SingletonFactory::init()
170 */
171 protected function init() {
47c269ea 172 $this->isACP = (class_exists(WCFACP::class, false) || !PACKAGE_ID);
ae6b590f
MS
173 $this->usersOnlyPermissions = UserGroupOptionCacheBuilder::getInstance()->getData(array(), 'usersOnlyOptions');
174 }
175
b1a3cc1e
AE
176 /**
177 * Suffix used to tell ACP and frontend cookies apart
178 *
179 * @param string $cookieSuffix cookie suffix
180 */
181 public function setCookieSuffix($cookieSuffix) {
182 $this->cookieSuffix = $cookieSuffix;
183 }
184
91082aee
AE
185 /**
186 * Sets a boolean value to determine if the client provided a valid session cookie.
187 *
188 * @param boolean $hasValidCookie
63b02e3f 189 * @since 2.2
91082aee
AE
190 */
191 public function setHasValidCookie($hasValidCookie) {
192 $this->hasValidCookie = $hasValidCookie;
193 }
194
195 /**
196 * Returns true if client provided a valid session cookie.
197 *
198 * @return boolean
63b02e3f 199 * @since 2.2
91082aee
AE
200 */
201 public function hasValidCookie() {
202 return $this->hasValidCookie;
203 }
204
11ade432
AE
205 /**
206 * Loads an existing session or creates a new one.
9f959ced 207 *
11ade432 208 * @param string $sessionEditorClassName
11ade432 209 * @param string $sessionID
a79013ef 210 */
75cf36c3 211 public function load($sessionEditorClassName, $sessionID) {
11ade432
AE
212 $this->sessionEditorClassName = $sessionEditorClassName;
213 $this->sessionClassName = call_user_func(array($sessionEditorClassName, 'getBaseClass'));
827e7aea 214 $this->supportsVirtualSessions = call_user_func(array($this->sessionClassName, 'supportsVirtualSessions'));
11ade432
AE
215
216 // try to get existing session
217 if (!empty($sessionID)) {
218 $this->getExistingSession($sessionID);
219 }
220
221 // create new session
222 if ($this->session === null) {
223 $this->create();
224 }
225 }
226
227 /**
228 * Initializes session system.
a79013ef 229 */
11ade432
AE
230 public function initSession() {
231 // init session environment
232 $this->loadVariables();
233 $this->initSecurityToken();
281ac362
TD
234
235 // session id change was delayed to the next request
236 // as the SID constants already were defined
237 if ($this->getVar('__changeSessionID')) {
238 $this->unregister('__changeSessionID');
239 $this->changeSessionID();
240 }
11ade432
AE
241 $this->defineConstants();
242
83736ee3
AE
243 // assign language and style id
244 $this->languageID = ($this->getVar('languageID') === null) ? $this->user->languageID : $this->getVar('languageID');
245 $this->styleID = ($this->getVar('styleID') === null) ? $this->user->styleID : $this->getVar('styleID');
11ade432
AE
246
247 // init environment variables
248 $this->initEnvironment();
249 }
250
281ac362
TD
251 /**
252 * Changes the session id to a new random one.
253 *
254 * Usually a change is requested after login to ensure
255 * that the user is not running a fixated session by an
256 * attacker.
257 */
258 protected function changeSessionID() {
259 $oldSessionID = $this->session->sessionID;
260 $newSessionID = StringUtil::getRandomID();
261
a935240b 262 /** @var \wcf\data\DatabaseObjectEditor $sessionEditor */
281ac362
TD
263 $sessionEditor = new $this->sessionEditorClassName($this->session);
264 $sessionEditor->update(array(
265 'sessionID' => $newSessionID
266 ));
267
268 // fetch new session data from database
269 $this->session = new $this->sessionClassName($newSessionID);
270
b1a3cc1e 271 HeaderUtil::setCookie('cookieHash'.$this->cookieSuffix, $newSessionID);
11ade432
AE
272 }
273
274 /**
275 * Initializes environment variables.
276 */
277 protected function initEnvironment() {
278 $this->environment = array(
279 'lastRequestURI' => $this->session->requestURI,
280 'lastRequestMethod' => $this->session->requestMethod,
281 'ipAddress' => UserUtil::getIpAddress(),
282 'userAgent' => UserUtil::getUserAgent(),
283 'requestURI' => UserUtil::getRequestURI(),
167068f7 284 'requestMethod' => (!empty($_SERVER['REQUEST_METHOD']) ? substr($_SERVER['REQUEST_METHOD'], 0, 7) : '')
11ade432
AE
285 );
286 }
287
288 /**
289 * Disables update on shutdown.
290 */
291 public function disableUpdate() {
292 $this->doNotUpdate = true;
293 }
294
295 /**
9f959ced 296 * Defines global wcf constants related to session.
11ade432
AE
297 */
298 protected function defineConstants() {
b1a3cc1e
AE
299 /* the SID*-constants below are deprecated since 2.2 */
300 if (!defined('SID_ARG_1ST')) define('SID_ARG_1ST', '');
301 if (!defined('SID_ARG_2ND')) define('SID_ARG_2ND', '');
302 if (!defined('SID_ARG_2ND_NOT_ENCODED')) define('SID_ARG_2ND_NOT_ENCODED', '');
303 if (!defined('SID')) define('SID', '');
304 if (!defined('SID_INPUT_TAG')) define('SID_INPUT_TAG', '');
11ade432
AE
305
306 // security token
307 if (!defined('SECURITY_TOKEN')) define('SECURITY_TOKEN', $this->getSecurityToken());
308 if (!defined('SECURITY_TOKEN_INPUT_TAG')) define('SECURITY_TOKEN_INPUT_TAG', '<input type="hidden" name="t" value="'.$this->getSecurityToken().'" />');
309 }
310
311 /**
312 * Initializes security token.
a79013ef 313 */
11ade432
AE
314 protected function initSecurityToken() {
315 if ($this->getVar('__SECURITY_TOKEN') === null) {
316 $this->register('__SECURITY_TOKEN', StringUtil::getRandomID());
317 }
318 }
319
320 /**
321 * Returns security token.
322 *
323 * @return string
324 */
325 public function getSecurityToken() {
326 return $this->getVar('__SECURITY_TOKEN');
327 }
328
329 /**
330 * Validates the given security token, returns false if
331 * given token is invalid.
332 *
333 * @param string $token
334 * @return boolean
335 */
336 public function checkSecurityToken($token) {
d4f5c98c 337 return PasswordUtil::secureCompare($this->getSecurityToken(), $token);
11ade432
AE
338 }
339
340 /**
341 * Registers a session variable.
9f959ced 342 *
11ade432
AE
343 * @param string $key
344 * @param string $value
a79013ef 345 */
11ade432
AE
346 public function register($key, $value) {
347 $this->variables[$key] = $value;
348 $this->variablesChanged = true;
349 }
350
351 /**
352 * Unsets a session variable.
353 *
354 * @param string $key
a79013ef 355 */
11ade432
AE
356 public function unregister($key) {
357 unset($this->variables[$key]);
358 $this->variablesChanged = true;
359 }
360
361 /**
362 * Returns the value of a session variable.
363 *
364 * @param string $key
893aace3 365 * @return mixed
11ade432
AE
366 */
367 public function getVar($key) {
368 if (isset($this->variables[$key])) {
369 return $this->variables[$key];
370 }
371
372 return null;
373 }
374
375 /**
376 * Initializes session variables.
a79013ef 377 */
11ade432 378 protected function loadVariables() {
34c95e16 379 @$this->variables = unserialize($this->virtualSession->sessionVariables);
11ade432
AE
380 if (!is_array($this->variables)) {
381 $this->variables = array();
382 }
383 }
384
385 /**
386 * Returns the user object of this session.
9f959ced 387 *
0ad90fc3 388 * @return \wcf\data\user\User $user
11ade432
AE
389 */
390 public function getUser() {
391 return $this->user;
392 }
393
394 /**
9f959ced
MS
395 * Tries to read existing session identified by the given session id.
396 *
397 * @param string $sessionID
a79013ef 398 */
11ade432
AE
399 protected function getExistingSession($sessionID) {
400 $this->session = new $this->sessionClassName($sessionID);
5a05fde9 401 if (!$this->session->sessionID) {
75cf36c3 402 $this->session = null;
11ade432
AE
403 return;
404 }
405
11ade432 406 $this->user = new User($this->session->userID);
47c269ea 407 if ($this->isACP) {
e1622bfa
TD
408 $this->virtualSession = ACPSessionVirtual::getExistingSession($sessionID);
409 }
410 else {
411 $this->virtualSession = SessionVirtual::getExistingSession($sessionID);
412 }
5a05fde9
AE
413
414 if (!$this->validate()) {
415 $this->session = null;
416 $this->user = null;
417 $this->virtualSession = false;
418
419 return;
420 }
06fa4443 421
e1622bfa 422 $this->loadVirtualSession();
5a05fde9
AE
423 }
424
425 /**
426 * Loads the virtual session object unless the user is not logged in or the session
427 * does not support virtual sessions. If there is no virtual session yet, it will be
428 * created on-the-fly.
429 *
430 * @param boolean $forceReload
431 */
432 protected function loadVirtualSession($forceReload = false) {
699c0793 433 if ($this->virtualSession === null || $forceReload) {
5a05fde9 434 $this->virtualSession = null;
47c269ea 435 if ($this->isACP) {
945667b7
TD
436 $virtualSessionAction = new ACPSessionVirtualAction(array(), 'create', array('data' => array('sessionID' => $this->session->sessionID)));
437 }
438 else {
439 $virtualSessionAction = new SessionVirtualAction(array(), 'create', array('data' => array('sessionID' => $this->session->sessionID)));
440 }
441
442 try {
443 $returnValues = $virtualSessionAction->executeAction();
444 $this->virtualSession = $returnValues['returnValues'];
445 }
446 catch (DatabaseException $e) {
447 // MySQL error 23000 = unique key
448 // do not check against the message itself, some weird systems localize them
449 if ($e->getCode() == 23000) {
47c269ea 450 if ($this->isACP) {
945667b7
TD
451 $this->virtualSession = ACPSessionVirtual::getExistingSession($this->session->sessionID);
452 }
453 else {
454 $this->virtualSession = SessionVirtual::getExistingSession($this->session->sessionID);
dbb0aa36
AE
455 }
456 }
5a05fde9
AE
457 }
458 }
11ade432
AE
459 }
460
461 /**
e89c399a 462 * Validates the ip address and the user agent of this session.
11ade432
AE
463 *
464 * @return boolean
11ade432
AE
465 */
466 protected function validate() {
e89c399a 467 if (SESSION_VALIDATE_IP_ADDRESS) {
945667b7 468 if ($this->virtualSession instanceof ACPSessionVirtual) {
5a05fde9
AE
469 if ($this->virtualSession->ipAddress != UserUtil::getIpAddress()) {
470 return false;
471 }
472 }
473 else if ($this->session->ipAddress != UserUtil::getIpAddress()) {
e89c399a
MW
474 return false;
475 }
476 }
5a05fde9 477
e89c399a 478 if (SESSION_VALIDATE_USER_AGENT) {
945667b7 479 if ($this->virtualSession instanceof ACPSessionVirtual) {
5a05fde9
AE
480 if ($this->virtualSession->userAgent != UserUtil::getUserAgent()) {
481 return false;
482 }
483 }
484 else if ($this->session->userAgent != UserUtil::getUserAgent()) {
e89c399a
MW
485 return false;
486 }
487 }
488
11ade432
AE
489 return true;
490 }
491
492 /**
493 * Creates a new session.
a79013ef 494 */
11ade432 495 protected function create() {
e8d26212
MW
496 $spiderID = null;
497 if ($this->sessionEditorClassName == 'wcf\data\session\SessionEditor') {
498 // get spider information
499 $spiderID = $this->getSpiderID(UserUtil::getUserAgent());
500 if ($spiderID !== null) {
501 // try to use existing session
502 if (($session = $this->getExistingSpiderSession($spiderID)) !== null) {
503 $this->user = new User(null);
504 $this->session = $session;
505 return;
506 }
507 }
508 }
509
11ade432
AE
510 // create new session hash
511 $sessionID = StringUtil::getRandomID();
512
513 // get user automatically
2a51a8f9 514 $this->user = UserAuthenticationFactory::getInstance()->getUserAuthentication()->loginAutomatically(call_user_func(array($this->sessionClassName, 'supportsPersistentLogins')));
11ade432
AE
515
516 // create user
517 if ($this->user === null) {
518 // no valid user found
519 // create guest user
520 $this->user = new User(null);
521 }
5a05fde9 522 else if (!$this->supportsVirtualSessions) {
05c21784 523 // delete all other sessions of this user
ece1a6c7 524 call_user_func(array($this->sessionEditorClassName, 'deleteUserSessions'), array($this->user->userID));
05c21784
MW
525 }
526
827e7aea 527 $createNewSession = true;
945667b7
TD
528 // find existing session
529 $session = call_user_func(array($this->sessionClassName, 'getSessionByUserID'), $this->user->userID);
530
531 if ($session !== null) {
532 // inherit existing session
533 $this->session = $session;
534 $this->loadVirtualSession(true);
535
536 $createNewSession = false;
827e7aea 537 }
5a05fde9 538
827e7aea
AE
539 if ($createNewSession) {
540 // save session
541 $sessionData = array(
542 'sessionID' => $sessionID,
543 'userID' => $this->user->userID,
544 'ipAddress' => UserUtil::getIpAddress(),
545 'userAgent' => UserUtil::getUserAgent(),
546 'lastActivityTime' => TIME_NOW,
547 'requestURI' => UserUtil::getRequestURI(),
548 'requestMethod' => (!empty($_SERVER['REQUEST_METHOD']) ? substr($_SERVER['REQUEST_METHOD'], 0, 7) : '')
549 );
550
551 if ($spiderID !== null) $sessionData['spiderID'] = $spiderID;
7ff7c082
AE
552
553 try {
554 $this->session = call_user_func(array($this->sessionEditorClassName, 'create'), $sessionData);
555 }
556 catch (DatabaseException $e) {
557 // MySQL error 23000 = unique key
558 // do not check against the message itself, some weird systems localize them
945667b7 559 if ($e->getCode() == 23000) {
7ff7c082
AE
560 // find existing session
561 $session = call_user_func(array($this->sessionClassName, 'getSessionByUserID'), $this->user->userID);
562
563 if ($session === null) {
564 // MySQL reported a unique key error, but no corresponding session exists, rethrow exception
565 throw $e;
566 }
567 else {
568 // inherit existing session
569 $this->session = $session;
570 $this->loadVirtualSession(true);
571 }
572 }
573 else {
574 // unrelated to user id
575 throw $e;
576 }
577 }
578
3d7fee9e 579 $this->firstVisit = true;
827e7aea
AE
580 $this->loadVirtualSession(true);
581 }
11ade432
AE
582 }
583
584 /**
585 * Returns the value of the permission with the given name.
9f959ced
MS
586 *
587 * @param string $permission
11ade432
AE
588 * @return mixed permission value
589 */
590 public function getPermission($permission) {
ae6b590f
MS
591 // check if a users only permission is checked for a guest and return
592 // false if that is the case
593 if (!$this->user->userID && in_array($permission, $this->usersOnlyPermissions)) {
594 return false;
595 }
596
11ade432
AE
597 $this->loadGroupData();
598
599 if (!isset($this->groupData[$permission])) return false;
600 return $this->groupData[$permission];
601 }
602
603 /**
e3369fd2 604 * Checks if the active user has the given permissions and throws a
7c720e36 605 * PermissionDeniedException if that isn't the case.
a935240b 606 *
893aace3
MS
607 * @param string[] $permissions ist of permissions where each one must pass
608 * @throws PermissionDeniedException
11ade432 609 */
7c720e36 610 public function checkPermissions(array $permissions) {
11ade432
AE
611 foreach ($permissions as $permission) {
612 if (!$this->getPermission($permission)) {
613 throw new PermissionDeniedException();
614 }
615 }
616 }
617
618 /**
619 * Loads group data from cache.
a79013ef 620 */
11ade432
AE
621 protected function loadGroupData() {
622 if ($this->groupData !== null) return;
623
624 // work-around for setup process (package wcf does not exist yet)
625 if (!PACKAGE_ID) {
11ade432
AE
626 $sql = "SELECT groupID
627 FROM wcf".WCF_N."_user_to_group
628 WHERE userID = ?";
629 $statement = WCF::getDB()->prepareStatement($sql);
630 $statement->execute(array($this->user->userID));
cd975610 631 $groupIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
11ade432
AE
632 }
633 else {
634 $groupIDs = $this->user->getGroupIDs();
635 }
636
11ade432 637 // get group data from cache
b401cd0d 638 $this->groupData = UserGroupPermissionCacheBuilder::getInstance()->getData($groupIDs);
23253208 639 if (isset($this->groupData['groupIDs']) && $this->groupData['groupIDs'] != $groupIDs) {
11ade432
AE
640 $this->groupData = array();
641 }
642 }
643
644 /**
645 * Returns language ids for active user.
59dc0db6 646 *
7a23a706 647 * @return integer[]
a79013ef 648 */
11ade432
AE
649 public function getLanguageIDs() {
650 $this->loadLanguageIDs();
651
652 return $this->languageIDs;
653 }
654
655 /**
656 * Loads language ids for active user.
a79013ef 657 */
11ade432
AE
658 protected function loadLanguageIDs() {
659 if ($this->languageIDs !== null) return;
660
661 $this->languageIDs = array();
662
663 if (!$this->user->userID) {
664 return;
665 }
666
667 // work-around for setup process (package wcf does not exist yet)
668 if (!PACKAGE_ID) {
669 $sql = "SELECT languageID
670 FROM wcf".WCF_N."_user_to_language
671 WHERE userID = ?";
672 $statement = WCF::getDB()->prepareStatement($sql);
673 $statement->execute(array($this->user->userID));
cd975610 674 $this->languageIDs = $statement->fetchAll(\PDO::FETCH_COLUMN);
11ade432
AE
675 }
676 else {
677 $this->languageIDs = $this->user->getLanguageIDs();
678 }
679 }
680
681 /**
682 * Stores a new user object in this session, e.g. a user was guest because not
683 * logged in, after the login his old session is used to store his full data.
9f959ced 684 *
a935240b 685 * @param \wcf\data\user\User $user
9f959ced 686 * @param boolean $hideSession if true, database won't be updated
a79013ef 687 */
e7fcae9d 688 public function changeUser(User $user, $hideSession = false) {
e1dc298d
JR
689 $eventParameters = array('user' => $user, 'hideSession' => $hideSession);
690
691 EventHandler::getInstance()->fireAction($this, 'beforeChangeUser', $eventParameters);
692
693 $user = $eventParameters['user'];
694 $hideSession = $eventParameters['hideSession'];
695
4eb06b20 696 // skip changeUserVirtual, if session will not be persistent anyway
945667b7 697 if (!$hideSession) {
43a32649 698 $this->changeUserVirtual($user);
ac094463 699 }
945667b7
TD
700
701 // update user reference
702 $this->user = $user;
ac094463
AE
703
704 // reset caches
705 $this->groupData = null;
706 $this->languageIDs = null;
707 $this->languageID = $this->user->languageID;
708 $this->styleID = $this->user->styleID;
709
2d90d60f
TD
710 // change language
711 WCF::setLanguage($this->languageID ?: 0);
712
02eafee6
AE
713 // in some cases the language id can be stuck in the session variables
714 $this->unregister('languageID');
715
e1dc298d 716 EventHandler::getInstance()->fireAction($this, 'afterChangeUser');
ac094463
AE
717 }
718
719 /**
945667b7 720 * Changes the user stored in the session.
ac094463 721 *
2b770bdd
MS
722 * @param User $user
723 * @throws DatabaseException
ac094463
AE
724 */
725 protected function changeUserVirtual(User $user) {
a935240b 726 /** @var \wcf\data\DatabaseObjectEditor $sessionEditor */
ac094463
AE
727
728 switch ($user->userID) {
729 //
730 // user -> guest (logout)
731 //
732 case 0:
733 // delete virtual session
1c5c58a0 734 if ($this->virtualSession) {
47c269ea 735 if ($this->isACP) {
e1622bfa
TD
736 $virtualSessionEditor = new ACPSessionVirtualEditor($this->virtualSession);
737 }
738 else {
739 $virtualSessionEditor = new SessionVirtualEditor($this->virtualSession);
740 }
1c5c58a0
MS
741 $virtualSessionEditor->delete();
742 }
ac094463 743
47c269ea 744 if ($this->isACP) {
e1622bfa
TD
745 $sessionCount = ACPSessionVirtual::countVirtualSessions($this->session->sessionID);
746 }
747 else {
748 $sessionCount = SessionVirtual::countVirtualSessions($this->session->sessionID);
749 }
750
ac094463 751 // there are still other virtual sessions, create a new session
e1622bfa 752 if ($sessionCount) {
ac094463
AE
753 // save session
754 $sessionData = array(
755 'sessionID' => StringUtil::getRandomID(),
756 'userID' => $user->userID,
757 'ipAddress' => UserUtil::getIpAddress(),
758 'userAgent' => UserUtil::getUserAgent(),
759 'lastActivityTime' => TIME_NOW,
760 'requestURI' => UserUtil::getRequestURI(),
761 'requestMethod' => (!empty($_SERVER['REQUEST_METHOD']) ? substr($_SERVER['REQUEST_METHOD'], 0, 7) : '')
762 );
763
764 $this->session = call_user_func(array($this->sessionEditorClassName, 'create'), $sessionData);
765
b1a3cc1e 766 HeaderUtil::setCookie('cookieHash'.$this->cookieSuffix, $this->session->sessionID);
ac094463
AE
767 }
768 else {
769 // this was the last virtual session, re-use current session
770 // update session
771 $sessionEditor = new $this->sessionEditorClassName($this->session);
772 $sessionEditor->update(array(
773 'userID' => $user->userID
774 ));
775 }
776 break;
777
778 //
779 // guest -> user (login)
780 //
781 default:
945667b7
TD
782 if (!$this->supportsVirtualSessions) {
783 // delete all other sessions of this user
784 call_user_func(array($this->sessionEditorClassName, 'deleteUserSessions'), array($user->userID));
785 }
786
ac094463 787 // find existing session for this user
5a05fde9
AE
788 $session = call_user_func(array($this->sessionClassName, 'getSessionByUserID'), $user->userID);
789
ac094463
AE
790 // no session exists, re-use current session
791 if ($session === null) {
792 // update session
793 $sessionEditor = new $this->sessionEditorClassName($this->session);
46dd7463
AE
794
795 try {
281ac362 796 $this->register('__changeSessionID', true);
254057de 797
46dd7463
AE
798 $sessionEditor->update(array(
799 'userID' => $user->userID
800 ));
801 }
802 catch (DatabaseException $e) {
803 // MySQL error 23000 = unique key
804 // do not check against the message itself, some weird systems localize them
805 if ($e->getCode() == 23000) {
796d4f14 806 // delete guest session
46dd7463 807 $sessionEditor = new $this->sessionEditorClassName($this->session);
796d4f14
TD
808 $sessionEditor->delete();
809
810 // inherit existing session
811 $this->session = $session;
46dd7463
AE
812 }
813 else {
814 // not our business
815 throw $e;
816 }
817 }
ac094463
AE
818 }
819 else {
5a05fde9
AE
820 // delete guest session
821 $sessionEditor = new $this->sessionEditorClassName($this->session);
822 $sessionEditor->delete();
823
824 // inherit existing session
825 $this->session = $session;
c0b043df
AE
826
827 // inherit security token
34c95e16 828 $variables = @unserialize($this->virtualSession->sessionVariables);
c0b043df
AE
829 if (is_array($variables) && !empty($variables['__SECURITY_TOKEN'])) {
830 $this->register('__SECURITY_TOKEN', $variables['__SECURITY_TOKEN']);
831 }
f48f5d48 832
b1a3cc1e 833 HeaderUtil::setCookie('cookieHash'.$this->cookieSuffix, $this->session->sessionID);
5a05fde9 834 }
ac094463 835 break;
11ade432
AE
836 }
837
ac094463 838 $this->loadVirtualSession(true);
11ade432
AE
839 }
840
841 /**
842 * Updates user session on shutdown.
a79013ef 843 */
11ade432
AE
844 public function update() {
845 if ($this->doNotUpdate) return;
846
75cf36c3
MW
847 // set up data
848 $data = array(
ccc092e2 849 'ipAddress' => UserUtil::getIpAddress(),
11ade432
AE
850 'userAgent' => $this->userAgent,
851 'requestURI' => $this->requestURI,
852 'requestMethod' => $this->requestMethod,
b955b9e3 853 'lastActivityTime' => TIME_NOW
75cf36c3 854 );
f341086b 855 if (!class_exists('wcf\system\CLIWCF', false) && PACKAGE_ID && RequestHandler::getInstance()->getActiveRequest() && RequestHandler::getInstance()->getActiveRequest()->getRequestObject() instanceof ITrackablePage && RequestHandler::getInstance()->getActiveRequest()->getRequestObject()->isTracked()) {
596e20e2
MW
856 $data['controller'] = RequestHandler::getInstance()->getActiveRequest()->getRequestObject()->getController();
857 $data['parentObjectType'] = RequestHandler::getInstance()->getActiveRequest()->getRequestObject()->getParentObjectType();
858 $data['parentObjectID'] = RequestHandler::getInstance()->getActiveRequest()->getRequestObject()->getParentObjectID();
859 $data['objectType'] = RequestHandler::getInstance()->getActiveRequest()->getRequestObject()->getObjectType();
860 $data['objectID'] = RequestHandler::getInstance()->getActiveRequest()->getRequestObject()->getObjectID();
f341086b 861 }
75cf36c3
MW
862
863 // update session
a935240b 864 /** @var \wcf\data\DatabaseObjectEditor $sessionEditor */
75cf36c3
MW
865 $sessionEditor = new $this->sessionEditorClassName($this->session);
866 $sessionEditor->update($data);
5a05fde9 867
e1622bfa 868 if ($this->virtualSession instanceof ACPSessionVirtual) {
47c269ea 869 if ($this->isACP) {
e1622bfa
TD
870 $virtualSessionEditor = new ACPSessionVirtualEditor($this->virtualSession);
871 }
872 else {
873 $virtualSessionEditor = new SessionVirtualEditor($this->virtualSession);
874 }
5a05fde9 875 $virtualSessionEditor->updateLastActivityTime();
34c95e16
TD
876
877 $data = [];
878 if ($this->variablesChanged) {
879 $data['sessionVariables'] = serialize($this->variables);
880 }
881 $virtualSessionEditor->update($data);
5a05fde9 882 }
11ade432
AE
883 }
884
dd932bc6
AE
885 /**
886 * Updates last activity time to protect session from expiring.
887 */
888 public function keepAlive() {
889 $this->disableUpdate();
890
891 // update last activity time
a935240b 892 /** @var \wcf\data\DatabaseObjectEditor $sessionEditor */
dd932bc6
AE
893 $sessionEditor = new $this->sessionEditorClassName($this->session);
894 $sessionEditor->update(array(
895 'lastActivityTime' => TIME_NOW
896 ));
5a05fde9 897
e1622bfa 898 if ($this->virtualSession instanceof ACPSessionVirtual) {
47c269ea 899 if ($this->isACP) {
e1622bfa
TD
900 $virtualSessionEditor = new ACPSessionVirtualEditor($this->virtualSession);
901 }
902 else {
903 $virtualSessionEditor = new SessionVirtualEditor($this->virtualSession);
904 }
5a05fde9
AE
905 $virtualSessionEditor->updateLastActivityTime();
906 }
dd932bc6
AE
907 }
908
11ade432
AE
909 /**
910 * Deletes this session and it's related data.
a79013ef 911 */
11ade432 912 public function delete() {
235c040a
MW
913 // clear storage
914 if ($this->user->userID) {
915 self::resetSessions(array($this->user->userID));
5a05fde9 916
235c040a 917 // update last activity time
47c269ea 918 if (!$this->isACP) {
235c040a
MW
919 $editor = new UserEditor($this->user);
920 $editor->update(array('lastActivityTime' => TIME_NOW));
921 }
922 }
923
43a32649
TD
924 // 1st: Change user to guest, otherwise other the entire session, including
925 // all virtual sessions of the user will be deleted
926 $this->changeUser(new User(null));
9b1e73a7 927
43a32649 928 // 2nd: Actually remove session
a935240b 929 /** @var \wcf\data\DatabaseObjectEditor $sessionEditor */
43a32649
TD
930 $sessionEditor = new $this->sessionEditorClassName($this->session);
931 $sessionEditor->delete();
11ade432 932
11ade432
AE
933 // disable update
934 $this->disableUpdate();
935 }
936
937 /**
81e33547 938 * Returns currently active language id.
9f959ced 939 *
11ade432 940 * @return integer
a79013ef 941 */
11ade432
AE
942 public function getLanguageID() {
943 return $this->languageID;
944 }
945
81e33547
MS
946 /**
947 * Sets the currently active language id.
9f959ced 948 *
81e33547 949 * @param integer $languageID
a79013ef 950 */
81e33547
MS
951 public function setLanguageID($languageID) {
952 $this->languageID = $languageID;
83736ee3
AE
953 $this->register('languageID', $this->languageID);
954 }
955
956 /**
957 * Returns currently active style id.
958 *
959 * @return integer
960 */
961 public function getStyleID() {
962 return $this->styleID;
963 }
964
965 /**
966 * Sets the currently active style id.
967 *
968 * @param integer $styleID
969 */
970 public function setStyleID($styleID) {
971 $this->styleID = $styleID;
972 $this->register('styleID', $this->styleID);
81e33547
MS
973 }
974
11ade432
AE
975 /**
976 * Resets session-specific storage data.
9f959ced 977 *
7a23a706 978 * @param integer[] $userIDs
a79013ef 979 */
11ade432 980 public static function resetSessions(array $userIDs = array()) {
15fa2802 981 if (!empty($userIDs)) {
a935240b
AE
982 UserStorageHandler::getInstance()->reset($userIDs, 'groupIDs');
983 UserStorageHandler::getInstance()->reset($userIDs, 'languageIDs');
11ade432
AE
984 }
985 else {
a935240b
AE
986 UserStorageHandler::getInstance()->resetAll('groupIDs');
987 UserStorageHandler::getInstance()->resetAll('languageIDs');
11ade432
AE
988 }
989 }
e8d26212
MW
990
991 /**
992 * Returns the spider id for given user agent.
59dc0db6 993 *
06355ec3 994 * @param string $userAgent
e8d26212
MW
995 * @return mixed
996 */
997 protected function getSpiderID($userAgent) {
998 $spiderList = SpiderCacheBuilder::getInstance()->getData();
999 $userAgent = strtolower($userAgent);
1000
1001 foreach ($spiderList as $spider) {
1002 if (strpos($userAgent, $spider->spiderIdentifier) !== false) {
1003 return $spider->spiderID;
1004 }
1005 }
1006
1007 return null;
1008 }
1009
1010 /**
1011 * Searches for existing session of a search spider.
59dc0db6 1012 *
06355ec3 1013 * @param integer $spiderID
0ad90fc3 1014 * @return \wcf\data\session\Session
e8d26212
MW
1015 */
1016 protected function getExistingSpiderSession($spiderID) {
06355ec3
MS
1017 $sql = "SELECT *
1018 FROM wcf".WCF_N."_session
1019 WHERE spiderID = ?
e8d26212
MW
1020 AND userID IS NULL";
1021 $statement = WCF::getDB()->prepareStatement($sql);
1022 $statement->execute(array($spiderID));
1023 $row = $statement->fetchArray();
1024 if ($row !== false) {
1025 // fix session validation
1026 $row['ipAddress'] = UserUtil::getIpAddress();
1027 $row['userAgent'] = UserUtil::getUserAgent();
06355ec3 1028
e8d26212
MW
1029 // return session object
1030 return new $this->sessionClassName(null, $row);
1031 }
1032
1033 return null;
1034 }
3d7fee9e
MW
1035
1036 /**
0f0590c2
MS
1037 * Returns true if this is a new session.
1038 *
3d7fee9e
MW
1039 * @return boolean
1040 */
1041 public function isFirstVisit() {
1042 return $this->firstVisit;
1043 }
11ade432 1044}