[BUGFIX] Correctly insert session data after deleting them
[Packages/TYPO3.CMS.git] / typo3 / sysext / frontend / Classes / Authentication / FrontendUserAuthentication.php
1 <?php
2 namespace TYPO3\CMS\Frontend\Authentication;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
18 use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
19 use TYPO3\CMS\Core\Utility\GeneralUtility;
20
21 /**
22 * Extension class for Front End User Authentication.
23 *
24 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
25 * @author René Fritz <r.fritz@colorcube.de>
26 */
27 class FrontendUserAuthentication extends AbstractUserAuthentication {
28
29 /**
30 * form field with 0 or 1
31 * 1 = permanent login enabled
32 * 0 = session is valid for a browser session only
33 * @var string
34 */
35 public $formfield_permanent = 'permalogin';
36
37 /**
38 * Lifetime of session data in seconds.
39 * @var int
40 */
41 protected $sessionDataLifetime = 86400;
42
43 /**
44 * @var string
45 */
46 public $usergroup_column = 'usergroup';
47
48 /**
49 * @var string
50 */
51 public $usergroup_table = 'fe_groups';
52
53 /**
54 * @var array
55 */
56 public $groupData = array(
57 'title' => array(),
58 'uid' => array(),
59 'pid' => array()
60 );
61
62 /**
63 * Used to accumulate the TSconfig data of the user
64 * @var array
65 */
66 public $TSdataArray = array();
67
68 /**
69 * @var array
70 */
71 public $userTS = array();
72
73 /**
74 * @var bool
75 */
76 public $userTSUpdated = FALSE;
77
78 /**
79 * Session and user data:
80 * There are two types of data that can be stored: UserData and Session-Data.
81 * Userdata is for the login-user, and session-data for anyone viewing the pages.
82 * 'Keys' are keys in the internal data array of the data.
83 * When you get or set a key in one of the data-spaces (user or session) you decide the type of the variable (not object though)
84 * 'Reserved' keys are:
85 * - 'recs': Array: Used to 'register' records, eg in a shopping basket. Structure: [recs][tablename][record_uid]=number
86 * - sys: Reserved for TypoScript standard code.
87 *
88 * @var array
89 */
90 public $sesData = array();
91
92 /**
93 * @var bool
94 */
95 public $sesData_change = FALSE;
96
97 /**
98 * @var bool
99 */
100 public $userData_change = FALSE;
101
102 /**
103 * @var bool
104 */
105 public $is_permanent;
106
107 /**
108 * @var int|NULL
109 */
110 protected $sessionDataTimestamp = NULL;
111
112 /**
113 * @var bool
114 */
115 protected $loginHidden = FALSE;
116
117 /**
118 * Default constructor.
119 */
120 public function __construct() {
121 parent::__construct();
122
123 // Disable cookie by default, will be activated if saveSessionData() is called,
124 // a user is logging-in or an existing session is found
125 $this->dontSetCookie = TRUE;
126
127 $this->session_table = 'fe_sessions';
128 $this->name = self::getCookieName();
129 $this->get_name = 'ftu';
130 $this->loginType = 'FE';
131 $this->user_table = 'fe_users';
132 $this->username_column = 'username';
133 $this->userident_column = 'password';
134 $this->userid_column = 'uid';
135 $this->lastLogin_column = 'lastlogin';
136 $this->enablecolumns = array(
137 'deleted' => 'deleted',
138 'disabled' => 'disable',
139 'starttime' => 'starttime',
140 'endtime' => 'endtime'
141 );
142 $this->formfield_uname = 'user';
143 $this->formfield_uident = 'pass';
144 $this->formfield_chalvalue = 'challenge';
145 $this->formfield_status = 'logintype';
146 $this->auth_timeout_field = 6000;
147 $this->sendNoCacheHeaders = FALSE;
148 $this->getFallBack = TRUE;
149 $this->getMethodEnabled = TRUE;
150 }
151
152 /**
153 * Returns the configured cookie name
154 *
155 * @return string
156 */
157 static public function getCookieName() {
158 $configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['FE']['cookieName']);
159 if (empty($configuredCookieName)) {
160 $configuredCookieName = 'fe_typo_user';
161 }
162 return $configuredCookieName;
163 }
164
165 /**
166 * Starts a user session
167 *
168 * @return void
169 * @see AbstractUserAuthentication::start()
170 */
171 public function start() {
172 if ((int)$this->auth_timeout_field > 0 && (int)$this->auth_timeout_field < $this->lifetime) {
173 // If server session timeout is non-zero but less than client session timeout: Copy this value instead.
174 $this->auth_timeout_field = $this->lifetime;
175 }
176 $this->sessionDataLifetime = (int)$GLOBALS['TYPO3_CONF_VARS']['FE']['sessionDataLifetime'];
177 if ($this->sessionDataLifetime <= 0) {
178 $this->sessionDataLifetime = 86400;
179 }
180 parent::start();
181 }
182
183 /**
184 * Returns a new session record for the current user for insertion into the DB.
185 *
186 * @param array $tempuser
187 * @return array User session record
188 */
189 public function getNewSessionRecord($tempuser) {
190 $insertFields = parent::getNewSessionRecord($tempuser);
191 $insertFields['ses_permanent'] = $this->is_permanent;
192 return $insertFields;
193 }
194
195 /**
196 * Determine whether a session cookie needs to be set (lifetime=0)
197 *
198 * @return bool
199 * @internal
200 */
201 public function isSetSessionCookie() {
202 return ($this->newSessionID || $this->forceSetCookie)
203 && ($this->lifetime == 0 || !isset($this->user['ses_permanent']) || !$this->user['ses_permanent']);
204 }
205
206 /**
207 * Determine whether a non-session cookie needs to be set (lifetime>0)
208 *
209 * @return bool
210 * @internal
211 */
212 public function isRefreshTimeBasedCookie() {
213 return $this->lifetime > 0 && isset($this->user['ses_permanent']) && $this->user['ses_permanent'];
214 }
215
216 /**
217 * Returns an info array with Login/Logout data submitted by a form or params
218 *
219 * @return array
220 * @see AbstractUserAuthentication::getLoginFormData()
221 */
222 public function getLoginFormData() {
223 $loginData = parent::getLoginFormData();
224 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 0 || $GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 1) {
225 if ($this->getMethodEnabled) {
226 $isPermanent = GeneralUtility::_GP($this->formfield_permanent);
227 } else {
228 $isPermanent = GeneralUtility::_POST($this->formfield_permanent);
229 }
230 if (strlen($isPermanent) != 1) {
231 $isPermanent = $GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'];
232 } elseif (!$isPermanent) {
233 // To make sure the user gets a session cookie and doesn't keep a possibly existing time based cookie,
234 // we need to force setting the session cookie here
235 $this->forceSetCookie = TRUE;
236 }
237 $isPermanent = $isPermanent ? 1 : 0;
238 } elseif ($GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 2) {
239 $isPermanent = 1;
240 } else {
241 $isPermanent = 0;
242 }
243 $loginData['permanent'] = $isPermanent;
244 $this->is_permanent = $isPermanent;
245 return $loginData;
246 }
247
248 /**
249 * Creates a user session record and returns its values.
250 * However, as the FE user cookie is normally not set, this has to be done
251 * before the parent class is doing the rest.
252 *
253 * @param array $tempuser User data array
254 * @return array The session data for the newly created session.
255 */
256 public function createUserSession($tempuser) {
257 // At this point we do not know if we need to set a session or a "permanant" cookie
258 // So we force the cookie to be set after authentication took place, which will
259 // then call setSessionCookie(), which will set a cookie with correct settings.
260 $this->dontSetCookie = FALSE;
261 return parent::createUserSession($tempuser);
262 }
263
264 /**
265 * Will select all fe_groups records that the current fe_user is member of
266 * and which groups are also allowed in the current domain.
267 * It also accumulates the TSconfig for the fe_user/fe_groups in ->TSdataArray
268 *
269 * @return int Returns the number of usergroups for the frontend users (if the internal user record exists and the usergroup field contains a value)
270 */
271 public function fetchGroupData() {
272 $this->TSdataArray = array();
273 $this->userTS = array();
274 $this->userTSUpdated = FALSE;
275 $this->groupData = array(
276 'title' => array(),
277 'uid' => array(),
278 'pid' => array()
279 );
280 // Setting default configuration:
281 $this->TSdataArray[] = $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultUserTSconfig'];
282 // Get the info data for auth services
283 $authInfo = $this->getAuthInfoArray();
284 if ($this->writeDevLog) {
285 if (is_array($this->user)) {
286 GeneralUtility::devLog('Get usergroups for user: ' . GeneralUtility::arrayToLogString($this->user, array($this->userid_column, $this->username_column)), __CLASS__);
287 } else {
288 GeneralUtility::devLog('Get usergroups for "anonymous" user', __CLASS__);
289 }
290 }
291 $groupDataArr = array();
292 // Use 'auth' service to find the groups for the user
293 $serviceChain = '';
294 $subType = 'getGroups' . $this->loginType;
295 while (is_object($serviceObj = GeneralUtility::makeInstanceService('auth', $subType, $serviceChain))) {
296 $serviceChain .= ',' . $serviceObj->getServiceKey();
297 $serviceObj->initAuth($subType, array(), $authInfo, $this);
298 $groupData = $serviceObj->getGroups($this->user, $groupDataArr);
299 if (is_array($groupData) && count($groupData)) {
300 // Keys in $groupData should be unique ids of the groups (like "uid") so this function will override groups.
301 $groupDataArr = $groupData + $groupDataArr;
302 }
303 unset($serviceObj);
304 }
305 if ($this->writeDevLog && $serviceChain) {
306 GeneralUtility::devLog($subType . ' auth services called: ' . $serviceChain, __CLASS__);
307 }
308 if ($this->writeDevLog && !count($groupDataArr)) {
309 GeneralUtility::devLog('No usergroups found by services', __CLASS__);
310 }
311 if ($this->writeDevLog && count($groupDataArr)) {
312 GeneralUtility::devLog(count($groupDataArr) . ' usergroup records found by services', __CLASS__);
313 }
314 // Use 'auth' service to check the usergroups if they are really valid
315 foreach ($groupDataArr as $groupData) {
316 // By default a group is valid
317 $validGroup = TRUE;
318 $serviceChain = '';
319 $subType = 'authGroups' . $this->loginType;
320 while (is_object($serviceObj = GeneralUtility::makeInstanceService('auth', $subType, $serviceChain))) {
321 $serviceChain .= ',' . $serviceObj->getServiceKey();
322 $serviceObj->initAuth($subType, array(), $authInfo, $this);
323 if (!$serviceObj->authGroup($this->user, $groupData)) {
324 $validGroup = FALSE;
325 if ($this->writeDevLog) {
326 GeneralUtility::devLog($subType . ' auth service did not auth group: ' . GeneralUtility::arrayToLogString($groupData, 'uid,title'), __CLASS__, 2);
327 }
328 break;
329 }
330 unset($serviceObj);
331 }
332 unset($serviceObj);
333 if ($validGroup) {
334 $this->groupData['title'][$groupData['uid']] = $groupData['title'];
335 $this->groupData['uid'][$groupData['uid']] = $groupData['uid'];
336 $this->groupData['pid'][$groupData['uid']] = $groupData['pid'];
337 $this->groupData['TSconfig'][$groupData['uid']] = $groupData['TSconfig'];
338 }
339 }
340 if (count($this->groupData) && count($this->groupData['TSconfig'])) {
341 // TSconfig: collect it in the order it was collected
342 foreach ($this->groupData['TSconfig'] as $TSdata) {
343 $this->TSdataArray[] = $TSdata;
344 }
345 $this->TSdataArray[] = $this->user['TSconfig'];
346 // Sort information
347 ksort($this->groupData['title']);
348 ksort($this->groupData['uid']);
349 ksort($this->groupData['pid']);
350 }
351 return count($this->groupData['uid']) ?: 0;
352 }
353
354 /**
355 * Returns the parsed TSconfig for the fe_user
356 * The TSconfig will be cached in $this->userTS.
357 *
358 * @return array TSconfig array for the fe_user
359 */
360 public function getUserTSconf() {
361 if (!$this->userTSUpdated) {
362 // Parsing the user TS (or getting from cache)
363 $this->TSdataArray = TypoScriptParser::checkIncludeLines_array($this->TSdataArray);
364 $userTS = implode(LF . '[GLOBAL]' . LF, $this->TSdataArray);
365 $parseObj = GeneralUtility::makeInstance(TypoScriptParser::class);
366 $parseObj->parse($userTS);
367 $this->userTS = $parseObj->setup;
368 $this->userTSUpdated = TRUE;
369 }
370 return $this->userTS;
371 }
372
373 /*****************************************
374 *
375 * Session data management functions
376 *
377 ****************************************/
378 /**
379 * Fetches the session data for the user (from the fe_session_data table) based on the ->id of the current user-session.
380 * The session data is restored to $this->sesData
381 * 1/100 calls will also do a garbage collection.
382 *
383 * @return void
384 * @access private
385 * @see storeSessionData()
386 */
387 public function fetchSessionData() {
388 // Gets SesData if any AND if not already selected by session fixation check in ->isExistingSessionRecord()
389 if ($this->id && !count($this->sesData)) {
390 $statement = $this->db->prepare_SELECTquery('*', 'fe_session_data', 'hash = :hash');
391 $statement->execute(array(':hash' => $this->id));
392 if (($sesDataRow = $statement->fetch()) !== FALSE) {
393 $this->sesData = unserialize($sesDataRow['content']);
394 $this->sessionDataTimestamp = $sesDataRow['tstamp'];
395 }
396 $statement->free();
397 }
398 }
399
400 /**
401 * Will write UC and session data.
402 * If the flag $this->userData_change has been set, the function ->writeUC is called (which will save persistent user session data)
403 * If the flag $this->sesData_change has been set, the fe_session_data table is updated with the content of $this->sesData
404 * If the $this->sessionDataTimestamp is NULL there was no session record yet, so we need to insert it into the database
405 *
406 * @return void
407 * @see fetchSessionData(), getKey(), setKey()
408 */
409 public function storeSessionData() {
410 // Saves UC and SesData if changed.
411 if ($this->userData_change) {
412 $this->writeUC('');
413 }
414 if ($this->sesData_change && $this->id) {
415 if (empty($this->sesData)) {
416 // Remove session-data
417 $this->removeSessionData();
418 // Remove cookie if not logged in as the session data is removed as well
419 if (empty($this->user['uid']) && !$this->loginHidden && $this->isCookieSet()) {
420 $this->removeCookie($this->name);
421 }
422 } elseif ($this->sessionDataTimestamp === NULL) {
423 // Write new session-data
424 $insertFields = array(
425 'hash' => $this->id,
426 'content' => serialize($this->sesData),
427 'tstamp' => $GLOBALS['EXEC_TIME']
428 );
429 $this->sessionDataTimestamp = $GLOBALS['EXEC_TIME'];
430 $this->db->exec_INSERTquery('fe_session_data', $insertFields);
431 // Now set the cookie (= fix the session)
432 $this->setSessionCookie();
433 } else {
434 // Update session data
435 $updateFields = array(
436 'content' => serialize($this->sesData),
437 'tstamp' => $GLOBALS['EXEC_TIME']
438 );
439 $this->sessionDataTimestamp = $GLOBALS['EXEC_TIME'];
440 $this->db->exec_UPDATEquery('fe_session_data', 'hash=' . $this->db->fullQuoteStr($this->id, 'fe_session_data'), $updateFields);
441 }
442 }
443 }
444
445 /**
446 * Removes data of the current session.
447 *
448 * @return void
449 */
450 public function removeSessionData() {
451 $this->sessionDataTimestamp = NULL;
452 $this->db->exec_DELETEquery('fe_session_data', 'hash=' . $this->db->fullQuoteStr($this->id, 'fe_session_data'));
453 }
454
455 /**
456 * Log out current user!
457 * Removes the current session record, sets the internal ->user array to a blank string
458 * Thereby the current user (if any) is effectively logged out!
459 * Additionally the cookie is removed
460 *
461 * @return void
462 */
463 public function logoff() {
464 parent::logoff();
465 // Remove the cookie on log-off, but only if we do not have an anonymous session
466 if (!$this->isExistingSessionRecord($this->id) && $this->isCookieSet()) {
467 $this->removeCookie($this->name);
468 }
469 }
470
471 /**
472 * Executes the garbage collection of session data and session.
473 * The lifetime of session data is defined by $TYPO3_CONF_VARS['FE']['sessionDataLifetime'].
474 *
475 * @return void
476 */
477 public function gc() {
478 $timeoutTimeStamp = (int)($GLOBALS['EXEC_TIME'] - $this->sessionDataLifetime);
479 $this->db->exec_DELETEquery('fe_session_data', 'tstamp < ' . $timeoutTimeStamp);
480 parent::gc();
481 }
482
483 /**
484 * Returns session data for the fe_user; Either persistent data following the fe_users uid/profile (requires login)
485 * or current-session based (not available when browse is closed, but does not require login)
486 *
487 * @param string $type Session data type; Either "user" (persistent, bound to fe_users profile) or "ses" (temporary, bound to current session cookie)
488 * @param string $key Key from the data array to return; The session data (in either case) is an array ($this->uc / $this->sesData) and this value determines which key to return the value for.
489 * @return mixed Returns whatever value there was in the array for the key, $key
490 * @see setKey()
491 */
492 public function getKey($type, $key) {
493 if (!$key) {
494 return NULL;
495 }
496 $value = NULL;
497 switch ($type) {
498 case 'user':
499 $value = $this->uc[$key];
500 break;
501 case 'ses':
502 $value = $this->sesData[$key];
503 break;
504 }
505 return $value;
506 }
507
508 /**
509 * Saves session data, either persistent or bound to current session cookie. Please see getKey() for more details.
510 * When a value is set the flags $this->userData_change or $this->sesData_change will be set so that the final call to ->storeSessionData() will know if a change has occurred and needs to be saved to the database.
511 * Notice: The key "recs" is already used by the function record_registration() which stores table/uid=value pairs in that key. This is used for the shopping basket among other things.
512 * Notice: Simply calling this function will not save the data to the database! The actual saving is done in storeSessionData() which is called as some of the last things in index_ts.php. So if you exit before this point, nothing gets saved of course! And the solution is to call $GLOBALS['TSFE']->storeSessionData(); before you exit.
513 *
514 * @param string $type Session data type; Either "user" (persistent, bound to fe_users profile) or "ses" (temporary, bound to current session cookie)
515 * @param string $key Key from the data array to store incoming data in; The session data (in either case) is an array ($this->uc / $this->sesData) and this value determines in which key the $data value will be stored.
516 * @param mixed $data The data value to store in $key
517 * @return void
518 * @see setKey(), storeSessionData(), record_registration()
519 */
520 public function setKey($type, $key, $data) {
521 if (!$key) {
522 return;
523 }
524 switch ($type) {
525 case 'user':
526 if ($this->user['uid']) {
527 if ($data === NULL) {
528 unset($this->uc[$key]);
529 } else {
530 $this->uc[$key] = $data;
531 }
532 $this->userData_change = TRUE;
533 }
534 break;
535 case 'ses':
536 if ($data === NULL) {
537 unset($this->sesData[$key]);
538 } else {
539 $this->sesData[$key] = $data;
540 }
541 $this->sesData_change = TRUE;
542 break;
543 }
544 }
545
546 /**
547 * Returns the session data stored for $key.
548 * The data will last only for this login session since it is stored in the session table.
549 *
550 * @param string $key
551 * @return mixed
552 */
553 public function getSessionData($key) {
554 return $this->getKey('ses', $key);
555 }
556
557 /**
558 * Saves the tokens so that they can be used by a later incarnation of this class.
559 *
560 * @param string $key
561 * @param mixed $data
562 * @return void
563 */
564 public function setAndSaveSessionData($key, $data) {
565 $this->setKey('ses', $key, $data);
566 $this->storeSessionData();
567 }
568
569 /**
570 * Registration of records/"shopping basket" in session data
571 * This will take the input array, $recs, and merge into the current "recs" array found in the session data.
572 * If a change in the recs storage happens (which it probably does) the function setKey() is called in order to store the array again.
573 *
574 * @param array $recs The data array to merge into/override the current recs values. The $recs array is constructed as [table]][uid] = scalar-value (eg. string/integer).
575 * @param int $maxSizeOfSessionData The maximum size of stored session data. If zero, no limit is applied and even confirmation of cookie session is discarded.
576 * @return void
577 */
578 public function record_registration($recs, $maxSizeOfSessionData = 0) {
579 // Storing value ONLY if there is a confirmed cookie set,
580 // otherwise a shellscript could easily be spamming the fe_sessions table
581 // with bogus content and thus bloat the database
582 if (!$maxSizeOfSessionData || $this->isCookieSet()) {
583 if ($recs['clear_all']) {
584 $this->setKey('ses', 'recs', array());
585 }
586 $change = 0;
587 $recs_array = $this->getKey('ses', 'recs');
588 foreach ($recs as $table => $data) {
589 if (is_array($data)) {
590 foreach ($data as $rec_id => $value) {
591 if ($value != $recs_array[$table][$rec_id]) {
592 $recs_array[$table][$rec_id] = $value;
593 $change = 1;
594 }
595 }
596 }
597 }
598 if ($change && (!$maxSizeOfSessionData || strlen(serialize($recs_array)) < $maxSizeOfSessionData)) {
599 $this->setKey('ses', 'recs', $recs_array);
600 }
601 }
602 }
603
604 /**
605 * Determine whether there's an according session record to a given session_id
606 * in the database. Don't care if session record is still valid or not.
607 *
608 * This calls the parent function but additionally tries to look up the session ID in the "fe_session_data" table.
609 *
610 * @param int $id Claimed Session ID
611 * @return bool Returns TRUE if a corresponding session was found in the database
612 */
613 public function isExistingSessionRecord($id) {
614 // Perform check in parent function
615 $count = parent::isExistingSessionRecord($id);
616 // Check if there are any fe_session_data records for the session ID the client claims to have
617 if ($count == FALSE) {
618 $statement = $this->db->prepare_SELECTquery('content,tstamp', 'fe_session_data', 'hash = :hash');
619 $res = $statement->execute(array(':hash' => $id));
620 if ($res !== FALSE) {
621 if ($sesDataRow = $statement->fetch()) {
622 $count = TRUE;
623 $this->sesData = unserialize($sesDataRow['content']);
624 $this->sessionDataTimestamp = $sesDataRow['tstamp'];
625 }
626 $statement->free();
627 }
628 }
629 return $count;
630 }
631
632 /**
633 * Hide the current login
634 *
635 * This is used by the fe_login_mode feature for pages.
636 * A current login is unset, but we remember that there has been one.
637 *
638 * @return void
639 */
640 public function hideActiveLogin() {
641 $this->user = NULL;
642 $this->loginHidden = TRUE;
643 }
644
645 }