'', '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 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; } }