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