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