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