2 namespace TYPO3\CMS\Core\Authentication
;
5 * This file is part of the TYPO3 CMS project.
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
14 * The TYPO3 project - inspiring people to share!
17 use TYPO3\CMS\Backend\Utility\BackendUtility
;
18 use TYPO3\CMS\Core\Database\Connection
;
19 use TYPO3\CMS\Core\Database\ConnectionPool
;
20 use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder
;
21 use TYPO3\CMS\Core\Database\Query\QueryHelper
;
22 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction
;
23 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
;
24 use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction
;
25 use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction
;
26 use TYPO3\CMS\Core\
Resource\ResourceStorage
;
27 use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation
;
28 use TYPO3\CMS\Core\Type\Bitmask\Permission
;
29 use TYPO3\CMS\Core\Type\Exception\InvalidEnumerationValueException
;
30 use TYPO3\CMS\Core\Utility\GeneralUtility
;
33 * TYPO3 backend user authentication
34 * Contains most of the functions used for checking permissions, authenticating users,
35 * setting up the user, and API for user from outside.
36 * This class contains the configuration of the database fields used plus some
37 * functions for the authentication process of backend users.
39 class BackendUserAuthentication
extends AbstractUserAuthentication
41 const ROLE_SYSTEMMAINTAINER
= 'systemMaintainer';
44 * Should be set to the usergroup-column (id-list) in the user-record
47 public $usergroup_column = 'usergroup';
50 * The name of the group-table
53 public $usergroup_table = 'be_groups';
56 * holds lists of eg. tables, fields and other values related to the permission-system. See fetchGroupData
65 * This array will hold the groups that the user is a member of
68 public $userGroups = [];
71 * This array holds the uid's of the groups in the listed order
74 public $userGroupsUID = [];
77 * This is $this->userGroupsUID imploded to a comma list... Will correspond to the 'usergroup_cached_list'
80 public $groupList = '';
84 * -99 is ERROR (none available)
87 * >0 is custom workspaces
90 public $workspace = -99;
93 * Custom workspace record if any
96 public $workspaceRec = [];
99 * Used to accumulate data for the user-group.
100 * DON NOT USE THIS EXTERNALLY!
101 * Use $this->groupData instead
105 public $dataLists = [
106 'webmount_list' => '',
107 'filemount_list' => '',
108 'file_permissions' => '',
110 'tables_select' => '',
111 'tables_modify' => '',
112 'pagetypes_select' => '',
113 'non_exclude_fields' => '',
114 'explicit_allowdeny' => '',
115 'allowed_languages' => '',
116 'workspace_perms' => '',
117 'custom_options' => ''
121 * List of group_id's in the order they are processed.
124 public $includeGroupArray = [];
127 * Used to accumulate the TSconfig data of the user
130 public $TSdataArray = [];
133 * Contains the non-parsed user TSconfig
136 public $userTS_text = '';
139 * Contains the parsed user TSconfig
145 * Set internally if the user TSconfig was parsed and needs to be cached.
148 public $userTSUpdated = false;
151 * Set this from outside if you want the user TSconfig to ALWAYS be parsed and not fetched from cache.
154 public $userTS_dontGetCached = false;
157 * Contains last error message
160 public $errorMsg = '';
163 * Cache for checkWorkspaceCurrent()
166 public $checkWorkspaceCurrent_cache = null;
169 * @var \TYPO3\CMS\Core\Resource\ResourceStorage[]
171 protected $fileStorages;
176 protected $filePermissions;
179 * Table in database with user data
182 public $user_table = 'be_users';
185 * Column for login-name
188 public $username_column = 'username';
191 * Column for password
194 public $userident_column = 'password';
200 public $userid_column = 'uid';
205 public $lastLogin_column = 'lastlogin';
210 public $enablecolumns = [
212 'deleted' => 'deleted',
213 'disabled' => 'disable',
214 'starttime' => 'starttime',
215 'endtime' => 'endtime'
219 * Form field with login-name
222 public $formfield_uname = 'username';
225 * Form field with password
228 public $formfield_uident = 'userident';
231 * Form field with status: *'login', 'logout'
234 public $formfield_status = 'login_status';
237 * Decides if the writelog() function is called at login and logout
240 public $writeStdLog = true;
243 * If the writelog() functions is called if a login-attempt has be tried without success
246 public $writeAttemptLog = true;
249 * Session timeout (on the server), defaults to 8 hours for backend user
251 * If >0: session-timeout in seconds.
252 * If <=0: Instant logout after login.
253 * The value must be at least 180 to avoid side effects.
257 public $sessionTimeout = 28800;
262 public $firstMainGroup = 0;
271 * User Config Default values:
272 * The array may contain other fields for configuration.
273 * For this, see "setup" extension and "TSConfig" document (User TSconfig, "setup.[xxx]....")
274 * Reserved keys for other storage of session data:
279 public $uc_default = [
280 'interfaceSetup' => '',
281 // serialized content that is used to store interface pane and menu positions. Set by the logout.php-script
283 // user-data for the modules
284 'thumbnailsByDefault' => 1,
285 'emailMeAtLogin' => 0,
286 'startModule' => 'help_AboutAbout',
289 'edit_docModuleUpload' => '1',
290 'resizeTextareas' => 1,
291 'resizeTextareas_MaxHeight' => 500,
292 'resizeTextareas_Flexible' => 0
298 public function __construct()
300 parent
::__construct();
301 $this->name
= self
::getCookieName();
302 $this->loginType
= 'BE';
303 $this->warningEmail
= $GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr'];
304 $this->lockIP
= $GLOBALS['TYPO3_CONF_VARS']['BE']['lockIP'];
305 $this->sessionTimeout
= (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['sessionTimeout'];
309 * Returns TRUE if user is admin
310 * Basically this function evaluates if the ->user[admin] field has bit 0 set. If so, user is admin.
314 public function isAdmin()
316 return is_array($this->user
) && ($this->user
['admin'] & 1) == 1;
320 * Returns TRUE if the current user is a member of group $groupId
321 * $groupId must be set. $this->groupList must contain groups
322 * Will return TRUE also if the user is a member of a group through subgroups.
324 * @param int $groupId Group ID to look for in $this->groupList
327 public function isMemberOfGroup($groupId)
329 $groupId = (int)$groupId;
330 if ($this->groupList
&& $groupId) {
331 return GeneralUtility
::inList($this->groupList
, $groupId);
337 * Checks if the permissions is granted based on a page-record ($row) and $perms (binary and'ed)
339 * Bits for permissions, see $perms variable:
341 * 1 - Show: See/Copy page and the pagecontent.
342 * 16- Edit pagecontent: Change/Add/Delete/Move pagecontent.
343 * 2- Edit page: Change/Move the page, eg. change title, startdate, hidden.
344 * 4- Delete page: Delete the page and pagecontent.
345 * 8- New pages: Create new pages under the page.
347 * @param array $row Is the pagerow for which the permissions is checked
348 * @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.
351 public function doesUserHaveAccess($row, $perms)
353 $userPerms = $this->calcPerms($row);
354 return ($userPerms & $perms) == $perms;
358 * Checks if the page id, $id, is found within the webmounts set up for the user.
359 * This should ALWAYS be checked for any page id a user works with, whether it's about reading, writing or whatever.
360 * The point is that this will add the security that a user can NEVER touch parts outside his mounted
361 * pages in the page tree. This is otherwise possible if the raw page permissions allows for it.
362 * So this security check just makes it easier to make safe user configurations.
363 * If the user is admin OR if this feature is disabled
364 * (fx. by setting TYPO3_CONF_VARS['BE']['lockBeUserToDBmounts']=0) then it returns "1" right away
365 * Otherwise the function will return the uid of the webmount which was first found in the rootline of the input page $id
367 * @param int $id Page ID to check
368 * @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!)
369 * @param bool|int $exitOnError If set, then the function will exit with an error message.
370 * @throws \RuntimeException
371 * @return int|null The page UID of a page in the rootline that matched a mount point
373 public function isInWebMount($id, $readPerms = '', $exitOnError = 0)
375 if (!$GLOBALS['TYPO3_CONF_VARS']['BE']['lockBeUserToDBmounts'] ||
$this->isAdmin()) {
379 // Check if input id is an offline version page in which case we will map id to the online version:
380 $checkRec = BackendUtility
::getRecord('pages', $id, 'pid,t3ver_oid');
381 if ($checkRec['pid'] == -1) {
382 $id = (int)$checkRec['t3ver_oid'];
385 $readPerms = $this->getPagePermsClause(1);
388 $wM = $this->returnWebmounts();
389 $rL = BackendUtility
::BEgetRootLine($id, ' AND ' . $readPerms);
390 foreach ($rL as $v) {
391 if ($v['uid'] && in_array($v['uid'], $wM)) {
397 throw new \
RuntimeException('Access Error: This page is not within your DB-mounts', 1294586445);
403 * Checks access to a backend module with the $MCONF passed as first argument
405 * @param array $conf $MCONF array of a backend module!
406 * @param bool $exitOnError If set, an array will issue an error message and exit.
407 * @throws \RuntimeException
408 * @return bool Will return TRUE if $MCONF['access'] is not set at all, if the BE_USER is admin or if the module is enabled in the be_users/be_groups records of the user (specifically enabled). Will return FALSE if the module name is not even found in $TBE_MODULES
410 public function modAccess($conf, $exitOnError)
412 if (!BackendUtility
::isModuleSetInTBE_MODULES($conf['name'])) {
414 throw new \
RuntimeException('Fatal Error: This module "' . $conf['name'] . '" is not enabled in TBE_MODULES', 1294586446);
420 !empty($conf['workspaces'])
421 && \TYPO3\CMS\Core\Utility\ExtensionManagementUtility
::isLoaded('workspaces')
422 && ($this->workspace
!== 0 ||
!GeneralUtility
::inList($conf['workspaces'], 'online'))
423 && ($this->workspace
!== -1 ||
!GeneralUtility
::inList($conf['workspaces'], 'offline'))
424 && ($this->workspace
<= 0 ||
!GeneralUtility
::inList($conf['workspaces'], 'custom'))
427 throw new \
RuntimeException('Workspace Error: This module "' . $conf['name'] . '" is not available under the current workspace', 1294586447);
431 // Returns false if conf[access] is set to system maintainers and the user is system maintainer
432 if (strpos($conf['access'], self
::ROLE_SYSTEMMAINTAINER
) !== false && !$this->isSystemMaintainer()) {
434 throw new \
RuntimeException('This module "' . $conf['name'] . '" is only available as system maintainer', 1504804727);
438 // Returns TRUE if conf[access] is not set at all or if the user is admin
439 if (!$conf['access'] ||
$this->isAdmin()) {
442 // If $conf['access'] is set but not with 'admin' then we return TRUE, if the module is found in the modList
444 if (!strstr($conf['access'], 'admin') && $conf['name']) {
445 $acs = $this->check('modules', $conf['name']);
447 if (!$acs && $exitOnError) {
448 throw new \
RuntimeException('Access Error: You don\'t have access to this module.', 1294586448);
454 * Checks if the user is in the valid list of allowed system maintainers, if the list is not set.
455 * then all admins are system maintainers. If the list is empty, no one is system maintainer (good for production
460 public function isSystemMaintainer(): bool
462 if (GeneralUtility
::getApplicationContext()->isDevelopment() && $this->isAdmin()) {
465 $systemMaintainers = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ??
[];
466 $systemMaintainers = array_map('intval', $systemMaintainers);
467 if (!empty($systemMaintainers)) {
468 return in_array($this->getRealUserId(), $systemMaintainers, true);
470 // No system maintainers set up yet, so any admin is allowed to access the modules
471 // but explicitly no system maintainers allowed (empty string in TYPO3_CONF_VARS).
472 // @todo: this needs to be adjusted once system maintainers can log into the install tool with their credentials
473 if (isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'])
474 && empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'])) {
477 return $this->isAdmin();
481 * If a user has actually logged in and switched to a different user (admins can use the SU switch user method)
482 * the real UID is sometimes needed (when checking for permissions for example).
484 protected function getRealUserId(): int
486 return (int)($GLOBALS['BE_USER']->user
['ses_backuserid'] ?
: $this->user
['uid']);
490 * Returns a WHERE-clause for the pages-table where user permissions according to input argument, $perms, is validated.
491 * $perms is the "mask" used to select. Fx. if $perms is 1 then you'll get all pages that a user can actually see!
496 * If the user is 'admin' " 1=1" is returned (no effect)
497 * 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)
498 * The 95% use of this function is "->getPagePermsClause(1)" which will
499 * return WHERE clauses for *selecting* pages in backend listings - in other words this will check read permissions.
501 * @param int $perms Permission mask to use, see function description
502 * @return string Part of where clause. Prefix " AND " to this.
504 public function getPagePermsClause($perms)
506 if (is_array($this->user
)) {
507 if ($this->isAdmin()) {
510 // Make sure it's integer.
511 $perms = (int)$perms;
512 $expressionBuilder = GeneralUtility
::makeInstance(ConnectionPool
::class)
513 ->getQueryBuilderForTable('pages')
517 $constraint = $expressionBuilder->orX(
518 $expressionBuilder->comparison(
519 $expressionBuilder->bitAnd('pages.perms_everybody', $perms),
520 ExpressionBuilder
::EQ
,
523 $expressionBuilder->andX(
524 $expressionBuilder->eq('pages.perms_userid', (int)$this->user
['uid']),
525 $expressionBuilder->comparison(
526 $expressionBuilder->bitAnd('pages.perms_user', $perms),
527 ExpressionBuilder
::EQ
,
533 // Group (if any is set)
534 if ($this->groupList
) {
536 $expressionBuilder->andX(
537 $expressionBuilder->in(
538 'pages.perms_groupid',
539 GeneralUtility
::intExplode(',', $this->groupList
)
541 $expressionBuilder->comparison(
542 $expressionBuilder->bitAnd('pages.perms_group', $perms),
543 ExpressionBuilder
::EQ
,
550 $constraint = ' (' . (string)$constraint . ')';
553 // getPagePermsClause-HOOK
555 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getPagePermsClause'])) {
556 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getPagePermsClause'] as $_funcRef) {
557 $_params = ['currentClause' => $constraint, 'perms' => $perms];
558 $constraint = GeneralUtility
::callUserFunction($_funcRef, $_params, $this);
567 * Returns a combined binary representation of the current users permissions for the page-record, $row.
568 * The perms for user, group and everybody is OR'ed together (provided that the page-owner is the user
569 * and for the groups that the user is a member of the group.
570 * If the user is admin, 31 is returned (full permissions for all five flags)
572 * @param array $row Input page row with all perms_* fields available.
573 * @return int Bitwise representation of the users permissions in relation to input page row, $row
575 public function calcPerms($row)
577 // Return 31 for admin users.
578 if ($this->isAdmin()) {
579 return Permission
::ALL
;
581 // Return 0 if page is not within the allowed web mount
582 // Always do this for the default language page record
583 if (!$this->isInWebMount($row['l10n_parent'] ?
: $row['uid'])) {
584 return Permission
::NOTHING
;
586 $out = Permission
::NOTHING
;
588 isset($row['perms_userid']) && isset($row['perms_user']) && isset($row['perms_groupid'])
589 && isset($row['perms_group']) && isset($row['perms_everybody']) && isset($this->groupList
)
591 if ($this->user
['uid'] == $row['perms_userid']) {
592 $out |
= $row['perms_user'];
594 if ($this->isMemberOfGroup($row['perms_groupid'])) {
595 $out |
= $row['perms_group'];
597 $out |
= $row['perms_everybody'];
602 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['calcPerms'])) {
603 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['calcPerms'] as $_funcRef) {
606 'outputPermissions' => $out
608 $out = GeneralUtility
::callUserFunction($_funcRef, $_params, $this);
615 * Returns TRUE if the RTE (Rich Text Editor) is enabled for the user.
619 public function isRTE()
621 return (bool)$this->uc
['edit_RTE'];
625 * Returns TRUE if the $value is found in the list in a $this->groupData[] index pointed to by $type (array key).
626 * Can thus be users to check for modules, exclude-fields, select/modify permissions for tables etc.
627 * If user is admin TRUE is also returned
628 * Please see the document Inside TYPO3 for examples.
630 * @param string $type The type value; "webmounts", "filemounts", "pagetypes_select", "tables_select", "tables_modify", "non_exclude_fields", "modules
631 * @param string $value String to search for in the groupData-list
632 * @return bool TRUE if permission is granted (that is, the value was found in the groupData list - or the BE_USER is "admin")
634 public function check($type, $value)
636 return isset($this->groupData
[$type])
637 && ($this->isAdmin() || GeneralUtility
::inList($this->groupData
[$type], $value));
641 * Checking the authMode of a select field with authMode set
643 * @param string $table Table name
644 * @param string $field Field name (must be configured in TCA and of type "select" with authMode set!)
645 * @param string $value Value to evaluation (single value, must not contain any of the chars ":,|")
646 * @param string $authMode Auth mode keyword (explicitAllow, explicitDeny, individual)
647 * @return bool Whether access is granted or not
649 public function checkAuthMode($table, $field, $value, $authMode)
651 // Admin users can do anything:
652 if ($this->isAdmin()) {
655 // Allow all blank values:
656 if ((string)$value === '') {
659 // Certain characters are not allowed in the value
660 if (preg_match('/[:|,]/', $value)) {
664 $testValue = $table . ':' . $field . ':' . $value;
667 switch ((string)$authMode) {
668 case 'explicitAllow':
669 if (!GeneralUtility
::inList($this->groupData
['explicit_allowdeny'], ($testValue . ':ALLOW'))) {
674 if (GeneralUtility
::inList($this->groupData
['explicit_allowdeny'], $testValue . ':DENY')) {
679 if (is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
680 $items = $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'];
681 if (is_array($items)) {
682 foreach ($items as $iCfg) {
683 if ((string)$iCfg[1] === (string)$value && $iCfg[4]) {
684 switch ((string)$iCfg[4]) {
686 if (!GeneralUtility
::inList($this->groupData
['explicit_allowdeny'], ($testValue . ':ALLOW'))) {
691 if (GeneralUtility
::inList($this->groupData
['explicit_allowdeny'], $testValue . ':DENY')) {
707 * Checking if a language value (-1, 0 and >0 for sys_language records) is allowed to be edited by the user.
709 * @param int $langValue Language value to evaluate
710 * @return bool Returns TRUE if the language value is allowed, otherwise FALSE.
712 public function checkLanguageAccess($langValue)
714 // The users language list must be non-blank - otherwise all languages are allowed.
715 if (trim($this->groupData
['allowed_languages']) !== '') {
716 $langValue = (int)$langValue;
717 // Language must either be explicitly allowed OR the lang Value be "-1" (all languages)
718 if ($langValue != -1 && !$this->check('allowed_languages', $langValue)) {
726 * Check if user has access to all existing localizations for a certain record
728 * @param string $table The table
729 * @param array $record The current record
732 public function checkFullLanguagesAccess($table, $record)
734 $recordLocalizationAccess = $this->checkLanguageAccess(0);
735 if ($recordLocalizationAccess && BackendUtility
::isTableLocalizable($table)) {
736 $pointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
737 $pointerValue = $record[$pointerField] > 0 ?
$record[$pointerField] : $record['uid'];
738 $queryBuilder = GeneralUtility
::makeInstance(ConnectionPool
::class)->getQueryBuilderForTable($table);
739 $queryBuilder->getRestrictions()
741 ->add(GeneralUtility
::makeInstance(DeletedRestriction
::class))
742 ->add(GeneralUtility
::makeInstance(BackendWorkspaceRestriction
::class));
743 $recordLocalization = $queryBuilder->select('*')
746 $queryBuilder->expr()->eq(
748 $queryBuilder->createNamedParameter($pointerValue, \PDO
::PARAM_INT
)
755 if (is_array($recordLocalization)) {
756 $languageAccess = $this->checkLanguageAccess(
757 $recordLocalization[$GLOBALS['TCA'][$table]['ctrl']['languageField']]
759 $recordLocalizationAccess = $recordLocalizationAccess && $languageAccess;
762 return $recordLocalizationAccess;
766 * Checking if a user has editing access to a record from a $GLOBALS['TCA'] table.
767 * The checks does not take page permissions and other "environmental" things into account.
768 * It only deal with record internals; If any values in the record fields disallows it.
769 * For instance languages settings, authMode selector boxes are evaluated (and maybe more in the future).
770 * It will check for workspace dependent access.
771 * The function takes an ID (int) or row (array) as second argument.
773 * @param string $table Table name
774 * @param mixed $idOrRow If integer, then this is the ID of the record. If Array this just represents fields in the record.
775 * @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.
776 * @param bool $deletedRecord Set, if testing a deleted record array.
777 * @param bool $checkFullLanguageAccess Set, whenever access to all translations of the record is required
778 * @return bool TRUE if OK, otherwise FALSE
780 public function recordEditAccessInternals($table, $idOrRow, $newRecord = false, $deletedRecord = false, $checkFullLanguageAccess = false)
782 if (!isset($GLOBALS['TCA'][$table])) {
785 // Always return TRUE for Admin users.
786 if ($this->isAdmin()) {
789 // Fetching the record if the $idOrRow variable was not an array on input:
790 if (!is_array($idOrRow)) {
791 if ($deletedRecord) {
792 $idOrRow = BackendUtility
::getRecord($table, $idOrRow, '*', '', false);
794 $idOrRow = BackendUtility
::getRecord($table, $idOrRow);
796 if (!is_array($idOrRow)) {
797 $this->errorMsg
= 'ERROR: Record could not be fetched.';
801 // Checking languages:
802 if ($table === 'pages' && $checkFullLanguageAccess && !$this->checkFullLanguagesAccess($table, $idOrRow)) {
805 if ($GLOBALS['TCA'][$table]['ctrl']['languageField']) {
806 // Language field must be found in input row - otherwise it does not make sense.
807 if (isset($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
808 if (!$this->checkLanguageAccess($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
809 $this->errorMsg
= 'ERROR: Language was not allowed.';
813 $checkFullLanguageAccess && $idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']] == 0
814 && !$this->checkFullLanguagesAccess($table, $idOrRow)
816 $this->errorMsg
= 'ERROR: Related/affected language was not allowed.';
820 $this->errorMsg
= 'ERROR: The "languageField" field named "'
821 . $GLOBALS['TCA'][$table]['ctrl']['languageField'] . '" was not found in testing record!';
825 // Checking authMode fields:
826 if (is_array($GLOBALS['TCA'][$table]['columns'])) {
827 foreach ($GLOBALS['TCA'][$table]['columns'] as $fieldName => $fieldValue) {
828 if (isset($idOrRow[$fieldName])) {
830 $fieldValue['config']['type'] === 'select' && $fieldValue['config']['authMode']
831 && $fieldValue['config']['authMode_enforce'] === 'strict'
833 if (!$this->checkAuthMode($table, $fieldName, $idOrRow[$fieldName], $fieldValue['config']['authMode'])) {
834 $this->errorMsg
= 'ERROR: authMode "' . $fieldValue['config']['authMode']
835 . '" failed for field "' . $fieldName . '" with value "'
836 . $idOrRow[$fieldName] . '" evaluated';
843 // Checking "editlock" feature (doesn't apply to new records)
844 if (!$newRecord && $GLOBALS['TCA'][$table]['ctrl']['editlock']) {
845 if (isset($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['editlock']])) {
846 if ($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['editlock']]) {
847 $this->errorMsg
= 'ERROR: Record was locked for editing. Only admin users can change this state.';
851 $this->errorMsg
= 'ERROR: The "editLock" field named "' . $GLOBALS['TCA'][$table]['ctrl']['editlock']
852 . '" was not found in testing record!';
856 // Checking record permissions
857 // THIS is where we can include a check for "perms_" fields for other records than pages...
859 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['recordEditAccessInternals'])) {
860 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['recordEditAccessInternals'] as $funcRef) {
863 'idOrRow' => $idOrRow,
864 'newRecord' => $newRecord
866 if (!GeneralUtility
::callUserFunction($funcRef, $params, $this)) {
871 // Finally, return TRUE if all is well.
876 * Checks a type of permission against the compiled permission integer,
877 * $compiledPermissions, and in relation to table, $tableName
879 * @param int $compiledPermissions Could typically be the "compiled permissions" integer returned by ->calcPerms
880 * @param string $tableName Is the tablename to check: If "pages" table then edit,new,delete and editcontent permissions can be checked. Other tables will be checked for "editcontent" only (and $type will be ignored)
881 * @param string $actionType For $tableName='pages' this can be 'edit' (2), 'new' (8 or 16), 'delete' (4), 'editcontent' (16). For all other tables this is ignored. (16 is used)
883 * @access public (used by ClickMenuController)
885 public function isPSet($compiledPermissions, $tableName, $actionType = '')
887 if ($this->isAdmin()) {
889 } elseif ($tableName === 'pages') {
890 switch ($actionType) {
892 $result = ($compiledPermissions & Permission
::PAGE_EDIT
) !== 0;
895 // Create new page OR page content
896 $result = ($compiledPermissions & Permission
::PAGE_NEW + Permission
::CONTENT_EDIT
) !== 0;
899 $result = ($compiledPermissions & Permission
::PAGE_DELETE
) !== 0;
902 $result = ($compiledPermissions & Permission
::CONTENT_EDIT
) !== 0;
908 $result = ($compiledPermissions & Permission
::CONTENT_EDIT
) !== 0;
914 * Returns TRUE if the BE_USER is allowed to *create* shortcuts in the backend modules
918 public function mayMakeShortcut()
920 return $this->getTSConfigVal('options.enableBookmarks')
921 && !$this->getTSConfigVal('options.mayNotCreateEditBookmarks');
925 * Checking if editing of an existing record is allowed in current workspace if that is offline.
926 * Rules for editing in offline mode:
927 * - record supports versioning and is an offline version from workspace and has the corrent stage
928 * - or record (any) is in a branch where there is a page which is a version from the workspace
929 * and where the stage is not preventing records
931 * @param string $table Table of record
932 * @param array|int $recData Integer (record uid) or array where fields are at least: pid, t3ver_wsid, t3ver_stage (if versioningWS is set)
933 * @return string String error code, telling the failure state. FALSE=All ok
935 public function workspaceCannotEditRecord($table, $recData)
937 // Only test offline spaces:
938 if ($this->workspace
!== 0) {
939 if (!is_array($recData)) {
940 $recData = BackendUtility
::getRecord(
943 'pid' . ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] ?
',t3ver_wsid,t3ver_stage' : '')
946 if (is_array($recData)) {
947 // We are testing a "version" (identified by a pid of -1): it can be edited provided
948 // that workspace matches and versioning is enabled for the table.
949 if ((int)$recData['pid'] === -1) {
950 // No versioning, basic error, inconsistency even! Such records should not have a pid of -1!
951 if (!$GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
952 return 'Versioning disabled for table';
954 if ((int)$recData['t3ver_wsid'] !== $this->workspace
) {
955 // So does workspace match?
956 return 'Workspace ID of record didn\'t match current workspace';
958 // So is the user allowed to "use" the edit stage within the workspace?
959 return $this->workspaceCheckStageForCurrent(0)
961 : 'User\'s access level did not allow for editing';
963 // We are testing a "live" record:
964 // For "Live" records, check that PID for table allows editing
965 if ($res = $this->workspaceAllowLiveRecordsInPID($recData['pid'], $table)) {
966 // Live records are OK in this branch, but what about the stage of branch point, if any:
970 : 'Stage for versioning root point and users access level did not allow for editing';
972 // If not offline and not in versionized branch, output error:
973 return 'Online record was not in versionized branch!';
977 // OK because workspace is 0
982 * Evaluates if a user is allowed to edit the offline version
984 * @param string $table Table of record
985 * @param array|int $recData Integer (record uid) or array where fields are at least: pid, t3ver_wsid, t3ver_stage (if versioningWS is set)
986 * @return string String error code, telling the failure state. FALSE=All ok
987 * @see workspaceCannotEditRecord()
989 public function workspaceCannotEditOfflineVersion($table, $recData)
991 if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
992 if (!is_array($recData)) {
993 $recData = BackendUtility
::getRecord($table, $recData, 'uid,pid,t3ver_wsid,t3ver_stage');
995 if (is_array($recData)) {
996 if ((int)$recData['pid'] === -1) {
997 return $this->workspaceCannotEditRecord($table, $recData);
999 return 'Not an offline version';
1003 return 'Table does not support versioning.';
1007 * Check if "live" records from $table may be created or edited in this PID.
1008 * If the answer is FALSE it means the only valid way to create or edit records in the PID is by versioning
1009 * If the answer is 1 or 2 it means it is OK to create a record, if -1 it means that it is OK in terms
1010 * of versioning because the element was within a versionized branch
1011 * but NOT ok in terms of the state the root point had!
1013 * @param int $pid PID value to check for. OBSOLETE!
1014 * @param string $table Table name
1015 * @return mixed Returns FALSE if a live record cannot be created and must be versionized in order to do so. 2 means a) Workspace is "Live" or workspace allows "live edit" of records from non-versionized tables (and the $table is not versionizable). 1 and -1 means the pid is inside a versionized branch where -1 means that the branch-point did NOT allow a new record according to its state.
1017 public function workspaceAllowLiveRecordsInPID($pid, $table)
1019 // Always for Live workspace AND if live-edit is enabled
1020 // and tables are completely without versioning it is ok as well.
1022 $this->workspace
=== 0
1023 ||
$this->workspaceRec
['live_edit'] && !$GLOBALS['TCA'][$table]['ctrl']['versioningWS']
1024 ||
$GLOBALS['TCA'][$table]['ctrl']['versioningWS_alwaysAllowLiveEdit']
1026 // OK to create for this table.
1029 // If the answer is FALSE it means the only valid way to create or edit records in the PID is by versioning
1034 * Evaluates if a record from $table can be created in $pid
1036 * @param int $pid Page id. This value must be the _ORIG_uid if available: So when you have pages versionized as "page" or "element" you must supply the id of the page version in the workspace!
1037 * @param string $table Table name
1038 * @return bool TRUE if OK.
1040 public function workspaceCreateNewRecord($pid, $table)
1042 if ($res = $this->workspaceAllowLiveRecordsInPID($pid, $table)) {
1043 // If LIVE records cannot be created in the current PID due to workspace restrictions, prepare creation of placeholder-record
1045 // Stage for versioning root point and users access level did not allow for editing
1048 } elseif (!$GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1049 // So, if no live records were allowed, we have to create a new version of this record:
1056 * Evaluates if auto creation of a version of a record is allowed.
1058 * @param string $table Table of the record
1059 * @param int $id UID of record
1060 * @param int $recpid PID of record
1061 * @return bool TRUE if ok.
1063 public function workspaceAllowAutoCreation($table, $id, $recpid)
1065 // Auto-creation of version: In offline workspace, test if versioning is
1066 // enabled and look for workspace version of input record.
1067 // If there is no versionized record found we will create one and save to that.
1069 $this->workspace
!== 0
1070 && $GLOBALS['TCA'][$table]['ctrl']['versioningWS'] && $recpid >= 0
1071 && !BackendUtility
::getWorkspaceVersionOfRecord($this->workspace
, $table, $id, 'uid')
1073 // There must be no existing version of this record in workspace.
1080 * Checks if an element stage allows access for the user in the current workspace
1081 * In live workspace (= 0) access is always granted for any stage.
1082 * Admins are always allowed.
1083 * An option for custom workspaces allows members to also edit when the stage is "Review"
1085 * @param int $stage Stage id from an element: -1,0 = editing, 1 = reviewer, >1 = owner
1086 * @return bool TRUE if user is allowed access
1088 public function workspaceCheckStageForCurrent($stage)
1090 // Always allow for admins
1091 if ($this->isAdmin()) {
1094 if ($this->workspace
!== 0 && \TYPO3\CMS\Core\Utility\ExtensionManagementUtility
::isLoaded('workspaces')) {
1095 $stage = (int)$stage;
1096 $stat = $this->checkWorkspaceCurrent();
1097 // Check if custom staging is activated
1098 $workspaceRec = BackendUtility
::getRecord('sys_workspace', $stat['uid']);
1099 if ($workspaceRec['custom_stages'] > 0 && $stage !== 0 && $stage !== -10) {
1100 // Get custom stage record
1101 $workspaceStageRec = BackendUtility
::getRecord('sys_workspace_stage', $stage);
1102 // Check if the user is responsible for the current stage
1104 $stat['_ACCESS'] === 'owner'
1105 ||
$stat['_ACCESS'] === 'member'
1106 && GeneralUtility
::inList($workspaceStageRec['responsible_persons'], 'be_users_' . $this->user
['uid'])
1110 // Check if the user is in a group which is responsible for the current stage
1111 foreach ($this->userGroupsUID
as $groupUid) {
1113 $stat['_ACCESS'] === 'owner'
1114 ||
$stat['_ACCESS'] === 'member'
1115 && GeneralUtility
::inList($workspaceStageRec['responsible_persons'], 'be_groups_' . $groupUid)
1120 } elseif ($stage == -10 ||
$stage == -20) {
1121 if ($stat['_ACCESS'] === 'owner') {
1126 $memberStageLimit = $this->workspaceRec
['review_stage_edit'] ?
1 : 0;
1128 $stat['_ACCESS'] === 'owner'
1129 ||
$stat['_ACCESS'] === 'reviewer' && $stage <= 1
1130 ||
$stat['_ACCESS'] === 'member' && $stage <= $memberStageLimit
1136 // Always OK for live workspace.
1143 * Returns TRUE if the user has access to publish content from the workspace ID given.
1144 * Admin-users are always granted access to do this
1145 * If the workspace ID is 0 (live) all users have access also
1146 * For custom workspaces it depends on whether the user is owner OR like with
1147 * draft workspace if the user has access to Live workspace.
1149 * @param int $wsid Workspace UID; 0,1+
1150 * @return bool Returns TRUE if the user has access to publish content from the workspace ID given.
1152 public function workspacePublishAccess($wsid)
1154 if ($this->isAdmin()) {
1157 // If no access to workspace, of course you cannot publish!
1159 $wsAccess = $this->checkWorkspace($wsid);
1161 switch ($wsAccess['uid']) {
1164 // If access to Live workspace, no problem.
1169 $retVal = $wsAccess['_ACCESS'] === 'owner' ||
$this->checkWorkspace(0) && !($wsAccess['publish_access'] & Permission
::PAGE_EDIT
);
1170 // Either be an adminuser OR have access to online
1171 // workspace which is OK as well as long as publishing
1172 // access is not limited by workspace option.
1179 * Workspace swap-mode access?
1181 * @return bool Returns TRUE if records can be swapped in the current workspace, otherwise FALSE
1183 public function workspaceSwapAccess()
1185 if ($this->workspace
> 0 && (int)$this->workspaceRec
['swap_modes'] === 2) {
1192 * Returns the value/properties of a TS-object as given by $objectString, eg. 'options.dontMountAdminMounts'
1193 * Nice (general!) function for returning a part of a TypoScript array!
1195 * @param string $objectString Pointer to an "object" in the TypoScript array, fx. 'options.dontMountAdminMounts'
1196 * @param array|string $config Optional TSconfig array: If array, then this is used and not $this->userTS. If not array, $this->userTS is used.
1197 * @return array An array with two keys, "value" and "properties" where "value" is a string with the value of the object string and "properties" is an array with the properties of the object string.
1199 public function getTSConfig($objectString, $config = '')
1201 if (!is_array($config)) {
1202 // Getting Root-ts if not sent
1203 $config = $this->userTS
;
1205 $TSConf = ['value' => null, 'properties' => null];
1206 $parts = GeneralUtility
::trimExplode('.', $objectString, true, 2);
1209 if (count($parts) > 1 && $parts[1] !== '') {
1210 // Go on, get the next level
1211 if (is_array($config[$key . '.'] ??
false)) {
1212 $TSConf = $this->getTSConfig($parts[1], $config[$key . '.']);
1215 $TSConf['value'] = $config[$key] ??
null;
1216 $TSConf['properties'] = $config[$key . '.'] ??
null;
1223 * Returns the "value" of the $objectString from the BE_USERS "User TSconfig" array
1225 * @param string $objectString Object string, eg. "somestring.someproperty.somesubproperty
1226 * @return string The value for that object string (object path)
1227 * @see getTSConfig()
1229 public function getTSConfigVal($objectString)
1231 $TSConf = $this->getTSConfig($objectString);
1232 return $TSConf['value'];
1236 * Returns the "properties" of the $objectString from the BE_USERS "User TSconfig" array
1238 * @param string $objectString Object string, eg. "somestring.someproperty.somesubproperty
1239 * @return array The properties for that object string (object path) - if any
1240 * @see getTSConfig()
1242 public function getTSConfigProp($objectString)
1244 $TSConf = $this->getTSConfig($objectString);
1245 return $TSConf['properties'];
1249 * Returns an array with the webmounts.
1250 * If no webmounts, and empty array is returned.
1251 * NOTICE: Deleted pages WILL NOT be filtered out! So if a mounted page has been deleted
1252 * it is STILL coming out as a webmount. This is not checked due to performance.
1256 public function returnWebmounts()
1258 return (string)$this->groupData
['webmounts'] != '' ?
explode(',', $this->groupData
['webmounts']) : [];
1262 * Initializes the given mount points for the current Backend user.
1264 * @param array $mountPointUids Page UIDs that should be used as web mountpoints
1265 * @param bool $append If TRUE the given mount point will be appended. Otherwise the current mount points will be replaced.
1267 public function setWebmounts(array $mountPointUids, $append = false)
1269 if (empty($mountPointUids)) {
1273 $currentWebMounts = GeneralUtility
::intExplode(',', $this->groupData
['webmounts']);
1274 $mountPointUids = array_merge($currentWebMounts, $mountPointUids);
1276 $this->groupData
['webmounts'] = implode(',', array_unique($mountPointUids));
1280 * Returns TRUE or FALSE, depending if an alert popup (a javascript confirmation) should be shown
1281 * call like $GLOBALS['BE_USER']->jsConfirmation($BITMASK).
1283 * @param int $bitmask Bitmask, one of \TYPO3\CMS\Core\Type\Bitmask\JsConfirmation
1284 * @return bool TRUE if the confirmation should be shown
1285 * @see JsConfirmation
1287 public function jsConfirmation($bitmask)
1290 $alertPopupsSetting = trim((string)$this->getTSConfig('options.alertPopups')['value']);
1291 $alertPopup = JsConfirmation
::cast($alertPopupsSetting === '' ?
null : (int)$alertPopupsSetting);
1292 } catch (InvalidEnumerationValueException
$e) {
1293 $alertPopup = new JsConfirmation();
1296 return JsConfirmation
::cast($bitmask)->matches($alertPopup);
1300 * Initializes a lot of stuff like the access-lists, database-mountpoints and filemountpoints
1301 * This method is called by ->backendCheckLogin() (from extending BackendUserAuthentication)
1302 * if the backend user login has verified OK.
1303 * Generally this is required initialization of a backend user.
1306 * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
1308 public function fetchGroupData()
1310 if ($this->user
['uid']) {
1311 // Get lists for the be_user record and set them as default/primary values.
1312 // Enabled Backend Modules
1313 $this->dataLists
['modList'] = $this->user
['userMods'];
1314 // Add Allowed Languages
1315 $this->dataLists
['allowed_languages'] = $this->user
['allowed_languages'];
1316 // Set user value for workspace permissions.
1317 $this->dataLists
['workspace_perms'] = $this->user
['workspace_perms'];
1318 // Database mountpoints
1319 $this->dataLists
['webmount_list'] = $this->user
['db_mountpoints'];
1321 $this->dataLists
['filemount_list'] = $this->user
['file_mountpoints'];
1322 // Fileoperation permissions
1323 $this->dataLists
['file_permissions'] = $this->user
['file_permissions'];
1324 // Setting default User TSconfig:
1325 $this->TSdataArray
[] = $this->addTScomment('From $GLOBALS["TYPO3_CONF_VARS"]["BE"]["defaultUserTSconfig"]:')
1326 . $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig'];
1327 // Default TSconfig for admin-users
1328 if ($this->isAdmin()) {
1329 $this->TSdataArray
[] = $this->addTScomment('"admin" user presets:') . '
1330 admPanel.enable.all = 1
1332 if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility
::isLoaded('sys_note')) {
1333 $this->TSdataArray
[] = '
1334 // Setting defaults for sys_note author / email...
1335 TCAdefaults.sys_note.author = ' . $this->user
['realName'] . '
1336 TCAdefaults.sys_note.email = ' . $this->user
['email'] . '
1341 // Get the groups...
1342 if (!empty($this->user
[$this->usergroup_column
])) {
1343 // Fetch groups will add a lot of information to the internal arrays: modules, accesslists, TSconfig etc.
1344 // Refer to fetchGroups() function.
1345 $this->fetchGroups($this->user
[$this->usergroup_column
]);
1348 // Populating the $this->userGroupsUID -array with the groups in the order in which they were LAST included.!!
1349 $this->userGroupsUID
= array_reverse(array_unique(array_reverse($this->includeGroupArray
)));
1350 // Finally this is the list of group_uid's in the order they are parsed (including subgroups!)
1351 // and without duplicates (duplicates are presented with their last entrance in the list,
1352 // which thus reflects the order of the TypoScript in TSconfig)
1353 $this->groupList
= implode(',', $this->userGroupsUID
);
1354 $this->setCachedList($this->groupList
);
1356 // Add the TSconfig for this specific user:
1357 $this->TSdataArray
[] = $this->addTScomment('USER TSconfig field') . $this->user
['TSconfig'];
1358 // Check include lines.
1359 $this->TSdataArray
= \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
::checkIncludeLines_array($this->TSdataArray
);
1360 // Imploding with "[global]" will make sure that non-ended confinements with braces are ignored.
1361 $this->userTS_text
= implode(LF
. '[GLOBAL]' . LF
, $this->TSdataArray
);
1362 if (!$this->userTS_dontGetCached
) {
1363 // Perform TS-Config parsing with condition matching
1364 $parseObj = GeneralUtility
::makeInstance(\TYPO3\CMS\Backend\Configuration\TsConfigParser
::class);
1365 $res = $parseObj->parseTSconfig($this->userTS_text
, 'userTS');
1367 $this->userTS
= $res['TSconfig'];
1368 $this->userTSUpdated
= (bool)$res['cached'];
1371 // Parsing the user TSconfig (or getting from cache)
1372 $hash = md5('userTS:' . $this->userTS_text
);
1373 $cachedContent = BackendUtility
::getHash($hash);
1374 if (is_array($cachedContent) && !$this->userTS_dontGetCached
) {
1375 $this->userTS
= $cachedContent;
1377 $parseObj = GeneralUtility
::makeInstance(\TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
::class);
1378 $parseObj->parse($this->userTS_text
);
1379 $this->userTS
= $parseObj->setup
;
1380 BackendUtility
::storeHash($hash, $this->userTS
, 'BE_USER_TSconfig');
1382 $this->userTSUpdated
= true;
1385 // Processing webmounts
1386 // Admin's always have the root mounted
1387 if ($this->isAdmin() && !$this->getTSConfigVal('options.dontMountAdminMounts')) {
1388 $this->dataLists
['webmount_list'] = '0,' . $this->dataLists
['webmount_list'];
1390 // The lists are cleaned for duplicates
1391 $this->groupData
['webmounts'] = GeneralUtility
::uniqueList($this->dataLists
['webmount_list']);
1392 $this->groupData
['pagetypes_select'] = GeneralUtility
::uniqueList($this->dataLists
['pagetypes_select']);
1393 $this->groupData
['tables_select'] = GeneralUtility
::uniqueList($this->dataLists
['tables_modify'] . ',' . $this->dataLists
['tables_select']);
1394 $this->groupData
['tables_modify'] = GeneralUtility
::uniqueList($this->dataLists
['tables_modify']);
1395 $this->groupData
['non_exclude_fields'] = GeneralUtility
::uniqueList($this->dataLists
['non_exclude_fields']);
1396 $this->groupData
['explicit_allowdeny'] = GeneralUtility
::uniqueList($this->dataLists
['explicit_allowdeny']);
1397 $this->groupData
['allowed_languages'] = GeneralUtility
::uniqueList($this->dataLists
['allowed_languages']);
1398 $this->groupData
['custom_options'] = GeneralUtility
::uniqueList($this->dataLists
['custom_options']);
1399 $this->groupData
['modules'] = GeneralUtility
::uniqueList($this->dataLists
['modList']);
1400 $this->groupData
['file_permissions'] = GeneralUtility
::uniqueList($this->dataLists
['file_permissions']);
1401 $this->groupData
['workspace_perms'] = $this->dataLists
['workspace_perms'];
1403 // Checking read access to webmounts:
1404 if (trim($this->groupData
['webmounts']) !== '') {
1405 $webmounts = explode(',', $this->groupData
['webmounts']);
1407 // Selecting all webmounts with permission clause for reading
1408 $queryBuilder = GeneralUtility
::makeInstance(ConnectionPool
::class)->getQueryBuilderForTable('pages');
1409 $queryBuilder->getRestrictions()
1411 ->add(GeneralUtility
::makeInstance(DeletedRestriction
::class));
1413 $MProws = $queryBuilder->select('uid')
1415 // @todo DOCTRINE: check how to make getPagePermsClause() portable
1417 $this->getPagePermsClause(1),
1418 $queryBuilder->expr()->in(
1420 $queryBuilder->createNamedParameter(
1421 GeneralUtility
::intExplode(',', $this->groupData
['webmounts']),
1422 Connection
::PARAM_INT_ARRAY
1428 $MProws = array_column(($MProws ?
: []), 'uid', 'uid');
1429 foreach ($webmounts as $idx => $mountPointUid) {
1430 // If the mount ID is NOT found among selected pages, unset it:
1431 if ($mountPointUid > 0 && !isset($MProws[$mountPointUid])) {
1432 unset($webmounts[$idx]);
1435 // Implode mounts in the end.
1436 $this->groupData
['webmounts'] = implode(',', $webmounts);
1438 // Setting up workspace situation (after webmounts are processed!):
1439 $this->workspaceInit();
1444 * Fetches the group records, subgroups and fills internal arrays.
1445 * Function is called recursively to fetch subgroups
1447 * @param string $grList Commalist of be_groups uid numbers
1448 * @param string $idList List of already processed be_groups-uids so the function will not fall into an eternal recursion.
1451 public function fetchGroups($grList, $idList = '')
1453 // Fetching records of the groups in $grList (which are not blocked by lockedToDomain either):
1454 $queryBuilder = GeneralUtility
::makeInstance(ConnectionPool
::class)->getQueryBuilderForTable($this->usergroup_table
);
1455 $expressionBuilder = $queryBuilder->expr();
1456 $constraints = $expressionBuilder->andX(
1457 $expressionBuilder->eq(
1459 $queryBuilder->createNamedParameter(0, \PDO
::PARAM_INT
)
1461 $expressionBuilder->in(
1463 $queryBuilder->createNamedParameter(
1464 GeneralUtility
::intExplode(',', $grList),
1465 Connection
::PARAM_INT_ARRAY
1468 $expressionBuilder->orX(
1469 $expressionBuilder->eq('lockToDomain', $queryBuilder->quote('')),
1470 $expressionBuilder->isNull('lockToDomain'),
1471 $expressionBuilder->eq(
1473 $queryBuilder->createNamedParameter(GeneralUtility
::getIndpEnv('HTTP_HOST'), \PDO
::PARAM_STR
)
1477 // Hook for manipulation of the WHERE sql sentence which controls which BE-groups are included
1478 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroupQuery'])) {
1479 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroupQuery'] as $className) {
1480 $hookObj = GeneralUtility
::makeInstance($className);
1481 if (method_exists($hookObj, 'fetchGroupQuery_processQuery')) {
1482 $constraints = $hookObj->fetchGroupQuery_processQuery($this, $grList, $idList, (string)$constraints);
1486 $res = $queryBuilder->select('*')
1487 ->from($this->usergroup_table
)
1488 ->where($constraints)
1490 // The userGroups array is filled
1491 while ($row = $res->fetch(\PDO
::FETCH_ASSOC
)) {
1492 $this->userGroups
[$row['uid']] = $row;
1494 // Traversing records in the correct order
1495 foreach (explode(',', $grList) as $uid) {
1497 $row = $this->userGroups
[$uid];
1498 // Must be an array and $uid should not be in the idList, because then it is somewhere previously in the grouplist
1499 if (is_array($row) && !GeneralUtility
::inList($idList, $uid)) {
1500 // Include sub groups
1501 if (trim($row['subgroup'])) {
1502 // Make integer list
1503 $theList = implode(',', GeneralUtility
::intExplode(',', $row['subgroup']));
1504 // Call recursively, pass along list of already processed groups so they are not recursed again.
1505 $this->fetchGroups($theList, $idList . ',' . $uid);
1507 // Add the group uid, current list, TSconfig to the internal arrays.
1508 $this->includeGroupArray
[] = $uid;
1509 $this->TSdataArray
[] = $this->addTScomment('Group "' . $row['title'] . '" [' . $row['uid'] . '] TSconfig field:') . $row['TSconfig'];
1510 // Mount group database-mounts
1511 if (($this->user
['options'] & Permission
::PAGE_SHOW
) == 1) {
1512 $this->dataLists
['webmount_list'] .= ',' . $row['db_mountpoints'];
1514 // Mount group file-mounts
1515 if (($this->user
['options'] & Permission
::PAGE_EDIT
) == 2) {
1516 $this->dataLists
['filemount_list'] .= ',' . $row['file_mountpoints'];
1518 // The lists are made: groupMods, tables_select, tables_modify, pagetypes_select, non_exclude_fields, explicit_allowdeny, allowed_languages, custom_options
1519 $this->dataLists
['modList'] .= ',' . $row['groupMods'];
1520 $this->dataLists
['tables_select'] .= ',' . $row['tables_select'];
1521 $this->dataLists
['tables_modify'] .= ',' . $row['tables_modify'];
1522 $this->dataLists
['pagetypes_select'] .= ',' . $row['pagetypes_select'];
1523 $this->dataLists
['non_exclude_fields'] .= ',' . $row['non_exclude_fields'];
1524 $this->dataLists
['explicit_allowdeny'] .= ',' . $row['explicit_allowdeny'];
1525 $this->dataLists
['allowed_languages'] .= ',' . $row['allowed_languages'];
1526 $this->dataLists
['custom_options'] .= ',' . $row['custom_options'];
1527 $this->dataLists
['file_permissions'] .= ',' . $row['file_permissions'];
1528 // Setting workspace permissions:
1529 $this->dataLists
['workspace_perms'] |
= $row['workspace_perms'];
1530 // If this function is processing the users OWN group-list (not subgroups) AND
1531 // if the ->firstMainGroup is not set, then the ->firstMainGroup will be set.
1532 if ($idList === '' && !$this->firstMainGroup
) {
1533 $this->firstMainGroup
= $uid;
1537 // HOOK: fetchGroups_postProcessing
1538 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroups_postProcessing'])) {
1539 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroups_postProcessing'] as $_funcRef) {
1541 GeneralUtility
::callUserFunction($_funcRef, $_params, $this);
1547 * Updates the field be_users.usergroup_cached_list if the groupList of the user
1548 * has changed/is different from the current list.
1549 * The field "usergroup_cached_list" contains the list of groups which the user is a member of.
1550 * After authentication (where these functions are called...) one can depend on this list being
1551 * a representation of the exact groups/subgroups which the BE_USER has membership with.
1553 * @param string $cList The newly compiled group-list which must be compared with the current list in the user record and possibly stored if a difference is detected.
1556 public function setCachedList($cList)
1558 if ((string)$cList != (string)$this->user
['usergroup_cached_list']) {
1559 GeneralUtility
::makeInstance(ConnectionPool
::class)->getConnectionForTable('be_users')->update(
1561 ['usergroup_cached_list' => $cList],
1562 ['uid' => (int)$this->user
['uid']]
1568 * Sets up all file storages for a user.
1569 * Needs to be called AFTER the groups have been loaded.
1571 protected function initializeFileStorages()
1573 $this->fileStorages
= [];
1574 /** @var $storageRepository \TYPO3\CMS\Core\Resource\StorageRepository */
1575 $storageRepository = GeneralUtility
::makeInstance(\TYPO3\CMS\Core\
Resource\StorageRepository
::class);
1576 // Admin users have all file storages visible, without any filters
1577 if ($this->isAdmin()) {
1578 $storageObjects = $storageRepository->findAll();
1579 foreach ($storageObjects as $storageObject) {
1580 $this->fileStorages
[$storageObject->getUid()] = $storageObject;
1583 // Regular users only have storages that are defined in their filemounts
1584 // Permissions and file mounts for the storage are added in StoragePermissionAspect
1585 foreach ($this->getFileMountRecords() as $row) {
1586 if (!array_key_exists((int)$row['base'], $this->fileStorages
)) {
1587 $storageObject = $storageRepository->findByUid($row['base']);
1588 if ($storageObject) {
1589 $this->fileStorages
[$storageObject->getUid()] = $storageObject;
1595 // This has to be called always in order to set certain filters
1596 $this->evaluateUserSpecificFileFilterSettings();
1600 * Returns an array of category mount points. The category permissions from BE Groups
1601 * are also taken into consideration and are merged into User permissions.
1605 public function getCategoryMountPoints()
1607 $categoryMountPoints = '';
1609 // Category mounts of the groups
1610 if (is_array($this->userGroups
)) {
1611 foreach ($this->userGroups
as $group) {
1612 if ($group['category_perms']) {
1613 $categoryMountPoints .= ',' . $group['category_perms'];
1618 // Category mounts of the user record
1619 if ($this->user
['category_perms']) {
1620 $categoryMountPoints .= ',' . $this->user
['category_perms'];
1623 // Make the ids unique
1624 $categoryMountPoints = GeneralUtility
::trimExplode(',', $categoryMountPoints);
1625 $categoryMountPoints = array_filter($categoryMountPoints); // remove empty value
1626 $categoryMountPoints = array_unique($categoryMountPoints); // remove unique value
1628 return $categoryMountPoints;
1632 * Returns an array of file mount records, taking workspaces and user home and group home directories into account
1633 * Needs to be called AFTER the groups have been loaded.
1638 public function getFileMountRecords()
1640 static $fileMountRecordCache = [];
1642 if (!empty($fileMountRecordCache)) {
1643 return $fileMountRecordCache;
1646 $connectionPool = GeneralUtility
::makeInstance(ConnectionPool
::class);
1648 // Processing file mounts (both from the user and the groups)
1649 $fileMounts = array_unique(GeneralUtility
::intExplode(',', $this->dataLists
['filemount_list'], true));
1651 // Limit file mounts if set in workspace record
1652 if ($this->workspace
> 0 && !empty($this->workspaceRec
['file_mountpoints'])) {
1653 $workspaceFileMounts = GeneralUtility
::intExplode(',', $this->workspaceRec
['file_mountpoints'], true);
1654 $fileMounts = array_intersect($fileMounts, $workspaceFileMounts);
1657 if (!empty($fileMounts)) {
1658 $orderBy = $GLOBALS['TCA']['sys_filemounts']['ctrl']['default_sortby'] ??
'sorting';
1660 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_filemounts');
1661 $queryBuilder->getRestrictions()
1663 ->add(GeneralUtility
::makeInstance(DeletedRestriction
::class))
1664 ->add(GeneralUtility
::makeInstance(HiddenRestriction
::class))
1665 ->add(GeneralUtility
::makeInstance(RootLevelRestriction
::class));
1667 $queryBuilder->select('*')
1668 ->from('sys_filemounts')
1670 $queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($fileMounts, Connection
::PARAM_INT_ARRAY
))
1673 foreach (QueryHelper
::parseOrderBy($orderBy) as $fieldAndDirection) {
1674 $queryBuilder->addOrderBy(...$fieldAndDirection);
1677 $fileMountRecords = $queryBuilder->execute()->fetchAll(\PDO
::FETCH_ASSOC
);
1678 if ($fileMountRecords !== false) {
1679 foreach ($fileMountRecords as $fileMount) {
1680 $fileMountRecordCache[$fileMount['base'] . $fileMount['path']] = $fileMount;
1685 // Read-only file mounts
1686 $readOnlyMountPoints = trim($GLOBALS['BE_USER']->getTSConfigVal('options.folderTree.altElementBrowserMountPoints'));
1687 if ($readOnlyMountPoints) {
1688 // We cannot use the API here but need to fetch the default storage record directly
1689 // to not instantiate it (which directly applies mount points) before all mount points are resolved!
1690 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_file_storage');
1691 $defaultStorageRow = $queryBuilder->select('uid')
1692 ->from('sys_file_storage')
1694 $queryBuilder->expr()->eq('is_default', $queryBuilder->createNamedParameter(1, \PDO
::PARAM_INT
))
1698 ->fetch(\PDO
::FETCH_ASSOC
);
1700 $readOnlyMountPointArray = GeneralUtility
::trimExplode(',', $readOnlyMountPoints);
1701 foreach ($readOnlyMountPointArray as $readOnlyMountPoint) {
1702 $readOnlyMountPointConfiguration = GeneralUtility
::trimExplode(':', $readOnlyMountPoint);
1703 if (count($readOnlyMountPointConfiguration) === 2) {
1704 // A storage is passed in the configuration
1705 $storageUid = (int)$readOnlyMountPointConfiguration[0];
1706 $path = $readOnlyMountPointConfiguration[1];
1708 if (empty($defaultStorageRow)) {
1709 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);
1711 // Backwards compatibility: If no storage is passed, we use the default storage
1712 $storageUid = $defaultStorageRow['uid'];
1713 $path = $readOnlyMountPointConfiguration[0];
1715 $fileMountRecordCache[$storageUid . $path] = [
1716 'base' => $storageUid,
1724 // Personal or Group filemounts are not accessible if file mount list is set in workspace record
1725 if ($this->workspace
<= 0 ||
empty($this->workspaceRec
['file_mountpoints'])) {
1726 // If userHomePath is set, we attempt to mount it
1727 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath']) {
1728 list($userHomeStorageUid, $userHomeFilter) = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath'], 2);
1729 $userHomeStorageUid = (int)$userHomeStorageUid;
1730 $userHomeFilter = '/' . ltrim($userHomeFilter, '/');
1731 if ($userHomeStorageUid > 0) {
1732 // Try and mount with [uid]_[username]
1733 $path = $userHomeFilter . $this->user
['uid'] . '_' . $this->user
['username'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1734 $fileMountRecordCache[$userHomeStorageUid . $path] = [
1735 'base' => $userHomeStorageUid,
1736 'title' => $this->user
['username'],
1738 'read_only' => false,
1739 'user_mount' => true
1741 // Try and mount with only [uid]
1742 $path = $userHomeFilter . $this->user
['uid'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1743 $fileMountRecordCache[$userHomeStorageUid . $path] = [
1744 'base' => $userHomeStorageUid,
1745 'title' => $this->user
['username'],
1747 'read_only' => false,
1748 'user_mount' => true
1753 // Mount group home-dirs
1754 if ((is_array($this->user
) && $this->user
['options'] & Permission
::PAGE_EDIT
) == 2 && $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'] != '') {
1755 // If groupHomePath is set, we attempt to mount it
1756 list($groupHomeStorageUid, $groupHomeFilter) = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'], 2);
1757 $groupHomeStorageUid = (int)$groupHomeStorageUid;
1758 $groupHomeFilter = '/' . ltrim($groupHomeFilter, '/');
1759 if ($groupHomeStorageUid > 0) {
1760 foreach ($this->userGroups
as $groupData) {
1761 $path = $groupHomeFilter . $groupData['uid'];
1762 $fileMountRecordCache[$groupHomeStorageUid . $path] = [
1763 'base' => $groupHomeStorageUid,
1764 'title' => $groupData['title'],
1766 'read_only' => false,
1767 'user_mount' => true
1774 return $fileMountRecordCache;
1778 * Returns an array with the filemounts for the user.
1779 * Each filemount is represented with an array of a "name", "path" and "type".
1780 * If no filemounts an empty array is returned.
1783 * @return \TYPO3\CMS\Core\Resource\ResourceStorage[]
1785 public function getFileStorages()
1787 // Initializing file mounts after the groups are fetched
1788 if ($this->fileStorages
=== null) {
1789 $this->initializeFileStorages();
1791 return $this->fileStorages
;
1795 * Adds filters based on what the user has set
1796 * this should be done in this place, and called whenever needed,
1797 * but only when needed
1799 public function evaluateUserSpecificFileFilterSettings()
1801 // Add the option for also displaying the non-hidden files
1802 if ($this->uc
['showHiddenFilesAndFolders']) {
1803 \TYPO3\CMS\Core\
Resource\Filter\FileNameFilter
::setShowHiddenFilesAndFolders(true);
1808 * Returns the information about file permissions.
1809 * Previously, this was stored in the DB field fileoper_perms now it is file_permissions.
1810 * Besides it can be handled via userTSconfig
1812 * permissions.file.default {
1828 * recursivedeleteFolder = 1
1831 * # overwrite settings for a specific storageObject
1832 * permissions.file.storage.StorageUid {
1834 * recursivedeleteFolder = 0
1837 * Please note that these permissions only apply, if the storage has the
1838 * capabilities (browseable, writable), and if the driver allows for writing etc
1843 public function getFilePermissions()
1845 if (!isset($this->filePermissions
)) {
1846 $filePermissions = [
1849 'readFile' => false,
1850 'writeFile' => false,
1851 'copyFile' => false,
1852 'moveFile' => false,
1853 'renameFile' => false,
1854 'deleteFile' => false,
1855 // Folder permissions
1856 'addFolder' => false,
1857 'readFolder' => false,
1858 'writeFolder' => false,
1859 'copyFolder' => false,
1860 'moveFolder' => false,
1861 'renameFolder' => false,
1862 'deleteFolder' => false,
1863 'recursivedeleteFolder' => false
1865 if ($this->isAdmin()) {
1866 $filePermissions = array_map('is_bool', $filePermissions);
1868 $userGroupRecordPermissions = GeneralUtility
::trimExplode(',', $this->groupData
['file_permissions'], true);
1870 $userGroupRecordPermissions,
1871 function ($permission) use (&$filePermissions) {
1872 $filePermissions[$permission] = true;
1876 // Finally overlay any userTSconfig
1877 $permissionsTsConfig = $this->getTSConfigProp('permissions.file.default');
1878 if (!empty($permissionsTsConfig)) {
1880 $permissionsTsConfig,
1881 function ($value, $permission) use (&$filePermissions) {
1882 $filePermissions[$permission] = (bool)$value;
1887 $this->filePermissions
= $filePermissions;
1889 return $this->filePermissions
;
1893 * Gets the file permissions for a storage
1894 * by merging any storage-specific permissions for a
1895 * storage with the default settings.
1896 * Admin users will always get the default settings.
1899 * @param \TYPO3\CMS\Core\Resource\ResourceStorage $storageObject
1902 public function getFilePermissionsForStorage(\TYPO3\CMS\Core\
Resource\ResourceStorage
$storageObject)
1904 $finalUserPermissions = $this->getFilePermissions();
1905 if (!$this->isAdmin()) {
1906 $storageFilePermissions = $this->getTSConfigProp('permissions.file.storage.' . $storageObject->getUid());
1907 if (!empty($storageFilePermissions)) {
1909 $storageFilePermissions,
1910 function ($value, $permission) use (&$finalUserPermissions) {
1911 $finalUserPermissions[$permission] = (bool)$value;
1916 return $finalUserPermissions;
1920 * Returns a \TYPO3\CMS\Core\Resource\Folder object that is used for uploading
1922 * This is used for RTE and its magic images, as well as uploads
1923 * in the TCEforms fields.
1925 * The default upload folder for a user is the defaultFolder on the first
1926 * filestorage/filemount that the user can access and to which files are allowed to be added
1927 * however, you can set the users' upload folder like this:
1929 * options.defaultUploadFolder = 3:myfolder/yourfolder/
1931 * @param int $pid PageUid
1932 * @param string $table Table name
1933 * @param string $field Field name
1934 * @return \TYPO3\CMS\Core\Resource\Folder|bool The default upload folder for this user
1936 public function getDefaultUploadFolder($pid = null, $table = null, $field = null)
1938 $uploadFolder = $this->getTSConfigVal('options.defaultUploadFolder');
1939 if ($uploadFolder) {
1940 $uploadFolder = \TYPO3\CMS\Core\
Resource\ResourceFactory
::getInstance()->getFolderObjectFromCombinedIdentifier($uploadFolder);
1942 foreach ($this->getFileStorages() as $storage) {
1943 if ($storage->isDefault() && $storage->isWritable()) {
1945 $uploadFolder = $storage->getDefaultFolder();
1946 if ($uploadFolder->checkActionPermission('write')) {
1949 $uploadFolder = null;
1950 } catch (\TYPO3\CMS\Core\
Resource\Exception
$folderAccessException) {
1951 // If the folder is not accessible (no permissions / does not exist) we skip this one.
1956 if (!$uploadFolder instanceof \TYPO3\CMS\Core\
Resource\Folder
) {
1957 /** @var ResourceStorage $storage */
1958 foreach ($this->getFileStorages() as $storage) {
1959 if ($storage->isWritable()) {
1961 $uploadFolder = $storage->getDefaultFolder();
1962 if ($uploadFolder->checkActionPermission('write')) {
1965 $uploadFolder = null;
1966 } catch (\TYPO3\CMS\Core\
Resource\Exception
$folderAccessException) {
1967 // If the folder is not accessible (no permissions / does not exist) try the next one.
1974 // HOOK: getDefaultUploadFolder
1975 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getDefaultUploadFolder'])) {
1976 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getDefaultUploadFolder'] as $_funcRef) {
1978 'uploadFolder' => $uploadFolder,
1983 $uploadFolder = GeneralUtility
::callUserFunction($_funcRef, $_params, $this);
1987 if ($uploadFolder instanceof \TYPO3\CMS\Core\
Resource\Folder
) {
1988 return $uploadFolder;
1994 * Returns a \TYPO3\CMS\Core\Resource\Folder object that could be used for uploading
1995 * temporary files in user context. The folder _temp_ below the default upload folder
1996 * of the user is used.
1998 * @return \TYPO3\CMS\Core\Resource\Folder|null
1999 * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::getDefaultUploadFolder();
2001 public function getDefaultUploadTemporaryFolder()
2003 $defaultTemporaryFolder = null;
2004 $defaultFolder = $this->getDefaultUploadFolder();
2006 if ($defaultFolder !== false) {
2007 $tempFolderName = '_temp_';
2008 $createFolder = !$defaultFolder->hasFolder($tempFolderName);
2009 if ($createFolder === true) {
2011 $defaultTemporaryFolder = $defaultFolder->createFolder($tempFolderName);
2012 } catch (\TYPO3\CMS\Core\
Resource\Exception
$folderAccessException) {
2015 $defaultTemporaryFolder = $defaultFolder->getSubfolder($tempFolderName);
2019 return $defaultTemporaryFolder;
2023 * Creates a TypoScript comment with the string text inside.
2025 * @param string $str The text to wrap in comment prefixes and delimiters.
2026 * @return string TypoScript comment with the string text inside.
2028 public function addTScomment($str)
2030 $delimiter = '# ***********************************************';
2031 $out = $delimiter . LF
;
2032 $lines = GeneralUtility
::trimExplode(LF
, $str);
2033 foreach ($lines as $v) {
2034 $out .= '# ' . $v . LF
;
2036 $out .= $delimiter . LF
;
2041 * Initializing workspace.
2042 * Called from within this function, see fetchGroupData()
2044 * @see fetchGroupData()
2046 public function workspaceInit()
2048 // Initializing workspace by evaluating and setting the workspace, possibly updating it in the user record!
2049 $this->setWorkspace($this->user
['workspace_id']);
2050 // Limiting the DB mountpoints if there any selected in the workspace record
2051 $this->initializeDbMountpointsInWorkspace();
2052 if ($allowed_languages = $this->getTSConfigVal('options.workspaces.allowed_languages.' . $this->workspace
)) {
2053 $this->groupData
['allowed_languages'] = $allowed_languages;
2054 $this->groupData
['allowed_languages'] = GeneralUtility
::uniqueList($this->groupData
['allowed_languages']);
2059 * Limiting the DB mountpoints if there any selected in the workspace record
2061 protected function initializeDbMountpointsInWorkspace()
2063 $dbMountpoints = trim($this->workspaceRec
['db_mountpoints'] ??
'');
2064 if ($this->workspace
> 0 && $dbMountpoints != '') {
2065 $filteredDbMountpoints = [];
2066 // Notice: We cannot call $this->getPagePermsClause(1);
2067 // as usual because the group-list is not available at this point.
2068 // But bypassing is fine because all we want here is check if the
2069 // workspace mounts are inside the current webmounts rootline.
2070 // The actual permission checking on page level is done elsewhere
2071 // as usual anyway before the page tree is rendered.
2073 // Traverse mount points of the
2074 $dbMountpoints = GeneralUtility
::intExplode(',', $dbMountpoints);
2075 foreach ($dbMountpoints as $mpId) {
2076 if ($this->isInWebMount($mpId, $readPerms)) {
2077 $filteredDbMountpoints[] = $mpId;
2080 // Re-insert webmounts:
2081 $filteredDbMountpoints = array_unique($filteredDbMountpoints);
2082 $this->groupData
['webmounts'] = implode(',', $filteredDbMountpoints);
2087 * Checking if a workspace is allowed for backend user
2089 * @param mixed $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 and -1 (online and offline)
2090 * @param string $fields List of fields to select. Default fields are: uid,title,adminusers,members,reviewers,publish_access,stagechg_notification
2091 * @return array Output will also show how access was granted. Admin users will have a true output regardless of input.
2093 public function checkWorkspace($wsRec, $fields = 'uid,title,adminusers,members,reviewers,publish_access,stagechg_notification')
2096 // If not array, look up workspace record:
2097 if (!is_array($wsRec)) {
2098 switch ((string)$wsRec) {
2100 $wsRec = ['uid' => $wsRec];
2103 if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility
::isLoaded('workspaces')) {
2104 $queryBuilder = GeneralUtility
::makeInstance(ConnectionPool
::class)->getQueryBuilderForTable('sys_workspace');
2105 $queryBuilder->getRestrictions()->add(GeneralUtility
::makeInstance(RootLevelRestriction
::class));
2106 $wsRec = $queryBuilder->select(...GeneralUtility
::trimExplode(',', $fields))
2107 ->from('sys_workspace')
2108 ->where($queryBuilder->expr()->eq(
2110 $queryBuilder->createNamedParameter($wsRec, \PDO
::PARAM_INT
)
2115 ->fetch(\PDO
::FETCH_ASSOC
);
2119 // If wsRec is set to an array, evaluate it:
2120 if (is_array($wsRec)) {
2121 if ($this->isAdmin()) {
2122 return array_merge($wsRec, ['_ACCESS' => 'admin']);
2124 switch ((string)$wsRec['uid']) {
2126 $retVal = $this->groupData
['workspace_perms'] & Permission
::PAGE_SHOW
2127 ?
array_merge($wsRec, ['_ACCESS' => 'online'])
2131 // Checking if the guy is admin:
2132 if (GeneralUtility
::inList($wsRec['adminusers'], 'be_users_' . $this->user
['uid'])) {
2133 return array_merge($wsRec, ['_ACCESS' => 'owner']);
2135 // Checking if he is owner through a user group of his:
2136 foreach ($this->userGroupsUID
as $groupUid) {
2137 if (GeneralUtility
::inList($wsRec['adminusers'], 'be_groups_' . $groupUid)) {
2138 return array_merge($wsRec, ['_ACCESS' => 'owner']);
2141 // Checking if he is reviewer user:
2142 if (GeneralUtility
::inList($wsRec['reviewers'], 'be_users_' . $this->user
['uid'])) {
2143 return array_merge($wsRec, ['_ACCESS' => 'reviewer']);
2145 // Checking if he is reviewer through a user group of his:
2146 foreach ($this->userGroupsUID
as $groupUid) {
2147 if (GeneralUtility
::inList($wsRec['reviewers'], 'be_groups_' . $groupUid)) {
2148 return array_merge($wsRec, ['_ACCESS' => 'reviewer']);
2151 // Checking if he is member as user:
2152 if (GeneralUtility
::inList($wsRec['members'], 'be_users_' . $this->user
['uid'])) {
2153 return array_merge($wsRec, ['_ACCESS' => 'member']);
2155 // Checking if he is member through a user group of his:
2156 foreach ($this->userGroupsUID
as $groupUid) {
2157 if (GeneralUtility
::inList($wsRec['members'], 'be_groups_' . $groupUid)) {
2158 return array_merge($wsRec, ['_ACCESS' => 'member']);
2167 * Uses checkWorkspace() to check if current workspace is available for user.
2168 * This function caches the result and so can be called many times with no performance loss.
2170 * @return array See checkWorkspace()
2171 * @see checkWorkspace()
2173 public function checkWorkspaceCurrent()
2175 if (!isset($this->checkWorkspaceCurrent_cache
)) {
2176 $this->checkWorkspaceCurrent_cache
= $this->checkWorkspace($this->workspace
);
2178 return $this->checkWorkspaceCurrent_cache
;
2182 * Setting workspace ID
2184 * @param int $workspaceId ID of workspace to set for backend user. If not valid the default workspace for BE user is found and set.
2186 public function setWorkspace($workspaceId)
2188 // Check workspace validity and if not found, revert to default workspace.
2189 if (!$this->setTemporaryWorkspace($workspaceId)) {
2190 $this->setDefaultWorkspace();
2192 // Unset access cache:
2193 $this->checkWorkspaceCurrent_cache
= null;
2194 // If ID is different from the stored one, change it:
2195 if ((int)$this->workspace
!== (int)$this->user
['workspace_id']) {
2196 $this->user
['workspace_id'] = $this->workspace
;
2197 GeneralUtility
::makeInstance(ConnectionPool
::class)->getConnectionForTable('be_users')->update(
2199 ['workspace_id' => $this->user
['workspace_id']],
2200 ['uid' => (int)$this->user
['uid']]
2202 $this->simplelog('User changed workspace to "' . $this->workspace
. '"');
2207 * Sets a temporary workspace in the context of the current backend user.
2209 * @param int $workspaceId
2212 public function setTemporaryWorkspace($workspaceId)
2215 $workspaceRecord = $this->checkWorkspace($workspaceId, '*');
2217 if ($workspaceRecord) {
2218 $this->workspaceRec
= $workspaceRecord;
2219 $this->workspace
= (int)$workspaceId;
2227 * Sets the default workspace in the context of the current backend user.
2229 public function setDefaultWorkspace()
2231 $this->workspace
= (int)$this->getDefaultWorkspace();
2232 $this->workspaceRec
= $this->checkWorkspace($this->workspace
, '*');
2236 * Setting workspace preview state for user:
2238 * @param bool $previewState State of user preview.
2240 public function setWorkspacePreview($previewState)
2242 $this->user
['workspace_preview'] = $previewState;
2243 GeneralUtility
::makeInstance(ConnectionPool
::class)->getConnectionForTable('be_users')->update(
2245 ['workspace_preview_id' => $this->user
['workspace_preview']],
2246 ['uid' => (int)$this->user
['uid']]
2251 * Return default workspace ID for user,
2252 * If EXT:workspaces is not installed the user will be pushed the the
2255 * @return int Default workspace id. If no workspace is available it will be "-99
2257 public function getDefaultWorkspace()
2259 $defaultWorkspace = -99;
2260 if (!\TYPO3\CMS\Core\Utility\ExtensionManagementUtility
::isLoaded('workspaces') ||
$this->checkWorkspace(0)) {
2262 $defaultWorkspace = 0;
2263 } elseif ($this->checkWorkspace(-1)) {
2265 $defaultWorkspace = -1;
2266 } elseif (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility
::isLoaded('workspaces')) {
2267 // Traverse custom workspaces:
2268 $queryBuilder = GeneralUtility
::makeInstance(ConnectionPool
::class)->getQueryBuilderForTable('sys_workspace');
2269 $queryBuilder->getRestrictions()->add(GeneralUtility
::makeInstance(RootLevelRestriction
::class));
2270 $workspaces = $queryBuilder->select('uid', 'title', 'adminusers', 'reviewers')
2271 ->from('sys_workspace')
2274 ->fetchAll(\PDO
::FETCH_ASSOC
);
2276 if ($workspaces !== false) {
2277 foreach ($workspaces as $rec) {
2278 if ($this->checkWorkspace($rec)) {
2279 $defaultWorkspace = $rec['uid'];
2285 return $defaultWorkspace;
2289 * Writes an entry in the logfile/table
2290 * Documentation in "TYPO3 Core API"
2292 * @param int $type Denotes which module that has submitted the entry. See "TYPO3 Core API". Use "4" for extensions.
2293 * @param int $action Denotes which specific operation that wrote the entry. Use "0" when no sub-categorizing applies
2294 * @param int $error Flag. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin)
2295 * @param int $details_nr The message number. Specific for each $type and $action. This will make it possible to translate errormessages to other languages
2296 * @param string $details Default text that follows the message (in english!). Possibly translated by identification through type/action/details_nr
2297 * @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
2298 * @param string $tablename Table name. Special field used by tce_main.php.
2299 * @param int|string $recuid Record UID. Special field used by tce_main.php.
2300 * @param int|string $recpid Record PID. Special field used by tce_main.php. OBSOLETE
2301 * @param int $event_pid The page_uid (pid) where the event occurred. Used to select log-content for specific pages.
2302 * @param string $NEWid Special field used by tce_main.php. NEWid string of newly created records.
2303 * @param int $userId Alternative Backend User ID (used for logging login actions where this is not yet known).
2304 * @return int Log entry ID.
2306 public function writelog($type, $action, $error, $details_nr, $details, $data, $tablename = '', $recuid = '', $recpid = '', $event_pid = -1, $NEWid = '', $userId = 0)
2308 if (!$userId && !empty($this->user
['uid'])) {
2309 $userId = $this->user
['uid'];
2312 if (!empty($this->user
['ses_backuserid'])) {
2316 $data['originalUser'] = $this->user
['ses_backuserid'];
2320 'userid' => (int)$userId,
2321 'type' => (int)$type,
2322 'action' => (int)$action,
2323 'error' => (int)$error,
2324 'details_nr' => (int)$details_nr,
2325 'details' => $details,
2326 'log_data' => serialize($data),
2327 'tablename' => $tablename,
2328 'recuid' => (int)$recuid,
2329 'IP' => (string)GeneralUtility
::getIndpEnv('REMOTE_ADDR'),
2330 'tstamp' => $GLOBALS['EXEC_TIME'] ??
time(),
2331 'event_pid' => (int)$event_pid,
2333 'workspace' => $this->workspace
2336 $connection = GeneralUtility
::makeInstance(ConnectionPool
::class)->getConnectionForTable('sys_log');
2337 $connection->insert(
2358 return (int)$connection->lastInsertId('sys_log');
2362 * Simple logging function
2364 * @param string $message Log message
2365 * @param string $extKey Option extension key / module name
2366 * @param int $error Error level. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin)
2367 * @return int Log entry UID
2369 public function simplelog($message, $extKey = '', $error = 0)
2371 return $this->writelog(4, 0, $error, 0, ($extKey ?
'[' . $extKey . '] ' : '') . $message, []);
2375 * Sends a warning to $email if there has been a certain amount of failed logins during a period.
2376 * If a login fails, this function is called. It will look up the sys_log to see if there
2377 * have been more than $max failed logins the last $secondsBack seconds (default 3600).
2378 * If so, an email with a warning is sent to $email.
2380 * @param string $email Email address
2381 * @param int $secondsBack Number of sections back in time to check. This is a kind of limit for how many failures an hour for instance.
2382 * @param int $max Max allowed failures before a warning mail is sent
2385 public function checkLogFailures($email, $secondsBack = 3600, $max = 3)
2388 $connectionPool = GeneralUtility
::makeInstance(ConnectionPool
::class);
2390 // Get last flag set in the log for sending
2391 $theTimeBack = $GLOBALS['EXEC_TIME'] - $secondsBack;
2392 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
2393 $queryBuilder->select('tstamp')
2396 $queryBuilder->expr()->eq(
2398 $queryBuilder->createNamedParameter(255, \PDO
::PARAM_INT
)
2400 $queryBuilder->expr()->eq(
2402 $queryBuilder->createNamedParameter(4, \PDO
::PARAM_INT
)
2404 $queryBuilder->expr()->gt(
2406 $queryBuilder->createNamedParameter($theTimeBack, \PDO
::PARAM_INT
)
2409 ->orderBy('tstamp', 'DESC')
2411 if ($testRow = $queryBuilder->execute()->fetch(\PDO
::FETCH_ASSOC
)) {
2412 $theTimeBack = $testRow['tstamp'];
2415 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
2416 $result = $queryBuilder->select('*')
2419 $queryBuilder->expr()->eq(
2421 $queryBuilder->createNamedParameter(255, \PDO
::PARAM_INT
)
2423 $queryBuilder->expr()->eq(
2425 $queryBuilder->createNamedParameter(3, \PDO
::PARAM_INT
)
2427 $queryBuilder->expr()->neq(
2429 $queryBuilder->createNamedParameter(0, \PDO
::PARAM_INT
)
2431 $queryBuilder->expr()->gt(
2433 $queryBuilder->createNamedParameter($theTimeBack, \PDO
::PARAM_INT
)
2439 // Check for more than $max number of error failures with the last period.
2440 if ($result->rowCount() > $max) {
2441 // OK, so there were more than the max allowed number of login failures - so we will send an email then.
2442 $subject = 'TYPO3 Login Failure Warning (at ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . ')';
2443 $email_body = 'There have been some attempts (' . $result->rowCount() . ') to login at the TYPO3
2444 site "' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . '" (' . GeneralUtility
::getIndpEnv('HTTP_HOST') . ').
2446 This is a dump of the failures:
2449 while ($row = $result->fetch(\PDO
::FETCH_ASSOC
)) {
2450 $theData = unserialize($row['log_data']);
2451 $email_body .= date(
2452 $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
2454 ) . ': ' . @sprintf
($row['details'], (string)$theData[0], (string)$theData[1], (string)$theData[2]);
2457 /** @var $mail \TYPO3\CMS\Core\Mail\MailMessage */
2458 $mail = GeneralUtility
::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage
::class);
2459 $mail->setTo($email)->setSubject($subject)->setBody($email_body);
2461 // Logout written to log
2462 $this->writelog(255, 4, 0, 3, 'Failure warning (%s failures within %s seconds) sent by email to %s', [$result->rowCount(), $secondsBack, $email]);
2468 * Getter for the cookie name
2471 * @return string returns the configured cookie name
2473 public static function getCookieName()
2475 $configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']);
2476 if (empty($configuredCookieName)) {
2477 $configuredCookieName = 'be_typo_user';
2479 return $configuredCookieName;
2483 * If TYPO3_CONF_VARS['BE']['enabledBeUserIPLock'] is enabled and
2484 * an IP-list is found in the User TSconfig objString "options.lockToIP",
2485 * then make an IP comparison with REMOTE_ADDR and check if the IP address matches
2487 * @return bool TRUE, if IP address validates OK (or no check is done at all because no restriction is set)
2489 public function checkLockToIP()
2492 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['enabledBeUserIPLock']) {
2493 $IPList = $this->getTSConfigVal('options.lockToIP');
2494 if (trim($IPList)) {
2495 $isValid = GeneralUtility
::cmpIP(GeneralUtility
::getIndpEnv('REMOTE_ADDR'), $IPList);
2502 * Check if user is logged in and if so, call ->fetchGroupData() to load group information and
2503 * access lists of all kind, further check IP, set the ->uc array and send login-notification email if required.
2504 * If no user is logged in the default behaviour is to exit with an error message.
2505 * This function is called right after ->start() in fx. the TYPO3 Bootstrap.
2507 * @param bool $proceedIfNoUserIsLoggedIn if this option is set, then there won't be a redirect to the login screen of the Backend - used for areas in the backend which do not need user rights like the login page.
2508 * @throws \RuntimeException
2510 public function backendCheckLogin($proceedIfNoUserIsLoggedIn = false)
2512 if (empty($this->user
['uid'])) {
2513 if ($proceedIfNoUserIsLoggedIn === false) {
2514 $url = GeneralUtility
::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir
;
2515 \TYPO3\CMS\Core\Utility\HttpUtility
::redirect($url);
2518 // ...and if that's the case, call these functions
2519 $this->fetchGroupData();
2520 // The groups are fetched and ready for permission checking in this initialization.
2521 // Tables.php must be read before this because stuff like the modules has impact in this
2522 if ($this->checkLockToIP()) {
2523 if ($this->isUserAllowedToLogin()) {
2524 // Setting the UC array. It's needed with fetchGroupData first, due to default/overriding of values.
2525 $this->backendSetUC();
2526 // Email at login - if option set.
2527 $this->emailAtLogin();
2529 throw new \
RuntimeException('Login Error: TYPO3 is in maintenance mode at the moment. Only administrators are allowed access.', 1294585860);
2532 throw new \
RuntimeException('Login Error: IP locking prevented you from being authorized. Can\'t proceed, sorry.', 1294585861);
2538 * Initialize the internal ->uc array for the backend user
2539 * Will make the overrides if necessary, and write the UC back to the be_users record if changes has happened
2543 public function backendSetUC()
2545 // UC - user configuration is a serialized array inside the user object
2546 // If there is a saved uc we implement that instead of the default one.
2548 // Setting defaults if uc is empty
2551 if (is_array($this->uc
) && isset($this->uc
['ucSetByInstallTool'])) {
2552 $originalUc = $this->uc
;
2553 unset($originalUc['ucSetByInstallTool'], $this->uc
);
2555 if (!is_array($this->uc
)) {
2556 $this->uc
= array_merge(
2558 (array)$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUC'],
2559 GeneralUtility
::removeDotsFromTS((array)$this->getTSConfigProp('setup.default')),
2562 $this->overrideUC();
2565 // If TSconfig is updated, update the defaultUC.
2566 if ($this->userTSUpdated
) {
2567 $this->overrideUC();
2570 // Setting default lang from be_user record.
2571 if (!isset($this->uc
['lang'])) {
2572 $this->uc
['lang'] = $this->user
['lang'];
2575 // Setting the time of the first login:
2576 if (!isset($this->uc
['firstLoginTimeStamp'])) {
2577 $this->uc
['firstLoginTimeStamp'] = $GLOBALS['EXEC_TIME'];
2580 // Saving if updated.
2587 * Override: Call this function every time the uc is updated.
2588 * That is 1) by reverting to default values, 2) in the setup-module, 3) userTS changes (userauthgroup)
2592 public function overrideUC()
2594 $this->uc
= array_merge((array)$this->uc
, (array)$this->getTSConfigProp('setup.override'));
2598 * Clears the user[uc] and ->uc to blank strings. Then calls ->backendSetUC() to fill it again with reset contents
2602 public function resetUC()
2604 $this->user
['uc'] = '';
2606 $this->backendSetUC();
2610 * Will send an email notification to warning_email_address/the login users email address when a login session is just started.
2611 * Depends on various parameters whether mails are send and to whom.
2615 private function emailAtLogin()
2617 if ($this->loginSessionStarted
) {
2619 $subject = 'At "' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . '"' . ' from '
2620 . GeneralUtility
::getIndpEnv('REMOTE_ADDR')
2621 . (GeneralUtility
::getIndpEnv('REMOTE_HOST') ?
' (' . GeneralUtility
::getIndpEnv('REMOTE_HOST') . ')' : '');
2623 'User "%s" logged in from %s (%s) at "%s" (%s)',
2624 $this->user
['username'],
2625 GeneralUtility
::getIndpEnv('REMOTE_ADDR'),
2626 GeneralUtility
::getIndpEnv('REMOTE_HOST'),
2627 $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'],
2628 GeneralUtility
::getIndpEnv('HTTP_HOST')
2630 // Warning email address
2631 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr']) {
2634 if ((int)$GLOBALS['TYPO3_CONF_VARS']['BE']['warning_mode'] & 1) {
2635 // first bit: All logins
2637 $prefix = $this->isAdmin() ?
'[AdminLoginWarning]' : '[LoginWarning]';
2639 if ($this->isAdmin() && (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['warning_mode'] & 2) {
2640 // second bit: Only admin-logins
2642 $prefix = '[AdminLoginWarning]';
2645 /** @var $mail \TYPO3\CMS\Core\Mail\MailMessage */
2646 $mail = GeneralUtility
::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage
::class);
2647 $mail->setTo($GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr'])->setSubject($prefix . ' ' . $subject)->setBody($msg);
2651 // If An email should be sent to the current user, do that:
2652 if ($this->uc
['emailMeAtLogin'] && strstr($this->user
['email'], '@')) {
2653 /** @var $mail \TYPO3\CMS\Core\Mail\MailMessage */
2654 $mail = GeneralUtility
::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage
::class);
2655 $mail->setTo($this->user
['email'])->setSubject($subject)->setBody($msg);
2662 * Determines whether a backend user is allowed to access the backend.
2664 * The conditions are:
2665 * + backend user is a regular user and adminOnly is not defined
2666 * + backend user is an admin user
2667 * + backend user is used in CLI context and adminOnly is explicitly set to "2" (see CommandLineUserAuthentication)
2668 * + backend user is being controlled by an admin user
2670 * @return bool Whether a backend user is allowed to access the backend
2672 protected function isUserAllowedToLogin()
2674 $isUserAllowedToLogin = false;
2675 $adminOnlyMode = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'];
2676 // Backend user is allowed if adminOnly is not set or user is an admin:
2677 if (!$adminOnlyMode ||
$this->isAdmin()) {
2678 $isUserAllowedToLogin = true;
2679 } elseif ($this->user
['ses_backuserid']) {
2680 $backendUserId = (int)$this->user
['ses_backuserid'];
2681 $queryBuilder = GeneralUtility
::makeInstance(ConnectionPool
::class)->getQueryBuilderForTable('be_users');
2682 $isUserAllowedToLogin = (bool)$queryBuilder->count('uid')
2685 $queryBuilder->expr()->eq(
2687 $queryBuilder->createNamedParameter($backendUserId, \PDO
::PARAM_INT
)
2689 $queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, \PDO
::PARAM_INT
))
2694 return $isUserAllowedToLogin;
2698 * Logs out the current user and clears the form protection tokens.
2700 public function logoff()
2702 if (isset($GLOBALS['BE_USER']) && $GLOBALS['BE_USER'] instanceof self
&& isset($GLOBALS['BE_USER']->user
['uid'])) {
2703 \TYPO3\CMS\Core\FormProtection\FormProtectionFactory
::get()->clean();