Fixed time zone calculation issue
[GitHub/WoltLab/WCF.git] / wcfsetup / install / files / lib / system / session / SessionHandler.class.php
1 <?php
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;
13 use wcf\system\WCF;
14 use wcf\util\PasswordUtil;
15 use wcf\util\StringUtil;
16 use wcf\util\UserUtil;
17
18 /**
19 * Handles sessions.
20 *
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
27 */
28 class SessionHandler extends SingletonFactory {
29 /**
30 * prevents update on shutdown
31 * @var boolean
32 */
33 protected $doNotUpdate = false;
34
35 /**
36 * various environment variables
37 * @var array
38 */
39 protected $environment = array();
40
41 /**
42 * group data and permissions
43 * @var array<array>
44 */
45 protected $groupData = null;
46
47 /**
48 * language id for active user
49 * @var integer
50 */
51 protected $languageID = 0;
52
53 /**
54 * language ids for active user
55 * @var array<integer>
56 */
57 protected $languageIDs = null;
58
59 /**
60 * session object
61 * @var \wcf\data\acp\session\ACPSession
62 */
63 protected $session = null;
64
65 /**
66 * session class name
67 * @var string
68 */
69 protected $sessionClassName = '';
70
71 /**
72 * session editor class name
73 * @var string
74 */
75 protected $sessionEditorClassName = '';
76
77 /**
78 * style id
79 * @var integer
80 */
81 protected $styleID = null;
82
83 /**
84 * enable cookie support
85 * @var boolean
86 */
87 protected $useCookies = false;
88
89 /**
90 * user object
91 * @var \wcf\data\user\User
92 */
93 protected $user = null;
94
95 /**
96 * session variables
97 * @var array
98 */
99 protected $variables = null;
100
101 /**
102 * indicates if session variables changed and must be saved upon shutdown
103 * @var boolean
104 */
105 protected $variablesChanged = false;
106
107 /**
108 * Provides access to session data.
109 *
110 * @param string $key
111 * @return mixed
112 */
113 public function __get($key) {
114 if (isset($this->environment[$key])) {
115 return $this->environment[$key];
116 }
117
118 return $this->session->{$key};
119 }
120
121 /**
122 * Loads an existing session or creates a new one.
123 *
124 * @param string $sessionEditorClassName
125 * @param string $sessionID
126 */
127 public function load($sessionEditorClassName, $sessionID) {
128 $this->sessionEditorClassName = $sessionEditorClassName;
129 $this->sessionClassName = call_user_func(array($sessionEditorClassName, 'getBaseClass'));
130
131 // try to get existing session
132 if (!empty($sessionID)) {
133 $this->getExistingSession($sessionID);
134 }
135
136 // create new session
137 if ($this->session === null) {
138 $this->create();
139 }
140 }
141
142 /**
143 * Initializes session system.
144 */
145 public function initSession() {
146 // init session environment
147 $this->loadVariables();
148 $this->initSecurityToken();
149 $this->defineConstants();
150
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');
154
155 // init environment variables
156 $this->initEnvironment();
157 }
158
159 /**
160 * Enables cookie support.
161 */
162 public function enableCookies() {
163 $this->useCookies = true;
164 }
165
166 /**
167 * Initializes environment variables.
168 */
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) : '')
177 );
178 }
179
180 /**
181 * Disables update on shutdown.
182 */
183 public function disableUpdate() {
184 $this->doNotUpdate = true;
185 }
186
187 /**
188 * Defines global wcf constants related to session.
189 */
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', '');
197 }
198 else {
199 if (!defined('SID_ARG_1ST')) define('SID_ARG_1ST', '?s='.$this->sessionID);
200 if (!defined('SID_ARG_2ND')) define('SID_ARG_2ND', '&amp;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.'" />');
204 }
205
206 // security token
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().'" />');
209 }
210
211 /**
212 * Initializes security token.
213 */
214 protected function initSecurityToken() {
215 if ($this->getVar('__SECURITY_TOKEN') === null) {
216 $this->register('__SECURITY_TOKEN', StringUtil::getRandomID());
217 }
218 }
219
220 /**
221 * Returns security token.
222 *
223 * @return string
224 */
225 public function getSecurityToken() {
226 return $this->getVar('__SECURITY_TOKEN');
227 }
228
229 /**
230 * Validates the given security token, returns false if
231 * given token is invalid.
232 *
233 * @param string $token
234 * @return boolean
235 */
236 public function checkSecurityToken($token) {
237 return PasswordUtil::secureCompare($this->getSecurityToken(), $token);
238 }
239
240 /**
241 * Registers a session variable.
242 *
243 * @param string $key
244 * @param string $value
245 */
246 public function register($key, $value) {
247 $this->variables[$key] = $value;
248 $this->variablesChanged = true;
249 }
250
251 /**
252 * Unsets a session variable.
253 *
254 * @param string $key
255 */
256 public function unregister($key) {
257 unset($this->variables[$key]);
258 $this->variablesChanged = true;
259 }
260
261 /**
262 * Returns the value of a session variable.
263 *
264 * @param string $key
265 */
266 public function getVar($key) {
267 if (isset($this->variables[$key])) {
268 return $this->variables[$key];
269 }
270
271 return null;
272 }
273
274 /**
275 * Initializes session variables.
276 */
277 protected function loadVariables() {
278 @$this->variables = unserialize($this->session->sessionVariables);
279 if (!is_array($this->variables)) {
280 $this->variables = array();
281 }
282 }
283
284 /**
285 * Returns the user object of this session.
286 *
287 * @return \wcf\data\user\User $user
288 */
289 public function getUser() {
290 return $this->user;
291 }
292
293 /**
294 * Tries to read existing session identified by the given session id.
295 *
296 * @param string $sessionID
297 */
298 protected function getExistingSession($sessionID) {
299 $this->session = new $this->sessionClassName($sessionID);
300 if (!$this->session->sessionID || !$this->validate()) {
301 $this->session = null;
302 return;
303 }
304
305 // load user
306 $this->user = new User($this->session->userID);
307 }
308
309 /**
310 * Validates the ip address and the user agent of this session.
311 *
312 * @return boolean
313 */
314 protected function validate() {
315 if (SESSION_VALIDATE_IP_ADDRESS) {
316 if ($this->session->ipAddress != UserUtil::getIpAddress()) {
317 return false;
318 }
319 }
320 if (SESSION_VALIDATE_USER_AGENT) {
321 if ($this->session->userAgent != UserUtil::getUserAgent()) {
322 return false;
323 }
324 }
325
326 return true;
327 }
328
329 /**
330 * Creates a new session.
331 */
332 protected function create() {
333 $spiderID = null;
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;
342 return;
343 }
344 }
345 }
346
347 // create new session hash
348 $sessionID = StringUtil::getRandomID();
349
350 // get user automatically
351 $this->user = UserAuthenticationFactory::getInstance()->getUserAuthentication()->loginAutomatically(call_user_func(array($this->sessionClassName, 'supportsPersistentLogins')));
352
353 // create user
354 if ($this->user === null) {
355 // no valid user found
356 // create guest user
357 $this->user = new User(null);
358 }
359
360 if ($this->user->userID != 0) {
361 // user is no guest
362 // delete all other sessions of this user
363 call_user_func(array($this->sessionEditorClassName, 'deleteUserSessions'), array($this->user->userID));
364 }
365
366 // save session
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) : '')
375 );
376 if ($spiderID !== null) $sessionData['spiderID'] = $spiderID;
377 $this->session = call_user_func(array($this->sessionEditorClassName, 'create'), $sessionData);
378 }
379
380 /**
381 * Returns the value of the permission with the given name.
382 *
383 * @param string $permission
384 * @return mixed permission value
385 */
386 public function getPermission($permission) {
387 $this->loadGroupData();
388
389 if (!isset($this->groupData[$permission])) return false;
390 return $this->groupData[$permission];
391 }
392
393 /**
394 * Checks if the active user has the given permissions and throws a
395 * PermissionDeniedException if that isn't the case.
396 */
397 public function checkPermissions(array $permissions) {
398 foreach ($permissions as $permission) {
399 if (!$this->getPermission($permission)) {
400 throw new PermissionDeniedException();
401 }
402 }
403 }
404
405 /**
406 * Loads group data from cache.
407 */
408 protected function loadGroupData() {
409 if ($this->groupData !== null) return;
410
411 // work-around for setup process (package wcf does not exist yet)
412 if (!PACKAGE_ID) {
413 $groupIDs = array();
414 $sql = "SELECT groupID
415 FROM wcf".WCF_N."_user_to_group
416 WHERE userID = ?";
417 $statement = WCF::getDB()->prepareStatement($sql);
418 $statement->execute(array($this->user->userID));
419 while ($row = $statement->fetchArray()) {
420 $groupIDs[] = $row['groupID'];
421 }
422 }
423 else {
424 $groupIDs = $this->user->getGroupIDs();
425 }
426
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();
431 }
432 }
433
434 /**
435 * Returns language ids for active user.
436 *
437 * @return array<integer>
438 */
439 public function getLanguageIDs() {
440 $this->loadLanguageIDs();
441
442 return $this->languageIDs;
443 }
444
445 /**
446 * Loads language ids for active user.
447 */
448 protected function loadLanguageIDs() {
449 if ($this->languageIDs !== null) return;
450
451 $this->languageIDs = array();
452
453 if (!$this->user->userID) {
454 return;
455 }
456
457 // work-around for setup process (package wcf does not exist yet)
458 if (!PACKAGE_ID) {
459 $sql = "SELECT languageID
460 FROM wcf".WCF_N."_user_to_language
461 WHERE userID = ?";
462 $statement = WCF::getDB()->prepareStatement($sql);
463 $statement->execute(array($this->user->userID));
464 while ($row = $statement->fetchArray()) {
465 $this->languageIDs[] = $row['languageID'];
466 }
467 }
468 else {
469 $this->languageIDs = $this->user->getLanguageIDs();
470 }
471 }
472
473 /**
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.
476 *
477 * @param \wcf\data\userUser $user
478 * @param boolean $hideSession if true, database won't be updated
479 */
480 public function changeUser(User $user, $hideSession = false) {
481 $sessionTable = call_user_func(array($this->sessionClassName, 'getDatabaseTableName'));
482
483 if ($user->userID && !$hideSession) {
484 // user is not a guest, delete all other sessions of this user
485 $sql = "DELETE FROM ".$sessionTable."
486 WHERE sessionID <> ?
487 AND userID = ?";
488 $statement = WCF::getDB()->prepareStatement($sql);
489 $statement->execute(array($this->sessionID, $user->userID));
490
491 // reset session variables
492 $this->variables = array();
493 $this->variablesChanged = true;
494 }
495
496 // update user reference
497 $this->user = $user;
498
499 if (!$hideSession) {
500 // update session
501 $sessionEditor = new $this->sessionEditorClassName($this->session);
502 $sessionEditor->update(array(
503 'userID' => $this->user->userID
504 ));
505 }
506
507 // reset caches
508 $this->groupData = null;
509 $this->languageIDs = null;
510 $this->languageID = $this->user->languageID;
511 $this->styleID = $this->user->styleID;
512
513 // truncate session variables
514 }
515
516 /**
517 * Updates user session on shutdown.
518 */
519 public function update() {
520 if ($this->doNotUpdate) return;
521
522 // set up data
523 $data = array(
524 'ipAddress' => UserUtil::getIpAddress(),
525 'userAgent' => $this->userAgent,
526 'requestURI' => $this->requestURI,
527 'requestMethod' => $this->requestMethod,
528 'lastActivityTime' => TIME_NOW
529 );
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();
536 }
537 if ($this->variablesChanged) {
538 $data['sessionVariables'] = serialize($this->variables);
539 }
540
541 // update session
542 $sessionEditor = new $this->sessionEditorClassName($this->session);
543 $sessionEditor->update($data);
544 }
545
546 /**
547 * Updates last activity time to protect session from expiring.
548 */
549 public function keepAlive() {
550 $this->disableUpdate();
551
552 // update last activity time
553 $sessionEditor = new $this->sessionEditorClassName($this->session);
554 $sessionEditor->update(array(
555 'lastActivityTime' => TIME_NOW
556 ));
557 }
558
559 /**
560 * Deletes this session and it's related data.
561 */
562 public function delete() {
563 // clear storage
564 if ($this->user->userID) {
565 self::resetSessions(array($this->user->userID));
566
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));
571 }
572 }
573
574 // set user to guest
575 $this->changeUser(new User(null), true);
576
577 // remove session
578 $sessionEditor = new $this->sessionEditorClassName($this->session);
579 $sessionEditor->delete();
580
581 // disable update
582 $this->disableUpdate();
583 }
584
585 /**
586 * Returns currently active language id.
587 *
588 * @return integer
589 */
590 public function getLanguageID() {
591 return $this->languageID;
592 }
593
594 /**
595 * Sets the currently active language id.
596 *
597 * @param integer $languageID
598 */
599 public function setLanguageID($languageID) {
600 $this->languageID = $languageID;
601 $this->register('languageID', $this->languageID);
602 }
603
604 /**
605 * Returns currently active style id.
606 *
607 * @return integer
608 */
609 public function getStyleID() {
610 return $this->styleID;
611 }
612
613 /**
614 * Sets the currently active style id.
615 *
616 * @param integer $styleID
617 */
618 public function setStyleID($styleID) {
619 $this->styleID = $styleID;
620 $this->register('styleID', $this->styleID);
621 }
622
623 /**
624 * Resets session-specific storage data.
625 *
626 * @param array<integer> $userIDs
627 */
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);
632 }
633 else {
634 UserStorageHandler::getInstance()->resetAll('groupIDs', 1);
635 UserStorageHandler::getInstance()->resetAll('languageIDs', 1);
636 }
637 }
638
639 /**
640 * Returns the spider id for given user agent.
641 *
642 * @param string $userAgent
643 * @return mixed
644 */
645 protected function getSpiderID($userAgent) {
646 $spiderList = SpiderCacheBuilder::getInstance()->getData();
647 $userAgent = strtolower($userAgent);
648
649 foreach ($spiderList as $spider) {
650 if (strpos($userAgent, $spider->spiderIdentifier) !== false) {
651 return $spider->spiderID;
652 }
653 }
654
655 return null;
656 }
657
658 /**
659 * Searches for existing session of a search spider.
660 *
661 * @param integer $spiderID
662 * @return \wcf\data\session\Session
663 */
664 protected function getExistingSpiderSession($spiderID) {
665 $sql = "SELECT *
666 FROM wcf".WCF_N."_session
667 WHERE spiderID = ?
668 AND userID IS NULL";
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();
676
677 // return session object
678 return new $this->sessionClassName(null, $row);
679 }
680
681 return null;
682 }
683 }