2174 lines
92 KiB
PHP
2174 lines
92 KiB
PHP
<?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\Core\Authentication;
|
|
|
|
use Doctrine\DBAL\Types\Type;
|
|
use Doctrine\DBAL\Types\Types;
|
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
|
use Psr\Http\Message\ServerRequestInterface;
|
|
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
|
use TYPO3\CMS\Core\Authentication\Event\AfterUserLoggedInEvent;
|
|
use TYPO3\CMS\Core\Cache\CacheManager;
|
|
use TYPO3\CMS\Core\Core\Environment;
|
|
use TYPO3\CMS\Core\Database\Connection;
|
|
use TYPO3\CMS\Core\Database\ConnectionPool;
|
|
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
|
|
use TYPO3\CMS\Core\Database\Query\QueryHelper;
|
|
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
|
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
|
|
use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction;
|
|
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
|
|
use TYPO3\CMS\Core\DataHandling\TableColumnType;
|
|
use TYPO3\CMS\Core\Domain\RecordInterface;
|
|
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
|
|
use TYPO3\CMS\Core\Http\ImmediateResponseException;
|
|
use TYPO3\CMS\Core\Http\RedirectResponse;
|
|
use TYPO3\CMS\Core\Package\PackageManager;
|
|
use TYPO3\CMS\Core\Resource\Filter\FileNameFilter;
|
|
use TYPO3\CMS\Core\Resource\StorageRepository;
|
|
use TYPO3\CMS\Core\Routing\BackendEntryPointResolver;
|
|
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
|
use TYPO3\CMS\Core\Schema\TcaSchema;
|
|
use TYPO3\CMS\Core\Schema\TcaSchemaFactory;
|
|
use TYPO3\CMS\Core\Security\PermissionSet\PrincipalRole;
|
|
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
|
|
use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction;
|
|
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
|
|
use TYPO3\CMS\Core\SysLog\Repository\LogEntryRepository;
|
|
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
|
|
use TYPO3\CMS\Core\Type\Bitmask\BackendGroupMountOption;
|
|
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
|
use TYPO3\CMS\Core\TypoScript\UserTsConfig;
|
|
use TYPO3\CMS\Core\TypoScript\UserTsConfigFactory;
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
use TYPO3\CMS\Core\Utility\MathUtility;
|
|
use TYPO3\CMS\Core\Utility\StringUtility;
|
|
use TYPO3\CMS\Install\Service\SessionService;
|
|
|
|
/**
|
|
* TYPO3 backend user authentication
|
|
* Contains most of the functions used for checking permissions, authenticating users,
|
|
* setting up the user, and API for user from outside.
|
|
* This class contains the configuration of the database fields used plus some
|
|
* functions for the authentication process of backend users.
|
|
*/
|
|
class BackendUserAuthentication extends AbstractUserAuthentication
|
|
{
|
|
public const ROLE_SYSTEMMAINTAINER = 'systemMaintainer';
|
|
|
|
/**
|
|
* Should be set to the usergroup-column (id-list) in the user-record
|
|
* @var string
|
|
*/
|
|
public $usergroup_column = 'usergroup';
|
|
|
|
/**
|
|
* The name of the group-table
|
|
* @var string
|
|
*/
|
|
public $usergroup_table = 'be_groups';
|
|
|
|
/**
|
|
* holds lists of eg. tables, fields and other values related to the permission-system. See fetchGroupData
|
|
* @var array
|
|
* @internal
|
|
*/
|
|
public $groupData = [
|
|
'allowed_languages' => '',
|
|
'tables_select' => '',
|
|
'tables_modify' => '',
|
|
'pagetypes_select' => '',
|
|
'non_exclude_fields' => '',
|
|
'explicit_allowdeny' => '',
|
|
'custom_options' => '',
|
|
'file_permissions' => '',
|
|
];
|
|
|
|
/**
|
|
* This array holds the uid's of the groups in the listed order
|
|
* @var array
|
|
*/
|
|
public $userGroupsUID = [];
|
|
|
|
/**
|
|
* User workspace.
|
|
* -99 is ERROR (none available)
|
|
* 0 is online
|
|
* >0 is custom workspaces
|
|
*/
|
|
public int $workspace = -99;
|
|
|
|
/**
|
|
* Custom workspace record if any
|
|
* @var array
|
|
*/
|
|
public $workspaceRec = [];
|
|
|
|
protected ?UserTsConfig $userTsConfig = null;
|
|
|
|
/**
|
|
* Cached user settings object
|
|
*/
|
|
private ?UserSettings $userSettings = null;
|
|
|
|
/**
|
|
* True if the user TSconfig was parsed and needs to be cached.
|
|
* @todo: Should vanish, see todo below.
|
|
*/
|
|
protected bool $userTSUpdated = false;
|
|
|
|
/**
|
|
* Cache for checkWorkspaceCurrent()
|
|
* @var array|null
|
|
*/
|
|
protected $checkWorkspaceCurrent_cache;
|
|
|
|
/**
|
|
* @var \TYPO3\CMS\Core\Resource\ResourceStorage[]
|
|
*/
|
|
protected $fileStorages;
|
|
|
|
/**
|
|
* @var array|null
|
|
*/
|
|
protected $filePermissions;
|
|
|
|
/**
|
|
* Table in database with user data
|
|
* @var string
|
|
*/
|
|
public $user_table = 'be_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';
|
|
|
|
/**
|
|
* @var string
|
|
*/
|
|
public $lastLogin_column = 'lastlogin';
|
|
|
|
/**
|
|
* Enable field columns of user table
|
|
* @var array
|
|
*/
|
|
public $enablecolumns = [
|
|
'rootLevel' => 1,
|
|
'deleted' => 'deleted',
|
|
'disabled' => 'disable',
|
|
'starttime' => 'starttime',
|
|
'endtime' => 'endtime',
|
|
];
|
|
|
|
/**
|
|
* Form field with login-name
|
|
* @var string
|
|
* @internal
|
|
*/
|
|
protected $formfield_uname = 'username';
|
|
|
|
/**
|
|
* Form field with password
|
|
* @var string
|
|
* @internal
|
|
*/
|
|
protected $formfield_uident = 'userident';
|
|
|
|
/**
|
|
* Form field with status: *'login', 'logout'
|
|
* @var string
|
|
* @internal
|
|
*/
|
|
protected $formfield_status = 'login_status';
|
|
|
|
/**
|
|
* Decides if the writelog() function is called at login and logout
|
|
* @var bool
|
|
*/
|
|
public $writeStdLog = true;
|
|
|
|
/**
|
|
* If the writelog() functions is called if a login-attempt has be tried without success
|
|
* @var bool
|
|
*/
|
|
public $writeAttemptLog = true;
|
|
|
|
/**
|
|
* @var int
|
|
* @internal should only be used from within TYPO3 Core
|
|
*/
|
|
public $firstMainGroup = 0;
|
|
|
|
/**
|
|
* User Config Default values:
|
|
* The array may contain other fields for configuration.
|
|
* For this, see "setup" extension and "TSconfig" document (User TSconfig, "setup.[xxx]....")
|
|
* Reserved keys for other storage of session data:
|
|
* moduleData
|
|
* moduleSessionID
|
|
* @var array
|
|
* @internal should only be used from within TYPO3 Core
|
|
*/
|
|
public $uc_default = [
|
|
// serialized content that is used to store interface pane and menu positions. Set by the logout.php-script
|
|
'moduleData' => [],
|
|
// user-data for the modules
|
|
'emailMeAtLogin' => 0,
|
|
'titleLen' => 50,
|
|
'edit_docModuleUpload' => '1',
|
|
];
|
|
|
|
/**
|
|
* Login type, used for services.
|
|
* @var string
|
|
*/
|
|
public $loginType = 'BE';
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct()
|
|
{
|
|
$this->name = self::getCookieName();
|
|
parent::__construct();
|
|
}
|
|
|
|
public function getUserSettings(): UserSettings
|
|
{
|
|
if ($this->userSettings === null) {
|
|
$factory = GeneralUtility::makeInstance(UserSettingsFactory::class);
|
|
$this->userSettings = $factory->createFromUserRecord($this->user ?? [], $this->uc);
|
|
}
|
|
return $this->userSettings;
|
|
}
|
|
|
|
protected function resetUserSettingsCache(): void
|
|
{
|
|
$this->userSettings = null;
|
|
}
|
|
|
|
/**
|
|
* Returns TRUE if user is admin
|
|
* Basically this function evaluates if the ->user[admin] field has bit 0 set. If so, user is admin.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isAdmin()
|
|
{
|
|
return is_array($this->user) && (($this->user['admin'] ?? 0) & 1) == 1;
|
|
}
|
|
|
|
/**
|
|
* Returns TRUE if the current user is a member of group $groupId
|
|
* $groupId must be set. $this->userGroupsUID must contain groups
|
|
* Will return TRUE also if the user is a member of a group through subgroups.
|
|
*
|
|
* @internal should only be used from within TYPO3 Core, use Context API for quicker access
|
|
*/
|
|
protected function isMemberOfGroup(int $groupId): bool
|
|
{
|
|
if (!empty($this->userGroupsUID) && $groupId) {
|
|
return in_array($groupId, $this->userGroupsUID, true);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks if the permissions is granted based on a page-record ($row) and $perms (binary and'ed)
|
|
*
|
|
* Bits for permissions, see $perms variable:
|
|
*
|
|
* 1 - Show: See/Copy page and the pagecontent.
|
|
* 2 - Edit page: Change/Move the page, eg. change title, startdate, hidden.
|
|
* 4 - Delete page: Delete the page and pagecontent.
|
|
* 8 - New pages: Create new pages under the page.
|
|
* 16 - Edit pagecontent: Change/Add/Delete/Move pagecontent.
|
|
*
|
|
* @param array $row Is the pagerow for which the permissions is checked
|
|
* @param int $perms Is the binary representation of the permission we are going to check. Every bit in this number represents a permission that must be set. See function explanation.
|
|
* @param bool $useDeleteClause Use the delete clause to check if a record is deleted
|
|
* @return bool
|
|
*/
|
|
public function doesUserHaveAccess($row, $perms, bool $useDeleteClause = true)
|
|
{
|
|
$userPerms = $this->calcPerms($row, $useDeleteClause);
|
|
return ($userPerms & $perms) == $perms;
|
|
}
|
|
|
|
/**
|
|
* Checks if the page id or page record ($idOrRow) is found within the webmounts set up for the user.
|
|
* This should ALWAYS be checked for any page id a user works with, whether it's about reading, writing or whatever.
|
|
* The point is that this will add the security that a user can NEVER touch parts outside his mounted
|
|
* pages in the page tree. This is otherwise possible if the raw page permissions allows for it.
|
|
* So this security check just makes it easier to make safe user configurations.
|
|
* If the user is admin then it returns "1" right away
|
|
* Otherwise the function will return the uid of the webmount which was first found in the rootline of the input page $id
|
|
*
|
|
* @param int|array $idOrRow Page ID or full page record to check
|
|
* @param string $readPerms Content of "->getPagePermsClause(1)" (read-permissions). If not set, they will be internally calculated (but if you have the correct value right away you can save that database lookup!)
|
|
* @param bool $useDeleteClause Use the deleteClause to check if a record is deleted (default TRUE)
|
|
* @throws \RuntimeException
|
|
* @return int|null The page UID of a page in the rootline that matched a mount point
|
|
*/
|
|
public function isInWebMount($idOrRow, $readPerms = '', bool $useDeleteClause = true)
|
|
{
|
|
if ($this->isAdmin()) {
|
|
return 1;
|
|
}
|
|
$schema = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get('pages');
|
|
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
|
|
$languageFieldName = $languageCapability->getLanguageField()->getName();
|
|
$transOrigPointerFieldName = $languageCapability->getTranslationOriginPointerField()->getName();
|
|
$checkRec = [];
|
|
$fetchPageFromDatabase = true;
|
|
if (is_array($idOrRow)) {
|
|
if (!isset($idOrRow['uid'])) {
|
|
throw new \RuntimeException('The given page record is invalid. Missing uid.', 1578950324);
|
|
}
|
|
$checkRec = $idOrRow;
|
|
$id = (int)$idOrRow['uid'];
|
|
// ensure the required fields are present on the record
|
|
if (isset($checkRec['t3ver_oid'], $checkRec[$languageFieldName], $checkRec[$transOrigPointerFieldName])) {
|
|
$fetchPageFromDatabase = false;
|
|
}
|
|
} else {
|
|
$id = (int)$idOrRow;
|
|
}
|
|
if ($fetchPageFromDatabase) {
|
|
// Check if input id is an offline version page in which case we will map id to the online version:
|
|
$checkRec = BackendUtility::getRecord(
|
|
'pages',
|
|
$id,
|
|
't3ver_oid,' . $transOrigPointerFieldName . ',' . $languageFieldName,
|
|
'',
|
|
$useDeleteClause,
|
|
);
|
|
}
|
|
if (!is_array($checkRec)) {
|
|
return null;
|
|
}
|
|
if ((int)($checkRec['t3ver_oid'] ?? 0) > 0) {
|
|
$id = (int)$checkRec['t3ver_oid'];
|
|
}
|
|
// if current rec is a translation then get uid from l10n_parent instead
|
|
// because web mounts point to pages in default language and rootline returns uids of default languages
|
|
if ((int)($checkRec[$languageFieldName]) !== 0 && (int)($checkRec[$transOrigPointerFieldName]) !== 0) {
|
|
$id = (int)$checkRec[$transOrigPointerFieldName];
|
|
}
|
|
if (!$readPerms) {
|
|
$readPerms = $this->getPagePermsClause(Permission::PAGE_SHOW);
|
|
}
|
|
if ($id > 0) {
|
|
$wM = $this->getWebmounts();
|
|
$rL = BackendUtility::BEgetRootLine($id, ' AND ' . $readPerms, true, [], $useDeleteClause);
|
|
foreach ($rL as $v) {
|
|
if ($v['uid'] && in_array($v['uid'], $wM)) {
|
|
return $v['uid'];
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Checks if the user is in the valid list of allowed system maintainers. if the list is not set,
|
|
* then all admins are system maintainers. If the list is empty, no one is system maintainer (good for production
|
|
* systems). If the currently logged in user is in "switch user" mode, this method will return false.
|
|
*
|
|
* @param bool $pure Whether to apply pure behavior (ignore development & skip fallback for empty setting)
|
|
*/
|
|
public function isSystemMaintainer(bool $pure = false): bool
|
|
{
|
|
if (!$this->isAdmin()) {
|
|
return false;
|
|
}
|
|
|
|
if (!$pure && $GLOBALS['BE_USER']->getOriginalUserIdWhenInSwitchUserMode()) {
|
|
return false;
|
|
}
|
|
if (!$pure && Environment::getContext()->isDevelopment()) {
|
|
return true;
|
|
}
|
|
$systemMaintainers = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? [];
|
|
$systemMaintainers = array_map(intval(...), $systemMaintainers);
|
|
if (!empty($systemMaintainers)) {
|
|
return in_array((int)$this->user['uid'], $systemMaintainers, true);
|
|
}
|
|
// No system maintainers set up yet, so any admin is allowed to access the modules
|
|
// but explicitly no system maintainers allowed (empty string in TYPO3_CONF_VARS).
|
|
// @todo: this needs to be adjusted once system maintainers can log into the install tool with their credentials
|
|
if (!$pure && !isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'])) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public function getRole(): PrincipalRole
|
|
{
|
|
if ($this->isSystemMaintainer()) {
|
|
return PrincipalRole::MAINTAINER;
|
|
}
|
|
if ($this->isAdmin()) {
|
|
return PrincipalRole::ADMIN;
|
|
}
|
|
return PrincipalRole::USER;
|
|
}
|
|
|
|
/**
|
|
* Returns a WHERE-clause for the pages-table where user permissions according to input argument, $perms, is validated.
|
|
* $perms is the "mask" used to select. Fx. if $perms is 1 then you'll get all pages that a user can actually see!
|
|
* 2^0 = show (1)
|
|
* 2^1 = edit (2)
|
|
* 2^2 = delete (4)
|
|
* 2^3 = new (8)
|
|
* If the user is 'admin' " 1=1" is returned (no effect)
|
|
* If the user is not set at all (->user is not an array), then " 1=0" is returned (will cause no selection results at all)
|
|
* The 95% use of this function is "->getPagePermsClause(1)" which will
|
|
* return WHERE clauses for *selecting* pages in backend listings - in other words this will check read permissions.
|
|
*
|
|
* @param int $perms Permission mask to use, see function description
|
|
* @return string Part of where clause. Prefix " AND " to this.
|
|
* @internal should only be used from within TYPO3 Core, use PagePermissionDatabaseRestriction instead.
|
|
*/
|
|
public function getPagePermsClause($perms)
|
|
{
|
|
if (is_array($this->user)) {
|
|
if ($this->isAdmin()) {
|
|
return ' 1=1';
|
|
}
|
|
// Make sure it's integer.
|
|
$perms = (int)$perms;
|
|
$expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
|
->getQueryBuilderForTable('pages')
|
|
->expr();
|
|
|
|
// User
|
|
$constraint = $expressionBuilder->or(
|
|
$expressionBuilder->comparison(
|
|
$expressionBuilder->bitAnd('pages.perms_everybody', $perms),
|
|
ExpressionBuilder::EQ,
|
|
$perms
|
|
),
|
|
$expressionBuilder->and(
|
|
$expressionBuilder->eq('pages.perms_userid', (int)$this->user['uid']),
|
|
$expressionBuilder->comparison(
|
|
$expressionBuilder->bitAnd('pages.perms_user', $perms),
|
|
ExpressionBuilder::EQ,
|
|
$perms
|
|
)
|
|
)
|
|
);
|
|
|
|
// Group (if any is set)
|
|
if (!empty($this->userGroupsUID)) {
|
|
$constraint = $constraint->with(
|
|
$expressionBuilder->and(
|
|
$expressionBuilder->in(
|
|
'pages.perms_groupid',
|
|
$this->userGroupsUID
|
|
),
|
|
$expressionBuilder->comparison(
|
|
$expressionBuilder->bitAnd('pages.perms_group', $perms),
|
|
ExpressionBuilder::EQ,
|
|
$perms
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
$constraint = ' (' . (string)$constraint . ')';
|
|
|
|
// ****************
|
|
// getPagePermsClause-HOOK
|
|
// ****************
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getPagePermsClause'] ?? [] as $_funcRef) {
|
|
$_params = ['currentClause' => $constraint, 'perms' => $perms];
|
|
$constraint = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
|
|
}
|
|
return $constraint;
|
|
}
|
|
return ' 1=0';
|
|
}
|
|
|
|
/**
|
|
* Returns a combined binary representation of the current users permissions for the page-record, $row.
|
|
* The perms for user, group and everybody is OR'ed together (provided that the page-owner is the user
|
|
* and for the groups that the user is a member of the group.
|
|
* If the user is admin, 31 is returned (full permissions for all five flags)
|
|
*
|
|
* @param array $row Input page row with all perms_* fields available.
|
|
* @param bool $useDeleteClause Use the deleteClause to check if a record is deleted (default TRUE)
|
|
* @return int Bitwise representation of the users permissions in relation to input page row, $row
|
|
*/
|
|
public function calcPerms($row, bool $useDeleteClause = true)
|
|
{
|
|
// Return 31 for admin users.
|
|
if ($this->isAdmin()) {
|
|
return Permission::ALL;
|
|
}
|
|
// Return 0 if page is not within the allowed web mount
|
|
if (!$this->isInWebMount($row, '', $useDeleteClause)) {
|
|
return Permission::NOTHING;
|
|
}
|
|
$out = Permission::NOTHING;
|
|
if (
|
|
isset($row['perms_userid']) && isset($row['perms_user']) && isset($row['perms_groupid'])
|
|
&& isset($row['perms_group']) && isset($row['perms_everybody']) && !empty($this->userGroupsUID)
|
|
) {
|
|
if ($this->user['uid'] == $row['perms_userid']) {
|
|
$out |= $row['perms_user'];
|
|
}
|
|
if ($this->isMemberOfGroup((int)$row['perms_groupid'])) {
|
|
$out |= $row['perms_group'];
|
|
}
|
|
$out |= $row['perms_everybody'];
|
|
}
|
|
// ****************
|
|
// CALCPERMS hook
|
|
// ****************
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['calcPerms'] ?? [] as $_funcRef) {
|
|
$_params = [
|
|
'row' => $row,
|
|
'outputPermissions' => $out,
|
|
];
|
|
$out = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* Returns TRUE if the $value is found in the list in a $this->groupData[] index pointed to by $type (array key).
|
|
* Can thus be users to check for modules, exclude-fields, select/modify permissions for tables etc.
|
|
* If user is admin TRUE is also returned
|
|
*
|
|
* @param string $type The type value; "webmounts", "filemounts", "pagetypes_select", "tables_select", "tables_modify", "non_exclude_fields", "modules", "available_widgets", "mfa_providers"
|
|
* @param string|int $value String to search for in the groupData-list, can also be an integer for "pagetypes_select" or "allowed_languages")
|
|
* @return bool TRUE if permission is granted (that is, the value was found in the groupData list - or the BE_USER is "admin")
|
|
*/
|
|
public function check($type, $value)
|
|
{
|
|
return isset($this->groupData[$type])
|
|
&& ($this->isAdmin() || GeneralUtility::inList($this->groupData[$type], (string)$value));
|
|
}
|
|
|
|
/**
|
|
* Checking the authMode of a select field with authMode set
|
|
*
|
|
* @param string $table Table name
|
|
* @param string $field Field name (must be configured in TCA and of type "select" with authMode set!)
|
|
* @param string $value Value to evaluation (single value, must not contain any of the chars ":,|")
|
|
* @return bool Whether access is granted or not
|
|
*/
|
|
public function checkAuthMode($table, $field, $value)
|
|
{
|
|
// Admin users can do anything:
|
|
if ($this->isAdmin()) {
|
|
return true;
|
|
}
|
|
// Allow all blank values:
|
|
if ((string)$value === '') {
|
|
return true;
|
|
}
|
|
// Allow dividers:
|
|
if ($value === '--div--') {
|
|
return true;
|
|
}
|
|
// Certain characters are not allowed in the value
|
|
if (preg_match('/[:|,]/', $value)) {
|
|
return false;
|
|
}
|
|
// Initialize:
|
|
$testValue = $table . ':' . $field . ':' . $value;
|
|
$out = true;
|
|
if (!GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue)) {
|
|
$out = false;
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* Checking if a language value (-1, 0 and >0) is allowed to be edited by the user.
|
|
*
|
|
* @param int|SiteLanguage|string $langValue Language value to evaluate
|
|
* @return bool Returns TRUE if the language value is allowed, otherwise FALSE.
|
|
*/
|
|
public function checkLanguageAccess($langValue)
|
|
{
|
|
// The users language list must be non-blank - otherwise all languages are allowed.
|
|
if (trim($this->groupData['allowed_languages']) === '') {
|
|
return true;
|
|
}
|
|
if ($langValue instanceof SiteLanguage) {
|
|
$langValue = $langValue->getLanguageId();
|
|
} else {
|
|
$langValue = (int)$langValue;
|
|
}
|
|
// Language must either be explicitly allowed OR the lang Value be "-1" (all languages)
|
|
if ($langValue !== -1 && !$this->check('allowed_languages', (string)$langValue)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check if user has access to all existing localizations for a certain record
|
|
*
|
|
* @param string|TcaSchema $table The table/schema
|
|
* @param array $record The current record
|
|
* @return bool
|
|
*/
|
|
public function checkFullLanguagesAccess(string|TcaSchema $table, array $record): bool
|
|
{
|
|
if (!$this->checkLanguageAccess(0)) {
|
|
return false;
|
|
}
|
|
if ($table instanceof TcaSchema) {
|
|
$schema = $table;
|
|
$table = $table->getName();
|
|
} else {
|
|
$schema = GeneralUtility::makeInstance(TcaSchemaFactory::class)->get($table);
|
|
}
|
|
|
|
if ($schema->isLanguageAware()) {
|
|
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
|
|
$languageField = $languageCapability->getLanguageField()->getName();
|
|
$pointerField = $languageCapability->getTranslationOriginPointerField()->getName();
|
|
$pointerValue = $record[$pointerField] > 0 ? $record[$pointerField] : $record['uid'];
|
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
|
|
$queryBuilder->getRestrictions()
|
|
->removeAll()
|
|
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
|
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->workspace));
|
|
$recordLocalizations = $queryBuilder->select('*')
|
|
->from($table)
|
|
->where(
|
|
$queryBuilder->expr()->eq(
|
|
$pointerField,
|
|
$queryBuilder->createNamedParameter($pointerValue, Connection::PARAM_INT)
|
|
)
|
|
)
|
|
->executeQuery()
|
|
->fetchAllAssociative();
|
|
|
|
foreach ($recordLocalizations as $recordLocalization) {
|
|
if (!$this->checkLanguageAccess($recordLocalization[$languageField])) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check if a user has editing access to a record from a $GLOBALS['TCA'] table.
|
|
* Returns a result object with access status and error message.
|
|
*
|
|
* The checks do not take page permissions and other "environmental" things into account.
|
|
* It only deals with record internals; If any values in the record fields disallows it.
|
|
* For instance languages settings, authMode selector boxes are evaluated (and maybe more in the future).
|
|
* It will check for workspace-dependent access.
|
|
*
|
|
* @param string $table Table name
|
|
* @param array|RecordInterface $row Full record row
|
|
* @param bool $newRecord Set, if testing a new (non-existing) record array. Will disable certain checks that doesn't make much sense in that context.
|
|
* @param bool $checkFullLanguageAccess Set, whenever access to all translations of the record is required
|
|
* @return AccessCheckResult Result object with access decision and error message
|
|
* @internal should only be used from within TYPO3 Core
|
|
*/
|
|
public function checkRecordEditAccess(
|
|
string $table,
|
|
array|RecordInterface $row,
|
|
bool $newRecord = false,
|
|
bool $checkFullLanguageAccess = false
|
|
): AccessCheckResult {
|
|
$schemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class);
|
|
if (!$schemaFactory->has($table)) {
|
|
return new AccessCheckResult(false);
|
|
}
|
|
if ($row instanceof RecordInterface) {
|
|
$row = $row->getRawRecord()->toArray();
|
|
}
|
|
$schema = $schemaFactory->get($table);
|
|
// Always return TRUE for Admin users.
|
|
if ($this->isAdmin()) {
|
|
return new AccessCheckResult(true);
|
|
}
|
|
// Checking languages:
|
|
if ($table === 'pages' && $checkFullLanguageAccess && !$this->checkFullLanguagesAccess($schema, $row)) {
|
|
return new AccessCheckResult(false);
|
|
}
|
|
if ($schema->isLanguageAware()) {
|
|
$languageCapability = $schema->getCapability(TcaSchemaCapability::Language);
|
|
$languageField = $languageCapability->getLanguageField()->getName();
|
|
|
|
// Language field must be found in input row - otherwise it does not make sense.
|
|
if (isset($row[$languageField])) {
|
|
if (!$this->checkLanguageAccess($row[$languageField])) {
|
|
return new AccessCheckResult(false, 'ERROR: Language was not allowed.');
|
|
}
|
|
if (
|
|
$checkFullLanguageAccess && $row[$languageField] == 0
|
|
&& !$this->checkFullLanguagesAccess($table, $row)
|
|
) {
|
|
return new AccessCheckResult(false, 'ERROR: Related/affected language was not allowed.');
|
|
}
|
|
} else {
|
|
return new AccessCheckResult(false, 'ERROR: The "languageField" field named "' . $languageField . '" was not found in testing record!');
|
|
}
|
|
}
|
|
// Checking authMode fields:
|
|
foreach ($schema->getFields() as $fieldName => $fieldType) {
|
|
if (isset($row[$fieldName])
|
|
&& $fieldType->isType(TableColumnType::SELECT)
|
|
&& ($fieldType->getConfiguration()['authMode'] ?? false)
|
|
&& !$this->checkAuthMode($table, $fieldName, $row[$fieldName])) {
|
|
return new AccessCheckResult(
|
|
false,
|
|
'ERROR: authMode "' . $fieldType->getConfiguration()['authMode']
|
|
. '" failed for field "' . $fieldName . '" with value "'
|
|
. $row[$fieldName] . '" evaluated'
|
|
);
|
|
}
|
|
}
|
|
// Checking "editlock" feature (doesn't apply to new records)
|
|
if (!$newRecord && $schema->hasCapability(TcaSchemaCapability::EditLock)) {
|
|
$editLockFieldName = $schema->getCapability(TcaSchemaCapability::EditLock)->getFieldName();
|
|
if (isset($row[$editLockFieldName])) {
|
|
if ($row[$editLockFieldName]) {
|
|
return new AccessCheckResult(false, 'ERROR: Record was locked for editing. Only admin users can change this state.');
|
|
}
|
|
} else {
|
|
return new AccessCheckResult(false, 'ERROR: The "editLock" field named "' . $editLockFieldName
|
|
. '" was not found in testing record!');
|
|
}
|
|
}
|
|
// Checking record permissions
|
|
// THIS is where we can include a check for "perms_" fields for other records than pages...
|
|
// Process any hooks
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['recordEditAccessInternals'] ?? [] as $funcRef) {
|
|
$params = [
|
|
'table' => $table,
|
|
'idOrRow' => $row,
|
|
'newRecord' => $newRecord,
|
|
];
|
|
if (!GeneralUtility::callUserFunction($funcRef, $params, $this)) {
|
|
return new AccessCheckResult(false);
|
|
}
|
|
}
|
|
// Finally, return TRUE if all is well.
|
|
return new AccessCheckResult(true);
|
|
}
|
|
|
|
/**
|
|
* Returns TRUE if the BE_USER is allowed to *create* shortcuts in the backend modules
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function mayMakeShortcut()
|
|
{
|
|
return ($this->getTSConfig()['options.']['enableBookmarks'] ?? false)
|
|
&& !($this->getTSConfig()['options.']['mayNotCreateEditBookmarks'] ?? false);
|
|
}
|
|
|
|
/**
|
|
* Checks if a record is allowed to be edited in the current workspace.
|
|
* This is not bound to an actual record, but to the mere fact if the user is in a workspace
|
|
* and depending on the table settings.
|
|
*
|
|
* @internal should only be used from within TYPO3 Core
|
|
*/
|
|
public function workspaceAllowsLiveEditingInTable(string $table): bool
|
|
{
|
|
// In live workspace the record can be added/modified
|
|
if ($this->workspace === 0) {
|
|
return true;
|
|
}
|
|
// Workspace setting allows to "live edit" records of tables without versioning
|
|
if (($this->workspaceRec['live_edit'] ?? false)
|
|
&& !$this->getTcaSchema($table)?->isWorkspaceAware()
|
|
) {
|
|
return true;
|
|
}
|
|
// Always for Live workspace, AND if live-edit is enabled
|
|
// and tables are completely without versioning it is ok as well.
|
|
if ($this->getTcaSchema($table)?->getRawConfiguration()['versioningWS_alwaysAllowLiveEdit'] ?? false) {
|
|
return true;
|
|
}
|
|
// If the answer is FALSE it means the only valid way to create or edit records by creating records in the workspace
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Evaluates if a record from $table can be created. If the table is not set up for versioning,
|
|
* and the "live edit" flag of the page is set, return false. In live workspace this is always true,
|
|
* as all records can be created in live workspace
|
|
*
|
|
* @param string $table Table name
|
|
* @internal should only be used from within TYPO3 Core
|
|
*/
|
|
public function workspaceCanCreateNewRecord(string $table): bool
|
|
{
|
|
// If LIVE records cannot be created due to workspace restrictions, prepare creation of placeholder-record
|
|
if (!$this->workspaceAllowsLiveEditingInTable($table) && !$this->getTcaSchema($table)?->isWorkspaceAware()) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks if an element stage allows access for the user in the current workspace
|
|
* In live workspace (= 0) access is always granted for any stage.
|
|
* Admins are always allowed.
|
|
* An option for custom workspaces allows members to also edit when the stage is "Review"
|
|
*
|
|
* @param int $stage Stage id from an element: -1,0 = editing, 1 = reviewer, >1 = owner
|
|
* @return bool TRUE if user is allowed access
|
|
* @internal should only be used from within TYPO3 Core
|
|
*/
|
|
public function workspaceCheckStageForCurrent($stage): bool
|
|
{
|
|
// Always allow for admins
|
|
if ($this->isAdmin()) {
|
|
return true;
|
|
}
|
|
// Always OK for live workspace
|
|
if ($this->workspace === 0 || $this->getTcaSchema('sys_workspace') === null) {
|
|
return true;
|
|
}
|
|
$stage = (int)$stage;
|
|
$stat = $this->checkWorkspaceCurrent();
|
|
$accessType = $stat['_ACCESS'];
|
|
// Workspace owners are always allowed for stage change
|
|
if ($accessType === 'owner') {
|
|
return true;
|
|
}
|
|
|
|
// Check if custom staging is activated
|
|
$workspaceRec = BackendUtility::getRecord('sys_workspace', $stat['uid']);
|
|
if ($workspaceRec['custom_stages'] > 0 && $stage !== 0 && $stage !== -10) {
|
|
// Get custom stage record
|
|
$workspaceStageRec = BackendUtility::getRecord('sys_workspace_stage', $stage);
|
|
// Check if the user is responsible for the current stage
|
|
if (
|
|
$accessType === 'member'
|
|
&& GeneralUtility::inList($workspaceStageRec['responsible_persons'] ?? '', 'be_users_' . $this->user['uid'])
|
|
) {
|
|
return true;
|
|
}
|
|
// Check if the user is in a group which is responsible for the current stage
|
|
foreach ($this->userGroupsUID as $groupUid) {
|
|
if (
|
|
$accessType === 'member'
|
|
&& GeneralUtility::inList($workspaceStageRec['responsible_persons'] ?? '', 'be_groups_' . $groupUid)
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
} elseif ($stage === -10 || $stage === -20) {
|
|
// Nobody is allowed to do that except the owner (which was checked above)
|
|
return false;
|
|
} elseif (
|
|
$accessType === 'reviewer' && $stage <= 1
|
|
|| $accessType === 'member' && $stage <= 0
|
|
) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns full parsed user TSconfig array, merged with TSconfig from groups.
|
|
*
|
|
* Example:
|
|
* [
|
|
* 'options.' => [
|
|
* 'fooEnabled' => '0',
|
|
* 'fooEnabled.' => [
|
|
* 'tt_content' => 1,
|
|
* ],
|
|
* ],
|
|
* ]
|
|
*
|
|
* @return array Parsed and merged user TSconfig array
|
|
*/
|
|
public function getTSConfig(): array
|
|
{
|
|
return $this->getUserTsConfig()?->getUserTsConfigArray() ?? [];
|
|
}
|
|
|
|
/**
|
|
* Return the full user TSconfig object instead of just the array as in getTSConfig()
|
|
*
|
|
* @internal for now until API stabilized
|
|
*/
|
|
public function getUserTsConfig(): ?UserTsConfig
|
|
{
|
|
return $this->userTsConfig;
|
|
}
|
|
|
|
/**
|
|
* Returns an unique array with the webmounts.
|
|
* If no webmounts, and empty array is returned.
|
|
* Webmounts permissions are checked in fetchGroupData()
|
|
*
|
|
* @return list<int> of web mounts uids (may include 0)
|
|
*/
|
|
public function getWebmounts(): array
|
|
{
|
|
$webMounts = $this->groupData['webmounts'] ?? null;
|
|
return is_string($webMounts) && $webMounts !== ''
|
|
? array_unique(GeneralUtility::intExplode(',', $webMounts))
|
|
: [];
|
|
}
|
|
|
|
/**
|
|
* Initializes the given mount points for the current Backend user.
|
|
*
|
|
* @param array $mountPointUids Page UIDs that should be used as web mountpoints
|
|
* @param bool $append If TRUE the given mount point will be appended. Otherwise the current mount points will be replaced.
|
|
*/
|
|
public function setWebmounts(array $mountPointUids, $append = false)
|
|
{
|
|
if (empty($mountPointUids)) {
|
|
return;
|
|
}
|
|
if ($append) {
|
|
$currentWebMounts = GeneralUtility::intExplode(',', (string)($this->groupData['webmounts'] ?? ''));
|
|
$mountPointUids = array_merge($currentWebMounts, $mountPointUids);
|
|
}
|
|
$this->groupData['webmounts'] = implode(',', array_unique($mountPointUids));
|
|
}
|
|
|
|
/**
|
|
* Checks for alternative web mount points for the element browser.
|
|
*
|
|
* If there is a temporary mount point active in the page tree it will be used.
|
|
*
|
|
* If the user TSconfig options.pageTree.altElementBrowserMountPoints is not empty the pages configured
|
|
* there are used as web mounts If options.pageTree.altElementBrowserMountPoints.append is enabled,
|
|
* they are appended to the existing webmounts.
|
|
*
|
|
* @internal - do not use in your own extension
|
|
*/
|
|
public function initializeWebmountsForElementBrowser()
|
|
{
|
|
$alternativeWebmountPoint = (int)$this->getSessionData('pageTree_temporaryMountPoint');
|
|
if ($alternativeWebmountPoint) {
|
|
$alternativeWebmountPoint = GeneralUtility::intExplode(',', (string)$alternativeWebmountPoint);
|
|
$this->setWebmounts($alternativeWebmountPoint);
|
|
return;
|
|
}
|
|
|
|
$alternativeWebmountPoints = trim($this->getTSConfig()['options.']['pageTree.']['altElementBrowserMountPoints'] ?? '');
|
|
$appendAlternativeWebmountPoints = $this->getTSConfig()['options.']['pageTree.']['altElementBrowserMountPoints.']['append'] ?? '';
|
|
if ($alternativeWebmountPoints) {
|
|
$alternativeWebmountPoints = GeneralUtility::intExplode(',', $alternativeWebmountPoints);
|
|
$this->setWebmounts($alternativeWebmountPoints, $appendAlternativeWebmountPoints);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns TRUE or FALSE, depending if an alert popup (a javascript confirmation) should be shown
|
|
* call like $GLOBALS['BE_USER']->jsConfirmation($BITMASK).
|
|
*
|
|
* @param int $bitmask Bitmask, one of \TYPO3\CMS\Core\Authentication\JsConfirmation
|
|
* @return bool TRUE if the confirmation should be shown
|
|
* @see JsConfirmation
|
|
*/
|
|
public function jsConfirmation(int $bitmask): bool
|
|
{
|
|
$alertPopupsSetting = trim((string)($this->getTSConfig()['options.']['alertPopups'] ?? ''));
|
|
$alertPopupsSetting = MathUtility::canBeInterpretedAsInteger($alertPopupsSetting)
|
|
? MathUtility::forceIntegerInRange((int)$alertPopupsSetting, 0, JsConfirmation::ALL)
|
|
: JsConfirmation::ALL;
|
|
|
|
return (new JsConfirmation($alertPopupsSetting))->get($bitmask);
|
|
}
|
|
|
|
/**
|
|
* Initializes a lot of stuff like the access-lists, database-mountpoints and filemountpoints
|
|
* This method is called by ->backendCheckLogin() (from extending BackendUserAuthentication)
|
|
* if the backend user login has verified OK.
|
|
* Generally this is required initialization of a backend user.
|
|
*
|
|
* @internal
|
|
*/
|
|
public function fetchGroupData()
|
|
{
|
|
if ($this->user['uid']) {
|
|
// Get lists for the be_user record and set them as default/primary values.
|
|
// Enabled Backend Modules
|
|
$this->groupData['modules'] = $this->user['userMods'] ?? '';
|
|
// Add available widgets
|
|
$this->groupData['available_widgets'] = $this->user['available_widgets'] ?? '';
|
|
// Add allowed mfa providers
|
|
$this->groupData['mfa_providers'] = $this->user['mfa_providers'] ?? '';
|
|
// Add Allowed Languages
|
|
$this->groupData['allowed_languages'] = $this->user['allowed_languages'] ?? '';
|
|
// Set user value for workspace permissions.
|
|
$this->groupData['workspace_perms'] = $this->user['workspace_perms'] ?? 0;
|
|
// Database mountpoints
|
|
$this->groupData['webmounts'] = $this->user['db_mountpoints'] ?? '';
|
|
// File mountpoints
|
|
$this->groupData['filemounts'] = $this->user['file_mountpoints'] ?? '';
|
|
// Fileoperation permissions
|
|
$this->groupData['file_permissions'] = $this->user['file_permissions'] ?? '';
|
|
// Category mounts
|
|
$this->groupData['category_perms'] = $this->user['category_perms'] ?? '';
|
|
|
|
// Get the groups and accumulate their permission settings
|
|
$mountOptions = new BackendGroupMountOption((int)($this->user['options'] ?? 0));
|
|
$groupResolver = GeneralUtility::makeInstance(GroupResolver::class);
|
|
$resolvedGroups = $groupResolver->resolveGroupsForUser($this->user, $this->usergroup_table);
|
|
foreach ($resolvedGroups as $groupInfo) {
|
|
$groupInfo += [
|
|
'uid' => 0,
|
|
'db_mountpoints' => '',
|
|
'file_mountpoints' => '',
|
|
'groupMods' => '',
|
|
'availableWidgets' => '',
|
|
'mfa_providers' => '',
|
|
'tables_select' => '',
|
|
'tables_modify' => '',
|
|
'pagetypes_select' => '',
|
|
'non_exclude_fields' => '',
|
|
'explicit_allowdeny' => '',
|
|
'allowed_languages' => '',
|
|
'custom_options' => '',
|
|
'file_permissions' => '',
|
|
'category_perms' => '',
|
|
'workspace_perms' => 0, // Bitflag.
|
|
];
|
|
// Add the group uid to internal arrays.
|
|
$this->userGroupsUID[] = (int)$groupInfo['uid'];
|
|
$this->userGroups[(int)$groupInfo['uid']] = $groupInfo;
|
|
// Mount group database-mounts
|
|
if ($mountOptions->shouldUserIncludePageMountsFromAssociatedGroups()) {
|
|
$this->groupData['webmounts'] .= ',' . $groupInfo['db_mountpoints'];
|
|
}
|
|
// Mount group file-mounts
|
|
if ($mountOptions->shouldUserIncludeFileMountsFromAssociatedGroups()) {
|
|
$this->groupData['filemounts'] .= ',' . $groupInfo['file_mountpoints'];
|
|
}
|
|
// Gather permission detail fields
|
|
$this->groupData['modules'] .= ',' . $groupInfo['groupMods'];
|
|
$this->groupData['available_widgets'] .= ',' . $groupInfo['availableWidgets'];
|
|
$this->groupData['mfa_providers'] .= ',' . $groupInfo['mfa_providers'];
|
|
$this->groupData['tables_select'] .= ',' . $groupInfo['tables_select'];
|
|
$this->groupData['tables_modify'] .= ',' . $groupInfo['tables_modify'];
|
|
$this->groupData['pagetypes_select'] .= ',' . $groupInfo['pagetypes_select'];
|
|
$this->groupData['non_exclude_fields'] .= ',' . $groupInfo['non_exclude_fields'];
|
|
$this->groupData['explicit_allowdeny'] .= ',' . $groupInfo['explicit_allowdeny'];
|
|
$this->groupData['allowed_languages'] .= ',' . $groupInfo['allowed_languages'];
|
|
$this->groupData['custom_options'] .= ',' . $groupInfo['custom_options'];
|
|
$this->groupData['file_permissions'] .= ',' . $groupInfo['file_permissions'];
|
|
$this->groupData['category_perms'] .= ',' . $groupInfo['category_perms'];
|
|
// Setting workspace permissions:
|
|
$this->groupData['workspace_perms'] |= $groupInfo['workspace_perms'];
|
|
if (!$this->firstMainGroup) {
|
|
$this->firstMainGroup = (int)$groupInfo['uid'];
|
|
}
|
|
}
|
|
|
|
// Populating the $this->userGroupsUID -array with the groups in the order in which they were LAST included.
|
|
// Finally, this is the list of group_uid's in the order they are parsed (including subgroups)
|
|
// and without duplicates (duplicates are presented with their last entrance in the list,
|
|
// which thus reflects the order of the TypoScript in TSconfig)
|
|
$this->userGroupsUID = array_reverse(array_unique(array_reverse($this->userGroupsUID)));
|
|
|
|
$this->prepareUserTsConfig();
|
|
|
|
// Processing webmounts
|
|
// Admin's always have the root mounted
|
|
if ($this->isAdmin() && !($this->getTSConfig()['options.']['dontMountAdminMounts'] ?? false)) {
|
|
$this->groupData['webmounts'] = '0,' . $this->groupData['webmounts'];
|
|
}
|
|
// The lists are cleaned for duplicates
|
|
$this->groupData['webmounts'] = StringUtility::uniqueList($this->groupData['webmounts'] ?? '');
|
|
$this->groupData['filemounts'] = StringUtility::uniqueList($this->groupData['filemounts'] ?? '');
|
|
$this->groupData['pagetypes_select'] = StringUtility::uniqueList($this->groupData['pagetypes_select'] ?? '');
|
|
$this->groupData['tables_select'] = StringUtility::uniqueList(($this->groupData['tables_modify'] ?? '') . ',' . ($this->groupData['tables_select'] ?? ''));
|
|
$this->groupData['tables_modify'] = StringUtility::uniqueList($this->groupData['tables_modify'] ?? '');
|
|
$this->groupData['non_exclude_fields'] = StringUtility::uniqueList($this->groupData['non_exclude_fields'] ?? '');
|
|
$this->groupData['explicit_allowdeny'] = StringUtility::uniqueList($this->groupData['explicit_allowdeny'] ?? '');
|
|
$this->groupData['allowed_languages'] = StringUtility::uniqueList($this->groupData['allowed_languages'] ?? '');
|
|
$this->groupData['custom_options'] = StringUtility::uniqueList($this->groupData['custom_options'] ?? '');
|
|
$this->groupData['modules'] = StringUtility::uniqueList($this->groupData['modules'] ?? '');
|
|
$this->groupData['available_widgets'] = StringUtility::uniqueList($this->groupData['available_widgets'] ?? '');
|
|
$this->groupData['mfa_providers'] = StringUtility::uniqueList($this->groupData['mfa_providers'] ?? '');
|
|
$this->groupData['file_permissions'] = StringUtility::uniqueList($this->groupData['file_permissions'] ?? '');
|
|
$this->groupData['category_perms'] = StringUtility::uniqueList($this->groupData['category_perms'] ?? '');
|
|
|
|
// Check if the user access to all web mounts set
|
|
if (!empty(trim($this->groupData['webmounts']))) {
|
|
$validWebMounts = $this->filterValidWebMounts($this->groupData['webmounts']);
|
|
$this->groupData['webmounts'] = implode(',', $validWebMounts);
|
|
}
|
|
// Setting up workspace situation (after webmounts are processed!):
|
|
$this->workspaceInit();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checking read access to web mounts, but keeps "0" or empty strings.
|
|
* In any case, checks if the list of pages is visible for the backend user but also
|
|
* if the page is not deleted.
|
|
*
|
|
* @param string $listOfWebMounts a comma-separated list of webmounts, could also be empty, or contain "0"
|
|
* @return array a list of all valid web mounts the user has access to
|
|
*/
|
|
protected function filterValidWebMounts(string $listOfWebMounts): array
|
|
{
|
|
// Checking read access to web mounts if there are mounts points (not empty string, false or 0)
|
|
$allWebMounts = explode(',', $listOfWebMounts);
|
|
// Selecting all web mounts with permission clause for reading
|
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
|
|
$queryBuilder->getRestrictions()
|
|
->removeAll()
|
|
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
|
|
|
$readablePagesOfWebMounts = $queryBuilder->select('uid')
|
|
->from('pages')
|
|
// @todo DOCTRINE: check how to make getPagePermsClause() portable
|
|
->where(
|
|
$this->getPagePermsClause(Permission::PAGE_SHOW),
|
|
$queryBuilder->expr()->in(
|
|
'uid',
|
|
$queryBuilder->createNamedParameter(
|
|
GeneralUtility::intExplode(',', $listOfWebMounts),
|
|
Connection::PARAM_INT_ARRAY
|
|
)
|
|
)
|
|
)
|
|
->executeQuery()
|
|
->fetchAllAssociative();
|
|
$readablePagesOfWebMounts = array_column(($readablePagesOfWebMounts ?: []), 'uid', 'uid');
|
|
foreach ($allWebMounts as $key => $mountPointUid) {
|
|
// If the mount ID is NOT found among selected pages, unset it:
|
|
if ($mountPointUid > 0 && !isset($readablePagesOfWebMounts[$mountPointUid])) {
|
|
unset($allWebMounts[$key]);
|
|
}
|
|
}
|
|
return $allWebMounts;
|
|
}
|
|
|
|
/**
|
|
* Parse user TSconfig from current user and its groups and set it as $this->userTS.
|
|
*/
|
|
protected function prepareUserTsConfig(): void
|
|
{
|
|
$tsConfigFactory = GeneralUtility::makeInstance(UserTsConfigFactory::class);
|
|
$this->userTsConfig = $tsConfigFactory->create($this);
|
|
if (!empty($this->getUserTsConfig()->getUserTsConfigArray()['setup.']['override.'])) {
|
|
// @todo: This logic is ugly. user TSconfig "setup.override." is used to force options
|
|
// in the user settings module, along with "setup.fields." and "setup.default.".
|
|
// See the docs about this.
|
|
// The fun part is, this is merged into user UC. As such, whenever these setup
|
|
// options are used, user UC has to be updated. The toggle below triggers this
|
|
// and initiates an update query of this users UC.
|
|
// Before v12, this was only triggered if user TSconfig could not be fetched from
|
|
// cache, but this was flawed, too: When two users had the same user TSconfig, UC
|
|
// of one user would be updated, but not UC of the other user if caches were not
|
|
// cleared in between their two calls.
|
|
// This toggle and UC overriding should vanish altogether: It would be better if
|
|
// user TSconfig no longer overlays UC, instead the settings / setup module
|
|
// controller should look at user TSconfig "setup." on the fly when rendering, and
|
|
// consumers that access user setup / settings values should get values overloaded
|
|
// on the fly as well using some helper or a late init logic or similar.
|
|
$this->userTSUpdated = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets up all file storages for a user.
|
|
* Needs to be called AFTER the groups have been loaded.
|
|
*/
|
|
protected function initializeFileStorages()
|
|
{
|
|
$this->fileStorages = [];
|
|
$storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
|
|
// Admin users have all file storages visible, without any filters
|
|
if ($this->isAdmin()) {
|
|
$storageObjects = $storageRepository->findAll();
|
|
foreach ($storageObjects as $storageObject) {
|
|
$this->fileStorages[$storageObject->getUid()] = $storageObject;
|
|
}
|
|
} else {
|
|
// Regular users only have storages that are defined in their file mounts
|
|
// Permissions and file mounts for the storage are added in StoragePermissionAspect
|
|
foreach ($this->getFileMountRecords() as $row) {
|
|
if (!str_contains($row['identifier'] ?? '', ':')) {
|
|
// Skip record since the file mount identifier is invalid, this usually happens
|
|
// when file storages are selected. file mounts and groupHomePath and userHomePath should go through
|
|
continue;
|
|
}
|
|
[$base] = GeneralUtility::trimExplode(':', $row['identifier'], true);
|
|
$base = (int)$base;
|
|
if (!array_key_exists($base, $this->fileStorages)) {
|
|
$storageObject = $storageRepository->findByUid($base);
|
|
if ($storageObject) {
|
|
$this->fileStorages[$storageObject->getUid()] = $storageObject;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// This has to be called always in order to set certain filters
|
|
$this->evaluateUserSpecificFileFilterSettings();
|
|
}
|
|
|
|
/**
|
|
* Returns an array of category mount points. The category permissions from BE Groups
|
|
* are also taken into consideration and are merged into User permissions.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getCategoryMountPoints()
|
|
{
|
|
$categoryMountPoints = '';
|
|
|
|
// Category mounts of the groups
|
|
foreach ($this->userGroups as $group) {
|
|
if ($group['category_perms']) {
|
|
$categoryMountPoints .= ',' . $group['category_perms'];
|
|
}
|
|
}
|
|
|
|
// Category mounts of the user record
|
|
if ($this->user['category_perms']) {
|
|
$categoryMountPoints .= ',' . $this->user['category_perms'];
|
|
}
|
|
|
|
// Make the ids unique
|
|
$categoryMountPoints = GeneralUtility::trimExplode(',', $categoryMountPoints);
|
|
$categoryMountPoints = array_filter($categoryMountPoints); // remove empty value
|
|
$categoryMountPoints = array_unique($categoryMountPoints); // remove unique value
|
|
|
|
return $categoryMountPoints;
|
|
}
|
|
|
|
/**
|
|
* Returns an array of file mount records, taking workspaces and user home and group home directories into account
|
|
* Needs to be called AFTER the groups have been loaded.
|
|
*
|
|
* @return array
|
|
* @internal
|
|
*/
|
|
public function getFileMountRecords()
|
|
{
|
|
$runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
|
|
$fileMountRecordCache = $runtimeCache->get('backendUserAuthenticationFileMountRecords') ?: [];
|
|
|
|
if (!empty($fileMountRecordCache)) {
|
|
return $fileMountRecordCache;
|
|
}
|
|
|
|
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
|
|
|
|
// Processing file mounts (both from the user and the groups)
|
|
$fileMounts = array_unique(GeneralUtility::intExplode(',', (string)($this->groupData['filemounts'] ?? ''), true));
|
|
|
|
// Limit file mounts if set in workspace record
|
|
if ($this->workspace > 0 && !empty($this->workspaceRec['file_mountpoints'])) {
|
|
$workspaceFileMounts = GeneralUtility::intExplode(',', (string)$this->workspaceRec['file_mountpoints'], true);
|
|
$fileMounts = array_intersect($fileMounts, $workspaceFileMounts);
|
|
}
|
|
|
|
if ($fileMounts !== []) {
|
|
$schema = $this->getTcaSchema('sys_filemounts');
|
|
$orderBy = $schema->hasCapability(TcaSchemaCapability::DefaultSorting)
|
|
? $schema->getCapability(TcaSchemaCapability::DefaultSorting)->getValue()
|
|
: 'sorting';
|
|
|
|
$queryBuilder = $connectionPool->getQueryBuilderForTable('sys_filemounts');
|
|
$queryBuilder->getRestrictions()
|
|
->removeAll()
|
|
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
|
->add(GeneralUtility::makeInstance(HiddenRestriction::class))
|
|
->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
|
|
|
|
$queryBuilder->select('*')
|
|
->from('sys_filemounts')
|
|
->where(
|
|
$queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($fileMounts, Connection::PARAM_INT_ARRAY))
|
|
);
|
|
|
|
foreach (QueryHelper::parseOrderBy($orderBy) as $fieldAndDirection) {
|
|
$queryBuilder->addOrderBy(...$fieldAndDirection);
|
|
}
|
|
|
|
$fileMountRecords = $queryBuilder->executeQuery()->fetchAllAssociative();
|
|
foreach ($fileMountRecords as $fileMount) {
|
|
$readOnlySuffix = $fileMount['read_only'] ? '-readonly' : '';
|
|
$fileMountRecordCache[$fileMount['identifier'] . $readOnlySuffix] = $fileMount;
|
|
}
|
|
}
|
|
|
|
// Read-only file mounts
|
|
$readOnlyMountPoints = trim($this->getTSConfig()['options.']['folderTree.']['altElementBrowserMountPoints'] ?? '');
|
|
if ($readOnlyMountPoints) {
|
|
// We cannot use the API here but need to fetch the default storage record directly
|
|
// to not instantiate it (which directly applies mount points) before all mount points are resolved!
|
|
$queryBuilder = $connectionPool->getQueryBuilderForTable('sys_file_storage');
|
|
$defaultStorageRow = $queryBuilder->select('uid')
|
|
->from('sys_file_storage')
|
|
->where(
|
|
$queryBuilder->expr()->eq('is_default', $queryBuilder->createNamedParameter(1, Connection::PARAM_INT))
|
|
)
|
|
->setMaxResults(1)
|
|
->executeQuery()
|
|
->fetchAssociative();
|
|
|
|
$readOnlyMountPointArray = GeneralUtility::trimExplode(',', $readOnlyMountPoints);
|
|
foreach ($readOnlyMountPointArray as $readOnlyMountPoint) {
|
|
$readOnlyMountPointConfiguration = GeneralUtility::trimExplode(':', $readOnlyMountPoint);
|
|
if (count($readOnlyMountPointConfiguration) === 2) {
|
|
// A storage is passed in the configuration
|
|
$storageUid = (int)$readOnlyMountPointConfiguration[0];
|
|
$path = $readOnlyMountPointConfiguration[1];
|
|
} else {
|
|
if (empty($defaultStorageRow)) {
|
|
throw new \RuntimeException('Read only mount points have been defined in user TSconfig without specific storage, but a default storage could not be resolved.', 1404472382);
|
|
}
|
|
// Backwards compatibility: If no storage is passed, we use the default storage
|
|
$storageUid = $defaultStorageRow['uid'];
|
|
$path = $readOnlyMountPointConfiguration[0];
|
|
}
|
|
$fileMountRecordCache[$storageUid . $path . '-readonly'] = [
|
|
'base' => $storageUid,
|
|
'identifier' => $storageUid . ':' . $path,
|
|
'title' => $path,
|
|
'path' => $path,
|
|
'read_only' => true,
|
|
];
|
|
}
|
|
}
|
|
|
|
// Personal or Group file mounts are not accessible if file mount list is set in workspace record
|
|
if ($this->workspace <= 0 || empty($this->workspaceRec['file_mountpoints'])) {
|
|
// If userHomePath is set, we attempt to mount it
|
|
if ($GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath'] ?? false) {
|
|
[$userHomeStorageUid, $userHomeFilter] = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath'], 2);
|
|
$userHomeStorageUid = (int)$userHomeStorageUid;
|
|
$userHomeFilter = '/' . ltrim($userHomeFilter, '/');
|
|
if ($userHomeStorageUid > 0) {
|
|
// Try and mount with [uid]_[username]
|
|
$path = $userHomeFilter . $this->user['uid'] . '_' . $this->user['username'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
|
|
$fileMountRecordCache[$userHomeStorageUid . $path] = [
|
|
'base' => $userHomeStorageUid,
|
|
'identifier' => $userHomeStorageUid . ':' . $path,
|
|
'title' => $this->user['username'],
|
|
'path' => $path,
|
|
'read_only' => false,
|
|
'user_mount' => true,
|
|
];
|
|
// Try and mount with only [uid]
|
|
$path = $userHomeFilter . $this->user['uid'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
|
|
$fileMountRecordCache[$userHomeStorageUid . $path] = [
|
|
'base' => $userHomeStorageUid,
|
|
'identifier' => $userHomeStorageUid . ':' . $path,
|
|
'title' => $this->user['username'],
|
|
'path' => $path,
|
|
'read_only' => false,
|
|
'user_mount' => true,
|
|
];
|
|
}
|
|
}
|
|
|
|
// Mount group home-dirs
|
|
$mountOptions = new BackendGroupMountOption((int)($this->user['options'] ?? 0));
|
|
if (($GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'] ?? '') !== '' && $mountOptions->shouldUserIncludeFileMountsFromAssociatedGroups()) {
|
|
// If groupHomePath is set, we attempt to mount it
|
|
[$groupHomeStorageUid, $groupHomeFilter] = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'], 2);
|
|
$groupHomeStorageUid = (int)$groupHomeStorageUid;
|
|
$groupHomeFilter = '/' . ltrim($groupHomeFilter, '/');
|
|
if ($groupHomeStorageUid > 0) {
|
|
foreach ($this->userGroups as $groupData) {
|
|
$path = $groupHomeFilter . $groupData['uid'];
|
|
$fileMountRecordCache[$groupHomeStorageUid . $path] = [
|
|
'base' => $groupHomeStorageUid,
|
|
'identifier' => $groupHomeStorageUid . ':' . $path,
|
|
'title' => $groupData['title'],
|
|
'path' => $path,
|
|
'read_only' => false,
|
|
'user_mount' => true,
|
|
];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$runtimeCache->set('backendUserAuthenticationFileMountRecords', $fileMountRecordCache);
|
|
return $fileMountRecordCache;
|
|
}
|
|
|
|
/**
|
|
* Returns an array with the file mounts for the user.
|
|
* Each file mount is represented with an array of a "name", "path" and "type".
|
|
* If no file mounts an empty array is returned.
|
|
*
|
|
* @return \TYPO3\CMS\Core\Resource\ResourceStorage[]
|
|
*/
|
|
public function getFileStorages()
|
|
{
|
|
// Initializing file mounts after the groups are fetched
|
|
if ($this->fileStorages === null) {
|
|
$this->initializeFileStorages();
|
|
}
|
|
return $this->fileStorages;
|
|
}
|
|
|
|
/**
|
|
* Adds filters based on what the user has set
|
|
* this should be done in this place, and called whenever needed,
|
|
* but only when needed.
|
|
*/
|
|
public function evaluateUserSpecificFileFilterSettings()
|
|
{
|
|
// Add the option for also displaying the non-hidden files
|
|
if ($this->uc['showHiddenFilesAndFolders'] ?? false) {
|
|
FileNameFilter::setShowHiddenFilesAndFolders(true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the information about file permissions.
|
|
* Previously, this was stored in the DB field fileoper_perms now it is file_permissions.
|
|
* Besides it can be handled via user TSconfig
|
|
*
|
|
* permissions.file.default {
|
|
* addFile = 1
|
|
* readFile = 1
|
|
* writeFile = 1
|
|
* copyFile = 1
|
|
* moveFile = 1
|
|
* renameFile = 1
|
|
* deleteFile = 1
|
|
*
|
|
* addFolder = 1
|
|
* readFolder = 1
|
|
* writeFolder = 1
|
|
* copyFolder = 1
|
|
* moveFolder = 1
|
|
* renameFolder = 1
|
|
* deleteFolder = 1
|
|
* recursivedeleteFolder = 1
|
|
* }
|
|
*
|
|
* # overwrite settings for a specific storageObject
|
|
* permissions.file.storage.StorageUid {
|
|
* readFile = 1
|
|
* recursivedeleteFolder = 0
|
|
* }
|
|
*
|
|
* Please note that these permissions only apply, if the storage has the
|
|
* capabilities (browseable, writable), and if the driver allows for writing etc
|
|
*/
|
|
public function getFilePermissions(): array
|
|
{
|
|
if ($this->filePermissions === null) {
|
|
$filePermissions = [
|
|
// File permissions
|
|
'addFile' => false,
|
|
'readFile' => false,
|
|
'writeFile' => false,
|
|
'copyFile' => false,
|
|
'moveFile' => false,
|
|
'renameFile' => false,
|
|
'deleteFile' => false,
|
|
// Folder permissions
|
|
'addFolder' => false,
|
|
'readFolder' => false,
|
|
'writeFolder' => false,
|
|
'copyFolder' => false,
|
|
'moveFolder' => false,
|
|
'renameFolder' => false,
|
|
'deleteFolder' => false,
|
|
'recursivedeleteFolder' => false,
|
|
];
|
|
if ($this->isAdmin()) {
|
|
$filePermissions = array_map(is_bool(...), $filePermissions);
|
|
} else {
|
|
$userGroupRecordPermissions = GeneralUtility::trimExplode(',', $this->groupData['file_permissions'] ?? '', true);
|
|
array_walk(
|
|
$userGroupRecordPermissions,
|
|
static function (string $permission) use (&$filePermissions): void {
|
|
$filePermissions[$permission] = true;
|
|
}
|
|
);
|
|
|
|
// Finally overlay any user TSconfig
|
|
$permissionsTsConfig = $this->getTSConfig()['permissions.']['file.']['default.'] ?? [];
|
|
if (!empty($permissionsTsConfig)) {
|
|
array_walk(
|
|
$permissionsTsConfig,
|
|
static function (string $value, string $permission) use (&$filePermissions): void {
|
|
$filePermissions[$permission] = (bool)$value;
|
|
}
|
|
);
|
|
}
|
|
}
|
|
$this->filePermissions = $filePermissions;
|
|
}
|
|
return $this->filePermissions;
|
|
}
|
|
|
|
/**
|
|
* Initializing workspace settings after all TSconfig has been parsed.
|
|
* Called from within fetchGroupData()
|
|
*
|
|
* @see fetchGroupData()
|
|
*/
|
|
protected function workspaceInit(): void
|
|
{
|
|
// Initializing workspace by evaluating and setting the workspace, possibly updating it in the user record!
|
|
$this->setWorkspace($this->user['workspace_id']);
|
|
// Limiting the Page Tree Entry Points if there any selected in the workspace record
|
|
$this->initializeDbMountpointsInWorkspace();
|
|
$allowed_languages = (string)($this->getTSConfig()['options.']['workspaces.']['allowed_languages.'][$this->workspace] ?? '');
|
|
if ($allowed_languages !== '') {
|
|
$this->groupData['allowed_languages'] = StringUtility::uniqueList($allowed_languages);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Limiting the Page Tree Entry Points if there are any selected in the workspace record
|
|
*/
|
|
protected function initializeDbMountpointsInWorkspace()
|
|
{
|
|
$dbMountpoints = trim($this->workspaceRec['db_mountpoints'] ?? '');
|
|
if ($this->workspace > 0 && $dbMountpoints != '') {
|
|
$filteredDbMountpoints = [];
|
|
// Notice: We cannot call $this->getPagePermsClause(1);
|
|
// as usual because the group-list is not available at this point.
|
|
// But bypassing is fine because all we want here is check if the
|
|
// workspace mounts are inside the current webmounts rootline.
|
|
// The actual permission checking on page level is done elsewhere
|
|
// as usual anyway before the page tree is rendered.
|
|
$readPerms = '1=1';
|
|
// Traverse mount points of the workspace, add them,
|
|
// but make sure they match against the users' Page Tree Entry Points
|
|
|
|
$workspaceWebMounts = GeneralUtility::intExplode(',', $dbMountpoints);
|
|
$webMountsOfUser = GeneralUtility::intExplode(',', (string)($this->groupData['webmounts'] ?? ''));
|
|
$webMountsOfUser = array_combine($webMountsOfUser, $webMountsOfUser) ?: [];
|
|
|
|
$entryPointRootLineUids = [];
|
|
foreach ($webMountsOfUser as $webMountPageId) {
|
|
$rootLine = BackendUtility::BEgetRootLine($webMountPageId, '', true);
|
|
$entryPointRootLineUids[$webMountPageId] = array_map(intval(...), array_column($rootLine, 'uid'));
|
|
}
|
|
foreach ($entryPointRootLineUids as $webMountOfUser => $uidsOfRootLine) {
|
|
// Remove the Page Tree Entry Point of the user if the Page Tree Entry Point is not in the list of
|
|
// workspace mounts
|
|
foreach ($workspaceWebMounts as $webmountOfWorkspace) {
|
|
// This workspace's Page Tree Entry Point is somewhere in the rootline of the users' web mount,
|
|
// so this is "OK" to be included
|
|
if (in_array($webmountOfWorkspace, $uidsOfRootLine, true)) {
|
|
continue;
|
|
}
|
|
// Remove the user's Page Tree Entry Points (possible via array_combine, see above)
|
|
unset($webMountsOfUser[$webMountOfUser]);
|
|
}
|
|
}
|
|
$dbMountpoints = array_merge($workspaceWebMounts, $webMountsOfUser);
|
|
$dbMountpoints = array_unique($dbMountpoints);
|
|
foreach ($dbMountpoints as $mpId) {
|
|
if ($this->isInWebMount($mpId, $readPerms)) {
|
|
$filteredDbMountpoints[] = $mpId;
|
|
}
|
|
}
|
|
// Re-insert webmounts
|
|
$this->groupData['webmounts'] = implode(',', $filteredDbMountpoints);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checking if a workspace is allowed for backend user
|
|
*
|
|
* @param int|array $wsRec If integer, workspace record is looked up, if array it is seen as a Workspace record with at least uid, title, members and adminusers columns. Can be faked for workspaces uid 0 (live)
|
|
* @return array|false Output will also show how access was granted. Admin users will have a true output regardless of input.
|
|
* @internal should only be used from within TYPO3 Core
|
|
*/
|
|
public function checkWorkspace(int|array $wsRec): array|false
|
|
{
|
|
// If not array, look up workspace record
|
|
if (!is_array($wsRec)) {
|
|
if ($wsRec === 0) {
|
|
$wsRec = ['uid' => 0];
|
|
} elseif ($this->getTcaSchema('sys_workspace')) {
|
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
|
|
$queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
|
|
$wsRec = $queryBuilder
|
|
->select('*')
|
|
->from('sys_workspace')
|
|
->where($queryBuilder->expr()->eq(
|
|
'uid',
|
|
$queryBuilder->createNamedParameter($wsRec, Connection::PARAM_INT)
|
|
))
|
|
->executeQuery()
|
|
->fetchAssociative();
|
|
}
|
|
}
|
|
// If wsRec is set to an array, evaluate it, otherwise return false
|
|
if (!is_array($wsRec)) {
|
|
return false;
|
|
}
|
|
if ($this->isAdmin()) {
|
|
return array_merge($wsRec, ['_ACCESS' => 'admin']);
|
|
}
|
|
// User is in live, and be_groups.workspace_perms has bitmask=1 included
|
|
if ($wsRec['uid'] === 0) {
|
|
return $this->hasEditAccessToLiveWorkspace()
|
|
? array_merge($wsRec, ['_ACCESS' => 'online'])
|
|
: false;
|
|
}
|
|
// Checking if the person is workspace owner
|
|
if (GeneralUtility::inList($wsRec['adminusers'], 'be_users_' . $this->user['uid'])) {
|
|
return array_merge($wsRec, ['_ACCESS' => 'owner']);
|
|
}
|
|
// Checking if the editor is owner through an included user group
|
|
foreach ($this->userGroupsUID as $groupUid) {
|
|
if (GeneralUtility::inList($wsRec['adminusers'], 'be_groups_' . $groupUid)) {
|
|
return array_merge($wsRec, ['_ACCESS' => 'owner']);
|
|
}
|
|
}
|
|
// Checking if the user is member of the workspace
|
|
if (GeneralUtility::inList($wsRec['members'], 'be_users_' . $this->user['uid'])) {
|
|
return array_merge($wsRec, ['_ACCESS' => 'member']);
|
|
}
|
|
// Checking if the user is member through an included user group
|
|
foreach ($this->userGroupsUID as $groupUid) {
|
|
if (GeneralUtility::inList($wsRec['members'], 'be_groups_' . $groupUid)) {
|
|
return array_merge($wsRec, ['_ACCESS' => 'member']);
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Checks if the user (or the group) has the workspace_perms set to 1 in order to allow
|
|
* editing records in live workspace.
|
|
*/
|
|
protected function hasEditAccessToLiveWorkspace(): bool
|
|
{
|
|
return (bool)(($this->groupData['workspace_perms'] ?? 0) & 1);
|
|
}
|
|
|
|
/**
|
|
* Uses checkWorkspace() to check if current workspace is available for user.
|
|
* This function caches the result and so can be called many times with no performance loss.
|
|
*
|
|
* @see checkWorkspace()
|
|
* @internal should only be used from within TYPO3 Core
|
|
*/
|
|
protected function checkWorkspaceCurrent(): false|array|null
|
|
{
|
|
if (!isset($this->checkWorkspaceCurrent_cache)) {
|
|
$this->checkWorkspaceCurrent_cache = $this->checkWorkspace($this->workspace);
|
|
}
|
|
return $this->checkWorkspaceCurrent_cache;
|
|
}
|
|
|
|
/**
|
|
* Setting workspace ID
|
|
*
|
|
* @param int $workspaceId ID of workspace to set for backend user. If not valid the default workspace for BE user is found and set.
|
|
* @internal should only be used from within TYPO3 Core
|
|
*/
|
|
public function setWorkspace($workspaceId)
|
|
{
|
|
// Check workspace validity and if not found, revert to default workspace.
|
|
if (!$this->setTemporaryWorkspace($workspaceId)) {
|
|
$this->setDefaultWorkspace();
|
|
}
|
|
// Unset access cache:
|
|
$this->checkWorkspaceCurrent_cache = null;
|
|
// If ID is different from the stored one, change it:
|
|
if ((int)$this->workspace !== (int)$this->user['workspace_id']) {
|
|
$this->user['workspace_id'] = $this->workspace;
|
|
GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users')->update(
|
|
'be_users',
|
|
['workspace_id' => $this->user['workspace_id']],
|
|
['uid' => (int)$this->user['uid']]
|
|
);
|
|
$this->writelog(SystemLogType::EXTENSION, SystemLogGenericAction::UNDEFINED, SystemLogErrorClassification::MESSAGE, null, 'User changed workspace to "{workspace}"', ['workspace' => $this->workspace]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets a temporary workspace in the context of the current backend user.
|
|
*
|
|
* @param int $workspaceId
|
|
* @return bool
|
|
* @internal should only be used from within TYPO3 Core
|
|
*/
|
|
public function setTemporaryWorkspace($workspaceId)
|
|
{
|
|
$workspaceRecord = $this->checkWorkspace((int)$workspaceId);
|
|
|
|
if ($workspaceRecord) {
|
|
$this->workspaceRec = $workspaceRecord;
|
|
$this->workspace = (int)$workspaceId;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Sets the default workspace in the context of the current backend user.
|
|
* @internal should only be used from within TYPO3 Core
|
|
*/
|
|
protected function setDefaultWorkspace(): void
|
|
{
|
|
$this->workspace = $this->getDefaultWorkspace();
|
|
$this->workspaceRec = $this->checkWorkspace($this->workspace);
|
|
}
|
|
|
|
/**
|
|
* Return default workspace ID for user,
|
|
* if EXT:workspaces is not installed the user will be pushed to the
|
|
* Live workspace, if he has access to. If no workspace is available for the user, the workspace ID is set to "-99"
|
|
*
|
|
* @return int Default workspace id.
|
|
* @internal should only be used from within TYPO3 Core
|
|
*/
|
|
protected function getDefaultWorkspace(): int
|
|
{
|
|
if ($this->getTcaSchema('sys_workspace') === null) {
|
|
return 0;
|
|
}
|
|
// Online is default
|
|
if ($this->checkWorkspace(0)) {
|
|
return 0;
|
|
}
|
|
// Traverse all workspaces
|
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
|
|
$queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
|
|
$result = $queryBuilder->select('*')
|
|
->from('sys_workspace')
|
|
->orderBy('title')
|
|
->executeQuery();
|
|
while ($workspaceRecord = $result->fetchAssociative()) {
|
|
if ($this->checkWorkspace($workspaceRecord)) {
|
|
return (int)$workspaceRecord['uid'];
|
|
}
|
|
}
|
|
// Otherwise -99 is the fallback
|
|
return -99;
|
|
}
|
|
|
|
/**
|
|
* Writes an entry in the logfile/table
|
|
*
|
|
* @param int $type Denotes which module that has submitted the entry. See "TYPO3 Core API". Use "4" for extensions.
|
|
* @param int $action Denotes which specific operation that wrote the entry. Use "0" when no sub-categorizing applies
|
|
* @param int $error Flag. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin)
|
|
* @param null $_ unused
|
|
* @param string $details Default text that follows the message (in english!). Possibly translated by identification through type/action
|
|
* @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 with the details-text
|
|
* @param string $tablename Table name. Special field used by tce_main.php.
|
|
* @param int|string $recuid Record UID. Special field used by tce_main.php.
|
|
* @param null $__ unused
|
|
* @param int $event_pid The page_uid (pid) where the event occurred. Used to select log-content for specific pages.
|
|
* @param null $___ unused
|
|
* @param int $userId Alternative Backend User ID (used for logging login actions where this is not yet known).
|
|
* @return int Log entry ID.
|
|
*/
|
|
public function writelog($type, $action, $error, $_, $details, $data, $tablename = '', $recuid = '', $__ = null, $event_pid = -1, $___ = null, $userId = 0)
|
|
{
|
|
return GeneralUtility::makeInstance(LogEntryRepository::class)->writeLogEntryForBackendUser(
|
|
$this,
|
|
(int)$type,
|
|
(int)$action,
|
|
(int)$error,
|
|
(string)$details,
|
|
(array)$data,
|
|
(string)$tablename,
|
|
(int)$recuid,
|
|
(int)$event_pid,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns the configured cookie name
|
|
*/
|
|
public static function getCookieName(): string
|
|
{
|
|
$configuredCookieName = trim((string)($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName'] ?? ''));
|
|
return $configuredCookieName !== '' ? $configuredCookieName : 'be_typo_user';
|
|
}
|
|
|
|
/**
|
|
* Check if user is logged in and if so, call ->fetchGroupData() to load group information and
|
|
* access lists of all kind, further check IP, set the ->uc array.
|
|
* If no user is logged in the default behaviour is to exit with an error message.
|
|
* This function is called right after ->start() in fx. the TYPO3 Bootstrap.
|
|
*
|
|
* @throws \RuntimeException
|
|
* @todo deprecate
|
|
*/
|
|
public function backendCheckLogin(?ServerRequestInterface $request = null)
|
|
{
|
|
if (empty($this->user['uid'])) {
|
|
// @todo: throw a proper AccessDeniedException in TYPO3 v12.0. and handle this functionality in the calling code
|
|
$entryPointResolver = GeneralUtility::makeInstance(BackendEntryPointResolver::class);
|
|
$url = $entryPointResolver->getUriFromRequest($GLOBALS['TYPO3_REQUEST']);
|
|
throw new ImmediateResponseException(new RedirectResponse($url, 303), 1607271747);
|
|
}
|
|
if ($this->isUserAllowedToLogin()) {
|
|
$this->initializeBackendLogin($request);
|
|
} else {
|
|
// @todo: throw a proper AccessDeniedException in TYPO3 v12.0.
|
|
throw new \RuntimeException('Login Error: TYPO3 is in maintenance mode at the moment. Only administrators are allowed access.', 1294585860);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
public function initializeBackendLogin(?ServerRequestInterface $request = null): void
|
|
{
|
|
// The groups are fetched and ready for permission checking in this initialization.
|
|
// Tables.php must be read before this because stuff like the modules has impact in this
|
|
$this->fetchGroupData();
|
|
// Setting the UC array. It's needed with fetchGroupData first, due to default/overriding of values.
|
|
$this->backendSetUC();
|
|
if ($this->loginSessionStarted && !($this->getSessionData('mfa') ?? false)) {
|
|
// Handling user logged in. By checking for the mfa session key, it's ensured, the
|
|
// handling is only done once, since MfaController does the handling on its own.
|
|
$this->handleUserLoggedIn($request);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Is called after a user has sucesfully logged in. So either by using only one factor
|
|
* (e.g. username/password) or after the multi-factor authentication process has been passed.
|
|
*
|
|
* @internal
|
|
*/
|
|
public function handleUserLoggedIn(?ServerRequestInterface $request = null): void
|
|
{
|
|
// Also, if there is a recovery link set, unset it now
|
|
// this will be moved into its own Event at a later stage.
|
|
// If a token was set previously, this is now unset, as it was now possible to log-in
|
|
if ($this->user['password_reset_token'] ?? '') {
|
|
GeneralUtility::makeInstance(ConnectionPool::class)
|
|
->getConnectionForTable($this->user_table)
|
|
->update($this->user_table, ['password_reset_token' => ''], ['uid' => $this->user['uid']]);
|
|
}
|
|
|
|
$event = new AfterUserLoggedInEvent($this, $request);
|
|
GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch($event);
|
|
}
|
|
|
|
/**
|
|
* Initialize the internal ->uc array for the backend user (UC - user configuration
|
|
* is a serialized array inside the user object). Will make the overrides if necessary,
|
|
* and write the UC back to the be_users record if changes has happened.
|
|
*
|
|
* @internal
|
|
*/
|
|
public function backendSetUC()
|
|
{
|
|
// Setting defaults if uc is empty
|
|
$updated = false;
|
|
if (empty($this->uc)) {
|
|
$this->uc = array_merge(
|
|
$this->uc_default,
|
|
(array)$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUC'],
|
|
GeneralUtility::removeDotsFromTS((array)($this->getTSConfig()['setup.']['default.'] ?? []))
|
|
);
|
|
$this->overrideUC();
|
|
$updated = true;
|
|
}
|
|
// If TSconfig is updated, update the defaultUC.
|
|
if ($this->userTSUpdated) {
|
|
$this->overrideUC();
|
|
$updated = true;
|
|
}
|
|
// Saving if updated.
|
|
if ($updated) {
|
|
$this->writeUC();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Override: Call this function every time the uc is updated.
|
|
* That is 1) by reverting to default values, 2) in the setup-module, 3) userTS changes (userauthgroup)
|
|
*
|
|
* @internal
|
|
*/
|
|
public function overrideUC()
|
|
{
|
|
$this->uc = array_merge($this->uc, (array)($this->getTSConfig()['setup.']['override.'] ?? []));
|
|
}
|
|
|
|
/**
|
|
* Clears the user[uc] and ->uc to blank strings. Then calls ->backendSetUC() to fill it again with reset contents
|
|
*
|
|
* @internal
|
|
*/
|
|
public function resetUC()
|
|
{
|
|
$this->user['uc'] = '';
|
|
$this->user['user_settings'] = '';
|
|
$this->uc = [];
|
|
$this->resetUserSettingsCache();
|
|
$this->backendSetUC();
|
|
}
|
|
|
|
public function writeUC(): void
|
|
{
|
|
$userId = $this->getUserId();
|
|
if (!$userId) {
|
|
return;
|
|
}
|
|
|
|
$this->logger->debug('writeUC: {userid_column}={value}', [
|
|
'userid_column' => $this->userid_column,
|
|
'value' => $userId,
|
|
]);
|
|
|
|
$schema = GeneralUtility::makeInstance(UserSettingsSchema::class);
|
|
$profileSettings = [];
|
|
foreach ($schema->getJsonFieldSettingKeys() as $key) {
|
|
if (array_key_exists($key, $this->uc)) {
|
|
$profileSettings[$key] = $this->uc[$key];
|
|
}
|
|
}
|
|
|
|
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
|
|
->getConnectionForTable($this->user_table);
|
|
|
|
$connection->update(
|
|
$this->user_table,
|
|
[
|
|
'uc' => serialize($this->uc),
|
|
// The array must be passed directly to prevent double JSON encoding.
|
|
// See: https://review.typo3.org/c/Packages/TYPO3.CMS/+/89293
|
|
'user_settings' => $profileSettings,
|
|
],
|
|
[$this->userid_column => $userId],
|
|
[
|
|
'uc' => Connection::PARAM_LOB,
|
|
// @todo This behavior cannot be modified yet; the array value must be passed directly
|
|
// until https://review.typo3.org/c/Packages/TYPO3.CMS/+/89293 is merged,
|
|
// otherwise the value will be JSON-encoded twice.
|
|
'user_settings' => Type::getType(Types::JSON),
|
|
],
|
|
);
|
|
// Set modified user settings `json_encoded()` to the instance user record to display the correct
|
|
// value on the same request without rereading whole user record and group information here. That
|
|
// ensures that the next call to `getUserSettings()` creates a new instance from this record with
|
|
// the new user settings using `UserSettingsFactory`.
|
|
$this->user['user_settings'] = json_encode($profileSettings);
|
|
|
|
$this->resetUserSettingsCache();
|
|
}
|
|
|
|
/**
|
|
* Determines whether a backend user is allowed to access the backend.
|
|
*
|
|
* The conditions are:
|
|
* + backend user is a regular user and adminOnly is not defined
|
|
* + backend user is an admin user
|
|
* + backend user is used in CLI context and adminOnly is explicitly set to "2" (see CommandLineUserAuthentication)
|
|
* + backend user is being controlled by an admin user
|
|
*
|
|
* @return bool Whether a backend user is allowed to access the backend
|
|
* @internal
|
|
*/
|
|
public function isUserAllowedToLogin()
|
|
{
|
|
$isUserAllowedToLogin = false;
|
|
$adminOnlyMode = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'];
|
|
// Backend user is allowed if adminOnly is not set or user is an admin:
|
|
if (!$adminOnlyMode || $this->isAdmin()) {
|
|
$isUserAllowedToLogin = true;
|
|
} elseif ($backUserId = $this->getOriginalUserIdWhenInSwitchUserMode()) {
|
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
|
|
$isUserAllowedToLogin = (bool)$queryBuilder->count('uid')
|
|
->from('be_users')
|
|
->where(
|
|
$queryBuilder->expr()->eq(
|
|
'uid',
|
|
$queryBuilder->createNamedParameter($backUserId, Connection::PARAM_INT)
|
|
),
|
|
$queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, Connection::PARAM_INT))
|
|
)
|
|
->executeQuery()
|
|
->fetchOne();
|
|
}
|
|
return $isUserAllowedToLogin;
|
|
}
|
|
|
|
/**
|
|
* Logs out the current user and clears the form protection tokens.
|
|
*/
|
|
public function logoff()
|
|
{
|
|
if (isset($GLOBALS['BE_USER'])
|
|
&& $GLOBALS['BE_USER'] instanceof self
|
|
&& isset($GLOBALS['BE_USER']->user['uid'])
|
|
) {
|
|
GeneralUtility::makeInstance(FormProtectionFactory::class)->createForType('backend')->clean();
|
|
// Release the locked records
|
|
$this->releaseLockedRecords((int)$GLOBALS['BE_USER']->user['uid']);
|
|
|
|
if ($this->isSystemMaintainer()) {
|
|
// @todo: This should be turned into a dispatched event EXT:install can listen on.
|
|
// This might be useful for others as well. The reasons this has not been
|
|
// implemented yet, is, that the method should be refactored to at least
|
|
// receive Request and probably be-user object correctly, instead of
|
|
// fetching it from globals, before creating API with an event.
|
|
$packageManager = GeneralUtility::makeInstance(PackageManager::class);
|
|
if ($packageManager->isPackageActive('install')) {
|
|
// If user is system maintainer, destroy its possibly valid install tool session.
|
|
$session = GeneralUtility::makeInstance(SessionService::class);
|
|
// @todo: It's kinda fishy installSessionHandler() is called here. We should be able to skip this.
|
|
$session->installSessionHandler($GLOBALS['TYPO3_REQUEST'] ?? null);
|
|
$session->destroySession($GLOBALS['TYPO3_REQUEST'] ?? null);
|
|
}
|
|
}
|
|
}
|
|
parent::logoff();
|
|
}
|
|
|
|
/**
|
|
* Remove any "locked records" added for editing for the given user (= current backend user)
|
|
*/
|
|
protected function releaseLockedRecords(int $userId)
|
|
{
|
|
if ($userId > 0) {
|
|
GeneralUtility::makeInstance(ConnectionPool::class)
|
|
->getConnectionForTable('sys_lockedrecords')
|
|
->delete(
|
|
'sys_lockedrecords',
|
|
['userid' => $userId]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the uid of the backend user to return to.
|
|
* This is set when the current session is a "switch-user" session.
|
|
*
|
|
* @return int|null The user id
|
|
* @internal should only be used from within TYPO3 Core
|
|
*/
|
|
public function getOriginalUserIdWhenInSwitchUserMode(): ?int
|
|
{
|
|
$originalUserId = $this->getSessionData('backuserid');
|
|
return $originalUserId ? (int)$originalUserId : null;
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
protected function evaluateMfaRequirements(): void
|
|
{
|
|
// In case the current session is a "switch-user" session, MFA is not required
|
|
if ($this->getOriginalUserIdWhenInSwitchUserMode() !== null) {
|
|
$this->logger->debug('MFA is skipped in switch user mode', [
|
|
$this->userid_column => $this->getUserId(),
|
|
$this->username_column => $this->getUserName(),
|
|
]);
|
|
return;
|
|
}
|
|
parent::evaluateMfaRequirements();
|
|
}
|
|
|
|
/**
|
|
* Evaluate whether the user is required to set up MFA, based on user TSconfig and global configuration
|
|
*
|
|
* @internal
|
|
*/
|
|
public function isMfaSetupRequired(): bool
|
|
{
|
|
$authConfig = $this->getTSConfig()['auth.']['mfa.'] ?? [];
|
|
|
|
if (isset($authConfig['required'])) {
|
|
// user TSconfig overrules global configuration
|
|
return (bool)$authConfig['required'];
|
|
}
|
|
|
|
$globalConfig = (int)($GLOBALS['TYPO3_CONF_VARS']['BE']['requireMfa'] ?? 0);
|
|
if ($globalConfig <= 1) {
|
|
// 0 and 1 can directly be used by type-casting to boolean
|
|
return (bool)$globalConfig;
|
|
}
|
|
|
|
// check the system maintainer / admin / non-admin options
|
|
$isAdmin = $this->isAdmin();
|
|
return ($globalConfig === 2 && !$isAdmin)
|
|
|| ($globalConfig === 3 && $isAdmin)
|
|
|| ($globalConfig === 4 && $this->isSystemMaintainer());
|
|
}
|
|
|
|
/**
|
|
* Returns if import functionality is available for current user
|
|
*
|
|
* @internal
|
|
*/
|
|
public function isImportEnabled(): bool
|
|
{
|
|
return $this->isAdmin()
|
|
|| ($this->getTSConfig()['options.']['impexp.']['enableImportForNonAdminUser'] ?? false);
|
|
}
|
|
|
|
/**
|
|
* Returns if export functionality is available for current user
|
|
*
|
|
* @internal
|
|
*/
|
|
public function isExportEnabled(): bool
|
|
{
|
|
return $this->isAdmin()
|
|
|| ($this->getTSConfig()['options.']['impexp.']['enableExportForNonAdminUser'] ?? false);
|
|
}
|
|
|
|
/**
|
|
* Returns whether debug information shall be displayed to the user
|
|
*
|
|
* @internal
|
|
*/
|
|
public function shallDisplayDebugInformation(): bool
|
|
{
|
|
return ($GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] ?? false) && $this->isAdmin();
|
|
}
|
|
|
|
protected function getTcaSchema(string $table): ?TcaSchema
|
|
{
|
|
$schemaFactory = GeneralUtility::makeInstance(TcaSchemaFactory::class);
|
|
return $schemaFactory->has($table) ? $schemaFactory->get($table) : null;
|
|
}
|
|
}
|