Commit | Line | Data |
---|---|---|
11ade432 AE |
1 | <?php |
2 | namespace wcf\system\session; | |
e1622bfa TD |
3 | use wcf\data\acp\session\virtual\ACPSessionVirtual; |
4 | use wcf\data\acp\session\virtual\ACPSessionVirtualAction; | |
5 | use wcf\data\acp\session\virtual\ACPSessionVirtualEditor; | |
5a05fde9 AE |
6 | use wcf\data\session\virtual\SessionVirtual; |
7 | use wcf\data\session\virtual\SessionVirtualAction; | |
8 | use wcf\data\session\virtual\SessionVirtualEditor; | |
11ade432 | 9 | use wcf\data\user\User; |
235c040a | 10 | use wcf\data\user\UserEditor; |
8f3fc897 | 11 | use wcf\page\ITrackablePage; |
e8d26212 | 12 | use wcf\system\cache\builder\SpiderCacheBuilder; |
ae6b590f | 13 | use wcf\system\cache\builder\UserGroupOptionCacheBuilder; |
b401cd0d | 14 | use wcf\system\cache\builder\UserGroupPermissionCacheBuilder; |
a5a77025 | 15 | use wcf\system\database\DatabaseException; |
e1dc298d | 16 | use wcf\system\event\EventHandler; |
11ade432 | 17 | use wcf\system\exception\PermissionDeniedException; |
7aa1a486 | 18 | use wcf\system\request\RequestHandler; |
75afb100 | 19 | use wcf\system\user\authentication\UserAuthenticationFactory; |
c96ee721 | 20 | use wcf\system\user\storage\UserStorageHandler; |
11ade432 AE |
21 | use wcf\system\SingletonFactory; |
22 | use wcf\system\WCF; | |
e1622bfa | 23 | use wcf\system\WCFACP; |
ac094463 | 24 | use wcf\util\HeaderUtil; |
d4f5c98c | 25 | use wcf\util\PasswordUtil; |
11ade432 AE |
26 | use wcf\util\StringUtil; |
27 | use 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 | */ |
39 | class 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 | } |