1<?php
2
3/*
4 * This file is part of the TYPO3 CMS project.
5 *
6 * It is free software; you can redistribute it and/or modify it under
7 * the terms of the GNU General Public License, either version 2
8 * of the License, or any later version.
9 *
10 * For the full copyright and license information, please read the
11 * LICENSE.txt file that was distributed with this source code.
12 *
13 * The TYPO3 project - inspiring people to share!
14 */
15
16namespace TYPO3\CMS\Core\Authentication;
17
18use Psr\Log\LoggerAwareInterface;
19use Psr\Log\LoggerAwareTrait;
20use Symfony\Component\HttpFoundation\Cookie;
21use TYPO3\CMS\Core\Core\Environment;
22use TYPO3\CMS\Core\Crypto\Random;
23use TYPO3\CMS\Core\Database\Connection;
24use TYPO3\CMS\Core\Database\ConnectionPool;
25use TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer;
26use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
27use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction;
28use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
29use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface;
30use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction;
31use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction;
32use TYPO3\CMS\Core\Exception;
33use TYPO3\CMS\Core\Http\CookieHeaderTrait;
34use TYPO3\CMS\Core\Session\Backend\Exception\SessionNotFoundException;
35use TYPO3\CMS\Core\Session\Backend\HashableSessionBackendInterface;
36use TYPO3\CMS\Core\Session\Backend\SessionBackendInterface;
37use TYPO3\CMS\Core\Session\SessionManager;
38use TYPO3\CMS\Core\SysLog\Action\Login as SystemLogLoginAction;
39use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
40use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
41use TYPO3\CMS\Core\Utility\GeneralUtility;
42
43/**
44 * Authentication of users in TYPO3
45 *
46 * This class is used to authenticate a login user.
47 * The class is used by both the frontend and backend.
48 * In both cases this class is a parent class to BackendUserAuthentication and FrontendUserAuthentication
49 *
50 * See Inside TYPO3 for more information about the API of the class and internal variables.
51 */
52abstract class AbstractUserAuthentication implements LoggerAwareInterface
53{
54    use LoggerAwareTrait;
55    use CookieHeaderTrait;
56
57    /**
58     * Session/Cookie name
59     * @var string
60     */
61    public $name = '';
62
63    /**
64     * Table in database with user data
65     * @var string
66     */
67    public $user_table = '';
68
69    /**
70     * Table in database with user groups
71     * @var string
72     */
73    public $usergroup_table = '';
74
75    /**
76     * Column for login-name
77     * @var string
78     */
79    public $username_column = '';
80
81    /**
82     * Column for password
83     * @var string
84     */
85    public $userident_column = '';
86
87    /**
88     * Column for user-id
89     * @var string
90     */
91    public $userid_column = '';
92
93    /**
94     * Column for user group information
95     * @var string
96     */
97    public $usergroup_column = '';
98
99    /**
100     * Column name for last login timestamp
101     * @var string
102     */
103    public $lastLogin_column = '';
104
105    /**
106     * Enable field columns of user table
107     * @var array
108     */
109    public $enablecolumns = [
110        'rootLevel' => '',
111        // Boolean: If TRUE, 'AND pid=0' will be a part of the query...
112        'disabled' => '',
113        'starttime' => '',
114        'endtime' => '',
115        'deleted' => '',
116    ];
117
118    /**
119     * @var bool
120     */
121    public $showHiddenRecords = false;
122
123    /**
124     * Form field with login-name
125     * @var string
126     */
127    public $formfield_uname = '';
128
129    /**
130     * Form field with password
131     * @var string
132     */
133    public $formfield_uident = '';
134
135    /**
136     * Form field with status: *'login', 'logout'. If empty login is not verified.
137     * @var string
138     */
139    public $formfield_status = '';
140
141    /**
142     * Session timeout (on the server)
143     *
144     * If >0: session-timeout in seconds.
145     * If <=0: Instant logout after login.
146     *
147     * @var int
148     */
149    public $sessionTimeout = 0;
150
151    /**
152     * Name for a field to fetch the server session timeout from.
153     * If not empty this is a field name from the user table where the timeout can be found.
154     * @var string
155     */
156    public $auth_timeout_field = '';
157
158    /**
159     * Lifetime for the session-cookie (on the client)
160     *
161     * If >0: permanent cookie with given lifetime
162     * If 0: session-cookie
163     * Session-cookie means the browser will remove it when the browser is closed.
164     *
165     * @var int
166     */
167    public $lifetime = 0;
168
169    /**
170     * GarbageCollection
171     * Purge all server session data older than $gc_time seconds.
172     * if $this->sessionTimeout > 0, then the session timeout is used instead.
173     * @var int
174     */
175    public $gc_time = 86400;
176
177    /**
178     * Probability for garbage collection to be run (in percent)
179     * @var int
180     */
181    public $gc_probability = 1;
182
183    /**
184     * Decides if the writelog() function is called at login and logout
185     * @var bool
186     */
187    public $writeStdLog = false;
188
189    /**
190     * Log failed login attempts
191     * @var bool
192     */
193    public $writeAttemptLog = false;
194
195    /**
196     * Send no-cache headers
197     * @var bool
198     */
199    public $sendNoCacheHeaders = true;
200
201    /**
202     * The ident-hash is normally 32 characters and should be!
203     * But if you are making sites for WAP-devices or other low-bandwidth stuff,
204     * you may shorten the length.
205     * Never let this value drop below 6!
206     * A length of 6 would give you more than 16 mio possibilities.
207     * @var int
208     */
209    public $hash_length = 32;
210
211    /**
212     * @var string
213     */
214    public $warningEmail = '';
215
216    /**
217     * Time span (in seconds) within the number of failed logins are collected
218     * @var int
219     */
220    public $warningPeriod = 3600;
221
222    /**
223     * The maximum accepted number of warnings before an email to $warningEmail is sent
224     * @var int
225     */
226    public $warningMax = 3;
227
228    /**
229     * If set, the user-record must be stored at the page defined by $checkPid_value
230     * @var bool
231     */
232    public $checkPid = true;
233
234    /**
235     * The page id the user record must be stored at
236     * @var int
237     */
238    public $checkPid_value = 0;
239
240    /**
241     * session_id (MD5-hash)
242     * @var string
243     * @internal
244     */
245    public $id;
246
247    /**
248     * Indicates if an authentication was started but failed
249     * @var bool
250     */
251    public $loginFailure = false;
252
253    /**
254     * Will be set to TRUE if the login session is actually written during auth-check.
255     * @var bool
256     */
257    public $loginSessionStarted = false;
258
259    /**
260     * @var array|null contains user- AND session-data from database (joined tables)
261     * @internal
262     */
263    public $user;
264
265    /**
266     * Will be set to TRUE if a new session ID was created
267     * @var bool
268     */
269    public $newSessionID = false;
270
271    /**
272     * Will force the session cookie to be set every time (lifetime must be 0)
273     * @var bool
274     */
275    public $forceSetCookie = false;
276
277    /**
278     * Will prevent the setting of the session cookie (takes precedence over forceSetCookie)
279     * @var bool
280     */
281    public $dontSetCookie = false;
282
283    /**
284     * @var bool
285     */
286    protected $cookieWasSetOnCurrentRequest = false;
287
288    /**
289     * Login type, used for services.
290     * @var string
291     */
292    public $loginType = '';
293
294    /**
295     * "auth" services configuration array from $GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth']
296     * @var array
297     */
298    public $svConfig = [];
299
300    /**
301     * @var array
302     */
303    public $uc;
304
305    /**
306     * @var IpLocker
307     */
308    protected $ipLocker;
309
310    /**
311     * @var SessionBackendInterface
312     */
313    protected $sessionBackend;
314
315    /**
316     * Holds deserialized data from session records.
317     * 'Reserved' keys are:
318     *   - 'sys': Reserved for TypoScript standard code.
319     * @var array
320     */
321    protected $sessionData = [];
322
323    /**
324     * Initialize some important variables
325     *
326     * @throws Exception
327     */
328    public function __construct()
329    {
330        $this->svConfig = $GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth'] ?? [];
331        // If there is a custom session timeout, use this instead of the 1d default gc time.
332        if ($this->sessionTimeout > 0) {
333            $this->gc_time = $this->sessionTimeout;
334        }
335        // Backend or frontend login - used for auth services
336        if (empty($this->loginType)) {
337            throw new Exception('No loginType defined, must be set explicitly by subclass', 1476045345);
338        }
339
340        $this->ipLocker = GeneralUtility::makeInstance(
341            IpLocker::class,
342            $GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['lockIP'],
343            $GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['lockIPv6']
344        );
345    }
346
347    /**
348     * Starts a user session
349     * Typical configurations will:
350     * a) check if session cookie was set and if not, set one,
351     * b) check if a password/username was sent and if so, try to authenticate the user
352     * c) Lookup a session attached to a user and check timeout etc.
353     * d) Garbage collection, setting of no-cache headers.
354     * If a user is authenticated the database record of the user (array) will be set in the ->user internal variable.
355     */
356    public function start()
357    {
358        $this->logger->debug('## Beginning of auth logging.');
359        $this->newSessionID = false;
360        // Make certain that NO user is set initially
361        $this->user = null;
362        // sessionID is set to ses_id if cookie is present. Otherwise a new session will start
363        $this->id = $this->getCookie($this->name);
364
365        // If new session or client tries to fix session...
366        if (!$this->isExistingSessionRecord($this->id)) {
367            $this->id = $this->createSessionId();
368            $this->newSessionID = true;
369        }
370
371        // Set all possible headers that could ensure that the script is not cached on the client-side
372        $this->sendHttpHeaders();
373        // Load user session, check to see if anyone has submitted login-information and if so authenticate
374        // the user with the session. $this->user[uid] may be used to write log...
375        $this->checkAuthentication();
376        // Set cookie if generally enabled or if the current session is a non-session cookie (FE permalogin)
377        if (!$this->dontSetCookie || $this->isRefreshTimeBasedCookie()) {
378            $this->setSessionCookie();
379        }
380        // Hook for alternative ways of filling the $this->user array (is used by the "timtaw" extension)
381        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postUserLookUp'] ?? [] as $funcName) {
382            $_params = [
383                'pObj' => $this,
384            ];
385            GeneralUtility::callUserFunction($funcName, $_params, $this);
386        }
387        // If we're lucky we'll get to clean up old sessions
388        if (random_int(0, mt_getrandmax()) % 100 <= $this->gc_probability) {
389            $this->gc();
390        }
391    }
392
393    /**
394     * Set all possible headers that could ensure that the script
395     * is not cached on the client-side.
396     *
397     * Only do this if $this->sendNoCacheHeaders is set.
398     */
399    protected function sendHttpHeaders()
400    {
401        // skip sending the "no-cache" headers if it's a CLI request or the no-cache headers should not be sent.
402        if (!$this->sendNoCacheHeaders || Environment::isCli()) {
403            return;
404        }
405        $httpHeaders = $this->getHttpHeaders();
406        foreach ($httpHeaders as $httpHeaderName => $value) {
407            header($httpHeaderName . ': ' . $value);
408        }
409    }
410
411    /**
412     * Get the http headers to be sent if an authenticated user is available, in order to disallow
413     * browsers to store the response on the client side.
414     *
415     * @return array
416     */
417    protected function getHttpHeaders(): array
418    {
419        $headers = [
420            'Expires' => 0,
421            'Last-Modified' => gmdate('D, d M Y H:i:s') . ' GMT'
422        ];
423        $cacheControlHeader = 'no-cache, must-revalidate';
424        $pragmaHeader = 'no-cache';
425        // Prevent error message in IE when using a https connection
426        // see https://forge.typo3.org/issues/24125
427        if (strpos(GeneralUtility::getIndpEnv('HTTP_USER_AGENT'), 'MSIE') !== false
428            && GeneralUtility::getIndpEnv('TYPO3_SSL')) {
429            // Some IEs can not handle no-cache
430            // see http://support.microsoft.com/kb/323308/en-us
431            $cacheControlHeader = 'must-revalidate';
432            // IE needs "Pragma: private" if SSL connection
433            $pragmaHeader = 'private';
434        }
435        $headers['Cache-Control'] = $cacheControlHeader;
436        $headers['Pragma'] = $pragmaHeader;
437        return $headers;
438    }
439
440    /**
441     * Sets the session cookie for the current disposal.
442     *
443     * @throws Exception
444     */
445    protected function setSessionCookie()
446    {
447        $isSetSessionCookie = $this->isSetSessionCookie();
448        $isRefreshTimeBasedCookie = $this->isRefreshTimeBasedCookie();
449        if ($isSetSessionCookie || $isRefreshTimeBasedCookie) {
450            $settings = $GLOBALS['TYPO3_CONF_VARS']['SYS'];
451            // Get the domain to be used for the cookie (if any):
452            $cookieDomain = $this->getCookieDomain();
453            // If no cookie domain is set, use the base path:
454            $cookiePath = $cookieDomain ? '/' : GeneralUtility::getIndpEnv('TYPO3_SITE_PATH');
455            // If the cookie lifetime is set, use it:
456            $cookieExpire = $isRefreshTimeBasedCookie ? $GLOBALS['EXEC_TIME'] + $this->lifetime : 0;
457            // Use the secure option when the current request is served by a secure connection:
458            $cookieSecure = (bool)$settings['cookieSecure'] && GeneralUtility::getIndpEnv('TYPO3_SSL');
459            // Valid options are "strict", "lax" or "none", whereas "none" only works in HTTPS requests (default & fallback is "strict")
460            $cookieSameSite = $this->sanitizeSameSiteCookieValue(
461                strtolower($GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['cookieSameSite'] ?? Cookie::SAMESITE_STRICT)
462            );
463            // SameSite "none" needs the secure option (only allowed on HTTPS)
464            if ($cookieSameSite === Cookie::SAMESITE_NONE) {
465                $cookieSecure = true;
466            }
467            // Do not set cookie if cookieSecure is set to "1" (force HTTPS) and no secure channel is used:
468            if ((int)$settings['cookieSecure'] !== 1 || GeneralUtility::getIndpEnv('TYPO3_SSL')) {
469                $cookie = new Cookie(
470                    $this->name,
471                    $this->id,
472                    $cookieExpire,
473                    $cookiePath,
474                    $cookieDomain,
475                    $cookieSecure,
476                    true,
477                    false,
478                    $cookieSameSite
479                );
480                header('Set-Cookie: ' . $cookie->__toString(), false);
481                $this->cookieWasSetOnCurrentRequest = true;
482            } else {
483                throw new Exception('Cookie was not set since HTTPS was forced in $TYPO3_CONF_VARS[SYS][cookieSecure].', 1254325546);
484            }
485            $this->logger->debug(
486                ($isRefreshTimeBasedCookie ? 'Updated Cookie: ' : 'Set Cookie: ')
487                . sha1($this->id) . ($cookieDomain ? ', ' . $cookieDomain : '')
488            );
489        }
490    }
491
492    /**
493     * Gets the domain to be used on setting cookies.
494     * The information is taken from the value in $GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain'].
495     *
496     * @return string The domain to be used on setting cookies
497     */
498    protected function getCookieDomain()
499    {
500        $result = '';
501        $cookieDomain = $GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain'];
502        // If a specific cookie domain is defined for a given TYPO3_MODE,
503        // use that domain
504        if (!empty($GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['cookieDomain'])) {
505            $cookieDomain = $GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['cookieDomain'];
506        }
507        if ($cookieDomain) {
508            if ($cookieDomain[0] === '/') {
509                $match = [];
510                $matchCnt = @preg_match($cookieDomain, GeneralUtility::getIndpEnv('TYPO3_HOST_ONLY'), $match);
511                if ($matchCnt === false) {
512                    $this->logger->critical('The regular expression for the cookie domain (' . $cookieDomain . ') contains errors. The session is not shared across sub-domains.');
513                } elseif ($matchCnt) {
514                    $result = $match[0];
515                }
516            } else {
517                $result = $cookieDomain;
518            }
519        }
520        return $result;
521    }
522
523    /**
524     * Get the value of a specified cookie.
525     *
526     * @param string $cookieName The cookie ID
527     * @return string The value stored in the cookie
528     */
529    protected function getCookie($cookieName)
530    {
531        return isset($_COOKIE[$cookieName]) ? stripslashes($_COOKIE[$cookieName]) : '';
532    }
533
534    /**
535     * Determine whether a session cookie needs to be set (lifetime=0)
536     *
537     * @return bool
538     * @internal
539     */
540    public function isSetSessionCookie()
541    {
542        return ($this->newSessionID || $this->forceSetCookie) && $this->lifetime == 0;
543    }
544
545    /**
546     * Determine whether a non-session cookie needs to be set (lifetime>0)
547     *
548     * @return bool
549     * @internal
550     */
551    public function isRefreshTimeBasedCookie()
552    {
553        return $this->lifetime > 0;
554    }
555
556    /**
557     * Checks if a submission of username and password is present or use other authentication by auth services
558     *
559     * @throws \RuntimeException
560     * @internal
561     */
562    public function checkAuthentication()
563    {
564        // No user for now - will be searched by service below
565        $tempuserArr = [];
566        $tempuser = false;
567        // User is not authenticated by default
568        $authenticated = false;
569        // User want to login with passed login data (name/password)
570        $activeLogin = false;
571        // Indicates if an active authentication failed (not auto login)
572        $this->loginFailure = false;
573        $this->logger->debug('Login type: ' . $this->loginType);
574        // The info array provide additional information for auth services
575        $authInfo = $this->getAuthInfoArray();
576        // Get Login/Logout data submitted by a form or params
577        $loginData = $this->getLoginFormData();
578        $this->logger->debug('Login data', $this->removeSensitiveLoginDataForLoggingInfo($loginData));
579        // Active logout (eg. with "logout" button)
580        if ($loginData['status'] === LoginType::LOGOUT) {
581            if ($this->writeStdLog) {
582                // $type,$action,$error,$details_nr,$details,$data,$tablename,$recuid,$recpid
583                $this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::LOGOUT, SystemLogErrorClassification::MESSAGE, 2, 'User %s logged out', [$this->user['username']], '', 0, 0);
584            }
585            $this->logger->info('User logged out. Id: ' . sha1($this->id));
586            $this->logoff();
587        }
588        // Determine whether we need to skip session update.
589        // This is used mainly for checking session timeout in advance without refreshing the current session's timeout.
590        $skipSessionUpdate = (bool)GeneralUtility::_GP('skipSessionUpdate');
591        $haveSession = false;
592        $anonymousSession = false;
593        if (!$this->newSessionID) {
594            // Read user session
595            $authInfo['userSession'] = $this->fetchUserSession($skipSessionUpdate);
596            $haveSession = is_array($authInfo['userSession']);
597            if ($haveSession && !empty($authInfo['userSession']['ses_anonymous'])) {
598                $anonymousSession = true;
599            }
600        }
601
602        // Active login (eg. with login form).
603        if (!$haveSession && $loginData['status'] === LoginType::LOGIN) {
604            $activeLogin = true;
605            $this->logger->debug('Active login (eg. with login form)');
606            // check referrer for submitted login values
607            if ($this->formfield_status && $loginData['uident'] && $loginData['uname']) {
608                // Delete old user session if any
609                $this->logoff();
610            }
611            // Refuse login for _CLI users, if not processing a CLI request type
612            // (although we shouldn't be here in case of a CLI request type)
613            if (stripos($loginData['uname'], '_CLI_') === 0 && !Environment::isCli()) {
614                throw new \RuntimeException('TYPO3 Fatal Error: You have tried to login using a CLI user. Access prohibited!', 1270853931);
615            }
616        }
617
618        // Cause elevation of privilege, make sure regenerateSessionId is called later on
619        if ($anonymousSession && $loginData['status'] === LoginType::LOGIN) {
620            $activeLogin = true;
621        }
622
623        if ($haveSession) {
624            $this->logger->debug('User session found', [
625                $this->userid_column => $authInfo['userSession'][$this->userid_column] ?? null,
626                $this->username_column => $authInfo['userSession'][$this->username_column] ?? null,
627            ]);
628        } else {
629            $this->logger->debug('No user session found');
630        }
631        if (is_array($this->svConfig['setup'] ?? false)) {
632            $this->logger->debug('SV setup', $this->svConfig['setup']);
633        }
634
635        // Fetch user if ...
636        if (
637            $activeLogin || !empty($this->svConfig['setup'][$this->loginType . '_alwaysFetchUser'])
638            || !$haveSession && !empty($this->svConfig['setup'][$this->loginType . '_fetchUserIfNoSession'])
639        ) {
640            // Use 'auth' service to find the user
641            // First found user will be used
642            $subType = 'getUser' . $this->loginType;
643            /** @var AuthenticationService $serviceObj */
644            foreach ($this->getAuthServices($subType, $loginData, $authInfo) as $serviceObj) {
645                if ($row = $serviceObj->getUser()) {
646                    $tempuserArr[] = $row;
647                    $this->logger->debug('User found', [
648                        $this->userid_column => $row[$this->userid_column],
649                        $this->username_column => $row[$this->username_column],
650                    ]);
651                    // User found, just stop to search for more if not configured to go on
652                    if (empty($this->svConfig['setup'][$this->loginType . '_fetchAllUsers'])) {
653                        break;
654                    }
655                }
656            }
657
658            if (!empty($this->svConfig['setup'][$this->loginType . '_alwaysFetchUser'])) {
659                $this->logger->debug($this->loginType . '_alwaysFetchUser option is enabled');
660            }
661            if (empty($tempuserArr)) {
662                $this->logger->debug('No user found by services');
663            } else {
664                $this->logger->debug(count($tempuserArr) . ' user records found by services');
665            }
666        }
667
668        // If no new user was set we use the already found user session
669        if (empty($tempuserArr) && $haveSession && !$anonymousSession) {
670            $tempuserArr[] = $authInfo['userSession'];
671            $tempuser = $authInfo['userSession'];
672            // User is authenticated because we found a user session
673            $authenticated = true;
674            $this->logger->debug('User session used', [
675                $this->userid_column => $authInfo['userSession'][$this->userid_column],
676                $this->username_column => $authInfo['userSession'][$this->username_column],
677            ]);
678        }
679        // Re-auth user when 'auth'-service option is set
680        if (!empty($this->svConfig['setup'][$this->loginType . '_alwaysAuthUser'])) {
681            $authenticated = false;
682            $this->logger->debug('alwaysAuthUser option is enabled');
683        }
684        // Authenticate the user if needed
685        if (!empty($tempuserArr) && !$authenticated) {
686            foreach ($tempuserArr as $tempuser) {
687                // Use 'auth' service to authenticate the user
688                // If one service returns FALSE then authentication failed
689                // a service might return 100 which means there's no reason to stop but the user can't be authenticated by that service
690                $this->logger->debug('Auth user', $this->removeSensitiveLoginDataForLoggingInfo($tempuser, true));
691                $subType = 'authUser' . $this->loginType;
692
693                /** @var AuthenticationService $serviceObj */
694                foreach ($this->getAuthServices($subType, $loginData, $authInfo) as $serviceObj) {
695                    if (($ret = $serviceObj->authUser($tempuser)) > 0) {
696                        // If the service returns >=200 then no more checking is needed - useful for IP checking without password
697                        if ((int)$ret >= 200) {
698                            $authenticated = true;
699                            break;
700                        }
701                        if ((int)$ret >= 100) {
702                        } else {
703                            $authenticated = true;
704                        }
705                    } else {
706                        $authenticated = false;
707                        break;
708                    }
709                }
710
711                if ($authenticated) {
712                    // Leave foreach() because a user is authenticated
713                    break;
714                }
715            }
716        }
717
718        // If user is authenticated a valid user is in $tempuser
719        if ($authenticated) {
720            // Reset failure flag
721            $this->loginFailure = false;
722            // Insert session record if needed:
723            if (!$haveSession || $anonymousSession || $tempuser['ses_id'] != $this->id && $tempuser['uid'] != $authInfo['userSession']['ses_userid']) {
724                $sessionData = $this->createUserSession($tempuser);
725
726                // Preserve session data on login
727                if ($anonymousSession) {
728                    $sessionData = $this->getSessionBackend()->update(
729                        $this->id,
730                        ['ses_data' => $authInfo['userSession']['ses_data']]
731                    );
732                }
733
734                $this->user = array_merge(
735                    $tempuser,
736                    $sessionData
737                );
738                // The login session is started.
739                $this->loginSessionStarted = true;
740                if (is_array($this->user)) {
741                    $this->logger->debug('User session finally read', [
742                        $this->userid_column => $this->user[$this->userid_column],
743                        $this->username_column => $this->user[$this->username_column],
744                    ]);
745                }
746            } elseif ($haveSession) {
747                // Validate the session ID and promote it
748                // This check can be removed in TYPO3 v11
749                if ($this->getSessionBackend() instanceof HashableSessionBackendInterface && !empty($authInfo['userSession']['ses_id'] ?? '')) {
750                    // The session is stored in plaintext, promote it
751                    if ($authInfo['userSession']['ses_id'] === $this->id) {
752                        $authInfo['userSession'] = $this->getSessionBackend()->update(
753                            $this->id,
754                            ['ses_data' => $authInfo['userSession']['ses_data']]
755                        );
756                    }
757                }
758                // if we come here the current session is for sure not anonymous as this is a pre-condition for $authenticated = true
759                $this->user = $authInfo['userSession'];
760            }
761
762            if ($activeLogin && !$this->newSessionID) {
763                $this->regenerateSessionId();
764            }
765
766            // User logged in - write that to the log!
767            if ($this->writeStdLog && $activeLogin) {
768                $this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::LOGIN, SystemLogErrorClassification::MESSAGE, 1, 'User %s logged in from ###IP###', [$tempuser[$this->username_column]], '', '', '');
769            }
770            if ($activeLogin) {
771                $this->logger->info('User ' . $tempuser[$this->username_column] . ' logged in from ' . GeneralUtility::getIndpEnv('REMOTE_ADDR'));
772            }
773            if (!$activeLogin) {
774                $this->logger->debug('User ' . $tempuser[$this->username_column] . ' authenticated from ' . GeneralUtility::getIndpEnv('REMOTE_ADDR'));
775            }
776        } else {
777            // User was not authenticated, so we should reuse the existing anonymous session
778            if ($anonymousSession) {
779                $this->user = $authInfo['userSession'];
780            }
781
782            // Mark the current login attempt as failed
783            if ($activeLogin || !empty($tempuserArr)) {
784                $this->loginFailure = true;
785                if (empty($tempuserArr) && $activeLogin) {
786                    $logData = [
787                        'loginData' => $this->removeSensitiveLoginDataForLoggingInfo($loginData)
788                    ];
789                    $this->logger->debug('Login failed', $logData);
790                }
791                if (!empty($tempuserArr)) {
792                    $logData = [
793                        $this->userid_column => $tempuser[$this->userid_column],
794                        $this->username_column => $tempuser[$this->username_column],
795                    ];
796                    $this->logger->debug('Login failed', $logData);
797                }
798            }
799        }
800
801        // If there were a login failure, check to see if a warning email should be sent:
802        if ($this->loginFailure && $activeLogin) {
803            $this->logger->debug(
804                'Call checkLogFailures',
805                [
806                    'warningEmail' => $this->warningEmail,
807                    'warningPeriod' => $this->warningPeriod,
808                    'warningMax' => $this->warningMax
809                ]
810            );
811
812            // Hook to implement login failure tracking methods
813            $_params = [];
814            $sleep = true;
815            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postLoginFailureProcessing'] ?? [] as $_funcRef) {
816                GeneralUtility::callUserFunction($_funcRef, $_params, $this);
817                $sleep = false;
818            }
819
820            if ($sleep) {
821                // No hooks were triggered - default login failure behavior is to sleep 5 seconds
822                sleep(5);
823            }
824
825            $this->checkLogFailures($this->warningEmail, $this->warningPeriod, $this->warningMax);
826        }
827    }
828
829    /**
830     * Creates a new session ID.
831     *
832     * @return string The new session ID
833     */
834    public function createSessionId()
835    {
836        return GeneralUtility::makeInstance(Random::class)->generateRandomHexString($this->hash_length);
837    }
838
839    /**
840     * Initializes authentication services to be used in a foreach loop
841     *
842     * @param string $subType e.g. getUserFE
843     * @param array $loginData
844     * @param array $authInfo
845     * @return \Traversable A generator of service objects
846     */
847    protected function getAuthServices(string $subType, array $loginData, array $authInfo): \Traversable
848    {
849        $serviceChain = [];
850        while (is_object($serviceObj = GeneralUtility::makeInstanceService('auth', $subType, $serviceChain))) {
851            $serviceChain[] = $serviceObj->getServiceKey();
852            $serviceObj->initAuth($subType, $loginData, $authInfo, $this);
853            yield $serviceObj;
854        }
855        if (!empty($serviceChain)) {
856            $this->logger->debug($subType . ' auth services called: ' . implode(', ', $serviceChain));
857        }
858    }
859
860    /**
861     * Regenerate the session ID and transfer the session to new ID
862     * Call this method whenever a user proceeds to a higher authorization level
863     * e.g. when an anonymous session is now authenticated.
864     *
865     * @param array $existingSessionRecord If given, this session record will be used instead of fetching again
866     * @param bool $anonymous If true session will be regenerated as anonymous session
867     */
868    protected function regenerateSessionId(array $existingSessionRecord = [], bool $anonymous = false)
869    {
870        if (empty($existingSessionRecord)) {
871            $existingSessionRecord = $this->getSessionBackend()->get($this->id);
872        }
873
874        // Update session record with new ID
875        $oldSessionId = $this->id;
876        $this->id = $this->createSessionId();
877        $updatedSession = $this->getSessionBackend()->set($this->id, $existingSessionRecord);
878        $this->sessionData = unserialize($updatedSession['ses_data']);
879        // Merge new session data into user/session array
880        $this->user = array_merge($this->user ?? [], $updatedSession);
881        $this->getSessionBackend()->remove($oldSessionId);
882        $this->newSessionID = true;
883    }
884
885    /*************************
886     *
887     * User Sessions
888     *
889     *************************/
890
891    /**
892     * Creates a user session record and returns its values.
893     *
894     * @param array $tempuser User data array
895     *
896     * @return array The session data for the newly created session.
897     */
898    public function createUserSession($tempuser)
899    {
900        $this->logger->debug('Create session ses_id = ' . sha1($this->id));
901        // Delete any session entry first
902        $this->getSessionBackend()->remove($this->id);
903        // Re-create session entry
904        $sessionRecord = $this->getNewSessionRecord($tempuser);
905        $sessionRecord = $this->getSessionBackend()->set($this->id, $sessionRecord);
906        // Updating lastLogin_column carrying information about last login.
907        $this->updateLoginTimestamp($tempuser[$this->userid_column]);
908        return $sessionRecord;
909    }
910
911    /**
912     * Updates the last login column in the user with the given id
913     *
914     * @param int $userId
915     */
916    protected function updateLoginTimestamp(int $userId)
917    {
918        if ($this->lastLogin_column) {
919            $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->user_table);
920            $connection->update(
921                $this->user_table,
922                [$this->lastLogin_column => $GLOBALS['EXEC_TIME']],
923                [$this->userid_column => $userId]
924            );
925        }
926    }
927
928    /**
929     * Returns a new session record for the current user for insertion into the DB.
930     * This function is mainly there as a wrapper for inheriting classes to override it.
931     *
932     * @param array $tempuser
933     * @return array User session record
934     */
935    public function getNewSessionRecord($tempuser)
936    {
937        $sessionIpLock = $this->ipLocker->getSessionIpLock((string)GeneralUtility::getIndpEnv('REMOTE_ADDR'), empty($tempuser['disableIPlock']));
938
939        return [
940            'ses_id' => $this->id,
941            'ses_iplock' => $sessionIpLock,
942            'ses_userid' => $tempuser[$this->userid_column] ?? 0,
943            'ses_tstamp' => $GLOBALS['EXEC_TIME'],
944            'ses_data' => '',
945        ];
946    }
947
948    /**
949     * Read the user session from db.
950     *
951     * @param bool $skipSessionUpdate
952     * @return array|bool User session data, false if $this->id does not represent valid session
953     */
954    public function fetchUserSession($skipSessionUpdate = false)
955    {
956        $this->logger->debug('Fetch session ses_id = ' . sha1($this->id));
957        try {
958            $sessionRecord = $this->getSessionBackend()->get($this->id);
959        } catch (SessionNotFoundException $e) {
960            return false;
961        }
962
963        $this->sessionData = unserialize($sessionRecord['ses_data']);
964        // Session is anonymous so no need to fetch user
965        if (!empty($sessionRecord['ses_anonymous'])) {
966            return $sessionRecord;
967        }
968
969        // Fetch the user from the DB
970        $userRecord = $this->getRawUserByUid((int)$sessionRecord['ses_userid']);
971        if ($userRecord) {
972            $userRecord = array_merge($sessionRecord, $userRecord);
973            // A user was found
974            $userRecord['ses_tstamp'] = (int)$userRecord['ses_tstamp'];
975            $userRecord['is_online'] = (int)$userRecord['ses_tstamp'];
976
977            if (!empty($this->auth_timeout_field)) {
978                // Get timeout-time from usertable
979                $timeout = (int)$userRecord[$this->auth_timeout_field];
980            } else {
981                $timeout = $this->sessionTimeout;
982            }
983            // If timeout > 0 (TRUE) and current time has not exceeded the latest sessions-time plus the timeout in seconds then accept user
984            // Use a gracetime-value to avoid updating a session-record too often
985            if ($timeout > 0 && $GLOBALS['EXEC_TIME'] < $userRecord['ses_tstamp'] + $timeout) {
986                $sessionUpdateGracePeriod = 61;
987                if (!$skipSessionUpdate && $GLOBALS['EXEC_TIME'] > ($userRecord['ses_tstamp'] + $sessionUpdateGracePeriod)) {
988                    // Update the session timestamp by writing a dummy update. (Backend will update the timestamp)
989                    $updatesSession = $this->getSessionBackend()->update($this->id, []);
990                    $userRecord = array_merge($userRecord, $updatesSession);
991                }
992            } else {
993                // Delete any user set...
994                $this->logoff();
995                $userRecord = false;
996            }
997        }
998        return $userRecord;
999    }
1000
1001    /**
1002     * Regenerates the session ID and sets the cookie again.
1003     *
1004     * @internal
1005     */
1006    public function enforceNewSessionId()
1007    {
1008        $this->regenerateSessionId();
1009        $this->setSessionCookie();
1010    }
1011
1012    /**
1013     * Log out current user!
1014     * Removes the current session record, sets the internal ->user array to a blank string;
1015     * Thereby the current user (if any) is effectively logged out!
1016     */
1017    public function logoff()
1018    {
1019        $this->logger->debug('logoff: ses_id = ' . sha1($this->id));
1020
1021        $_params = [];
1022        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_pre_processing'] ?? [] as $_funcRef) {
1023            if ($_funcRef) {
1024                GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1025            }
1026        }
1027        $this->performLogoff();
1028
1029        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_post_processing'] ?? [] as $_funcRef) {
1030            if ($_funcRef) {
1031                GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1032            }
1033        }
1034    }
1035
1036    /**
1037     * Perform the logoff action. Called from logoff() as a way to allow subclasses to override
1038     * what happens when a user logs off, without needing to reproduce the hook calls and logging
1039     * that happens in the public logoff() API method.
1040     */
1041    protected function performLogoff()
1042    {
1043        if ($this->id) {
1044            $this->getSessionBackend()->remove($this->id);
1045        }
1046        $this->sessionData = [];
1047        $this->user = null;
1048        if ($this->isCookieSet()) {
1049            $this->removeCookie($this->name);
1050        }
1051    }
1052
1053    /**
1054     * Empty / unset the cookie
1055     *
1056     * @param string $cookieName usually, this is $this->name
1057     */
1058    public function removeCookie($cookieName)
1059    {
1060        $cookieDomain = $this->getCookieDomain();
1061        // If no cookie domain is set, use the base path
1062        $cookiePath = $cookieDomain ? '/' : GeneralUtility::getIndpEnv('TYPO3_SITE_PATH');
1063        setcookie($cookieName, '', -1, $cookiePath, $cookieDomain);
1064    }
1065
1066    /**
1067     * Determine whether there's an according session record to a given session_id.
1068     * Don't care if session record is still valid or not.
1069     *
1070     * @param string $id Claimed Session ID
1071     * @return bool Returns TRUE if a corresponding session was found in the database
1072     */
1073    public function isExistingSessionRecord($id)
1074    {
1075        if (empty($id)) {
1076            return false;
1077        }
1078        try {
1079            $sessionRecord = $this->getSessionBackend()->get($id);
1080            if (empty($sessionRecord)) {
1081                return false;
1082            }
1083            // If the session does not match the current IP lock, it should be treated as invalid
1084            // and a new session should be created.
1085            return $this->ipLocker->validateRemoteAddressAgainstSessionIpLock((string)GeneralUtility::getIndpEnv('REMOTE_ADDR'), $sessionRecord['ses_iplock']);
1086        } catch (SessionNotFoundException $e) {
1087            return false;
1088        }
1089    }
1090
1091    /**
1092     * Returns whether this request is going to set a cookie
1093     * or a cookie was already found in the system
1094     *
1095     * @return bool Returns TRUE if a cookie is set
1096     */
1097    public function isCookieSet()
1098    {
1099        return $this->cookieWasSetOnCurrentRequest || $this->getCookie($this->name);
1100    }
1101
1102    /*************************
1103     *
1104     * SQL Functions
1105     *
1106     *************************/
1107    /**
1108     * This returns the restrictions needed to select the user respecting
1109     * enable columns and flags like deleted, hidden, starttime, endtime
1110     * and rootLevel
1111     *
1112     * @return QueryRestrictionContainerInterface
1113     * @internal
1114     */
1115    protected function userConstraints(): QueryRestrictionContainerInterface
1116    {
1117        $restrictionContainer = GeneralUtility::makeInstance(DefaultRestrictionContainer::class);
1118
1119        if (empty($this->enablecolumns['disabled'])) {
1120            $restrictionContainer->removeByType(HiddenRestriction::class);
1121        }
1122
1123        if (empty($this->enablecolumns['deleted'])) {
1124            $restrictionContainer->removeByType(DeletedRestriction::class);
1125        }
1126
1127        if (empty($this->enablecolumns['starttime'])) {
1128            $restrictionContainer->removeByType(StartTimeRestriction::class);
1129        }
1130
1131        if (empty($this->enablecolumns['endtime'])) {
1132            $restrictionContainer->removeByType(EndTimeRestriction::class);
1133        }
1134
1135        if (!empty($this->enablecolumns['rootLevel'])) {
1136            $restrictionContainer->add(GeneralUtility::makeInstance(RootLevelRestriction::class, [$this->user_table]));
1137        }
1138
1139        return $restrictionContainer;
1140    }
1141
1142    /*************************
1143     *
1144     * Session and Configuration Handling
1145     *
1146     *************************/
1147    /**
1148     * This writes $variable to the user-record. This is a way of providing session-data.
1149     * You can fetch the data again through $this->uc in this class!
1150     * If $variable is not an array, $this->uc is saved!
1151     *
1152     * @param array|string $variable An array you want to store for the user as session data. If $variable is not supplied (is null), the internal variable, ->uc, is stored by default
1153     */
1154    public function writeUC($variable = '')
1155    {
1156        if (is_array($this->user) && $this->user[$this->userid_column]) {
1157            if (!is_array($variable)) {
1158                $variable = $this->uc;
1159            }
1160            $this->logger->debug('writeUC: ' . $this->userid_column . '=' . (int)$this->user[$this->userid_column]);
1161            GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->user_table)->update(
1162                $this->user_table,
1163                ['uc' => serialize($variable)],
1164                [$this->userid_column => (int)$this->user[$this->userid_column]],
1165                ['uc' => Connection::PARAM_LOB]
1166            );
1167        }
1168    }
1169
1170    /**
1171     * Sets $theUC as the internal variable ->uc IF $theUC is an array.
1172     * If $theUC is FALSE, the 'uc' content from the ->user array will be unserialized and restored in ->uc
1173     *
1174     * @param mixed $theUC If an array, then set as ->uc, otherwise load from user record
1175     */
1176    public function unpack_uc($theUC = '')
1177    {
1178        if (!$theUC && isset($this->user['uc'])) {
1179            $theUC = unserialize($this->user['uc'], ['allowed_classes' => false]);
1180        }
1181        if (is_array($theUC)) {
1182            $this->uc = $theUC;
1183        }
1184    }
1185
1186    /**
1187     * Stores data for a module.
1188     * The data is stored with the session id so you can even check upon retrieval
1189     * if the module data is from a previous session or from the current session.
1190     *
1191     * @param string $module Is the name of the module ($MCONF['name'])
1192     * @param mixed $data Is the data you want to store for that module (array, string, ...)
1193     * @param bool|int $noSave If $noSave is set, then the ->uc array (which carries all kinds of user data) is NOT written immediately, but must be written by some subsequent call.
1194     */
1195    public function pushModuleData($module, $data, $noSave = 0)
1196    {
1197        $sessionHash = GeneralUtility::hmac(
1198            $this->id,
1199            'core-session-hash'
1200        );
1201        $this->uc['moduleData'][$module] = $data;
1202        $this->uc['moduleSessionID'][$module] = $sessionHash;
1203        if (!$noSave) {
1204            $this->writeUC();
1205        }
1206    }
1207
1208    /**
1209     * Gets module data for a module (from a loaded ->uc array)
1210     *
1211     * @param string $module Is the name of the module ($MCONF['name'])
1212     * @param string $type If $type = 'ses' then module data is returned only if it was stored in the current session, otherwise data from a previous session will be returned (if available).
1213     * @return mixed The module data if available: $this->uc['moduleData'][$module];
1214     */
1215    public function getModuleData($module, $type = '')
1216    {
1217        $sessionHash = GeneralUtility::hmac(
1218            $this->id,
1219            'core-session-hash'
1220        );
1221        $sessionData = $this->uc['moduleData'][$module] ?? null;
1222        $moduleSessionIdHash = $this->uc['moduleSessionID'][$module] ?? null;
1223        if ($type !== 'ses'
1224            || $sessionData !== null && $moduleSessionIdHash === $sessionHash
1225            // @todo Fallback for non-hashed values in `moduleSessionID`, remove for TYPO3 v11.5 LTS
1226            || $sessionData !== null && $moduleSessionIdHash === $this->id
1227        ) {
1228            return $sessionData;
1229        }
1230        return null;
1231    }
1232
1233    /**
1234     * Returns the session data stored for $key.
1235     * The data will last only for this login session since it is stored in the user session.
1236     *
1237     * @param string $key The key associated with the session data
1238     * @return mixed
1239     */
1240    public function getSessionData($key)
1241    {
1242        return $this->sessionData[$key] ?? null;
1243    }
1244
1245    /**
1246     * Set session data by key.
1247     * The data will last only for this login session since it is stored in the user session.
1248     *
1249     * @param string $key A non empty string to store the data under
1250     * @param mixed $data Data store store in session
1251     */
1252    public function setSessionData($key, $data)
1253    {
1254        if (empty($key)) {
1255            throw new \InvalidArgumentException('Argument key must not be empty', 1484311516);
1256        }
1257        $this->sessionData[$key] = $data;
1258    }
1259
1260    /**
1261     * Sets the session data ($data) for $key and writes all session data (from ->user['ses_data']) to the database.
1262     * The data will last only for this login session since it is stored in the session table.
1263     *
1264     * @param string $key Pointer to an associative key in the session data array which is stored serialized in the field "ses_data" of the session table.
1265     * @param mixed $data The data to store in index $key
1266     */
1267    public function setAndSaveSessionData($key, $data)
1268    {
1269        $this->sessionData[$key] = $data;
1270        $this->user['ses_data'] = serialize($this->sessionData);
1271        $this->logger->debug('setAndSaveSessionData: ses_id = ' . sha1($this->id));
1272        $updatedSession = $this->getSessionBackend()->update(
1273            $this->id,
1274            ['ses_data' => $this->user['ses_data']]
1275        );
1276        $this->user = array_merge($this->user ?? [], $updatedSession);
1277    }
1278
1279    /*************************
1280     *
1281     * Misc
1282     *
1283     *************************/
1284    /**
1285     * Returns an info array with Login/Logout data submitted by a form or params
1286     *
1287     * @return array
1288     * @internal
1289     */
1290    public function getLoginFormData()
1291    {
1292        $loginData = [
1293            'status' => GeneralUtility::_GP($this->formfield_status),
1294            'uname'  => GeneralUtility::_POST($this->formfield_uname),
1295            'uident' => GeneralUtility::_POST($this->formfield_uident)
1296        ];
1297        // Only process the login data if a login is requested
1298        if ($loginData['status'] === LoginType::LOGIN) {
1299            $loginData = $this->processLoginData($loginData);
1300        }
1301        return $loginData;
1302    }
1303
1304    /**
1305     * Processes Login data submitted by a form or params depending on the
1306     * passwordTransmissionStrategy
1307     *
1308     * @param array $loginData Login data array
1309     * @param string $passwordTransmissionStrategy Alternative passwordTransmissionStrategy. Used when authentication services wants to override the default.
1310     * @return array
1311     * @internal
1312     */
1313    public function processLoginData($loginData, $passwordTransmissionStrategy = '')
1314    {
1315        $loginSecurityLevel = trim($GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['loginSecurityLevel']) ?: 'normal';
1316        $passwordTransmissionStrategy = $passwordTransmissionStrategy ?: $loginSecurityLevel;
1317        $this->logger->debug('Login data before processing', $this->removeSensitiveLoginDataForLoggingInfo($loginData));
1318        $subType = 'processLoginData' . $this->loginType;
1319        $authInfo = $this->getAuthInfoArray();
1320        $isLoginDataProcessed = false;
1321        $processedLoginData = $loginData;
1322        /** @var AuthenticationService $serviceObject */
1323        foreach ($this->getAuthServices($subType, $loginData, $authInfo) as $serviceObject) {
1324            $serviceResult = $serviceObject->processLoginData($processedLoginData, $passwordTransmissionStrategy);
1325            if (!empty($serviceResult)) {
1326                $isLoginDataProcessed = true;
1327                // If the service returns >=200 then no more processing is needed
1328                if ((int)$serviceResult >= 200) {
1329                    break;
1330                }
1331            }
1332        }
1333        if ($isLoginDataProcessed) {
1334            $loginData = $processedLoginData;
1335            $this->logger->debug('Processed login data', $this->removeSensitiveLoginDataForLoggingInfo($processedLoginData));
1336        }
1337        return $loginData;
1338    }
1339
1340    /**
1341     * Removes any sensitive data from the incoming data (either from loginData, processedLogin data
1342     * or the user record from the DB).
1343     *
1344     * No type hinting is added because it might be possible that the incoming data is of any other type.
1345     *
1346     * @param mixed|array $data
1347     * @param bool $isUserRecord
1348     * @return mixed
1349     */
1350    protected function removeSensitiveLoginDataForLoggingInfo($data, bool $isUserRecord = false)
1351    {
1352        if ($isUserRecord && is_array($data)) {
1353            $fieldNames = ['uid', 'pid', 'tstamp', 'crdate', 'cruser_id', 'deleted', 'disabled', 'starttime', 'endtime', 'username', 'admin', 'usergroup', 'db_mountpoints', 'file_mountpoints', 'file_permissions', 'workspace_perms', 'lastlogin', 'workspace_id', 'category_perms'];
1354            $data = array_intersect_key($data, array_combine($fieldNames, $fieldNames));
1355        }
1356        if (isset($data['uident'])) {
1357            $data['uident'] = '********';
1358        }
1359        if (isset($data['uident_text'])) {
1360            $data['uident_text'] = '********';
1361        }
1362        if (isset($data['password'])) {
1363            $data['password'] = '********';
1364        }
1365        return $data;
1366    }
1367
1368    /**
1369     * Returns an info array which provides additional information for auth services
1370     *
1371     * @return array
1372     * @internal
1373     */
1374    public function getAuthInfoArray()
1375    {
1376        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->user_table);
1377        $expressionBuilder = $queryBuilder->expr();
1378        $authInfo = [];
1379        $authInfo['loginType'] = $this->loginType;
1380        $authInfo['refInfo'] = parse_url(GeneralUtility::getIndpEnv('HTTP_REFERER'));
1381        $authInfo['HTTP_HOST'] = GeneralUtility::getIndpEnv('HTTP_HOST');
1382        $authInfo['REMOTE_ADDR'] = GeneralUtility::getIndpEnv('REMOTE_ADDR');
1383        $authInfo['REMOTE_HOST'] = GeneralUtility::getIndpEnv('REMOTE_HOST');
1384        $authInfo['showHiddenRecords'] = $this->showHiddenRecords;
1385        // Can be overridden in localconf by SVCONF:
1386        $authInfo['db_user']['table'] = $this->user_table;
1387        $authInfo['db_user']['userid_column'] = $this->userid_column;
1388        $authInfo['db_user']['username_column'] = $this->username_column;
1389        $authInfo['db_user']['userident_column'] = $this->userident_column;
1390        $authInfo['db_user']['usergroup_column'] = $this->usergroup_column;
1391        $authInfo['db_user']['enable_clause'] = $this->userConstraints()->buildExpression(
1392            [$this->user_table => $this->user_table],
1393            $expressionBuilder
1394        );
1395        if ($this->checkPid && $this->checkPid_value !== null) {
1396            $authInfo['db_user']['checkPidList'] = $this->checkPid_value;
1397            $authInfo['db_user']['check_pid_clause'] = $expressionBuilder->in(
1398                'pid',
1399                GeneralUtility::intExplode(',', (string)$this->checkPid_value)
1400            );
1401        } else {
1402            $authInfo['db_user']['checkPidList'] = '';
1403            $authInfo['db_user']['check_pid_clause'] = '';
1404        }
1405        $authInfo['db_groups']['table'] = $this->usergroup_table;
1406        return $authInfo;
1407    }
1408
1409    /**
1410     * Garbage collector, removing old expired sessions.
1411     *
1412     * @internal
1413     */
1414    public function gc()
1415    {
1416        $this->getSessionBackend()->collectGarbage($this->gc_time);
1417    }
1418
1419    /**
1420     * DUMMY: Writes to log database table (in some extension classes)
1421     *
1422     * @param int $type denotes which module that has submitted the entry. This is the current list:  1=tce_db; 2=tce_file; 3=system (eg. sys_history save); 4=modules; 254=Personal settings changed; 255=login / out action: 1=login, 2=logout, 3=failed login (+ errorcode 3), 4=failure_warning_email sent
1423     * @param int $action denotes which specific operation that wrote the entry (eg. 'delete', 'upload', 'update' and so on...). Specific for each $type. Also used to trigger update of the interface. (see the log-module for the meaning of each number !!)
1424     * @param int $error flag. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin)
1425     * @param int $details_nr The message number. Specific for each $type and $action. in the future this will make it possible to translate error messages to other languages
1426     * @param string $details Default text that follows the message
1427     * @param array $data Data that follows the log. Might be used to carry special information. If an array the first 5 entries (0-4) will be sprintf'ed the details-text...
1428     * @param string $tablename Special field used by tce_main.php. These ($tablename, $recuid, $recpid) holds the reference to the record which the log-entry is about. (Was used in attic status.php to update the interface.)
1429     * @param int|string $recuid Special field used by tce_main.php. These ($tablename, $recuid, $recpid) holds the reference to the record which the log-entry is about. (Was used in attic status.php to update the interface.)
1430     * @param int|string $recpid Special field used by tce_main.php. These ($tablename, $recuid, $recpid) holds the reference to the record which the log-entry is about. (Was used in attic status.php to update the interface.)
1431     */
1432    public function writelog($type, $action, $error, $details_nr, $details, $data, $tablename, $recuid, $recpid)
1433    {
1434    }
1435
1436    /**
1437     * DUMMY: Check login failures (in some extension classes)
1438     *
1439     * @param string $email Email address
1440     * @param int $secondsBack Number of sections back in time to check. This is a kind of limit for how many failures an hour for instance
1441     * @param int $maxFailures Max allowed failures before a warning mail is sent
1442     * @ignore
1443     */
1444    public function checkLogFailures($email, $secondsBack, $maxFailures)
1445    {
1446    }
1447
1448    /**
1449     * Raw initialization of the be_user with uid=$uid
1450     * This will circumvent all login procedures and select a be_users record from the
1451     * database and set the content of ->user to the record selected.
1452     * Thus the BE_USER object will appear like if a user was authenticated - however without
1453     * a session id and the fields from the session table of course.
1454     * Will check the users for disabled, start/endtime, etc. ($this->user_where_clause())
1455     *
1456     * @param int $uid The UID of the backend user to set in ->user
1457     * @internal
1458     */
1459    public function setBeUserByUid($uid)
1460    {
1461        $this->user = $this->getRawUserByUid($uid);
1462    }
1463
1464    /**
1465     * Raw initialization of the be_user with username=$name
1466     *
1467     * @param string $name The username to look up.
1468     * @see \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication::setBeUserByUid()
1469     * @internal
1470     */
1471    public function setBeUserByName($name)
1472    {
1473        $this->user = $this->getRawUserByName($name);
1474    }
1475
1476    /**
1477     * Fetching raw user record with uid=$uid
1478     *
1479     * @param int $uid The UID of the backend user to set in ->user
1480     * @return array user record or FALSE
1481     * @internal
1482     */
1483    public function getRawUserByUid($uid)
1484    {
1485        $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->user_table);
1486        $query->setRestrictions($this->userConstraints());
1487        $query->select('*')
1488            ->from($this->user_table)
1489            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid, \PDO::PARAM_INT)));
1490
1491        return $query->execute()->fetch();
1492    }
1493
1494    /**
1495     * Fetching raw user record with username=$name
1496     *
1497     * @param string $name The username to look up.
1498     * @return array user record or FALSE
1499     * @see \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication::getUserByUid()
1500     * @internal
1501     */
1502    public function getRawUserByName($name)
1503    {
1504        $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->user_table);
1505        $query->setRestrictions($this->userConstraints());
1506        $query->select('*')
1507            ->from($this->user_table)
1508            ->where($query->expr()->eq('username', $query->createNamedParameter($name, \PDO::PARAM_STR)));
1509
1510        return $query->execute()->fetch();
1511    }
1512
1513    /**
1514     * @internal
1515     * @return string
1516     */
1517    public function getSessionId(): string
1518    {
1519        return $this->id;
1520    }
1521
1522    /**
1523     * @internal
1524     * @return string
1525     */
1526    public function getLoginType(): string
1527    {
1528        return $this->loginType;
1529    }
1530
1531    /**
1532     * Returns initialized session backend. Returns same session backend if called multiple times
1533     *
1534     * @return SessionBackendInterface
1535     */
1536    protected function getSessionBackend()
1537    {
1538        if (!isset($this->sessionBackend)) {
1539            $this->sessionBackend = GeneralUtility::makeInstance(SessionManager::class)->getSessionBackend($this->loginType);
1540        }
1541        return $this->sessionBackend;
1542    }
1543}
1544