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