[BUGFIX] Fallback to empty array if ExportController receives no input
[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\Authentication\AuthenticationService;
19 use TYPO3\CMS\Core\Configuration\Features;
20 use TYPO3\CMS\Core\Database\ConnectionPool;
21 use TYPO3\CMS\Core\Session\Backend\Exception\SessionNotFoundException;
22 use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
23 use TYPO3\CMS\Core\Utility\GeneralUtility;
24
25 /**
26 * Extension class for Front End User Authentication.
27 */
28 class FrontendUserAuthentication extends AbstractUserAuthentication
29 {
30 /**
31 * Login type, used for services.
32 * @var string
33 */
34 public $loginType = 'FE';
35
36 /**
37 * Form field with login-name
38 * @var string
39 */
40 public $formfield_uname = 'user';
41
42 /**
43 * Form field with password
44 * @var string
45 */
46 public $formfield_uident = 'pass';
47
48 /**
49 * Form field with status: *'login', 'logout'. If empty login is not verified.
50 * @var string
51 */
52 public $formfield_status = 'logintype';
53
54 /**
55 * form field with 0 or 1
56 * 1 = permanent login enabled
57 * 0 = session is valid for a browser session only
58 * @var string
59 */
60 public $formfield_permanent = 'permalogin';
61
62 /**
63 * Lifetime of anonymous session data in seconds.
64 * @var int
65 */
66 protected $sessionDataLifetime = 86400;
67
68 /**
69 * Session timeout (on the server)
70 *
71 * If >0: session-timeout in seconds.
72 * If <=0: Instant logout after login.
73 *
74 * @var int
75 */
76 public $sessionTimeout = 6000;
77
78 /**
79 * Table in database with user data
80 * @var string
81 */
82 public $user_table = 'fe_users';
83
84 /**
85 * Column for login-name
86 * @var string
87 */
88 public $username_column = 'username';
89
90 /**
91 * Column for password
92 * @var string
93 */
94 public $userident_column = 'password';
95
96 /**
97 * Column for user-id
98 * @var string
99 */
100 public $userid_column = 'uid';
101
102 /**
103 * Column name for last login timestamp
104 * @var string
105 */
106 public $lastLogin_column = 'lastlogin';
107
108 /**
109 * @var string
110 */
111 public $usergroup_column = 'usergroup';
112
113 /**
114 * @var string
115 */
116 public $usergroup_table = 'fe_groups';
117
118 /**
119 * Enable field columns of user table
120 * @var array
121 */
122 public $enablecolumns = [
123 'deleted' => 'deleted',
124 'disabled' => 'disable',
125 'starttime' => 'starttime',
126 'endtime' => 'endtime'
127 ];
128
129 /**
130 * @var array
131 */
132 public $groupData = [
133 'title' => [],
134 'uid' => [],
135 'pid' => []
136 ];
137
138 /**
139 * Used to accumulate the TSconfig data of the user
140 * @var array
141 */
142 public $TSdataArray = [];
143
144 /**
145 * @var array
146 */
147 public $userTS = [];
148
149 /**
150 * @var bool
151 */
152 public $userTSUpdated = false;
153
154 /**
155 * @var bool
156 */
157 public $sesData_change = false;
158
159 /**
160 * @var bool
161 */
162 public $userData_change = false;
163
164 /**
165 * @var bool
166 */
167 public $is_permanent = false;
168
169 /**
170 * @var bool
171 */
172 protected $loginHidden = false;
173
174 /**
175 * Will prevent the setting of the session cookie (takes precedence over forceSetCookie)
176 * Disable cookie by default, will be activated if saveSessionData() is called,
177 * a user is logging-in or an existing session is found
178 * @var bool
179 */
180 public $dontSetCookie = true;
181
182 /**
183 * Send no-cache headers (disabled by default, if no fixed session is there)
184 * @var bool
185 */
186 public $sendNoCacheHeaders = false;
187
188 public function __construct()
189 {
190 $this->name = self::getCookieName();
191 $this->lockIP = $GLOBALS['TYPO3_CONF_VARS']['FE']['lockIP'];
192 $this->checkPid = $GLOBALS['TYPO3_CONF_VARS']['FE']['checkFeUserPid'];
193 $this->lifetime = (int)$GLOBALS['TYPO3_CONF_VARS']['FE']['lifetime'];
194 $this->sessionTimeout = (int)$GLOBALS['TYPO3_CONF_VARS']['FE']['sessionTimeout'];
195 if ($this->sessionTimeout > 0 && $this->sessionTimeout < $this->lifetime) {
196 // If server session timeout is non-zero but less than client session timeout: Copy this value instead.
197 $this->sessionTimeout = $this->lifetime;
198 }
199 $this->sessionDataLifetime = (int)$GLOBALS['TYPO3_CONF_VARS']['FE']['sessionDataLifetime'];
200 if ($this->sessionDataLifetime <= 0) {
201 $this->sessionDataLifetime = 86400;
202 }
203 parent::__construct();
204 }
205
206 /**
207 * Returns the configured cookie name
208 *
209 * @return string
210 */
211 public static function getCookieName()
212 {
213 $configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['FE']['cookieName']);
214 if (empty($configuredCookieName)) {
215 $configuredCookieName = 'fe_typo_user';
216 }
217 return $configuredCookieName;
218 }
219
220 /**
221 * Returns a new session record for the current user for insertion into the DB.
222 *
223 * @param array $tempuser
224 * @return array User session record
225 */
226 public function getNewSessionRecord($tempuser)
227 {
228 $insertFields = parent::getNewSessionRecord($tempuser);
229 $insertFields['ses_permanent'] = $this->is_permanent ? 1 : 0;
230 return $insertFields;
231 }
232
233 /**
234 * Determine whether a session cookie needs to be set (lifetime=0)
235 *
236 * @return bool
237 * @internal
238 */
239 public function isSetSessionCookie()
240 {
241 return ($this->newSessionID || $this->forceSetCookie)
242 && ((int)$this->lifetime === 0 || !isset($this->user['ses_permanent']) || !$this->user['ses_permanent']);
243 }
244
245 /**
246 * Determine whether a non-session cookie needs to be set (lifetime>0)
247 *
248 * @return bool
249 * @internal
250 */
251 public function isRefreshTimeBasedCookie()
252 {
253 return $this->lifetime > 0 && isset($this->user['ses_permanent']) && $this->user['ses_permanent'];
254 }
255
256 /**
257 * Returns an info array with Login/Logout data submitted by a form or params
258 *
259 * @return array
260 * @see AbstractUserAuthentication::getLoginFormData()
261 */
262 public function getLoginFormData()
263 {
264 $loginData = parent::getLoginFormData();
265 if ($GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 0 || $GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 1) {
266 $isPermanent = GeneralUtility::_POST($this->formfield_permanent);
267 if (strlen($isPermanent) != 1) {
268 $isPermanent = $GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'];
269 } elseif (!$isPermanent) {
270 // To make sure the user gets a session cookie and doesn't keep a possibly existing time based cookie,
271 // we need to force setting the session cookie here
272 $this->forceSetCookie = true;
273 }
274 $isPermanent = (bool)$isPermanent;
275 } elseif ($GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 2) {
276 $isPermanent = true;
277 } else {
278 $isPermanent = false;
279 }
280 $loginData['permanent'] = $isPermanent;
281 $this->is_permanent = $isPermanent;
282 return $loginData;
283 }
284
285 /**
286 * Creates a user session record and returns its values.
287 * However, as the FE user cookie is normally not set, this has to be done
288 * before the parent class is doing the rest.
289 *
290 * @param array $tempuser User data array
291 * @return array The session data for the newly created session.
292 */
293 public function createUserSession($tempuser)
294 {
295 // At this point we do not know if we need to set a session or a permanent cookie
296 // So we force the cookie to be set after authentication took place, which will
297 // then call setSessionCookie(), which will set a cookie with correct settings.
298 $this->dontSetCookie = false;
299 return parent::createUserSession($tempuser);
300 }
301
302 /**
303 * Will select all fe_groups records that the current fe_user is member of
304 * and which groups are also allowed in the current domain.
305 * It also accumulates the TSconfig for the fe_user/fe_groups in ->TSdataArray
306 *
307 * @return int Returns the number of usergroups for the frontend users (if the internal user record exists and the usergroup field contains a value)
308 */
309 public function fetchGroupData()
310 {
311 $this->TSdataArray = [];
312 $this->userTS = [];
313 $this->userTSUpdated = false;
314 $this->groupData = [
315 'title' => [],
316 'uid' => [],
317 'pid' => []
318 ];
319 // Setting default configuration:
320 $this->TSdataArray[] = $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultUserTSconfig'];
321 // Get the info data for auth services
322 $authInfo = $this->getAuthInfoArray();
323 if (is_array($this->user)) {
324 $this->logger->debug('Get usergroups for user', [
325 $this->userid_column => $this->user[$this->userid_column],
326 $this->username_column => $this->user[$this->username_column]
327 ]);
328 } else {
329 $this->logger->debug('Get usergroups for "anonymous" user');
330 }
331 $groupDataArr = [];
332 // Use 'auth' service to find the groups for the user
333 $subType = 'getGroups' . $this->loginType;
334 /** @var AuthenticationService $serviceObj */
335 foreach ($this->getAuthServices($subType, [], $authInfo) as $serviceObj) {
336 $groupData = $serviceObj->getGroups($this->user, $groupDataArr);
337 if (is_array($groupData) && !empty($groupData)) {
338 // Keys in $groupData should be unique ids of the groups (like "uid") so this function will override groups.
339 $groupDataArr = $groupData + $groupDataArr;
340 }
341 }
342 if (empty($groupDataArr)) {
343 $this->logger->debug('No usergroups found by services');
344 }
345 if (!empty($groupDataArr)) {
346 $this->logger->debug(count($groupDataArr) . ' usergroup records found by services');
347 }
348 // Use 'auth' service to check the usergroups if they are really valid
349 foreach ($groupDataArr as $groupData) {
350 // By default a group is valid
351 $validGroup = true;
352 $subType = 'authGroups' . $this->loginType;
353 foreach ($this->getAuthServices($subType, [], $authInfo) as $serviceObj) {
354 // we assume that the service defines the authGroup function
355 if (!$serviceObj->authGroup($this->user, $groupData)) {
356 $validGroup = false;
357 $this->logger->debug($subType . ' auth service did not auth group', [
358 'uid ' => $groupData['uid'],
359 'title' => $groupData['title'],
360 ]);
361 break;
362 }
363 }
364 if ($validGroup && (string)$groupData['uid'] !== '') {
365 $this->groupData['title'][$groupData['uid']] = $groupData['title'];
366 $this->groupData['uid'][$groupData['uid']] = $groupData['uid'];
367 $this->groupData['pid'][$groupData['uid']] = $groupData['pid'];
368 $this->groupData['TSconfig'][$groupData['uid']] = $groupData['TSconfig'];
369 }
370 }
371 if (!empty($this->groupData) && !empty($this->groupData['TSconfig'])) {
372 // TSconfig: collect it in the order it was collected
373 foreach ($this->groupData['TSconfig'] as $TSdata) {
374 $this->TSdataArray[] = $TSdata;
375 }
376 $this->TSdataArray[] = $this->user['TSconfig'];
377 // Sort information
378 ksort($this->groupData['title']);
379 ksort($this->groupData['uid']);
380 ksort($this->groupData['pid']);
381 }
382 return !empty($this->groupData['uid']) ? count($this->groupData['uid']) : 0;
383 }
384
385 /**
386 * Returns the parsed TSconfig for the fe_user
387 * The TSconfig will be cached in $this->userTS.
388 *
389 * @return array TSconfig array for the fe_user
390 */
391 public function getUserTSconf()
392 {
393 if (!$this->userTSUpdated) {
394 // Parsing the user TS (or getting from cache)
395 $this->TSdataArray = TypoScriptParser::checkIncludeLines_array($this->TSdataArray);
396 $userTS = implode(LF . '[GLOBAL]' . LF, $this->TSdataArray);
397 $parseObj = GeneralUtility::makeInstance(TypoScriptParser::class);
398 $parseObj->parse($userTS);
399 $this->userTS = $parseObj->setup;
400 $this->userTSUpdated = true;
401 }
402 return $this->userTS;
403 }
404
405 /*****************************************
406 *
407 * Session data management functions
408 *
409 ****************************************/
410 /**
411 * Will write UC and session data.
412 * If the flag $this->userData_change has been set, the function ->writeUC is called (which will save persistent user session data)
413 * If the flag $this->sesData_change has been set, the current session record is updated with the content of $this->sessionData
414 *
415 * @see getKey(), setKey()
416 */
417 public function storeSessionData()
418 {
419 // Saves UC and SesData if changed.
420 if ($this->userData_change) {
421 $this->writeUC();
422 }
423
424 if ($this->sesData_change && $this->id) {
425 if (empty($this->sessionData)) {
426 // Remove session-data
427 $this->removeSessionData();
428 // Remove cookie if not logged in as the session data is removed as well
429 if (empty($this->user['uid']) && !$this->loginHidden && $this->isCookieSet()) {
430 $this->removeCookie($this->name);
431 }
432 } elseif (!$this->isExistingSessionRecord($this->id)) {
433 $sessionRecord = $this->getNewSessionRecord([]);
434 $sessionRecord['ses_anonymous'] = 1;
435 $sessionRecord['ses_data'] = serialize($this->sessionData);
436 $updatedSession = $this->getSessionBackend()->set($this->id, $sessionRecord);
437 $this->user = array_merge($this->user ?? [], $updatedSession);
438 // Now set the cookie (= fix the session)
439 $this->setSessionCookie();
440 } else {
441 // Update session data
442 $insertFields = [
443 'ses_data' => serialize($this->sessionData)
444 ];
445 $updatedSession = $this->getSessionBackend()->update($this->id, $insertFields);
446 $this->user = array_merge($this->user ?? [], $updatedSession);
447 }
448 }
449 }
450
451 /**
452 * Removes data of the current session.
453 */
454 public function removeSessionData()
455 {
456 if (!empty($this->sessionData)) {
457 $this->sesData_change = true;
458 }
459 $this->sessionData = [];
460
461 if ($this->isExistingSessionRecord($this->id)) {
462 // Remove session record if $this->user is empty is if session is anonymous
463 if ((empty($this->user) && !$this->loginHidden) || $this->user['ses_anonymous']) {
464 $this->getSessionBackend()->remove($this->id);
465 } else {
466 $this->getSessionBackend()->update($this->id, [
467 'ses_data' => ''
468 ]);
469 }
470 }
471 }
472
473 /**
474 * Removes the current session record, sets the internal ->user array to null,
475 * Thereby the current user (if any) is effectively logged out!
476 * Additionally the cookie is removed, but only if there is no session data.
477 * If session data exists, only the user information is removed and the session
478 * gets converted into an anonymous session if the feature toggle
479 * "security.frontend.keepSessionDataOnLogout" is set to true (default: false).
480 */
481 protected function performLogoff()
482 {
483 $sessionData = [];
484 try {
485 // Session might not be loaded at this point, so fetch it
486 $oldSession = $this->getSessionBackend()->get($this->id);
487 $sessionData = unserialize($oldSession['ses_data']);
488 } catch (SessionNotFoundException $e) {
489 // Leave uncaught, will unset cookie later in this method
490 }
491
492 $keepSessionDataOnLogout = GeneralUtility::makeInstance(Features::class)
493 ->isFeatureEnabled('security.frontend.keepSessionDataOnLogout');
494
495 if ($keepSessionDataOnLogout && !empty($sessionData)) {
496 // Regenerate session as anonymous
497 $this->regenerateSessionId($oldSession, true);
498 $this->user = null;
499 } else {
500 parent::performLogoff();
501 if ($this->isCookieSet()) {
502 $this->removeCookie($this->name);
503 }
504 }
505 }
506
507 /**
508 * Regenerate the session ID and transfer the session to new ID
509 * Call this method whenever a user proceeds to a higher authorization level
510 * e.g. when an anonymous session is now authenticated.
511 * Forces cookie to be set
512 *
513 * @param array $existingSessionRecord If given, this session record will be used instead of fetching again'
514 * @param bool $anonymous If true session will be regenerated as anonymous session
515 */
516 protected function regenerateSessionId(array $existingSessionRecord = [], bool $anonymous = false)
517 {
518 if (empty($existingSessionRecord)) {
519 $existingSessionRecord = $this->getSessionBackend()->get($this->id);
520 }
521 $existingSessionRecord['ses_anonymous'] = (int)$anonymous;
522 if ($anonymous) {
523 $existingSessionRecord['ses_userid'] = 0;
524 }
525 parent::regenerateSessionId($existingSessionRecord, $anonymous);
526 // We force the cookie to be set later in the authentication process
527 $this->dontSetCookie = false;
528 }
529
530 /**
531 * Returns session data for the fe_user; Either persistent data following the fe_users uid/profile (requires login)
532 * or current-session based (not available when browse is closed, but does not require login)
533 *
534 * @param string $type Session data type; Either "user" (persistent, bound to fe_users profile) or "ses" (temporary, bound to current session cookie)
535 * @param string $key Key from the data array to return; The session data (in either case) is an array ($this->uc / $this->sessionData) and this value determines which key to return the value for.
536 * @return mixed Returns whatever value there was in the array for the key, $key
537 * @see setKey()
538 */
539 public function getKey($type, $key)
540 {
541 if (!$key) {
542 return null;
543 }
544 $value = null;
545 switch ($type) {
546 case 'user':
547 $value = $this->uc[$key];
548 break;
549 case 'ses':
550 $value = $this->getSessionData($key);
551 break;
552 }
553 return $value;
554 }
555
556 /**
557 * Saves session data, either persistent or bound to current session cookie. Please see getKey() for more details.
558 * 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.
559 * 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 \TYPO3\CMS\Frontend\Http\RequestHandler. So if you exit before this point, nothing gets saved of course! And the solution is to call $GLOBALS['TSFE']->storeSessionData(); before you exit.
560 *
561 * @param string $type Session data type; Either "user" (persistent, bound to fe_users profile) or "ses" (temporary, bound to current session cookie)
562 * @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->sessionData) and this value determines in which key the $data value will be stored.
563 * @param mixed $data The data value to store in $key
564 * @see setKey(), storeSessionData()
565 */
566 public function setKey($type, $key, $data)
567 {
568 if (!$key) {
569 return;
570 }
571 switch ($type) {
572 case 'user':
573 if ($this->user['uid']) {
574 if ($data === null) {
575 unset($this->uc[$key]);
576 } else {
577 $this->uc[$key] = $data;
578 }
579 $this->userData_change = true;
580 }
581 break;
582 case 'ses':
583 $this->setSessionData($key, $data);
584 break;
585 }
586 }
587
588 /**
589 * Set session data by key.
590 * The data will last only for this login session since it is stored in the user session.
591 *
592 * @param string $key A non empty string to store the data under
593 * @param mixed $data Data store store in session
594 */
595 public function setSessionData($key, $data)
596 {
597 $this->sesData_change = true;
598 if ($data === null) {
599 unset($this->sessionData[$key]);
600 return;
601 }
602 parent::setSessionData($key, $data);
603 }
604
605 /**
606 * Saves the tokens so that they can be used by a later incarnation of this class.
607 *
608 * @param string $key
609 * @param mixed $data
610 */
611 public function setAndSaveSessionData($key, $data)
612 {
613 $this->setSessionData($key, $data);
614 $this->storeSessionData();
615 }
616
617 /**
618 * Garbage collector, removing old expired sessions.
619 *
620 * @internal
621 */
622 public function gc()
623 {
624 $this->getSessionBackend()->collectGarbage($this->gc_time, $this->sessionDataLifetime);
625 }
626
627 /**
628 * Hide the current login
629 *
630 * This is used by the fe_login_mode feature for pages.
631 * A current login is unset, but we remember that there has been one.
632 */
633 public function hideActiveLogin()
634 {
635 $this->user = null;
636 $this->loginHidden = true;
637 }
638
639 /**
640 * Update the field "is_online" every 60 seconds of a logged-in user
641 *
642 * @internal
643 */
644 public function updateOnlineTimestamp()
645 {
646 if (!is_array($this->user) || !$this->user['uid']
647 || $this->user['is_online'] >= $GLOBALS['EXEC_TIME'] - 60) {
648 return;
649 }
650 $dbConnection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->user_table);
651 $dbConnection->update(
652 $this->user_table,
653 ['is_online' => $GLOBALS['EXEC_TIME']],
654 ['uid' => (int)$this->user['uid']]
655 );
656 $this->user['is_online'] = $GLOBALS['EXEC_TIME'];
657 }
658 }