Passed
Branch master (6c65a4)
by Christian
16:31
created

AbstractUserAuthentication   F

Complexity

Total Complexity 203

Size/Duplication

Total Lines 1514
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 1514
rs 0.6314
c 0
b 0
f 0
wmc 203

44 Methods

Rating   Name   Duplication   Size   Complexity  
A getLoginFormData() 0 17 3
C processLoginData() 0 29 7
A createUserSession() 0 11 1
A isCookieSet() 0 3 2
A regenerateSessionId() 0 15 2
A writelog() 0 2 1
A updateLoginTimestamp() 0 8 2
A removeCookie() 0 6 2
A getLoginType() 0 3 1
A gc() 0 3 1
A ipLockClause_remoteIPNumber() 0 12 3
B getAuthInfoArray() 0 33 3
B isExistingSessionRecord() 0 15 5
C setSessionCookie() 0 24 10
B logoff() 0 17 5
A getAuthServices() 0 10 3
A getCookie() 0 3 2
A getSessionId() 0 3 1
A isSetSessionCookie() 0 3 3
B userConstraints() 0 25 6
A checkLogFailures() 0 2 1
A setBeUserByUid() 0 3 1
A isRefreshTimeBasedCookie() 0 3 1
A compareUident() 0 3 2
A getSessionData() 0 3 1
A createSessionId() 0 3 1
A setAndSaveSessionData() 0 10 1
A setBeUserByName() 0 3 1
A getModuleData() 0 6 4
A writeUC() 0 12 4
A setSessionData() 0 6 2
A getSessionBackend() 0 6 2
B getCookieDomain() 0 23 6
A getNewSessionRecord() 0 13 3
A pushModuleData() 0 6 2
A __construct() 0 2 1
A performLogoff() 0 6 2
D fetchUserSession() 0 45 9
A unpack_uc() 0 7 4
B fetchUserRecord() 0 32 4
F checkAuthentication() 0 257 65
F start() 0 79 21
A getRawUserByName() 0 9 1
A getRawUserByUid() 0 9 1

How to fix   Complexity   

Complex Class

Complex classes like AbstractUserAuthentication often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractUserAuthentication, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace TYPO3\CMS\Core\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 Psr\Log\LoggerAwareInterface;
18
use Psr\Log\LoggerAwareTrait;
19
use TYPO3\CMS\Backend\Utility\BackendUtility;
20
use TYPO3\CMS\Core\Crypto\Random;
21
use TYPO3\CMS\Core\Database\Connection;
22
use TYPO3\CMS\Core\Database\ConnectionPool;
23
use TYPO3\CMS\Core\Database\Query\QueryHelper;
24
use TYPO3\CMS\Core\Database\Query\Restriction\DefaultRestrictionContainer;
25
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
26
use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction;
27
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
28
use TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface;
29
use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction;
30
use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction;
31
use TYPO3\CMS\Core\Exception;
32
use TYPO3\CMS\Core\Session\Backend\Exception\SessionNotFoundException;
33
use TYPO3\CMS\Core\Session\Backend\SessionBackendInterface;
34
use TYPO3\CMS\Core\Session\SessionManager;
35
use TYPO3\CMS\Core\Utility\GeneralUtility;
36
use TYPO3\CMS\Core\Utility\MathUtility;
37
38
/**
39
 * Authentication of users in TYPO3
40
 *
41
 * This class is used to authenticate a login user.
42
 * The class is used by both the frontend and backend.
43
 * In both cases this class is a parent class to BackendUserAuthentication and FrontenUserAuthentication
44
 *
45
 * See Inside TYPO3 for more information about the API of the class and internal variables.
46
 */
47
abstract class AbstractUserAuthentication implements LoggerAwareInterface
48
{
49
    use LoggerAwareTrait;
50
51
    /**
52
     * Session/Cookie name
53
     * @var string
54
     */
55
    public $name = '';
56
57
    /**
58
     * Session/GET-var name
59
     * @var string
60
     */
61
    public $get_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
     * 0 = default to $this->sessionTimeout or use 86400 seconds (1 day) if $this->sessionTimeout == 0
173
     * @var int
174
     */
175
    public $gc_time = 0;
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
     * If this is set, authentication is also accepted by $_GET.
203
     * Notice that the identification is NOT 128bit MD5 hash but reduced.
204
     * This is done in order to minimize the size for mobile-devices, such as WAP-phones
205
     * @var bool
206
     */
207
    public $getFallBack = false;
208
209
    /**
210
     * The ident-hash is normally 32 characters and should be!
211
     * But if you are making sites for WAP-devices or other low-bandwidth stuff,
212
     * you may shorten the length.
213
     * Never let this value drop below 6!
214
     * A length of 6 would give you more than 16 mio possibilities.
215
     * @var int
216
     */
217
    public $hash_length = 32;
218
219
    /**
220
     * Setting this flag TRUE lets user-authentication happen from GET_VARS if
221
     * POST_VARS are not set. Thus you may supply username/password with the URL.
222
     * @var bool
223
     */
224
    public $getMethodEnabled = false;
225
226
    /**
227
     * If set to 4, the session will be locked to the user's IP address (all four numbers).
228
     * Reducing this to 1-3 means that only the given number of parts of the IP address is used.
229
     * @var int
230
     */
231
    public $lockIP = 4;
232
233
    /**
234
     * @var string
235
     */
236
    public $warningEmail = '';
237
238
    /**
239
     * Time span (in seconds) within the number of failed logins are collected
240
     * @var int
241
     */
242
    public $warningPeriod = 3600;
243
244
    /**
245
     * The maximum accepted number of warnings before an email to $warningEmail is sent
246
     * @var int
247
     */
248
    public $warningMax = 3;
249
250
    /**
251
     * If set, the user-record must be stored at the page defined by $checkPid_value
252
     * @var bool
253
     */
254
    public $checkPid = true;
255
256
    /**
257
     * The page id the user record must be stored at
258
     * @var int
259
     */
260
    public $checkPid_value = 0;
261
262
    /**
263
     * session_id (MD5-hash)
264
     * @var string
265
     * @internal
266
     */
267
    public $id;
268
269
    /**
270
     * Indicates if an authentication was started but failed
271
     * @var bool
272
     */
273
    public $loginFailure = false;
274
275
    /**
276
     * Will be set to TRUE if the login session is actually written during auth-check.
277
     * @var bool
278
     */
279
    public $loginSessionStarted = false;
280
281
    /**
282
     * @var array|null contains user- AND session-data from database (joined tables)
283
     * @internal
284
     */
285
    public $user = null;
286
287
    /**
288
     * Will be added to the url (eg. '&login=ab7ef8d...')
289
     * GET-auth-var if getFallBack is TRUE. Should be inserted in links!
290
     * @var string
291
     * @internal
292
     */
293
    public $get_URL_ID = '';
294
295
    /**
296
     * Will be set to TRUE if a new session ID was created
297
     * @var bool
298
     */
299
    public $newSessionID = false;
300
301
    /**
302
     * Will force the session cookie to be set every time (lifetime must be 0)
303
     * @var bool
304
     */
305
    public $forceSetCookie = false;
306
307
    /**
308
     * Will prevent the setting of the session cookie (takes precedence over forceSetCookie)
309
     * @var bool
310
     */
311
    public $dontSetCookie = false;
312
313
    /**
314
     * @var bool
315
     */
316
    protected $cookieWasSetOnCurrentRequest = false;
317
318
    /**
319
     * Login type, used for services.
320
     * @var string
321
     */
322
    public $loginType = '';
323
324
    /**
325
     * "auth" services configuration array from $GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth']
326
     * @var array
327
     */
328
    public $svConfig = [];
329
330
    /**
331
     * @var array
332
     */
333
    public $uc;
334
335
    /**
336
     * @var SessionBackendInterface
337
     */
338
    protected $sessionBackend;
339
340
    /**
341
     * Holds deserialized data from session records.
342
     * 'Reserved' keys are:
343
     *   - 'sys': Reserved for TypoScript standard code.
344
     * @var array
345
     */
346
    protected $sessionData = [];
347
348
    /**
349
     * Initialize some important variables
350
     */
351
    public function __construct()
352
    {
353
        // This function has to stay even if it's empty
354
        // Implementations of that abstract class might call parent::__construct();
355
    }
356
357
    /**
358
     * Starts a user session
359
     * Typical configurations will:
360
     * a) check if session cookie was set and if not, set one,
361
     * b) check if a password/username was sent and if so, try to authenticate the user
362
     * c) Lookup a session attached to a user and check timeout etc.
363
     * d) Garbage collection, setting of no-cache headers.
364
     * If a user is authenticated the database record of the user (array) will be set in the ->user internal variable.
365
     *
366
     * @throws Exception
367
     */
368
    public function start()
369
    {
370
        // Backend or frontend login - used for auth services
371
        if (empty($this->loginType)) {
372
            throw new Exception('No loginType defined, should be set explicitly by subclass', 1476045345);
373
        }
374
        $this->logger->debug('## Beginning of auth logging.');
375
        // Init vars.
376
        $mode = '';
377
        $this->newSessionID = false;
378
        // $id is set to ses_id if cookie is present. Else set to FALSE, which will start a new session
379
        $id = $this->getCookie($this->name);
380
        $this->svConfig = $GLOBALS['TYPO3_CONF_VARS']['SVCONF']['auth'] ?? [];
381
382
        // If fallback to get mode....
383
        if (!$id && $this->getFallBack && $this->get_name) {
384
            $id = isset($_GET[$this->get_name]) ? GeneralUtility::_GET($this->get_name) : '';
385
            if (strlen($id) != $this->hash_length) {
386
                $id = '';
387
            }
388
            $mode = 'get';
389
        }
390
391
        // If new session or client tries to fix session...
392
        if (!$id || !$this->isExistingSessionRecord($id)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $id of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
393
            // New random session-$id is made
394
            $id = $this->createSessionId();
395
            // New session
396
            $this->newSessionID = true;
397
        }
398
        // Internal var 'id' is set
399
        $this->id = $id;
400
        // If fallback to get mode....
401
        if ($mode === 'get' && $this->getFallBack && $this->get_name) {
402
            $this->get_URL_ID = '&' . $this->get_name . '=' . $id;
403
        }
404
        // Make certain that NO user is set initially
405
        $this->user = null;
406
        // Set all possible headers that could ensure that the script is not cached on the client-side
407
        if ($this->sendNoCacheHeaders && !(TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_CLI)) {
408
            header('Expires: 0');
409
            header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
410
            $cacheControlHeader = 'no-cache, must-revalidate';
411
            $pragmaHeader = 'no-cache';
412
            // Prevent error message in IE when using a https connection
413
            // see http://forge.typo3.org/issues/24125
414
            $clientInfo = GeneralUtility::clientInfo();
415
            if ($clientInfo['BROWSER'] === 'msie' && GeneralUtility::getIndpEnv('TYPO3_SSL')) {
416
                // Some IEs can not handle no-cache
417
                // see http://support.microsoft.com/kb/323308/en-us
418
                $cacheControlHeader = 'must-revalidate';
419
                // IE needs "Pragma: private" if SSL connection
420
                $pragmaHeader = 'private';
421
            }
422
            header('Cache-Control: ' . $cacheControlHeader);
423
            header('Pragma: ' . $pragmaHeader);
424
        }
425
        // Load user session, check to see if anyone has submitted login-information and if so authenticate
426
        // the user with the session. $this->user[uid] may be used to write log...
427
        $this->checkAuthentication();
428
        // Setting cookies
429
        if (!$this->dontSetCookie) {
430
            $this->setSessionCookie();
431
        }
432
        // Hook for alternative ways of filling the $this->user array (is used by the "timtaw" extension)
433
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postUserLookUp'] ?? [] as $funcName) {
434
            $_params = [
435
                'pObj' => $this,
436
            ];
437
            GeneralUtility::callUserFunction($funcName, $_params, $this);
438
        }
439
        // Set $this->gc_time if not explicitly specified
440
        if ($this->gc_time === 0) {
441
            // Default to 86400 seconds (1 day) if $this->sessionTimeout is 0
442
            $this->gc_time = $this->sessionTimeout === 0 ? 86400 : $this->sessionTimeout;
443
        }
444
        // If we're lucky we'll get to clean up old sessions
445
        if (rand() % 100 <= $this->gc_probability) {
0 ignored issues
show
Bug introduced by
The call to rand() has too few arguments starting with min. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

445
        if (/** @scrutinizer ignore-call */ rand() % 100 <= $this->gc_probability) {

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
446
            $this->gc();
447
        }
448
    }
449
450
    /**
451
     * Sets the session cookie for the current disposal.
452
     *
453
     * @throws Exception
454
     */
455
    protected function setSessionCookie()
456
    {
457
        $isSetSessionCookie = $this->isSetSessionCookie();
458
        $isRefreshTimeBasedCookie = $this->isRefreshTimeBasedCookie();
459
        if ($isSetSessionCookie || $isRefreshTimeBasedCookie) {
460
            $settings = $GLOBALS['TYPO3_CONF_VARS']['SYS'];
461
            // Get the domain to be used for the cookie (if any):
462
            $cookieDomain = $this->getCookieDomain();
463
            // If no cookie domain is set, use the base path:
464
            $cookiePath = $cookieDomain ? '/' : GeneralUtility::getIndpEnv('TYPO3_SITE_PATH');
465
            // If the cookie lifetime is set, use it:
466
            $cookieExpire = $isRefreshTimeBasedCookie ? $GLOBALS['EXEC_TIME'] + $this->lifetime : 0;
467
            // Use the secure option when the current request is served by a secure connection:
468
            $cookieSecure = (bool)$settings['cookieSecure'] && GeneralUtility::getIndpEnv('TYPO3_SSL');
469
            // Do not set cookie if cookieSecure is set to "1" (force HTTPS) and no secure channel is used:
470
            if ((int)$settings['cookieSecure'] !== 1 || GeneralUtility::getIndpEnv('TYPO3_SSL')) {
471
                setcookie($this->name, $this->id, $cookieExpire, $cookiePath, $cookieDomain, $cookieSecure, true);
472
                $this->cookieWasSetOnCurrentRequest = true;
473
            } else {
474
                throw new Exception('Cookie was not set since HTTPS was forced in $TYPO3_CONF_VARS[SYS][cookieSecure].', 1254325546);
475
            }
476
            $this->logger->debug(
477
                ($isRefreshTimeBasedCookie ? 'Updated Cookie: ' : 'Set Cookie: ')
478
                . $this->id . ($cookieDomain ? ', ' . $cookieDomain : '')
479
            );
480
        }
481
    }
482
483
    /**
484
     * Gets the domain to be used on setting cookies.
485
     * The information is taken from the value in $GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain'].
486
     *
487
     * @return string The domain to be used on setting cookies
488
     */
489
    protected function getCookieDomain()
490
    {
491
        $result = '';
492
        $cookieDomain = $GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieDomain'];
493
        // If a specific cookie domain is defined for a given TYPO3_MODE,
494
        // use that domain
495
        if (!empty($GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['cookieDomain'])) {
496
            $cookieDomain = $GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['cookieDomain'];
497
        }
498
        if ($cookieDomain) {
499
            if ($cookieDomain[0] === '/') {
500
                $match = [];
501
                $matchCnt = @preg_match($cookieDomain, GeneralUtility::getIndpEnv('TYPO3_HOST_ONLY'), $match);
502
                if ($matchCnt === false) {
503
                    $this->logger->critical('The regular expression for the cookie domain (' . $cookieDomain . ') contains errors. The session is not shared across sub-domains.');
504
                } elseif ($matchCnt) {
505
                    $result = $match[0];
506
                }
507
            } else {
508
                $result = $cookieDomain;
509
            }
510
        }
511
        return $result;
512
    }
513
514
    /**
515
     * Get the value of a specified cookie.
516
     *
517
     * @param string $cookieName The cookie ID
518
     * @return string The value stored in the cookie
519
     */
520
    protected function getCookie($cookieName)
521
    {
522
        return isset($_COOKIE[$cookieName]) ? stripslashes($_COOKIE[$cookieName]) : '';
523
    }
524
525
    /**
526
     * Determine whether a session cookie needs to be set (lifetime=0)
527
     *
528
     * @return bool
529
     * @internal
530
     */
531
    public function isSetSessionCookie()
532
    {
533
        return ($this->newSessionID || $this->forceSetCookie) && $this->lifetime == 0;
534
    }
535
536
    /**
537
     * Determine whether a non-session cookie needs to be set (lifetime>0)
538
     *
539
     * @return bool
540
     * @internal
541
     */
542
    public function isRefreshTimeBasedCookie()
543
    {
544
        return $this->lifetime > 0;
545
    }
546
547
    /**
548
     * Checks if a submission of username and password is present or use other authentication by auth services
549
     *
550
     * @throws \RuntimeException
551
     * @internal
552
     */
553
    public function checkAuthentication()
554
    {
555
        // No user for now - will be searched by service below
556
        $tempuserArr = [];
557
        $tempuser = false;
558
        // User is not authenticated by default
559
        $authenticated = false;
560
        // User want to login with passed login data (name/password)
561
        $activeLogin = false;
562
        // Indicates if an active authentication failed (not auto login)
563
        $this->loginFailure = false;
564
        $this->logger->debug('Login type: ' . $this->loginType);
565
        // The info array provide additional information for auth services
566
        $authInfo = $this->getAuthInfoArray();
567
        // Get Login/Logout data submitted by a form or params
568
        $loginData = $this->getLoginFormData();
569
        $this->logger->debug('Login data', $loginData);
570
        // Active logout (eg. with "logout" button)
571
        if ($loginData['status'] === 'logout') {
572
            if ($this->writeStdLog) {
573
                // $type,$action,$error,$details_nr,$details,$data,$tablename,$recuid,$recpid
574
                $this->writelog(255, 2, 0, 2, 'User %s logged out', [$this->user['username']], '', 0, 0);
575
            }
576
            $this->logger->info('User logged out. Id: ' . $this->id);
577
            $this->logoff();
578
        }
579
        // Determine whether we need to skip session update.
580
        // This is used mainly for checking session timeout in advance without refreshing the current session's timeout.
581
        $skipSessionUpdate = (bool)GeneralUtility::_GP('skipSessionUpdate');
582
        $haveSession = false;
583
        $anonymousSession = false;
584
        if (!$this->newSessionID) {
585
            // Read user session
586
            $authInfo['userSession'] = $this->fetchUserSession($skipSessionUpdate);
587
            $haveSession = is_array($authInfo['userSession']);
588
            if ($haveSession && !empty($authInfo['userSession']['ses_anonymous'])) {
589
                $anonymousSession = true;
590
            }
591
        }
592
593
        // Active login (eg. with login form).
594
        if (!$haveSession && $loginData['status'] === 'login') {
595
            $activeLogin = true;
596
            $this->logger->debug('Active login (eg. with login form)');
597
            // check referrer for submitted login values
598
            if ($this->formfield_status && $loginData['uident'] && $loginData['uname']) {
599
                $httpHost = GeneralUtility::getIndpEnv('TYPO3_HOST_ONLY');
600
                if (!$this->getMethodEnabled && ($httpHost != $authInfo['refInfo']['host'] && !$GLOBALS['TYPO3_CONF_VARS']['SYS']['doNotCheckReferer'])) {
601
                    throw new \RuntimeException('TYPO3 Fatal Error: Error: This host address ("' . $httpHost . '") and the referer host ("' . $authInfo['refInfo']['host'] . '") mismatches! ' .
602
                        'It is possible that the environment variable HTTP_REFERER is not passed to the script because of a proxy. ' .
603
                        'The site administrator can disable this check in the "All Configuration" section of the Install Tool (flag: TYPO3_CONF_VARS[SYS][doNotCheckReferer]).', 1270853930);
604
                }
605
                // Delete old user session if any
606
                $this->logoff();
607
            }
608
            // Refuse login for _CLI users, if not processing a CLI request type
609
            // (although we shouldn't be here in case of a CLI request type)
610
            if (strtoupper(substr($loginData['uname'], 0, 5)) === '_CLI_' && !(TYPO3_REQUESTTYPE & TYPO3_REQUESTTYPE_CLI)) {
611
                throw new \RuntimeException('TYPO3 Fatal Error: You have tried to login using a CLI user. Access prohibited!', 1270853931);
612
            }
613
        }
614
615
        // Cause elevation of privilege, make sure regenerateSessionId is called later on
616
        if ($anonymousSession && $loginData['status'] === 'login') {
617
            $activeLogin = true;
618
        }
619
620
        if ($haveSession) {
621
            $this->logger->debug('User session found', [
622
                $this->userid_column => $authInfo['userSession'][$this->userid_column],
623
                $this->username_column => $authInfo['userSession'][$this->username_column],
624
            ]);
625
        } else {
626
            $this->logger->debug('No user session found');
627
        }
628
        if (is_array($this->svConfig['setup'] ?? false)) {
629
            $this->logger->debug('SV setup', $this->svConfig['setup']);
630
        }
631
632
        // Fetch user if ...
633
        if (
634
            $activeLogin || !empty($this->svConfig['setup'][$this->loginType . '_alwaysFetchUser'])
635
            || !$haveSession && !empty($this->svConfig['setup'][$this->loginType . '_fetchUserIfNoSession'])
636
        ) {
637
            // Use 'auth' service to find the user
638
            // First found user will be used
639
            $subType = 'getUser' . $this->loginType;
640
            foreach ($this->getAuthServices($subType, $loginData, $authInfo) as $serviceObj) {
641
                if ($row = $serviceObj->getUser()) {
642
                    $tempuserArr[] = $row;
643
                    $this->logger->debug('User found', [
644
                        $this->userid_column => $row[$this->userid_column],
645
                        $this->username_column => $row[$this->username_column],
646
                    ]);
647
                    // User found, just stop to search for more if not configured to go on
648
                    if (!$this->svConfig['setup'][$this->loginType . '_fetchAllUsers']) {
649
                        break;
650
                    }
651
                }
652
            }
653
654
            if ($this->svConfig['setup'][$this->loginType . '_alwaysFetchUser']) {
655
                $this->logger->debug($this->loginType . '_alwaysFetchUser option is enabled');
656
            }
657
            if (empty($tempuserArr)) {
658
                $this->logger->debug('No user found by services');
659
            } else {
660
                $this->logger->debug(count($tempuserArr) . ' user records found by services');
661
            }
662
        }
663
664
        // If no new user was set we use the already found user session
665
        if (empty($tempuserArr) && $haveSession && !$anonymousSession) {
666
            $tempuserArr[] = $authInfo['userSession'];
667
            $tempuser = $authInfo['userSession'];
668
            // User is authenticated because we found a user session
669
            $authenticated = true;
670
            $this->logger->debug('User session used', [
671
                $this->userid_column => $authInfo['userSession'][$this->userid_column],
672
                $this->username_column => $authInfo['userSession'][$this->username_column],
673
            ]);
674
        }
675
        // Re-auth user when 'auth'-service option is set
676
        if (!empty($this->svConfig['setup'][$this->loginType . '_alwaysAuthUser'])) {
677
            $authenticated = false;
678
            $this->logger->debug('alwaysAuthUser option is enabled');
679
        }
680
        // Authenticate the user if needed
681
        if (!empty($tempuserArr) && !$authenticated) {
682
            foreach ($tempuserArr as $tempuser) {
683
                // Use 'auth' service to authenticate the user
684
                // If one service returns FALSE then authentication failed
685
                // a service might return 100 which means there's no reason to stop but the user can't be authenticated by that service
686
                $this->logger->debug('Auth user', $tempuser);
687
                $subType = 'authUser' . $this->loginType;
688
689
                foreach ($this->getAuthServices($subType, $loginData, $authInfo) as $serviceObj) {
690
                    if (($ret = $serviceObj->authUser($tempuser)) > 0) {
691
                        // If the service returns >=200 then no more checking is needed - useful for IP checking without password
692
                        if ((int)$ret >= 200) {
693
                            $authenticated = true;
694
                            break;
695
                        }
696
                        if ((int)$ret >= 100) {
697
                        } else {
698
                            $authenticated = true;
699
                        }
700
                    } else {
701
                        $authenticated = false;
702
                        break;
703
                    }
704
                }
705
706
                if ($authenticated) {
707
                    // Leave foreach() because a user is authenticated
708
                    break;
709
                }
710
            }
711
        }
712
713
        // If user is authenticated a valid user is in $tempuser
714
        if ($authenticated) {
715
            // Reset failure flag
716
            $this->loginFailure = false;
717
            // Insert session record if needed:
718
            if (!$haveSession || $anonymousSession || $tempuser['ses_id'] != $this->id && $tempuser['uid'] != $authInfo['userSession']['ses_userid']) {
719
                $sessionData = $this->createUserSession($tempuser);
0 ignored issues
show
Bug introduced by
It seems like $tempuser can also be of type false; however, parameter $tempuser of TYPO3\CMS\Core\Authentic...on::createUserSession() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

719
                $sessionData = $this->createUserSession(/** @scrutinizer ignore-type */ $tempuser);
Loading history...
720
721
                // Preserve session data on login
722
                if ($anonymousSession) {
723
                    $sessionData = $this->getSessionBackend()->update(
724
                        $this->id,
725
                        ['ses_data' => $authInfo['userSession']['ses_data']]
726
                    );
727
                }
728
729
                $this->user = array_merge(
730
                    $tempuser,
0 ignored issues
show
Bug introduced by
It seems like $tempuser can also be of type false; however, parameter $array1 of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

730
                    /** @scrutinizer ignore-type */ $tempuser,
Loading history...
731
                    $sessionData
732
                );
733
                // The login session is started.
734
                $this->loginSessionStarted = true;
735
                if (is_array($this->user)) {
736
                    $this->logger->debug('User session finally read', [
737
                        $this->userid_column => $this->user[$this->userid_column],
738
                        $this->username_column => $this->user[$this->username_column],
739
                    ]);
740
                }
741
            } elseif ($haveSession) {
742
                // if we come here the current session is for sure not anonymous as this is a pre-condition for $authenticated = true
743
                $this->user = $authInfo['userSession'];
744
            }
745
746
            if ($activeLogin && !$this->newSessionID) {
747
                $this->regenerateSessionId();
748
            }
749
750
            // User logged in - write that to the log!
751
            if ($this->writeStdLog && $activeLogin) {
752
                $this->writelog(255, 1, 0, 1, 'User %s logged in from %s (%s)', [$tempuser[$this->username_column], GeneralUtility::getIndpEnv('REMOTE_ADDR'), GeneralUtility::getIndpEnv('REMOTE_HOST')], '', '', '');
0 ignored issues
show
Bug introduced by
'' of type string is incompatible with the type integer expected by parameter $recpid of TYPO3\CMS\Core\Authentic...hentication::writelog(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

752
                $this->writelog(255, 1, 0, 1, 'User %s logged in from %s (%s)', [$tempuser[$this->username_column], GeneralUtility::getIndpEnv('REMOTE_ADDR'), GeneralUtility::getIndpEnv('REMOTE_HOST')], '', '', /** @scrutinizer ignore-type */ '');
Loading history...
Bug introduced by
'' of type string is incompatible with the type integer expected by parameter $recuid of TYPO3\CMS\Core\Authentic...hentication::writelog(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

752
                $this->writelog(255, 1, 0, 1, 'User %s logged in from %s (%s)', [$tempuser[$this->username_column], GeneralUtility::getIndpEnv('REMOTE_ADDR'), GeneralUtility::getIndpEnv('REMOTE_HOST')], '', /** @scrutinizer ignore-type */ '', '');
Loading history...
753
            }
754
            if ($activeLogin) {
755
                $this->logger->info('User ' . $tempuser[$this->username_column] . ' logged in from ' . GeneralUtility::getIndpEnv('REMOTE_ADDR') . ' (' . GeneralUtility::getIndpEnv('REMOTE_HOST') . ')');
756
            }
757
            if (!$activeLogin) {
758
                $this->logger->debug('User ' . $tempuser[$this->username_column] . ' authenticated from ' . GeneralUtility::getIndpEnv('REMOTE_ADDR') . ' (' . GeneralUtility::getIndpEnv('REMOTE_HOST') . ')');
759
            }
760
        } else {
761
            // User was not authenticated, so we should reuse the existing anonymous session
762
            if ($anonymousSession) {
763
                $this->user = $authInfo['userSession'];
764
            }
765
766
            // Mark the current login attempt as failed
767
            if ($activeLogin || !empty($tempuserArr)) {
768
                $this->loginFailure = true;
769
                if (empty($tempuserArr) && $activeLogin) {
770
                    $logData = [
771
                        'loginData' => $loginData
772
                    ];
773
                    $this->logger->warning('Login failed', $logData);
774
                }
775
                if (!empty($tempuserArr)) {
776
                    $logData = [
777
                        $this->userid_column => $tempuser[$this->userid_column],
778
                        $this->username_column => $tempuser[$this->username_column],
779
                    ];
780
                    $this->logger->warning('Login failed', $logData);
781
                }
782
            }
783
        }
784
785
        // If there were a login failure, check to see if a warning email should be sent:
786
        if ($this->loginFailure && $activeLogin) {
787
            $this->logger->debug(
788
                'Call checkLogFailures',
789
                [
790
                    'warningEmail' => $this->warningEmail,
791
                    'warningPeriod' => $this->warningPeriod,
792
                    'warningMax' => $this->warningMax
793
                ]
794
            );
795
796
            // Hook to implement login failure tracking methods
797
            $_params = [];
798
            $sleep = true;
799
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['postLoginFailureProcessing'] ?? [] as $_funcRef) {
800
                GeneralUtility::callUserFunction($_funcRef, $_params, $this);
801
                $sleep = false;
802
            }
803
804
            if ($sleep) {
805
                // No hooks were triggered - default login failure behavior is to sleep 5 seconds
806
                sleep(5);
807
            }
808
809
            $this->checkLogFailures($this->warningEmail, $this->warningPeriod, $this->warningMax);
810
        }
811
    }
812
813
    /**
814
     * Creates a new session ID.
815
     *
816
     * @return string The new session ID
817
     */
818
    public function createSessionId()
819
    {
820
        return GeneralUtility::makeInstance(Random::class)->generateRandomHexString($this->hash_length);
821
    }
822
823
    /**
824
     * Initializes authentication services to be used in a foreach loop
825
     *
826
     * @param string $subType e.g. getUserFE
827
     * @param array $loginData
828
     * @param array $authInfo
829
     * @return \Traversable A generator of service objects
830
     */
831
    protected function getAuthServices(string $subType, array $loginData, array $authInfo): \Traversable
832
    {
833
        $serviceChain = '';
834
        while (is_object($serviceObj = GeneralUtility::makeInstanceService('auth', $subType, $serviceChain))) {
835
            $serviceChain .= ',' . $serviceObj->getServiceKey();
836
            $serviceObj->initAuth($subType, $loginData, $authInfo, $this);
837
            yield $serviceObj;
838
        }
839
        if ($serviceChain) {
840
            $this->logger->debug($subType . ' auth services called: ' . $serviceChain);
841
        }
842
    }
843
844
    /**
845
     * Regenerate the session ID and transfer the session to new ID
846
     * Call this method whenever a user proceeds to a higher authorization level
847
     * e.g. when an anonymous session is now authenticated.
848
     *
849
     * @param array $existingSessionRecord If given, this session record will be used instead of fetching again
850
     * @param bool $anonymous If true session will be regenerated as anonymous session
851
     */
852
    protected function regenerateSessionId(array $existingSessionRecord = [], bool $anonymous = false)
0 ignored issues
show
Unused Code introduced by
The parameter $anonymous is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

852
    protected function regenerateSessionId(array $existingSessionRecord = [], /** @scrutinizer ignore-unused */ bool $anonymous = false)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
853
    {
854
        if (empty($existingSessionRecord)) {
855
            $existingSessionRecord = $this->getSessionBackend()->get($this->id);
856
        }
857
858
        // Update session record with new ID
859
        $oldSessionId = $this->id;
860
        $this->id = $this->createSessionId();
861
        $updatedSession = $this->getSessionBackend()->set($this->id, $existingSessionRecord);
862
        $this->sessionData = unserialize($updatedSession['ses_data']);
863
        // Merge new session data into user/session array
864
        $this->user = array_merge($this->user ?? [], $updatedSession);
865
        $this->getSessionBackend()->remove($oldSessionId);
866
        $this->newSessionID = true;
867
    }
868
869
    /*************************
870
     *
871
     * User Sessions
872
     *
873
     *************************/
874
875
    /**
876
     * Creates a user session record and returns its values.
877
     *
878
     * @param array $tempuser User data array
879
     *
880
     * @return array The session data for the newly created session.
881
     */
882
    public function createUserSession($tempuser)
883
    {
884
        $this->logger->debug('Create session ses_id = ' . $this->id);
885
        // Delete any session entry first
886
        $this->getSessionBackend()->remove($this->id);
887
        // Re-create session entry
888
        $sessionRecord = $this->getNewSessionRecord($tempuser);
889
        $sessionRecord = $this->getSessionBackend()->set($this->id, $sessionRecord);
890
        // Updating lastLogin_column carrying information about last login.
891
        $this->updateLoginTimestamp($tempuser[$this->userid_column]);
892
        return $sessionRecord;
893
    }
894
895
    /**
896
     * Updates the last login column in the user with the given id
897
     *
898
     * @param int $userId
899
     */
900
    protected function updateLoginTimestamp(int $userId)
901
    {
902
        if ($this->lastLogin_column) {
903
            $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->user_table);
904
            $connection->update(
905
                $this->user_table,
906
                [$this->lastLogin_column => $GLOBALS['EXEC_TIME']],
907
                [$this->userid_column => $userId]
908
            );
909
        }
910
    }
911
912
    /**
913
     * Returns a new session record for the current user for insertion into the DB.
914
     * This function is mainly there as a wrapper for inheriting classes to override it.
915
     *
916
     * @param array $tempuser
917
     * @return array User session record
918
     */
919
    public function getNewSessionRecord($tempuser)
920
    {
921
        $sessionIpLock = '[DISABLED]';
922
        if ($this->lockIP && empty($tempuser['disableIPlock'])) {
923
            $sessionIpLock = $this->ipLockClause_remoteIPNumber($this->lockIP);
924
        }
925
926
        return [
927
            'ses_id' => $this->id,
928
            'ses_iplock' => $sessionIpLock,
929
            'ses_userid' => $tempuser[$this->userid_column] ?? 0,
930
            'ses_tstamp' => $GLOBALS['EXEC_TIME'],
931
            'ses_data' => '',
932
        ];
933
    }
934
935
    /**
936
     * Read the user session from db.
937
     *
938
     * @param bool $skipSessionUpdate
939
     * @return array|bool User session data, false if $this->id does not represent valid session
940
     */
941
    public function fetchUserSession($skipSessionUpdate = false)
942
    {
943
        $this->logger->debug('Fetch session ses_id = ' . $this->id);
944
        try {
945
            $sessionRecord = $this->getSessionBackend()->get($this->id);
946
        } catch (SessionNotFoundException $e) {
947
            return false;
948
        }
949
950
        $this->sessionData = unserialize($sessionRecord['ses_data']);
951
        // Session is anonymous so no need to fetch user
952
        if (!empty($sessionRecord['ses_anonymous'])) {
953
            return $sessionRecord;
954
        }
955
956
        // Fetch the user from the DB
957
        $userRecord = $this->getRawUserByUid((int)$sessionRecord['ses_userid']);
958
        if ($userRecord) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $userRecord of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
959
            $userRecord = array_merge($sessionRecord, $userRecord);
960
            // A user was found
961
            $userRecord['ses_tstamp'] = (int)$userRecord['ses_tstamp'];
962
            $userRecord['is_online'] = (int)$userRecord['ses_tstamp'];
963
964
            if (!empty($this->auth_timeout_field)) {
965
                // Get timeout-time from usertable
966
                $timeout = (int)$userRecord[$this->auth_timeout_field];
967
            } else {
968
                $timeout = $this->sessionTimeout;
969
            }
970
            // If timeout > 0 (TRUE) and current time has not exceeded the latest sessions-time plus the timeout in seconds then accept user
971
            // Use a gracetime-value to avoid updating a session-record too often
972
            if ($timeout > 0 && $GLOBALS['EXEC_TIME'] < $userRecord['ses_tstamp'] + $timeout) {
973
                $sessionUpdateGracePeriod = 61;
974
                if (!$skipSessionUpdate && $GLOBALS['EXEC_TIME'] > ($userRecord['ses_tstamp'] + $sessionUpdateGracePeriod)) {
975
                    // Update the session timestamp by writing a dummy update. (Backend will update the timestamp)
976
                    $updatesSession = $this->getSessionBackend()->update($this->id, []);
977
                    $userRecord = array_merge($userRecord, $updatesSession);
978
                }
979
            } else {
980
                // Delete any user set...
981
                $this->logoff();
982
                $userRecord = false;
983
            }
984
        }
985
        return $userRecord;
986
    }
987
988
    /**
989
     * Log out current user!
990
     * Removes the current session record, sets the internal ->user array to a blank string;
991
     * Thereby the current user (if any) is effectively logged out!
992
     */
993
    public function logoff()
994
    {
995
        $this->logger->debug('logoff: ses_id = ' . $this->id);
996
        // Release the locked records
997
        BackendUtility::lockRecords();
998
999
        $_params = [];
1000
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_pre_processing'] ?? [] as $_funcRef) {
1001
            if ($_funcRef) {
1002
                GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1003
            }
1004
        }
1005
        $this->performLogoff();
1006
1007
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauth.php']['logoff_post_processing'] ?? [] as $_funcRef) {
1008
            if ($_funcRef) {
1009
                GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1010
            }
1011
        }
1012
    }
1013
1014
    /**
1015
     * Perform the logoff action. Called from logoff() as a way to allow subclasses to override
1016
     * what happens when a user logs off, without needing to reproduce the hook calls and logging
1017
     * that happens in the public logoff() API method.
1018
     */
1019
    protected function performLogoff()
1020
    {
1021
        if ($this->id) {
1022
            $this->getSessionBackend()->remove($this->id);
1023
        }
1024
        $this->user = null;
1025
    }
1026
1027
    /**
1028
     * Empty / unset the cookie
1029
     *
1030
     * @param string $cookieName usually, this is $this->name
1031
     */
1032
    public function removeCookie($cookieName)
1033
    {
1034
        $cookieDomain = $this->getCookieDomain();
1035
        // If no cookie domain is set, use the base path
1036
        $cookiePath = $cookieDomain ? '/' : GeneralUtility::getIndpEnv('TYPO3_SITE_PATH');
1037
        setcookie($cookieName, null, -1, $cookiePath, $cookieDomain);
1038
    }
1039
1040
    /**
1041
     * Determine whether there's an according session record to a given session_id.
1042
     * Don't care if session record is still valid or not.
1043
     *
1044
     * @param string $id Claimed Session ID
1045
     * @return bool Returns TRUE if a corresponding session was found in the database
1046
     */
1047
    public function isExistingSessionRecord($id)
1048
    {
1049
        try {
1050
            $sessionRecord = $this->getSessionBackend()->get($id);
1051
            if (empty($sessionRecord)) {
1052
                return false;
1053
            }
1054
            // If the session does not match the current IP lock, it should be treated as invalid
1055
            // and a new session should be created.
1056
            if ($sessionRecord['ses_iplock'] !== $this->ipLockClause_remoteIPNumber($this->lockIP) && $sessionRecord['ses_iplock'] !== '[DISABLED]') {
1057
                return false;
1058
            }
1059
            return true;
1060
        } catch (SessionNotFoundException $e) {
1061
            return false;
1062
        }
1063
    }
1064
1065
    /**
1066
     * Returns whether this request is going to set a cookie
1067
     * or a cookie was already found in the system
1068
     *
1069
     * @return bool Returns TRUE if a cookie is set
1070
     */
1071
    public function isCookieSet()
1072
    {
1073
        return $this->cookieWasSetOnCurrentRequest || $this->getCookie($this->name);
1074
    }
1075
1076
    /*************************
1077
     *
1078
     * SQL Functions
1079
     *
1080
     *************************/
1081
    /**
1082
     * This returns the restrictions needed to select the user respecting
1083
     * enable columns and flags like deleted, hidden, starttime, endtime
1084
     * and rootLevel
1085
     *
1086
     * @return QueryRestrictionContainerInterface
1087
     * @internal
1088
     */
1089
    protected function userConstraints(): QueryRestrictionContainerInterface
1090
    {
1091
        $restrictionContainer = GeneralUtility::makeInstance(DefaultRestrictionContainer::class);
1092
1093
        if (empty($this->enablecolumns['disabled'])) {
1094
            $restrictionContainer->removeByType(HiddenRestriction::class);
1095
        }
1096
1097
        if (empty($this->enablecolumns['deleted'])) {
1098
            $restrictionContainer->removeByType(DeletedRestriction::class);
1099
        }
1100
1101
        if (empty($this->enablecolumns['starttime'])) {
1102
            $restrictionContainer->removeByType(StartTimeRestriction::class);
1103
        }
1104
1105
        if (empty($this->enablecolumns['endtime'])) {
1106
            $restrictionContainer->removeByType(EndTimeRestriction::class);
1107
        }
1108
1109
        if (!empty($this->enablecolumns['rootLevel'])) {
1110
            $restrictionContainer->add(GeneralUtility::makeInstance(RootLevelRestriction::class, [$this->user_table]));
1111
        }
1112
1113
        return $restrictionContainer;
1114
    }
1115
1116
    /**
1117
     * Returns the IP address to lock to.
1118
     * The IP address may be partial based on $parts.
1119
     *
1120
     * @param int $parts 1-4: Indicates how many parts of the IP address to return. 4 means all, 1 means only first number.
1121
     * @return string (Partial) IP address for REMOTE_ADDR
1122
     */
1123
    protected function ipLockClause_remoteIPNumber($parts)
1124
    {
1125
        $IP = GeneralUtility::getIndpEnv('REMOTE_ADDR');
1126
        if ($parts >= 4) {
1127
            return $IP;
1128
        }
1129
        $parts = MathUtility::forceIntegerInRange($parts, 1, 3);
1130
        $IPparts = explode('.', $IP);
1131
        for ($a = 4; $a > $parts; $a--) {
1132
            unset($IPparts[$a - 1]);
1133
        }
1134
        return implode('.', $IPparts);
1135
    }
1136
1137
    /*************************
1138
     *
1139
     * Session and Configuration Handling
1140
     *
1141
     *************************/
1142
    /**
1143
     * This writes $variable to the user-record. This is a way of providing session-data.
1144
     * You can fetch the data again through $this->uc in this class!
1145
     * If $variable is not an array, $this->uc is saved!
1146
     *
1147
     * @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
1148
     */
1149
    public function writeUC($variable = '')
1150
    {
1151
        if (is_array($this->user) && $this->user[$this->userid_column]) {
1152
            if (!is_array($variable)) {
1153
                $variable = $this->uc;
1154
            }
1155
            $this->logger->debug('writeUC: ' . $this->userid_column . '=' . (int)$this->user[$this->userid_column]);
1156
            GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->user_table)->update(
1157
                $this->user_table,
1158
                ['uc' => serialize($variable)],
1159
                [$this->userid_column => (int)$this->user[$this->userid_column]],
1160
                ['uc' => Connection::PARAM_LOB]
1161
            );
1162
        }
1163
    }
1164
1165
    /**
1166
     * Sets $theUC as the internal variable ->uc IF $theUC is an array.
1167
     * If $theUC is FALSE, the 'uc' content from the ->user array will be unserialized and restored in ->uc
1168
     *
1169
     * @param mixed $theUC If an array, then set as ->uc, otherwise load from user record
1170
     */
1171
    public function unpack_uc($theUC = '')
1172
    {
1173
        if (!$theUC && isset($this->user['uc'])) {
1174
            $theUC = unserialize($this->user['uc']);
1175
        }
1176
        if (is_array($theUC)) {
1177
            $this->uc = $theUC;
1178
        }
1179
    }
1180
1181
    /**
1182
     * Stores data for a module.
1183
     * The data is stored with the session id so you can even check upon retrieval
1184
     * if the module data is from a previous session or from the current session.
1185
     *
1186
     * @param string $module Is the name of the module ($MCONF['name'])
1187
     * @param mixed $data Is the data you want to store for that module (array, string, ...)
1188
     * @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.
1189
     */
1190
    public function pushModuleData($module, $data, $noSave = 0)
1191
    {
1192
        $this->uc['moduleData'][$module] = $data;
1193
        $this->uc['moduleSessionID'][$module] = $this->id;
1194
        if (!$noSave) {
1195
            $this->writeUC();
1196
        }
1197
    }
1198
1199
    /**
1200
     * Gets module data for a module (from a loaded ->uc array)
1201
     *
1202
     * @param string $module Is the name of the module ($MCONF['name'])
1203
     * @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).
1204
     * @return mixed The module data if available: $this->uc['moduleData'][$module];
1205
     */
1206
    public function getModuleData($module, $type = '')
1207
    {
1208
        if ($type !== 'ses' || (isset($this->uc['moduleSessionID'][$module]) && $this->uc['moduleSessionID'][$module] == $this->id)) {
1209
            return $this->uc['moduleData'][$module];
1210
        }
1211
        return null;
1212
    }
1213
1214
    /**
1215
     * Returns the session data stored for $key.
1216
     * The data will last only for this login session since it is stored in the user session.
1217
     *
1218
     * @param string $key The key associated with the session data
1219
     * @return mixed
1220
     */
1221
    public function getSessionData($key)
1222
    {
1223
        return $this->sessionData[$key] ?? null;
1224
    }
1225
1226
    /**
1227
     * Set session data by key.
1228
     * The data will last only for this login session since it is stored in the user session.
1229
     *
1230
     * @param string $key A non empty string to store the data under
1231
     * @param mixed $data Data store store in session
1232
     */
1233
    public function setSessionData($key, $data)
1234
    {
1235
        if (empty($key)) {
1236
            throw new \InvalidArgumentException('Argument key must not be empty', 1484311516);
1237
        }
1238
        $this->sessionData[$key] = $data;
1239
    }
1240
1241
    /**
1242
     * Sets the session data ($data) for $key and writes all session data (from ->user['ses_data']) to the database.
1243
     * The data will last only for this login session since it is stored in the session table.
1244
     *
1245
     * @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.
1246
     * @param mixed $data The data to store in index $key
1247
     */
1248
    public function setAndSaveSessionData($key, $data)
1249
    {
1250
        $this->sessionData[$key] = $data;
1251
        $this->user['ses_data'] = serialize($this->sessionData);
1252
        $this->logger->debug('setAndSaveSessionData: ses_id = ' . $this->id);
1253
        $updatedSession = $this->getSessionBackend()->update(
1254
            $this->id,
1255
            ['ses_data' => $this->user['ses_data']]
1256
        );
1257
        $this->user = array_merge($this->user ?? [], $updatedSession);
1258
    }
1259
1260
    /*************************
1261
     *
1262
     * Misc
1263
     *
1264
     *************************/
1265
    /**
1266
     * Returns an info array with Login/Logout data submitted by a form or params
1267
     *
1268
     * @return array
1269
     * @internal
1270
     */
1271
    public function getLoginFormData()
1272
    {
1273
        $loginData = [];
1274
        $loginData['status'] = GeneralUtility::_GP($this->formfield_status);
1275
        if ($this->getMethodEnabled) {
1276
            $loginData['uname'] = GeneralUtility::_GP($this->formfield_uname);
1277
            $loginData['uident'] = GeneralUtility::_GP($this->formfield_uident);
1278
        } else {
1279
            $loginData['uname'] = GeneralUtility::_POST($this->formfield_uname);
1280
            $loginData['uident'] = GeneralUtility::_POST($this->formfield_uident);
1281
        }
1282
        // Only process the login data if a login is requested
1283
        if ($loginData['status'] === 'login') {
1284
            $loginData = $this->processLoginData($loginData);
1285
        }
1286
        $loginData = array_map('trim', $loginData);
1287
        return $loginData;
1288
    }
1289
1290
    /**
1291
     * Processes Login data submitted by a form or params depending on the
1292
     * passwordTransmissionStrategy
1293
     *
1294
     * @param array $loginData Login data array
1295
     * @param string $passwordTransmissionStrategy Alternative passwordTransmissionStrategy. Used when authentication services wants to override the default.
1296
     * @return array
1297
     * @internal
1298
     */
1299
    public function processLoginData($loginData, $passwordTransmissionStrategy = '')
1300
    {
1301
        $loginSecurityLevel = trim($GLOBALS['TYPO3_CONF_VARS'][$this->loginType]['loginSecurityLevel']) ?: 'normal';
1302
        $passwordTransmissionStrategy = $passwordTransmissionStrategy ?: $loginSecurityLevel;
1303
        $this->logger->debug('Login data before processing', $loginData);
1304
        $serviceChain = '';
1305
        $subType = 'processLoginData' . $this->loginType;
1306
        $authInfo = $this->getAuthInfoArray();
1307
        $isLoginDataProcessed = false;
1308
        $processedLoginData = $loginData;
1309
        while (is_object($serviceObject = GeneralUtility::makeInstanceService('auth', $subType, $serviceChain))) {
1310
            $serviceChain .= ',' . $serviceObject->getServiceKey();
1311
            $serviceObject->initAuth($subType, $loginData, $authInfo, $this);
1312
            $serviceResult = $serviceObject->processLoginData($processedLoginData, $passwordTransmissionStrategy);
1313
            if (!empty($serviceResult)) {
1314
                $isLoginDataProcessed = true;
1315
                // If the service returns >=200 then no more processing is needed
1316
                if ((int)$serviceResult >= 200) {
1317
                    unset($serviceObject);
1318
                    break;
1319
                }
1320
            }
1321
            unset($serviceObject);
1322
        }
1323
        if ($isLoginDataProcessed) {
1324
            $loginData = $processedLoginData;
1325
            $this->logger->debug('Processed login data', $processedLoginData);
1326
        }
1327
        return $loginData;
1328
    }
1329
1330
    /**
1331
     * Returns an info array which provides additional information for auth services
1332
     *
1333
     * @return array
1334
     * @internal
1335
     */
1336
    public function getAuthInfoArray()
1337
    {
1338
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->user_table);
1339
        $expressionBuilder = $queryBuilder->expr();
1340
        $authInfo = [];
1341
        $authInfo['loginType'] = $this->loginType;
1342
        $authInfo['refInfo'] = parse_url(GeneralUtility::getIndpEnv('HTTP_REFERER'));
1343
        $authInfo['HTTP_HOST'] = GeneralUtility::getIndpEnv('HTTP_HOST');
1344
        $authInfo['REMOTE_ADDR'] = GeneralUtility::getIndpEnv('REMOTE_ADDR');
1345
        $authInfo['REMOTE_HOST'] = GeneralUtility::getIndpEnv('REMOTE_HOST');
1346
        $authInfo['showHiddenRecords'] = $this->showHiddenRecords;
1347
        // Can be overidden in localconf by SVCONF:
1348
        $authInfo['db_user']['table'] = $this->user_table;
1349
        $authInfo['db_user']['userid_column'] = $this->userid_column;
1350
        $authInfo['db_user']['username_column'] = $this->username_column;
1351
        $authInfo['db_user']['userident_column'] = $this->userident_column;
1352
        $authInfo['db_user']['usergroup_column'] = $this->usergroup_column;
1353
        $authInfo['db_user']['enable_clause'] = $this->userConstraints()->buildExpression(
1354
            [$this->user_table => $this->user_table],
1355
            $expressionBuilder
1356
        );
1357
        if ($this->checkPid && $this->checkPid_value !== null) {
1358
            $authInfo['db_user']['checkPidList'] = $this->checkPid_value;
1359
            $authInfo['db_user']['check_pid_clause'] = $expressionBuilder->in(
1360
                'pid',
1361
                GeneralUtility::intExplode(',', $this->checkPid_value)
1362
            );
1363
        } else {
1364
            $authInfo['db_user']['checkPidList'] = '';
1365
            $authInfo['db_user']['check_pid_clause'] = '';
1366
        }
1367
        $authInfo['db_groups']['table'] = $this->usergroup_table;
1368
        return $authInfo;
1369
    }
1370
1371
    /**
1372
     * Check the login data with the user record data for builtin login methods
1373
     *
1374
     * @param array $user User data array
1375
     * @param array $loginData Login data array
1376
     * @param string $passwordCompareStrategy Alternative passwordCompareStrategy. Used when authentication services wants to override the default.
1377
     * @return bool TRUE if login data matched
1378
     */
1379
    public function compareUident($user, $loginData, $passwordCompareStrategy = '')
0 ignored issues
show
Unused Code introduced by
The parameter $passwordCompareStrategy is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1379
    public function compareUident($user, $loginData, /** @scrutinizer ignore-unused */ $passwordCompareStrategy = '')

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1380
    {
1381
        return (string)$loginData['uident_text'] !== '' && (string)$loginData['uident_text'] === (string)$user[$this->userident_column];
1382
    }
1383
1384
    /**
1385
     * Garbage collector, removing old expired sessions.
1386
     *
1387
     * @internal
1388
     */
1389
    public function gc()
1390
    {
1391
        $this->getSessionBackend()->collectGarbage($this->gc_time);
1392
    }
1393
1394
    /**
1395
     * DUMMY: Writes to log database table (in some extension classes)
1396
     *
1397
     * @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
1398
     * @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 !!)
1399
     * @param int $error flag. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin)
1400
     * @param int $details_nr The message number. Specific for each $type and $action. in the future this will make it possible to translate errormessages to other languages
1401
     * @param string $details Default text that follows the message
1402
     * @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...
1403
     * @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.)
1404
     * @param int $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.)
1405
     * @param int $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.)
1406
     */
1407
    public function writelog($type, $action, $error, $details_nr, $details, $data, $tablename, $recuid, $recpid)
0 ignored issues
show
Unused Code introduced by
The parameter $details is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1407
    public function writelog($type, $action, $error, $details_nr, /** @scrutinizer ignore-unused */ $details, $data, $tablename, $recuid, $recpid)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $recuid is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1407
    public function writelog($type, $action, $error, $details_nr, $details, $data, $tablename, /** @scrutinizer ignore-unused */ $recuid, $recpid)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $error is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1407
    public function writelog($type, $action, /** @scrutinizer ignore-unused */ $error, $details_nr, $details, $data, $tablename, $recuid, $recpid)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $tablename is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1407
    public function writelog($type, $action, $error, $details_nr, $details, $data, /** @scrutinizer ignore-unused */ $tablename, $recuid, $recpid)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $details_nr is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1407
    public function writelog($type, $action, $error, /** @scrutinizer ignore-unused */ $details_nr, $details, $data, $tablename, $recuid, $recpid)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $recpid is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1407
    public function writelog($type, $action, $error, $details_nr, $details, $data, $tablename, $recuid, /** @scrutinizer ignore-unused */ $recpid)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $action is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1407
    public function writelog($type, /** @scrutinizer ignore-unused */ $action, $error, $details_nr, $details, $data, $tablename, $recuid, $recpid)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1408
    {
1409
    }
1410
1411
    /**
1412
     * DUMMY: Check login failures (in some extension classes)
1413
     *
1414
     * @param string $email Email address
1415
     * @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
1416
     * @param int $maxFailures Max allowed failures before a warning mail is sent
1417
     * @ignore
1418
     */
1419
    public function checkLogFailures($email, $secondsBack, $maxFailures)
0 ignored issues
show
Unused Code introduced by
The parameter $email is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1419
    public function checkLogFailures(/** @scrutinizer ignore-unused */ $email, $secondsBack, $maxFailures)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $secondsBack is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1419
    public function checkLogFailures($email, /** @scrutinizer ignore-unused */ $secondsBack, $maxFailures)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $maxFailures is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

1419
    public function checkLogFailures($email, $secondsBack, /** @scrutinizer ignore-unused */ $maxFailures)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1420
    {
1421
    }
1422
1423
    /**
1424
     * Raw initialization of the be_user with uid=$uid
1425
     * This will circumvent all login procedures and select a be_users record from the
1426
     * database and set the content of ->user to the record selected.
1427
     * Thus the BE_USER object will appear like if a user was authenticated - however without
1428
     * a session id and the fields from the session table of course.
1429
     * Will check the users for disabled, start/endtime, etc. ($this->user_where_clause())
1430
     *
1431
     * @param int $uid The UID of the backend user to set in ->user
1432
     * @internal
1433
     * @see SC_mod_tools_be_user_index::compareUsers(), \TYPO3\CMS\Setup\Controller\SetupModuleController::simulateUser(), freesite_admin::startCreate()
1434
     */
1435
    public function setBeUserByUid($uid)
1436
    {
1437
        $this->user = $this->getRawUserByUid($uid);
1438
    }
1439
1440
    /**
1441
     * Raw initialization of the be_user with username=$name
1442
     *
1443
     * @param string $name The username to look up.
1444
     * @see \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication::setBeUserByUid()
1445
     * @internal
1446
     */
1447
    public function setBeUserByName($name)
1448
    {
1449
        $this->user = $this->getRawUserByName($name);
1450
    }
1451
1452
    /**
1453
     * Fetching raw user record with uid=$uid
1454
     *
1455
     * @param int $uid The UID of the backend user to set in ->user
1456
     * @return array user record or FALSE
1457
     * @internal
1458
     */
1459
    public function getRawUserByUid($uid)
1460
    {
1461
        $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->user_table);
1462
        $query->setRestrictions($this->userConstraints());
1463
        $query->select('*')
1464
            ->from($this->user_table)
1465
            ->where($query->expr()->eq('uid', $query->createNamedParameter($uid, \PDO::PARAM_INT)));
1466
1467
        return $query->execute()->fetch();
1468
    }
1469
1470
    /**
1471
     * Fetching raw user record with username=$name
1472
     *
1473
     * @param string $name The username to look up.
1474
     * @return array user record or FALSE
1475
     * @see \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication::getUserByUid()
1476
     * @internal
1477
     */
1478
    public function getRawUserByName($name)
1479
    {
1480
        $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->user_table);
1481
        $query->setRestrictions($this->userConstraints());
1482
        $query->select('*')
1483
            ->from($this->user_table)
1484
            ->where($query->expr()->eq('username', $query->createNamedParameter($name, \PDO::PARAM_STR)));
1485
1486
        return $query->execute()->fetch();
1487
    }
1488
1489
    /**
1490
     * Get a user from DB by username
1491
     * provided for usage from services
1492
     *
1493
     * @param array $dbUser User db table definition: $this->db_user
1494
     * @param string $username user name
1495
     * @param string $extraWhere Additional WHERE clause: " AND ...
1496
     * @return mixed User array or FALSE
1497
     */
1498
    public function fetchUserRecord($dbUser, $username, $extraWhere = '')
1499
    {
1500
        $user = false;
1501
        if ($username || $extraWhere) {
1502
            $query = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($dbUser['table']);
1503
            $query->getRestrictions()->removeAll()
1504
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1505
1506
            $constraints = array_filter([
1507
                QueryHelper::stripLogicalOperatorPrefix($dbUser['check_pid_clause']),
1508
                QueryHelper::stripLogicalOperatorPrefix($dbUser['enable_clause']),
1509
                QueryHelper::stripLogicalOperatorPrefix($extraWhere),
1510
            ]);
1511
1512
            if (!empty($username)) {
1513
                array_unshift(
1514
                    $constraints,
1515
                    $query->expr()->eq(
1516
                        $dbUser['username_column'],
1517
                        $query->createNamedParameter($username, \PDO::PARAM_STR)
1518
                    )
1519
                );
1520
            }
1521
1522
            $user = $query->select('*')
1523
                ->from($dbUser['table'])
1524
                ->where(...$constraints)
1525
                ->execute()
1526
                ->fetch();
1527
        }
1528
1529
        return $user;
1530
    }
1531
1532
    /**
1533
     * @internal
1534
     * @return string
1535
     */
1536
    public function getSessionId() : string
1537
    {
1538
        return $this->id;
1539
    }
1540
1541
    /**
1542
     * @internal
1543
     * @return string
1544
     */
1545
    public function getLoginType() : string
1546
    {
1547
        return $this->loginType;
1548
    }
1549
1550
    /**
1551
     * Returns initialized session backend. Returns same session backend if called multiple times
1552
     *
1553
     * @return SessionBackendInterface
1554
     */
1555
    protected function getSessionBackend()
1556
    {
1557
        if (!isset($this->sessionBackend)) {
1558
            $this->sessionBackend = GeneralUtility::makeInstance(SessionManager::class)->getSessionBackend($this->loginType);
1559
        }
1560
        return $this->sessionBackend;
1561
    }
1562
}
1563