Test Setup Failed
Push — master ( 0d2476...c29423 )
by
unknown
20:37
created

FrontendUserAuthentication::performLogoff()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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