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 Psr\EventDispatcher\EventDispatcherInterface;
19use Psr\Http\Message\ServerRequestInterface;
20use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
21use TYPO3\CMS\Core\Authentication\GroupResolver;
22use TYPO3\CMS\Core\Context\UserAspect;
23use TYPO3\CMS\Core\Database\ConnectionPool;
24use TYPO3\CMS\Core\Session\UserSession;
25use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
26use TYPO3\CMS\Core\Utility\GeneralUtility;
27
28/**
29 * Extension class for Front End User Authentication.
30 */
31class FrontendUserAuthentication extends AbstractUserAuthentication
32{
33    /**
34     * Login type, used for services.
35     * @var string
36     */
37    public $loginType = 'FE';
38
39    /**
40     * Form field with login-name
41     * @var string
42     */
43    public $formfield_uname = 'user';
44
45    /**
46     * Form field with password
47     * @var string
48     */
49    public $formfield_uident = 'pass';
50
51    /**
52     * Form field with status: *'login', 'logout'. If empty login is not verified.
53     * @var string
54     */
55    public $formfield_status = 'logintype';
56
57    /**
58     * form field with 0 or 1
59     * 1 = permanent login enabled
60     * 0 = session is valid for a browser session only
61     * @var string
62     */
63    public $formfield_permanent = 'permalogin';
64
65    /**
66     * Table in database with user data
67     * @var string
68     */
69    public $user_table = 'fe_users';
70
71    /**
72     * Column for login-name
73     * @var string
74     */
75    public $username_column = 'username';
76
77    /**
78     * Column for password
79     * @var string
80     */
81    public $userident_column = 'password';
82
83    /**
84     * Column for user-id
85     * @var string
86     */
87    public $userid_column = 'uid';
88
89    /**
90     * Column name for last login timestamp
91     * @var string
92     */
93    public $lastLogin_column = 'lastlogin';
94
95    /**
96     * @var string
97     */
98    public $usergroup_column = 'usergroup';
99
100    /**
101     * @var string
102     */
103    public $usergroup_table = 'fe_groups';
104
105    /**
106     * Enable field columns of user table
107     * @var array
108     */
109    public $enablecolumns = [
110        'deleted' => 'deleted',
111        'disabled' => 'disable',
112        'starttime' => 'starttime',
113        'endtime' => 'endtime',
114    ];
115
116    /**
117     * @var array
118     */
119    public $groupData = [
120        'title' => [],
121        'uid' => [],
122        'pid' => [],
123    ];
124
125    /**
126     * Used to accumulate the TSconfig data of the user
127     * @var array
128     */
129    protected $TSdataArray = [];
130
131    /**
132     * @var array
133     */
134    protected $userTS = [];
135
136    /**
137     * @var bool
138     */
139    protected $userData_change = false;
140
141    /**
142     * @var bool
143     */
144    public $is_permanent = false;
145
146    /**
147     * @var bool
148     */
149    protected $loginHidden = false;
150
151    /**
152     * Will force the session cookie to be set every time (lifetime must be 0).
153     * @var bool
154     */
155    protected $forceSetCookie = false;
156
157    /**
158     * Will prevent the setting of the session cookie (takes precedence over forceSetCookie)
159     * Disable cookie by default, will be activated if saveSessionData() is called,
160     * a user is logging-in or an existing session is found
161     * @var bool
162     */
163    public $dontSetCookie = true;
164
165    public function __construct()
166    {
167        $this->name = self::getCookieName();
168        parent::__construct();
169        $this->checkPid = $GLOBALS['TYPO3_CONF_VARS']['FE']['checkFeUserPid'];
170    }
171
172    /**
173     * Returns the configured cookie name
174     *
175     * @return string
176     */
177    public static function getCookieName()
178    {
179        $configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['FE']['cookieName']);
180        if (empty($configuredCookieName)) {
181            $configuredCookieName = 'fe_typo_user';
182        }
183        return $configuredCookieName;
184    }
185
186    /**
187     * Determine whether a session cookie needs to be set (lifetime=0)
188     *
189     * @return bool
190     * @internal
191     */
192    public function isSetSessionCookie()
193    {
194        return ($this->userSession->isNew() || $this->forceSetCookie)
195            && ((int)$this->lifetime === 0 || !$this->userSession->isPermanent());
196    }
197
198    /**
199     * Determine whether a non-session cookie needs to be set (lifetime>0)
200     *
201     * @return bool
202     * @internal
203     */
204    public function isRefreshTimeBasedCookie()
205    {
206        return $this->lifetime > 0 && $this->userSession->isPermanent();
207    }
208
209    /**
210     * Returns an info array with Login/Logout data submitted by a form or params
211     *
212     * @return array
213     * @see AbstractUserAuthentication::getLoginFormData()
214     */
215    public function getLoginFormData()
216    {
217        $loginData = parent::getLoginFormData();
218        if ($GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 0 || $GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 1) {
219            $isPermanent = GeneralUtility::_POST($this->formfield_permanent);
220            if (strlen((string)$isPermanent) != 1) {
221                $isPermanent = $GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'];
222            } elseif (!$isPermanent) {
223                // To make sure the user gets a session cookie and doesn't keep a possibly existing time based cookie,
224                // we need to force setting the session cookie here
225                $this->forceSetCookie = true;
226            }
227            $isPermanent = (bool)$isPermanent;
228        } elseif ($GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 2) {
229            $isPermanent = true;
230        } else {
231            $isPermanent = false;
232        }
233        $loginData['permanent'] = $isPermanent;
234        $this->is_permanent = $isPermanent;
235        return $loginData;
236    }
237
238    /**
239     * Creates a user session record and returns its values.
240     * However, as the FE user cookie is normally not set, this has to be done
241     * before the parent class is doing the rest.
242     *
243     * @param array $tempuser User data array
244     * @return UserSession The session data for the newly created session.
245     */
246    public function createUserSession(array $tempuser): UserSession
247    {
248        // At this point we do not know if we need to set a session or a permanent cookie
249        // So we force the cookie to be set after authentication took place, which will
250        // then call setSessionCookie(), which will set a cookie with correct settings.
251        $this->dontSetCookie = false;
252        $tempUserId = (int)($tempuser[$this->userid_column] ?? 0);
253        $session = $this->userSessionManager->elevateToFixatedUserSession(
254            $this->userSession,
255            $tempUserId,
256            (bool)$this->is_permanent
257        );
258        // Updating lastLogin_column carrying information about last login.
259        $this->updateLoginTimestamp($tempUserId);
260        return $session;
261    }
262
263    /**
264     * Will select all fe_groups records that the current fe_user is member of.
265     *
266     * It also accumulates the TSconfig for the fe_user/fe_groups in ->TSdataArray
267     *
268     * @param ServerRequestInterface|null $request (will become a requirement in v12.0)
269     */
270    public function fetchGroupData(ServerRequestInterface $request = null)
271    {
272        $this->TSdataArray = [];
273        $this->userTS = [];
274        $this->userGroups = [];
275        $this->groupData = [
276            'title' => [],
277            'uid' => [],
278            'pid' => [],
279        ];
280        // Setting default configuration:
281        $this->TSdataArray[] = $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultUserTSconfig'];
282
283        $groupDataArr = [];
284        if (is_array($this->user)) {
285            $this->logger->debug('Get usergroups for user', [
286                $this->userid_column => $this->user[$this->userid_column],
287                $this->username_column => $this->user[$this->username_column],
288            ]);
289            $groupDataArr = GeneralUtility::makeInstance(GroupResolver::class)->resolveGroupsForUser($this->user, $this->usergroup_table);
290        }
291        // Fire an event for any kind of user (even when no specific user is here, using hideLogin feature)
292        $dispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
293        $event = $dispatcher->dispatch(new ModifyResolvedFrontendGroupsEvent($this, $groupDataArr, $request ?? $GLOBALS['TYPO3_REQUEST'] ?? null));
294        $groupDataArr = $event->getGroups();
295
296        if (empty($groupDataArr)) {
297            $this->logger->debug('No usergroups found');
298        } else {
299            $this->logger->debug('{count} usergroup records found', ['count' => count($groupDataArr)]);
300        }
301        foreach ($groupDataArr as $groupData) {
302            $groupId = (int)$groupData['uid'];
303            $this->groupData['title'][$groupId] = $groupData['title'] ?? '';
304            $this->groupData['uid'][$groupId] = $groupData['uid'] ?? 0;
305            $this->groupData['pid'][$groupId] = $groupData['pid'] ?? 0;
306            $this->TSdataArray[] = $groupData['TSconfig'] ?? '';
307            $this->userGroups[$groupId] = $groupData;
308        }
309        $this->TSdataArray[] = $this->user['TSconfig'] ?? '';
310        // Sort information
311        ksort($this->groupData['title']);
312        ksort($this->groupData['uid']);
313        ksort($this->groupData['pid']);
314    }
315
316    /**
317     * Initializes the front-end user groups for the context API,
318     * based on the user groups and the logged-in state.
319     *
320     * @param bool $respectUserGroups used with the $TSFE->loginAllowedInBranch flag to disable the inclusion of the users' groups
321     * @return UserAspect
322     */
323    public function createUserAspect(bool $respectUserGroups = true): UserAspect
324    {
325        $userGroups = [0];
326        $isUserAndGroupSet = is_array($this->user) && !empty($this->userGroups);
327        if ($isUserAndGroupSet) {
328            // group -2 is not an existing group, but denotes a 'default' group when a user IS logged in.
329            // This is used to let elements be shown for all logged in users!
330            $userGroups[] = -2;
331            $groupsFromUserRecord = array_keys($this->userGroups);
332        } else {
333            // group -1 is not an existing group, but denotes a 'default' group when not logged in.
334            // This is used to let elements be hidden, when a user is logged in!
335            $userGroups[] = -1;
336            if ($respectUserGroups) {
337                // 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.
338                $groupsFromUserRecord = array_keys($this->userGroups);
339            } else {
340                // Set to blank since we will NOT risk any groups being set when no logins are allowed!
341                $groupsFromUserRecord = [];
342            }
343        }
344        // Make unique and sort the groups
345        $groupsFromUserRecord = array_unique($groupsFromUserRecord);
346        if ($respectUserGroups && !empty($groupsFromUserRecord)) {
347            sort($groupsFromUserRecord);
348            $userGroups = array_merge($userGroups, $groupsFromUserRecord);
349        }
350
351        // For every 60 seconds the is_online timestamp for a logged-in user is updated
352        if ($isUserAndGroupSet) {
353            $this->updateOnlineTimestamp();
354        }
355
356        $this->logger->debug('Valid frontend usergroups: {groups}', ['groups' => implode(',', $userGroups)]);
357        return GeneralUtility::makeInstance(UserAspect::class, $this, $userGroups);
358    }
359    /**
360     * Returns the parsed TSconfig for the fe_user
361     * The TSconfig will be cached in $this->userTS.
362     *
363     * @return array TSconfig array for the fe_user
364     */
365    public function getUserTSconf()
366    {
367        if ($this->userTS === [] && !empty($this->TSdataArray)) {
368            // Parsing the user TS (or getting from cache)
369            $this->TSdataArray = TypoScriptParser::checkIncludeLines_array($this->TSdataArray);
370            $userTS = implode(LF . '[GLOBAL]' . LF, $this->TSdataArray);
371            $parseObj = GeneralUtility::makeInstance(TypoScriptParser::class);
372            $parseObj->parse($userTS);
373            $this->userTS = $parseObj->setup;
374        }
375        return $this->userTS ?? [];
376    }
377
378    /*****************************************
379     *
380     * Session data management functions
381     *
382     ****************************************/
383    /**
384     * Will write UC and session data.
385     * If the flag $this->userData_change has been set, the function ->writeUC is called (which will save persistent user session data)
386     *
387     * @see getKey()
388     * @see setKey()
389     */
390    public function storeSessionData()
391    {
392        // Saves UC and SesData if changed.
393        if ($this->userData_change) {
394            $this->writeUC();
395        }
396
397        if ($this->userSession->dataWasUpdated()) {
398            if (!$this->userSession->hasData()) {
399                // Remove session-data
400                $this->removeSessionData();
401                // Remove cookie if not logged in as the session data is removed as well
402                if (empty($this->user['uid']) && !$this->loginHidden && $this->isCookieSet()) {
403                    $this->removeCookie($this->name);
404                }
405            } elseif (!$this->userSessionManager->isSessionPersisted($this->userSession)) {
406                // Create a new session entry in the backend
407                $this->userSession = $this->userSessionManager->fixateAnonymousSession($this->userSession, (bool)$this->is_permanent);
408                // Now set the cookie (= fix the session)
409                $this->setSessionCookie();
410            } else {
411                // Update session data of an already fixated session
412                $this->userSession = $this->userSessionManager->updateSession($this->userSession);
413            }
414        }
415    }
416
417    /**
418     * Removes data of the current session.
419     */
420    public function removeSessionData()
421    {
422        $this->userSession->overrideData([]);
423        if ($this->userSessionManager->isSessionPersisted($this->userSession)) {
424            // Remove session record if $this->user is empty is if session is anonymous
425            if ((empty($this->user) && !$this->loginHidden) || $this->userSession->isAnonymous()) {
426                $this->userSessionManager->removeSession($this->userSession);
427            } else {
428                $this->userSession = $this->userSessionManager->updateSession($this->userSession);
429            }
430        }
431    }
432
433    /**
434     * Regenerate the session ID and transfer the session to new ID
435     * Call this method whenever a user proceeds to a higher authorization level
436     * e.g. when an anonymous session is now authenticated.
437     * Forces cookie to be set
438     */
439    protected function regenerateSessionId()
440    {
441        parent::regenerateSessionId();
442        // We force the cookie to be set later in the authentication process
443        $this->dontSetCookie = false;
444    }
445
446    /**
447     * Returns session data for the fe_user; Either persistent data following the fe_users uid/profile (requires login)
448     * or current-session based (not available when browse is closed, but does not require login)
449     *
450     * @param string $type Session data type; Either "user" (persistent, bound to fe_users profile) or "ses" (temporary, bound to current session cookie)
451     * @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.
452     * @return mixed Returns whatever value there was in the array for the key, $key
453     * @see setKey()
454     */
455    public function getKey($type, $key)
456    {
457        if (!$key) {
458            return null;
459        }
460        $value = null;
461        switch ($type) {
462            case 'user':
463                $value = $this->uc[$key];
464                break;
465            case 'ses':
466                $value = $this->getSessionData($key);
467                break;
468        }
469        return $value;
470    }
471
472    /**
473     * Saves session data, either persistent or bound to current session cookie. Please see getKey() for more details.
474     * When a value is set the flag $this->userData_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.
475     * 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.
476     *
477     * @param string $type Session data type; Either "user" (persistent, bound to fe_users profile) or "ses" (temporary, bound to current session cookie)
478     * @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.
479     * @param mixed $data The data value to store in $key
480     * @see setKey()
481     * @see storeSessionData()
482     */
483    public function setKey($type, $key, $data)
484    {
485        if (!$key) {
486            return;
487        }
488        switch ($type) {
489            case 'user':
490                if ($this->user['uid'] ?? 0) {
491                    if ($data === null) {
492                        unset($this->uc[$key]);
493                    } else {
494                        $this->uc[$key] = $data;
495                    }
496                    $this->userData_change = true;
497                }
498                break;
499            case 'ses':
500                $this->setSessionData($key, $data);
501                break;
502        }
503    }
504
505    /**
506     * Saves the tokens so that they can be used by a later incarnation of this class.
507     *
508     * @param string $key
509     * @param mixed $data
510     */
511    public function setAndSaveSessionData($key, $data)
512    {
513        $this->setSessionData($key, $data);
514        $this->storeSessionData();
515    }
516
517    /**
518     * Hide the current login
519     *
520     * This is used by the fe_login_mode feature for pages.
521     * A current login is unset, but we remember that there has been one.
522     */
523    public function hideActiveLogin()
524    {
525        $this->user = null;
526        $this->loginHidden = true;
527    }
528
529    /**
530     * Update the field "is_online" every 60 seconds of a logged-in user
531     *
532     * @internal
533     */
534    public function updateOnlineTimestamp()
535    {
536        if (!is_array($this->user) || !($this->user['uid'] ?? 0)
537            || ($this->user['is_online'] ?? 0) >= $GLOBALS['EXEC_TIME'] - 60) {
538            return;
539        }
540        $dbConnection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->user_table);
541        $dbConnection->update(
542            $this->user_table,
543            ['is_online' => $GLOBALS['EXEC_TIME']],
544            ['uid' => (int)$this->user['uid']]
545        );
546        $this->user['is_online'] = $GLOBALS['EXEC_TIME'];
547    }
548}
549