TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:27 +02:00
commit 44c503b2d1
233 changed files with 31556 additions and 0 deletions
@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Authentication;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
/**
* TYPO3 backend user authentication in the Frontend rendering.
*
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
*/
class FrontendBackendUserAuthentication extends BackendUserAuthentication
{
/**
* Form field with login name.
*
* @var string
* @internal
*/
protected $formfield_uname = '';
/**
* Form field with password.
*
* @var string
* @internal
*/
protected $formfield_uident = '';
/**
* Formfield_status should be set to "". The value this->formfield_status is set to empty in order to
* disable login-attempts to the backend account through this script
*
* @var string
* @internal
*/
protected $formfield_status = '';
/**
* Decides if the writelog() function is called at login and logout.
*
* @var bool
*/
public $writeStdLog = false;
/**
* If the writelog() functions is called if a login-attempt has be tried without success.
*
* @var bool
*/
public $writeAttemptLog = false;
/**
* Implementing the access checks that the TYPO3 CMS bootstrap script does before a user is ever logged in.
* Used in the frontend.
*
* @return bool Returns TRUE if access is OK
*/
public function backendCheckLogin(?ServerRequestInterface $request = null)
{
if (empty($this->user['uid'])) {
return false;
}
// Check Hardcoded lock on BE
if ($GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'] < 0) {
return false;
}
return $this->isUserAllowedToLogin();
}
/**
* If a user is in a workspace, but previews the live workspace (GET keyword "LIVE") even if the user
* has no editing permissions for this, it should still be visible, even though "be_users.workspace_perms" is set to "0".
* If this ain't true, users without the live permission cannot see the live page, only the preview of the workspace of the user.
*/
protected function hasEditAccessToLiveWorkspace(): bool
{
return true;
}
}
@@ -0,0 +1,502 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Authentication;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Authentication\AbstractUserAuthentication;
use TYPO3\CMS\Core\Authentication\GroupResolver;
use TYPO3\CMS\Core\Authentication\LoginType;
use TYPO3\CMS\Core\Context\UserAspect;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Http\SetCookieService;
use TYPO3\CMS\Core\Session\UserSession;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Extension class for Front End User Authentication.
*/
class FrontendUserAuthentication extends AbstractUserAuthentication
{
/**
* Login type, used for services.
* @var string
*/
public $loginType = 'FE';
/**
* Form field with login-name
* @var string
* @internal
*/
protected $formfield_uname = 'user';
/**
* Form field with password
* @var string
* @internal
*/
protected $formfield_uident = 'pass';
/**
* Form field with status: *'login', 'logout'. If empty login is not verified.
* @var string
* @internal
*/
protected $formfield_status = 'logintype';
/**
* form field with 0 or 1
* 1 = permanent login enabled
* 0 = session is valid for a browser session only
* @var string
* @internal
*/
protected $formfield_permanent = 'permalogin';
/**
* Table in database with user data
* @var string
*/
public $user_table = 'fe_users';
/**
* Column for login-name
* @var string
*/
public $username_column = 'username';
/**
* Column for password
* @var string
*/
public $userident_column = 'password';
/**
* Column for user-id
* @var string
*/
public $userid_column = 'uid';
/**
* Column name for last login timestamp
* @var string
* @internal
*/
protected $lastLogin_column = 'lastlogin';
/**
* @var string
*/
public $usergroup_column = 'usergroup';
/**
* @var string
*/
public $usergroup_table = 'fe_groups';
/**
* Enable field columns of user table
* @var array
*/
public $enablecolumns = [
'deleted' => 'deleted',
'disabled' => 'disable',
'starttime' => 'starttime',
'endtime' => 'endtime',
];
/**
* @var array
*/
public $groupData = [
'title' => [],
'uid' => [],
'pid' => [],
];
/**
* @var bool
*/
protected $userData_change = false;
/**
* @var bool
* @internal
*/
protected $is_permanent = false;
/**
* Will force the session cookie to be set every time (lifetime must be 0).
* @var bool
*/
protected $forceSetCookie = false;
/**
* Will prevent the setting of the session cookie (takes precedence over forceSetCookie)
* Disable cookie by default, will be activated if saveSessionData() is called,
* a user is logging-in or an existing session is found
* @var bool
* @internal
*/
protected $dontSetCookie = true;
public function __construct()
{
$this->name = self::getCookieName();
parent::__construct();
$this->checkPid = (bool)($GLOBALS['TYPO3_CONF_VARS']['FE']['checkFeUserPid'] ?? true);
}
/**
* Returns the configured cookie name
*/
public static function getCookieName(): string
{
$configuredCookieName = trim((string)($GLOBALS['TYPO3_CONF_VARS']['FE']['cookieName'] ?? ''));
return $configuredCookieName !== '' ? $configuredCookieName : 'fe_typo_user';
}
/**
* Determine whether a session cookie needs to be set (lifetime=0)
*
* @return bool
* @internal
*/
protected function isSetSessionCookie()
{
return SetCookieService::create($this->name, $this->loginType)->isSetSessionCookie($this->userSession, $this->forceSetCookie);
}
/**
* Determine whether a non-session cookie needs to be set (lifetime>0)
*
* @return bool
* @internal
*/
protected function isRefreshTimeBasedCookie()
{
return SetCookieService::create($this->name, $this->loginType)->isRefreshTimeBasedCookie($this->userSession);
}
/**
* Returns an info array with Login/Logout data submitted by a form or params
*
* @return array
* @see AbstractUserAuthentication::getLoginFormData()
*/
public function getLoginFormData(ServerRequestInterface $request)
{
$loginData = parent::getLoginFormData($request);
// Needed in order to fetch users which are already logged-in due to fetching from session
if (LoginType::tryFrom($loginData['status'] ?? '') !== LoginType::LOGIN) {
$this->checkPid_value = null;
}
if ($GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 0 || $GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 1) {
$isPermanent = $request->getParsedBody()[$this->formfield_permanent] ?? '';
if (strlen((string)$isPermanent) != 1) {
$isPermanent = $GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'];
} elseif (!$isPermanent) {
// To make sure the user gets a session cookie and doesn't keep a possibly existing time based cookie,
// we need to force setting the session cookie here
$this->forceSetCookie = true;
}
$isPermanent = (bool)$isPermanent;
} elseif ($GLOBALS['TYPO3_CONF_VARS']['FE']['permalogin'] == 2) {
$isPermanent = true;
} else {
$isPermanent = false;
}
$loginData['permanent'] = $isPermanent;
$this->is_permanent = $isPermanent;
return $loginData;
}
/**
* Creates a user session record and returns its values.
* However, as the FE user cookie is normally not set, this has to be done
* before the parent class is doing the rest.
*
* @param array $tempuser User data array
* @return UserSession The session data for the newly created session.
*/
public function createUserSession(array $tempuser): UserSession
{
// At this point we do not know if we need to set a session or a permanent cookie
// So we force the cookie to be set after authentication took place, which will
// then call setSessionCookie(), which will set a cookie with correct settings.
$this->dontSetCookie = false;
$tempUserId = (int)($tempuser[$this->userid_column] ?? 0);
$session = $this->userSessionManager->elevateToFixatedUserSession(
$this->userSession,
$tempUserId,
(bool)$this->is_permanent
);
// Updating lastLogin_column carrying information about last login.
$this->updateLoginTimestamp($tempUserId);
return $session;
}
/**
* Will select all fe_groups records that the current fe_user is member of.
*
* @param ServerRequestInterface $request
*/
public function fetchGroupData(ServerRequestInterface $request)
{
$this->userGroups = [];
$this->groupData = [
'title' => [],
'uid' => [],
'pid' => [],
];
$groupDataArr = [];
if (is_array($this->user)) {
$this->logger->debug('Get usergroups for user', [
$this->userid_column => $this->getUserId(),
$this->username_column => $this->getUserName(),
]);
$groupDataArr = GeneralUtility::makeInstance(GroupResolver::class)->resolveGroupsForUser($this->user, $this->usergroup_table);
}
// Fire an event for any kind of user (even when no specific user is here, using hideLogin feature)
$dispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
$event = $dispatcher->dispatch(new ModifyResolvedFrontendGroupsEvent($this, $groupDataArr, $request));
$groupDataArr = $event->getGroups();
if (empty($groupDataArr)) {
$this->logger->debug('No usergroups found');
} else {
$this->logger->debug('{count} usergroup records found', ['count' => count($groupDataArr)]);
}
foreach ($groupDataArr as $groupData) {
$groupId = (int)$groupData['uid'];
$this->groupData['title'][$groupId] = $groupData['title'] ?? '';
$this->groupData['uid'][$groupId] = $groupData['uid'] ?? 0;
$this->groupData['pid'][$groupId] = $groupData['pid'] ?? 0;
$this->userGroups[$groupId] = $groupData;
}
// Sort information
ksort($this->groupData['title']);
ksort($this->groupData['uid']);
ksort($this->groupData['pid']);
}
/**
* Initializes the front-end user groups for the context API,
* based on the user groups and the logged-in state.
*
* @param bool $respectUserGroups used to disable the inclusion of the users' groups
*/
public function createUserAspect(bool $respectUserGroups = true): UserAspect
{
$userGroups = [0];
$isUserAndGroupSet = is_array($this->user) && !empty($this->userGroups);
if ($isUserAndGroupSet) {
// group -2 is not an existing group, but denotes a 'default' group when a user IS logged in.
// This is used to let elements be shown for all logged in users!
$userGroups[] = -2;
$groupsFromUserRecord = array_keys($this->userGroups);
} else {
// group -1 is not an existing group, but denotes a 'default' group when not logged in.
// This is used to let elements be hidden, when a user is logged in!
$userGroups[] = -1;
if ($respectUserGroups) {
// 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.
$groupsFromUserRecord = array_keys($this->userGroups);
} else {
// Set to blank since we will NOT risk any groups being set when no logins are allowed!
$groupsFromUserRecord = [];
}
}
// Make unique and sort the groups
$groupsFromUserRecord = array_unique($groupsFromUserRecord);
if ($respectUserGroups && !empty($groupsFromUserRecord)) {
sort($groupsFromUserRecord);
$userGroups = array_merge($userGroups, $groupsFromUserRecord);
}
// For every 60 seconds the is_online timestamp for a logged-in user is updated
if ($isUserAndGroupSet) {
$this->updateOnlineTimestamp();
}
$this->logger->debug('Valid frontend usergroups: {groups}', ['groups' => implode(',', $userGroups)]);
return new UserAspect($this, $userGroups);
}
/*****************************************
*
* Session data management functions
*
****************************************/
/**
* Will write UC and session data.
* If the flag $this->userData_change has been set, the function ->writeUC is called (which will save persistent user session data)
*
* @see getKey()
* @see setKey()
*/
public function storeSessionData()
{
// Saves UC and SesData if changed.
if ($this->userData_change) {
$this->writeUC();
}
if ($this->userSession->dataWasUpdated()) {
if (!$this->userSession->hasData()) {
// Remove session-data
$this->removeSessionData();
// Remove cookie if not logged in as the session data is removed as well
if (empty($this->user['uid']) && $this->isCookieSet()) {
$this->removeCookie();
}
} elseif (!$this->userSessionManager->isSessionPersisted($this->userSession)) {
// Create a new session entry in the backend
$this->userSession = $this->userSessionManager->fixateAnonymousSession($this->userSession, (bool)$this->is_permanent);
// Now set the cookie (= fix the session)
$this->setSessionCookie();
} else {
// Update session data of an already fixated session
$this->userSession = $this->userSessionManager->updateSession($this->userSession);
}
}
}
/**
* Removes data of the current session.
*/
public function removeSessionData()
{
$this->userSession->overrideData([]);
if ($this->userSessionManager->isSessionPersisted($this->userSession)) {
// Remove session record if $this->user is empty or in case the session is anonymous
if (empty($this->user) || $this->userSession->isAnonymous()) {
$this->userSessionManager->removeSession($this->userSession);
} else {
$this->userSession = $this->userSessionManager->updateSession($this->userSession);
}
}
}
/**
* Regenerate the session ID and transfer the session to new ID
* Call this method whenever a user proceeds to a higher authorization level
* e.g. when an anonymous session is now authenticated.
* Forces cookie to be set
*/
protected function regenerateSessionId()
{
parent::regenerateSessionId();
// We force the cookie to be set later in the authentication process
$this->dontSetCookie = false;
}
/**
* Returns session data for the fe_user; Either persistent data following the fe_users uid/profile (requires login)
* or current-session based (not available when browse is closed, but does not require login)
*
* @param string $type Session data type; Either "user" (persistent, bound to fe_users profile) or "ses" (temporary, bound to current session cookie)
* @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.
* @return mixed Returns whatever value there was in the array for the key, $key
* @see setKey()
*/
public function getKey($type, $key)
{
if (!$key) {
return null;
}
$value = null;
switch ($type) {
case 'user':
$value = $this->uc[$key] ?? null;
break;
case 'ses':
$value = $this->getSessionData($key);
break;
}
return $value;
}
/**
* Saves session data, either persistent or bound to current session cookie. Please see getKey() for more details.
* 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.
* 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.
*
* @param string $type Session data type; Either "user" (persistent, bound to fe_users profile) or "ses" (temporary, bound to current session cookie)
* @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.
* @param mixed $data The data value to store in $key
* @see setKey()
* @see storeSessionData()
*/
public function setKey($type, $key, $data)
{
if (!$key) {
return;
}
switch ($type) {
case 'user':
if ($this->user['uid'] ?? 0) {
if ($data === null) {
unset($this->uc[$key]);
} else {
$this->uc[$key] = $data;
}
$this->userData_change = true;
}
break;
case 'ses':
$this->setSessionData($key, $data);
break;
}
}
/**
* Saves the tokens so that they can be used by a later incarnation of this class.
*
* @param string $key
* @param mixed $data
*/
public function setAndSaveSessionData($key, $data)
{
$this->setSessionData($key, $data);
$this->storeSessionData();
}
/**
* Update the field "is_online" every 60 seconds of a logged-in user
*
* @internal
*/
public function updateOnlineTimestamp()
{
if (!is_array($this->user)
|| !($this->user['uid'] ?? 0)
|| $this->user['uid'] === PHP_INT_MAX // Simulated preview user (flagged with PHP_INT_MAX uid)
|| ($this->user['is_online'] ?? 0) >= $GLOBALS['EXEC_TIME'] - 60) {
return;
}
$dbConnection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->user_table);
$dbConnection->update(
$this->user_table,
['is_online' => $GLOBALS['EXEC_TIME']],
['uid' => (int)$this->user['uid']]
);
$this->user['is_online'] = $GLOBALS['EXEC_TIME'];
}
}
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace TYPO3\CMS\Frontend\Authentication;
use Psr\Http\Message\ServerRequestInterface;
/**
* Event listener to allow to add custom Frontend Groups to a (frontend) request
* regardless if a user is logged in or not.
*/
final class ModifyResolvedFrontendGroupsEvent
{
public function __construct(
private readonly FrontendUserAuthentication $user,
private array $groups,
private readonly ServerRequestInterface $request
) {}
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
public function getUser(): FrontendUserAuthentication
{
return $this->user;
}
public function getGroups(): array
{
return $this->groups;
}
public function setGroups(array $groups): void
{
$this->groups = $groups;
}
}