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