[TASK] Remove further evaluations of pid=-1
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Authentication / BackendUserAuthentication.php
1 <?php
2 namespace TYPO3\CMS\Core\Authentication;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
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.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Cache\CacheManager;
19 use TYPO3\CMS\Core\Database\Connection;
20 use TYPO3\CMS\Core\Database\ConnectionPool;
21 use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
22 use TYPO3\CMS\Core\Database\Query\QueryHelper;
23 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
24 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
25 use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
26 use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction;
27 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
28 use TYPO3\CMS\Core\Resource\ResourceStorage;
29 use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
30 use TYPO3\CMS\Core\Type\Bitmask\Permission;
31 use TYPO3\CMS\Core\Type\Exception\InvalidEnumerationValueException;
32 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
33 use TYPO3\CMS\Core\Utility\GeneralUtility;
34 use TYPO3\CMS\Install\Service\SessionService;
35
36 /**
37 * TYPO3 backend user authentication
38 * Contains most of the functions used for checking permissions, authenticating users,
39 * setting up the user, and API for user from outside.
40 * This class contains the configuration of the database fields used plus some
41 * functions for the authentication process of backend users.
42 */
43 class BackendUserAuthentication extends AbstractUserAuthentication
44 {
45 public const ROLE_SYSTEMMAINTAINER = 'systemMaintainer';
46
47 /**
48 * Should be set to the usergroup-column (id-list) in the user-record
49 * @var string
50 */
51 public $usergroup_column = 'usergroup';
52
53 /**
54 * The name of the group-table
55 * @var string
56 */
57 public $usergroup_table = 'be_groups';
58
59 /**
60 * holds lists of eg. tables, fields and other values related to the permission-system. See fetchGroupData
61 * @var array
62 * @internal
63 */
64 public $groupData = [
65 'filemounts' => []
66 ];
67
68 /**
69 * This array will hold the groups that the user is a member of
70 * @var array
71 */
72 public $userGroups = [];
73
74 /**
75 * This array holds the uid's of the groups in the listed order
76 * @var array
77 */
78 public $userGroupsUID = [];
79
80 /**
81 * This is $this->userGroupsUID imploded to a comma list... Will correspond to the 'usergroup_cached_list'
82 * @var string
83 */
84 public $groupList = '';
85
86 /**
87 * User workspace.
88 * -99 is ERROR (none available)
89 * 0 is online
90 * >0 is custom workspaces
91 * @var int
92 */
93 public $workspace = -99;
94
95 /**
96 * Custom workspace record if any
97 * @var array
98 */
99 public $workspaceRec = [];
100
101 /**
102 * Used to accumulate data for the user-group.
103 * DON NOT USE THIS EXTERNALLY!
104 * Use $this->groupData instead
105 * @var array
106 * @internal
107 */
108 public $dataLists = [
109 'webmount_list' => '',
110 'filemount_list' => '',
111 'file_permissions' => '',
112 'modList' => '',
113 'tables_select' => '',
114 'tables_modify' => '',
115 'pagetypes_select' => '',
116 'non_exclude_fields' => '',
117 'explicit_allowdeny' => '',
118 'allowed_languages' => '',
119 'workspace_perms' => '',
120 'custom_options' => ''
121 ];
122
123 /**
124 * List of group_id's in the order they are processed.
125 * @var array
126 */
127 public $includeGroupArray = [];
128
129 /**
130 * @var array Accumulated, unparsed TSconfig data array of the user
131 */
132 protected $TSdataArray = [];
133
134 /**
135 * @var string Accumulated, unparsed TSconfig data string of the user
136 */
137 protected $userTS_text = '';
138
139 /**
140 * @var array Parsed user TSconfig
141 */
142 protected $userTS = [];
143
144 /**
145 * @var bool True if the user TSconfig was parsed and needs to be cached.
146 */
147 protected $userTSUpdated = false;
148
149 /**
150 * Contains last error message
151 * @var string
152 */
153 public $errorMsg = '';
154
155 /**
156 * Cache for checkWorkspaceCurrent()
157 * @var array|null
158 */
159 protected $checkWorkspaceCurrent_cache;
160
161 /**
162 * @var \TYPO3\CMS\Core\Resource\ResourceStorage[]
163 */
164 protected $fileStorages;
165
166 /**
167 * @var array
168 */
169 protected $filePermissions;
170
171 /**
172 * Table in database with user data
173 * @var string
174 */
175 public $user_table = 'be_users';
176
177 /**
178 * Column for login-name
179 * @var string
180 */
181 public $username_column = 'username';
182
183 /**
184 * Column for password
185 * @var string
186 */
187 public $userident_column = 'password';
188
189 /**
190 * Column for user-id
191 * @var string
192 */
193 public $userid_column = 'uid';
194
195 /**
196 * @var string
197 */
198 public $lastLogin_column = 'lastlogin';
199
200 /**
201 * @var array
202 */
203 public $enablecolumns = [
204 'rootLevel' => 1,
205 'deleted' => 'deleted',
206 'disabled' => 'disable',
207 'starttime' => 'starttime',
208 'endtime' => 'endtime'
209 ];
210
211 /**
212 * Form field with login-name
213 * @var string
214 */
215 public $formfield_uname = 'username';
216
217 /**
218 * Form field with password
219 * @var string
220 */
221 public $formfield_uident = 'userident';
222
223 /**
224 * Form field with status: *'login', 'logout'
225 * @var string
226 */
227 public $formfield_status = 'login_status';
228
229 /**
230 * Decides if the writelog() function is called at login and logout
231 * @var bool
232 */
233 public $writeStdLog = true;
234
235 /**
236 * If the writelog() functions is called if a login-attempt has be tried without success
237 * @var bool
238 */
239 public $writeAttemptLog = true;
240
241 /**
242 * Session timeout (on the server), defaults to 8 hours for backend user
243 *
244 * If >0: session-timeout in seconds.
245 * If <=0: Instant logout after login.
246 * The value must be at least 180 to avoid side effects.
247 *
248 * @var int
249 */
250 public $sessionTimeout = 28800;
251
252 /**
253 * @var int
254 */
255 public $firstMainGroup = 0;
256
257 /**
258 * User Config
259 * @var array
260 */
261 public $uc;
262
263 /**
264 * User Config Default values:
265 * The array may contain other fields for configuration.
266 * For this, see "setup" extension and "TSconfig" document (User TSconfig, "setup.[xxx]....")
267 * Reserved keys for other storage of session data:
268 * moduleData
269 * moduleSessionID
270 * @var array
271 */
272 public $uc_default = [
273 'interfaceSetup' => '',
274 // serialized content that is used to store interface pane and menu positions. Set by the logout.php-script
275 'moduleData' => [],
276 // user-data for the modules
277 'thumbnailsByDefault' => 1,
278 'emailMeAtLogin' => 0,
279 'startModule' => 'help_AboutAbout',
280 'titleLen' => 50,
281 'edit_RTE' => '1',
282 'edit_docModuleUpload' => '1',
283 'resizeTextareas' => 1,
284 'resizeTextareas_MaxHeight' => 500,
285 'resizeTextareas_Flexible' => 0
286 ];
287
288 /**
289 * Login type, used for services.
290 * @var string
291 */
292 public $loginType = 'BE';
293
294 /**
295 * Constructor
296 */
297 public function __construct()
298 {
299 $this->name = self::getCookieName();
300 $this->warningEmail = $GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr'];
301 $this->lockIP = $GLOBALS['TYPO3_CONF_VARS']['BE']['lockIP'];
302 $this->sessionTimeout = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['sessionTimeout'];
303 parent::__construct();
304 }
305
306 /**
307 * Returns TRUE if user is admin
308 * Basically this function evaluates if the ->user[admin] field has bit 0 set. If so, user is admin.
309 *
310 * @return bool
311 */
312 public function isAdmin()
313 {
314 return is_array($this->user) && ($this->user['admin'] & 1) == 1;
315 }
316
317 /**
318 * Returns TRUE if the current user is a member of group $groupId
319 * $groupId must be set. $this->groupList must contain groups
320 * Will return TRUE also if the user is a member of a group through subgroups.
321 *
322 * @param int $groupId Group ID to look for in $this->groupList
323 * @return bool
324 */
325 public function isMemberOfGroup($groupId)
326 {
327 $groupId = (int)$groupId;
328 if ($this->groupList && $groupId) {
329 return GeneralUtility::inList($this->groupList, $groupId);
330 }
331 return false;
332 }
333
334 /**
335 * Checks if the permissions is granted based on a page-record ($row) and $perms (binary and'ed)
336 *
337 * Bits for permissions, see $perms variable:
338 *
339 * 1 - Show: See/Copy page and the pagecontent.
340 * 2 - Edit page: Change/Move the page, eg. change title, startdate, hidden.
341 * 4 - Delete page: Delete the page and pagecontent.
342 * 8 - New pages: Create new pages under the page.
343 * 16 - Edit pagecontent: Change/Add/Delete/Move pagecontent.
344 *
345 * @param array $row Is the pagerow for which the permissions is checked
346 * @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.
347 * @return bool
348 */
349 public function doesUserHaveAccess($row, $perms)
350 {
351 $userPerms = $this->calcPerms($row);
352 return ($userPerms & $perms) == $perms;
353 }
354
355 /**
356 * Checks if the page id, $id, is found within the webmounts set up for the user.
357 * This should ALWAYS be checked for any page id a user works with, whether it's about reading, writing or whatever.
358 * The point is that this will add the security that a user can NEVER touch parts outside his mounted
359 * pages in the page tree. This is otherwise possible if the raw page permissions allows for it.
360 * So this security check just makes it easier to make safe user configurations.
361 * If the user is admin OR if this feature is disabled
362 * (fx. by setting TYPO3_CONF_VARS['BE']['lockBeUserToDBmounts']=0) then it returns "1" right away
363 * Otherwise the function will return the uid of the webmount which was first found in the rootline of the input page $id
364 *
365 * @param int $id Page ID to check
366 * @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!)
367 * @param bool|int $exitOnError If set, then the function will exit with an error message.
368 * @throws \RuntimeException
369 * @return int|null The page UID of a page in the rootline that matched a mount point
370 */
371 public function isInWebMount($id, $readPerms = '', $exitOnError = 0)
372 {
373 if (!$GLOBALS['TYPO3_CONF_VARS']['BE']['lockBeUserToDBmounts'] || $this->isAdmin()) {
374 return 1;
375 }
376 $id = (int)$id;
377 // Check if input id is an offline version page in which case we will map id to the online version:
378 $checkRec = BackendUtility::getRecord(
379 'pages',
380 $id,
381 't3ver_oid,'
382 . $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'] . ','
383 . $GLOBALS['TCA']['pages']['ctrl']['languageField']
384 );
385 if ((int)$checkRec['t3ver_oid'] > 0) {
386 $id = (int)$checkRec['t3ver_oid'];
387 }
388 // if current rec is a translation then get uid from l10n_parent instead
389 // because web mounts point to pages in default language and rootline returns uids of default languages
390 if ((int)$checkRec[$GLOBALS['TCA']['pages']['ctrl']['languageField']] !== 0 && (int)$checkRec[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] !== 0) {
391 $id = (int)$checkRec[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']];
392 }
393 if (!$readPerms) {
394 $readPerms = $this->getPagePermsClause(Permission::PAGE_SHOW);
395 }
396 if ($id > 0) {
397 $wM = $this->returnWebmounts();
398 $rL = BackendUtility::BEgetRootLine($id, ' AND ' . $readPerms);
399 foreach ($rL as $v) {
400 if ($v['uid'] && in_array($v['uid'], $wM)) {
401 return $v['uid'];
402 }
403 }
404 }
405 if ($exitOnError) {
406 throw new \RuntimeException('Access Error: This page is not within your DB-mounts', 1294586445);
407 }
408 return null;
409 }
410
411 /**
412 * Checks access to a backend module with the $MCONF passed as first argument
413 *
414 * @param array $conf $MCONF array of a backend module!
415 * @throws \RuntimeException
416 * @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
417 */
418 public function modAccess($conf)
419 {
420 if (!BackendUtility::isModuleSetInTBE_MODULES($conf['name'])) {
421 throw new \RuntimeException('Fatal Error: This module "' . $conf['name'] . '" is not enabled in TBE_MODULES', 1294586446);
422 }
423 // Workspaces check:
424 if (
425 !empty($conf['workspaces'])
426 && ExtensionManagementUtility::isLoaded('workspaces')
427 && ($this->workspace !== 0 || !GeneralUtility::inList($conf['workspaces'], 'online'))
428 && ($this->workspace !== -1 || !GeneralUtility::inList($conf['workspaces'], 'offline'))
429 && ($this->workspace <= 0 || !GeneralUtility::inList($conf['workspaces'], 'custom'))
430 ) {
431 throw new \RuntimeException('Workspace Error: This module "' . $conf['name'] . '" is not available under the current workspace', 1294586447);
432 }
433 // Returns false if conf[access] is set to system maintainers and the user is system maintainer
434 if (strpos($conf['access'], self::ROLE_SYSTEMMAINTAINER) !== false && !$this->isSystemMaintainer()) {
435 throw new \RuntimeException('This module "' . $conf['name'] . '" is only available as system maintainer', 1504804727);
436 }
437 // Returns TRUE if conf[access] is not set at all or if the user is admin
438 if (!$conf['access'] || $this->isAdmin()) {
439 return true;
440 }
441 // If $conf['access'] is set but not with 'admin' then we return TRUE, if the module is found in the modList
442 $acs = false;
443 if (strpos($conf['access'], 'admin') === false && $conf['name']) {
444 $acs = $this->check('modules', $conf['name']);
445 }
446 if (!$acs) {
447 throw new \RuntimeException('Access Error: You don\'t have access to this module.', 1294586448);
448 }
449 return $acs;
450 }
451
452 /**
453 * Checks if the user is in the valid list of allowed system maintainers. if the list is not set,
454 * then all admins are system maintainers. If the list is empty, no one is system maintainer (good for production
455 * systems). If the currently logged in user is in "switch user" mode, this method will return false.
456 *
457 * @return bool
458 */
459 public function isSystemMaintainer(): bool
460 {
461 if ((int)$GLOBALS['BE_USER']->user['ses_backuserid'] !== 0) {
462 return false;
463 }
464 if (GeneralUtility::getApplicationContext()->isDevelopment() && $this->isAdmin()) {
465 return true;
466 }
467 $systemMaintainers = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? [];
468 $systemMaintainers = array_map('intval', $systemMaintainers);
469 if (!empty($systemMaintainers)) {
470 return in_array((int)$this->user['uid'], $systemMaintainers, true);
471 }
472 // No system maintainers set up yet, so any admin is allowed to access the modules
473 // but explicitly no system maintainers allowed (empty string in TYPO3_CONF_VARS).
474 // @todo: this needs to be adjusted once system maintainers can log into the install tool with their credentials
475 if (isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'])
476 && empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'])) {
477 return false;
478 }
479 return $this->isAdmin();
480 }
481
482 /**
483 * If a user has actually logged in and switched to a different user (admins can use the SU switch user method)
484 * the real UID is sometimes needed (when checking for permissions for example).
485 */
486 protected function getRealUserId(): int
487 {
488 return (int)($GLOBALS['BE_USER']->user['ses_backuserid'] ?: $this->user['uid']);
489 }
490
491 /**
492 * Returns a WHERE-clause for the pages-table where user permissions according to input argument, $perms, is validated.
493 * $perms is the "mask" used to select. Fx. if $perms is 1 then you'll get all pages that a user can actually see!
494 * 2^0 = show (1)
495 * 2^1 = edit (2)
496 * 2^2 = delete (4)
497 * 2^3 = new (8)
498 * If the user is 'admin' " 1=1" is returned (no effect)
499 * 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)
500 * The 95% use of this function is "->getPagePermsClause(1)" which will
501 * return WHERE clauses for *selecting* pages in backend listings - in other words this will check read permissions.
502 *
503 * @param int $perms Permission mask to use, see function description
504 * @return string Part of where clause. Prefix " AND " to this.
505 */
506 public function getPagePermsClause($perms)
507 {
508 if (is_array($this->user)) {
509 if ($this->isAdmin()) {
510 return ' 1=1';
511 }
512 // Make sure it's integer.
513 $perms = (int)$perms;
514 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
515 ->getQueryBuilderForTable('pages')
516 ->expr();
517
518 // User
519 $constraint = $expressionBuilder->orX(
520 $expressionBuilder->comparison(
521 $expressionBuilder->bitAnd('pages.perms_everybody', $perms),
522 ExpressionBuilder::EQ,
523 $perms
524 ),
525 $expressionBuilder->andX(
526 $expressionBuilder->eq('pages.perms_userid', (int)$this->user['uid']),
527 $expressionBuilder->comparison(
528 $expressionBuilder->bitAnd('pages.perms_user', $perms),
529 ExpressionBuilder::EQ,
530 $perms
531 )
532 )
533 );
534
535 // Group (if any is set)
536 if ($this->groupList) {
537 $constraint->add(
538 $expressionBuilder->andX(
539 $expressionBuilder->in(
540 'pages.perms_groupid',
541 GeneralUtility::intExplode(',', $this->groupList)
542 ),
543 $expressionBuilder->comparison(
544 $expressionBuilder->bitAnd('pages.perms_group', $perms),
545 ExpressionBuilder::EQ,
546 $perms
547 )
548 )
549 );
550 }
551
552 $constraint = ' (' . (string)$constraint . ')';
553
554 // ****************
555 // getPagePermsClause-HOOK
556 // ****************
557 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getPagePermsClause'] ?? [] as $_funcRef) {
558 $_params = ['currentClause' => $constraint, 'perms' => $perms];
559 $constraint = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
560 }
561 return $constraint;
562 }
563 return ' 1=0';
564 }
565
566 /**
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)
571 *
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
574 */
575 public function calcPerms($row)
576 {
577 // Return 31 for admin users.
578 if ($this->isAdmin()) {
579 return Permission::ALL;
580 }
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[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] ?: $row['uid'])) {
584 return Permission::NOTHING;
585 }
586 $out = Permission::NOTHING;
587 if (
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)
590 ) {
591 if ($this->user['uid'] == $row['perms_userid']) {
592 $out |= $row['perms_user'];
593 }
594 if ($this->isMemberOfGroup($row['perms_groupid'])) {
595 $out |= $row['perms_group'];
596 }
597 $out |= $row['perms_everybody'];
598 }
599 // ****************
600 // CALCPERMS hook
601 // ****************
602 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['calcPerms'] ?? [] as $_funcRef) {
603 $_params = [
604 'row' => $row,
605 'outputPermissions' => $out
606 ];
607 $out = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
608 }
609 return $out;
610 }
611
612 /**
613 * Returns TRUE if the RTE (Rich Text Editor) is enabled for the user.
614 *
615 * @return bool
616 */
617 public function isRTE()
618 {
619 return (bool)$this->uc['edit_RTE'];
620 }
621
622 /**
623 * Returns TRUE if the $value is found in the list in a $this->groupData[] index pointed to by $type (array key).
624 * Can thus be users to check for modules, exclude-fields, select/modify permissions for tables etc.
625 * If user is admin TRUE is also returned
626 * Please see the document Inside TYPO3 for examples.
627 *
628 * @param string $type The type value; "webmounts", "filemounts", "pagetypes_select", "tables_select", "tables_modify", "non_exclude_fields", "modules
629 * @param string $value String to search for in the groupData-list
630 * @return bool TRUE if permission is granted (that is, the value was found in the groupData list - or the BE_USER is "admin")
631 */
632 public function check($type, $value)
633 {
634 return isset($this->groupData[$type])
635 && ($this->isAdmin() || GeneralUtility::inList($this->groupData[$type], $value));
636 }
637
638 /**
639 * Checking the authMode of a select field with authMode set
640 *
641 * @param string $table Table name
642 * @param string $field Field name (must be configured in TCA and of type "select" with authMode set!)
643 * @param string $value Value to evaluation (single value, must not contain any of the chars ":,|")
644 * @param string $authMode Auth mode keyword (explicitAllow, explicitDeny, individual)
645 * @return bool Whether access is granted or not
646 */
647 public function checkAuthMode($table, $field, $value, $authMode)
648 {
649 // Admin users can do anything:
650 if ($this->isAdmin()) {
651 return true;
652 }
653 // Allow all blank values:
654 if ((string)$value === '') {
655 return true;
656 }
657 // Certain characters are not allowed in the value
658 if (preg_match('/[:|,]/', $value)) {
659 return false;
660 }
661 // Initialize:
662 $testValue = $table . ':' . $field . ':' . $value;
663 $out = true;
664 // Checking value:
665 switch ((string)$authMode) {
666 case 'explicitAllow':
667 if (!GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':ALLOW')) {
668 $out = false;
669 }
670 break;
671 case 'explicitDeny':
672 if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) {
673 $out = false;
674 }
675 break;
676 case 'individual':
677 if (is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
678 $items = $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'];
679 if (is_array($items)) {
680 foreach ($items as $iCfg) {
681 if ((string)$iCfg[1] === (string)$value && $iCfg[4]) {
682 switch ((string)$iCfg[4]) {
683 case 'EXPL_ALLOW':
684 if (!GeneralUtility::inList(
685 $this->groupData['explicit_allowdeny'],
686 $testValue . ':ALLOW'
687 )) {
688 $out = false;
689 }
690 break;
691 case 'EXPL_DENY':
692 if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) {
693 $out = false;
694 }
695 break;
696 }
697 break;
698 }
699 }
700 }
701 }
702 break;
703 }
704 return $out;
705 }
706
707 /**
708 * Checking if a language value (-1, 0 and >0 for sys_language records) is allowed to be edited by the user.
709 *
710 * @param int $langValue Language value to evaluate
711 * @return bool Returns TRUE if the language value is allowed, otherwise FALSE.
712 */
713 public function checkLanguageAccess($langValue)
714 {
715 // The users language list must be non-blank - otherwise all languages are allowed.
716 if (trim($this->groupData['allowed_languages']) !== '') {
717 $langValue = (int)$langValue;
718 // Language must either be explicitly allowed OR the lang Value be "-1" (all languages)
719 if ($langValue != -1 && !$this->check('allowed_languages', $langValue)) {
720 return false;
721 }
722 }
723 return true;
724 }
725
726 /**
727 * Check if user has access to all existing localizations for a certain record
728 *
729 * @param string $table The table
730 * @param array $record The current record
731 * @return bool
732 */
733 public function checkFullLanguagesAccess($table, $record)
734 {
735 if (!$this->checkLanguageAccess(0)) {
736 return false;
737 }
738
739 if (BackendUtility::isTableLocalizable($table)) {
740 $pointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
741 $pointerValue = $record[$pointerField] > 0 ? $record[$pointerField] : $record['uid'];
742 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
743 $queryBuilder->getRestrictions()
744 ->removeAll()
745 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
746 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
747 $recordLocalizations = $queryBuilder->select('*')
748 ->from($table)
749 ->where(
750 $queryBuilder->expr()->eq(
751 $pointerField,
752 $queryBuilder->createNamedParameter($pointerValue, \PDO::PARAM_INT)
753 )
754 )
755 ->execute()
756 ->fetchAll();
757
758 foreach ($recordLocalizations as $recordLocalization) {
759 if (!$this->checkLanguageAccess($recordLocalization[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
760 return false;
761 }
762 }
763 }
764 return true;
765 }
766
767 /**
768 * Checking if a user has editing access to a record from a $GLOBALS['TCA'] table.
769 * The checks does not take page permissions and other "environmental" things into account.
770 * It only deal with record internals; If any values in the record fields disallows it.
771 * For instance languages settings, authMode selector boxes are evaluated (and maybe more in the future).
772 * It will check for workspace dependent access.
773 * The function takes an ID (int) or row (array) as second argument.
774 *
775 * @param string $table Table name
776 * @param mixed $idOrRow If integer, then this is the ID of the record. If Array this just represents fields in the record.
777 * @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.
778 * @param bool $deletedRecord Set, if testing a deleted record array.
779 * @param bool $checkFullLanguageAccess Set, whenever access to all translations of the record is required
780 * @return bool TRUE if OK, otherwise FALSE
781 */
782 public function recordEditAccessInternals($table, $idOrRow, $newRecord = false, $deletedRecord = false, $checkFullLanguageAccess = false)
783 {
784 if (!isset($GLOBALS['TCA'][$table])) {
785 return false;
786 }
787 // Always return TRUE for Admin users.
788 if ($this->isAdmin()) {
789 return true;
790 }
791 // Fetching the record if the $idOrRow variable was not an array on input:
792 if (!is_array($idOrRow)) {
793 if ($deletedRecord) {
794 $idOrRow = BackendUtility::getRecord($table, $idOrRow, '*', '', false);
795 } else {
796 $idOrRow = BackendUtility::getRecord($table, $idOrRow);
797 }
798 if (!is_array($idOrRow)) {
799 $this->errorMsg = 'ERROR: Record could not be fetched.';
800 return false;
801 }
802 }
803 // Checking languages:
804 if ($table === 'pages' && $checkFullLanguageAccess && !$this->checkFullLanguagesAccess($table, $idOrRow)) {
805 return false;
806 }
807 if ($GLOBALS['TCA'][$table]['ctrl']['languageField']) {
808 // Language field must be found in input row - otherwise it does not make sense.
809 if (isset($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
810 if (!$this->checkLanguageAccess($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
811 $this->errorMsg = 'ERROR: Language was not allowed.';
812 return false;
813 }
814 if (
815 $checkFullLanguageAccess && $idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']] == 0
816 && !$this->checkFullLanguagesAccess($table, $idOrRow)
817 ) {
818 $this->errorMsg = 'ERROR: Related/affected language was not allowed.';
819 return false;
820 }
821 } else {
822 $this->errorMsg = 'ERROR: The "languageField" field named "'
823 . $GLOBALS['TCA'][$table]['ctrl']['languageField'] . '" was not found in testing record!';
824 return false;
825 }
826 }
827 // Checking authMode fields:
828 if (is_array($GLOBALS['TCA'][$table]['columns'])) {
829 foreach ($GLOBALS['TCA'][$table]['columns'] as $fieldName => $fieldValue) {
830 if (isset($idOrRow[$fieldName])) {
831 if (
832 $fieldValue['config']['type'] === 'select' && $fieldValue['config']['authMode']
833 && $fieldValue['config']['authMode_enforce'] === 'strict'
834 ) {
835 if (!$this->checkAuthMode($table, $fieldName, $idOrRow[$fieldName], $fieldValue['config']['authMode'])) {
836 $this->errorMsg = 'ERROR: authMode "' . $fieldValue['config']['authMode']
837 . '" failed for field "' . $fieldName . '" with value "'
838 . $idOrRow[$fieldName] . '" evaluated';
839 return false;
840 }
841 }
842 }
843 }
844 }
845 // Checking "editlock" feature (doesn't apply to new records)
846 if (!$newRecord && $GLOBALS['TCA'][$table]['ctrl']['editlock']) {
847 if (isset($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['editlock']])) {
848 if ($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['editlock']]) {
849 $this->errorMsg = 'ERROR: Record was locked for editing. Only admin users can change this state.';
850 return false;
851 }
852 } else {
853 $this->errorMsg = 'ERROR: The "editLock" field named "' . $GLOBALS['TCA'][$table]['ctrl']['editlock']
854 . '" was not found in testing record!';
855 return false;
856 }
857 }
858 // Checking record permissions
859 // THIS is where we can include a check for "perms_" fields for other records than pages...
860 // Process any hooks
861 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['recordEditAccessInternals'] ?? [] as $funcRef) {
862 $params = [
863 'table' => $table,
864 'idOrRow' => $idOrRow,
865 'newRecord' => $newRecord
866 ];
867 if (!GeneralUtility::callUserFunction($funcRef, $params, $this)) {
868 return false;
869 }
870 }
871 // Finally, return TRUE if all is well.
872 return true;
873 }
874
875 /**
876 * Returns TRUE if the BE_USER is allowed to *create* shortcuts in the backend modules
877 *
878 * @return bool
879 */
880 public function mayMakeShortcut()
881 {
882 return $this->getTSConfig()['options.']['enableBookmarks'] ?? false
883 && !($this->getTSConfig()['options.']['mayNotCreateEditBookmarks'] ?? false);
884 }
885
886 /**
887 * Checking if editing of an existing record is allowed in current workspace if that is offline.
888 * Rules for editing in offline mode:
889 * - record supports versioning and is an offline version from workspace and has the corrent stage
890 * - or record (any) is in a branch where there is a page which is a version from the workspace
891 * and where the stage is not preventing records
892 *
893 * @param string $table Table of record
894 * @param array|int $recData Integer (record uid) or array where fields are at least: pid, t3ver_wsid, t3ver_stage (if versioningWS is set)
895 * @return string String error code, telling the failure state. FALSE=All ok
896 */
897 public function workspaceCannotEditRecord($table, $recData)
898 {
899 // Only test offline spaces:
900 if ($this->workspace !== 0) {
901 if (!is_array($recData)) {
902 $recData = BackendUtility::getRecord(
903 $table,
904 $recData,
905 'pid' . (BackendUtility::isTableWorkspaceEnabled($table) ? ',t3ver_oid,t3ver_wsid,t3ver_stage' : '')
906 );
907 }
908 if (is_array($recData)) {
909 // We are testing a "version" (identified by having a t3ver_oid): it can be edited provided
910 // that workspace matches and versioning is enabled for the table.
911 if ((int)($recData['t3ver_oid'] ?? 0) > 0) {
912 if ((int)$recData['t3ver_wsid'] !== $this->workspace) {
913 // So does workspace match?
914 return 'Workspace ID of record didn\'t match current workspace';
915 }
916 // So is the user allowed to "use" the edit stage within the workspace?
917 return $this->workspaceCheckStageForCurrent(0)
918 ? false
919 : 'User\'s access level did not allow for editing';
920 }
921 // We are testing a "live" record:
922 // For "Live" records, check that PID for table allows editing
923 if ($res = $this->workspaceAllowLiveRecordsInPID($recData['pid'], $table)) {
924 // Live records are OK in this branch, but what about the stage of branch point, if any:
925 // OK
926 return $res > 0
927 ? false
928 : 'Stage for versioning root point and users access level did not allow for editing';
929 }
930 // If not offline and not in versionized branch, output error:
931 return 'Online record was not in versionized branch!';
932 }
933 return 'No record';
934 }
935 // OK because workspace is 0
936 return false;
937 }
938
939 /**
940 * Evaluates if a user is allowed to edit the offline version
941 *
942 * @param string $table Table of record
943 * @param array|int $recData Integer (record uid) or array where fields are at least: pid, t3ver_wsid, t3ver_stage (if versioningWS is set)
944 * @return string String error code, telling the failure state. FALSE=All ok
945 * @see workspaceCannotEditRecord()
946 * @internal this method will be moved to EXT:workspaces
947 */
948 public function workspaceCannotEditOfflineVersion($table, $recData)
949 {
950 if (BackendUtility::isTableWorkspaceEnabled($table)) {
951 if (!is_array($recData)) {
952 $recData = BackendUtility::getRecord($table, $recData, 'uid,pid,t3ver_oid,t3ver_wsid,t3ver_stage');
953 }
954 if (is_array($recData)) {
955 if ((int)$recData['t3ver_oid'] > 0) {
956 return $this->workspaceCannotEditRecord($table, $recData);
957 }
958 return 'Not an offline version';
959 }
960 return 'No record';
961 }
962 return 'Table does not support versioning.';
963 }
964
965 /**
966 * Check if "live" records from $table may be created or edited in this PID.
967 * If the answer is FALSE it means the only valid way to create or edit records in the PID is by versioning
968 * 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
969 * of versioning because the element was within a versionized branch
970 * but NOT ok in terms of the state the root point had!
971 *
972 * @param int $pid PID value to check for. OBSOLETE!
973 * @param string $table Table name
974 * @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.
975 */
976 public function workspaceAllowLiveRecordsInPID($pid, $table)
977 {
978 // Always for Live workspace AND if live-edit is enabled
979 // and tables are completely without versioning it is ok as well.
980 if (
981 $this->workspace === 0
982 || $this->workspaceRec['live_edit'] && !BackendUtility::isTableWorkspaceEnabled($table)
983 || $GLOBALS['TCA'][$table]['ctrl']['versioningWS_alwaysAllowLiveEdit']
984 ) {
985 // OK to create for this table.
986 return 2;
987 }
988 // If the answer is FALSE it means the only valid way to create or edit records in the PID is by versioning
989 return false;
990 }
991
992 /**
993 * Evaluates if a record from $table can be created in $pid
994 *
995 * @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!
996 * @param string $table Table name
997 * @return bool TRUE if OK.
998 */
999 public function workspaceCreateNewRecord($pid, $table)
1000 {
1001 if ($res = $this->workspaceAllowLiveRecordsInPID($pid, $table)) {
1002 // If LIVE records cannot be created in the current PID due to workspace restrictions, prepare creation of placeholder-record
1003 if ($res < 0) {
1004 // Stage for versioning root point and users access level did not allow for editing
1005 return false;
1006 }
1007 } elseif (!BackendUtility::isTableWorkspaceEnabled($table)) {
1008 // So, if no live records were allowed, we have to create a new version of this record:
1009 return false;
1010 }
1011 return true;
1012 }
1013
1014 /**
1015 * Evaluates if auto creation of a version of a record is allowed.
1016 *
1017 * @param string $table Table of the record
1018 * @param int $id UID of record
1019 * @param int $recpid PID of record
1020 * @return bool TRUE if ok.
1021 */
1022 public function workspaceAllowAutoCreation($table, $id, $recpid)
1023 {
1024 // Auto-creation of version: In offline workspace, test if versioning is
1025 // enabled and look for workspace version of input record.
1026 // If there is no versionized record found we will create one and save to that.
1027 if (
1028 $this->workspace !== 0
1029 && BackendUtility::isTableWorkspaceEnabled($table) && $recpid >= 0
1030 && !BackendUtility::getWorkspaceVersionOfRecord($this->workspace, $table, $id, 'uid')
1031 ) {
1032 // There must be no existing version of this record in workspace.
1033 return true;
1034 }
1035 return false;
1036 }
1037
1038 /**
1039 * Checks if an element stage allows access for the user in the current workspace
1040 * In live workspace (= 0) access is always granted for any stage.
1041 * Admins are always allowed.
1042 * An option for custom workspaces allows members to also edit when the stage is "Review"
1043 *
1044 * @param int $stage Stage id from an element: -1,0 = editing, 1 = reviewer, >1 = owner
1045 * @return bool TRUE if user is allowed access
1046 */
1047 public function workspaceCheckStageForCurrent($stage)
1048 {
1049 // Always allow for admins
1050 if ($this->isAdmin()) {
1051 return true;
1052 }
1053 if ($this->workspace !== 0 && ExtensionManagementUtility::isLoaded('workspaces')) {
1054 $stage = (int)$stage;
1055 $stat = $this->checkWorkspaceCurrent();
1056 // Check if custom staging is activated
1057 $workspaceRec = BackendUtility::getRecord('sys_workspace', $stat['uid']);
1058 if ($workspaceRec['custom_stages'] > 0 && $stage !== 0 && $stage !== -10) {
1059 // Get custom stage record
1060 $workspaceStageRec = BackendUtility::getRecord('sys_workspace_stage', $stage);
1061 // Check if the user is responsible for the current stage
1062 if (
1063 $stat['_ACCESS'] === 'owner'
1064 || $stat['_ACCESS'] === 'member'
1065 && GeneralUtility::inList($workspaceStageRec['responsible_persons'], 'be_users_' . $this->user['uid'])
1066 ) {
1067 return true;
1068 }
1069 // Check if the user is in a group which is responsible for the current stage
1070 foreach ($this->userGroupsUID as $groupUid) {
1071 if (
1072 $stat['_ACCESS'] === 'owner'
1073 || $stat['_ACCESS'] === 'member'
1074 && GeneralUtility::inList($workspaceStageRec['responsible_persons'], 'be_groups_' . $groupUid)
1075 ) {
1076 return true;
1077 }
1078 }
1079 } elseif ($stage == -10 || $stage == -20) {
1080 if ($stat['_ACCESS'] === 'owner') {
1081 return true;
1082 }
1083 return false;
1084 } else {
1085 $memberStageLimit = $this->workspaceRec['review_stage_edit'] ? 1 : 0;
1086 if (
1087 $stat['_ACCESS'] === 'owner'
1088 || $stat['_ACCESS'] === 'member' && $stage <= $memberStageLimit
1089 ) {
1090 return true;
1091 }
1092 }
1093 } else {
1094 // Always OK for live workspace.
1095 return true;
1096 }
1097 return false;
1098 }
1099
1100 /**
1101 * Returns TRUE if the user has access to publish content from the workspace ID given.
1102 * Admin-users are always granted access to do this
1103 * If the workspace ID is 0 (live) all users have access also
1104 * For custom workspaces it depends on whether the user is owner OR like with
1105 * draft workspace if the user has access to Live workspace.
1106 *
1107 * @param int $wsid Workspace UID; 0,1+
1108 * @return bool Returns TRUE if the user has access to publish content from the workspace ID given.
1109 * @internal this method will be moved to EXT:workspaces
1110 */
1111 public function workspacePublishAccess($wsid)
1112 {
1113 if ($this->isAdmin()) {
1114 return true;
1115 }
1116 // If no access to workspace, of course you cannot publish!
1117 $retVal = false;
1118 $wsAccess = $this->checkWorkspace($wsid);
1119 if ($wsAccess) {
1120 switch ($wsAccess['uid']) {
1121 case 0:
1122 // Live workspace
1123 // If access to Live workspace, no problem.
1124 $retVal = true;
1125 break;
1126 default:
1127 // Custom workspace
1128 $retVal = $wsAccess['_ACCESS'] === 'owner' || $this->checkWorkspace(0) && !($wsAccess['publish_access'] & Permission::PAGE_EDIT);
1129 // Either be an adminuser OR have access to online
1130 // workspace which is OK as well as long as publishing
1131 // access is not limited by workspace option.
1132 }
1133 }
1134 return $retVal;
1135 }
1136
1137 /**
1138 * Workspace swap-mode access?
1139 *
1140 * @return bool Returns TRUE if records can be swapped in the current workspace, otherwise FALSE
1141 * @internal this method will be moved to EXT:workspaces
1142 */
1143 public function workspaceSwapAccess()
1144 {
1145 if ($this->workspace > 0 && (int)$this->workspaceRec['swap_modes'] === 2) {
1146 return false;
1147 }
1148 return true;
1149 }
1150
1151 /**
1152 * Returns full parsed user TSconfig array, merged with TSconfig from groups.
1153 *
1154 * Example:
1155 * [
1156 * 'options.' => [
1157 * 'fooEnabled' => '0',
1158 * 'fooEnabled.' => [
1159 * 'tt_content' => 1,
1160 * ],
1161 * ],
1162 * ]
1163 *
1164 * @return array Parsed and merged user TSconfig array
1165 */
1166 public function getTSConfig()
1167 {
1168 return $this->userTS;
1169 }
1170
1171 /**
1172 * Returns an array with the webmounts.
1173 * If no webmounts, and empty array is returned.
1174 * NOTICE: Deleted pages WILL NOT be filtered out! So if a mounted page has been deleted
1175 * it is STILL coming out as a webmount. This is not checked due to performance.
1176 *
1177 * @return array
1178 */
1179 public function returnWebmounts()
1180 {
1181 return (string)$this->groupData['webmounts'] != '' ? explode(',', $this->groupData['webmounts']) : [];
1182 }
1183
1184 /**
1185 * Initializes the given mount points for the current Backend user.
1186 *
1187 * @param array $mountPointUids Page UIDs that should be used as web mountpoints
1188 * @param bool $append If TRUE the given mount point will be appended. Otherwise the current mount points will be replaced.
1189 */
1190 public function setWebmounts(array $mountPointUids, $append = false)
1191 {
1192 if (empty($mountPointUids)) {
1193 return;
1194 }
1195 if ($append) {
1196 $currentWebMounts = GeneralUtility::intExplode(',', $this->groupData['webmounts']);
1197 $mountPointUids = array_merge($currentWebMounts, $mountPointUids);
1198 }
1199 $this->groupData['webmounts'] = implode(',', array_unique($mountPointUids));
1200 }
1201
1202 /**
1203 * Checks for alternative web mount points for the element browser.
1204 *
1205 * If there is a temporary mount point active in the page tree it will be used.
1206 *
1207 * If the User TSconfig options.pageTree.altElementBrowserMountPoints is not empty the pages configured
1208 * there are used as web mounts If options.pageTree.altElementBrowserMountPoints.append is enabled,
1209 * they are appended to the existing webmounts.
1210 *
1211 * @internal - do not use in your own extension
1212 */
1213 public function initializeWebmountsForElementBrowser()
1214 {
1215 $alternativeWebmountPoint = (int)$this->getSessionData('pageTree_temporaryMountPoint');
1216 if ($alternativeWebmountPoint) {
1217 $alternativeWebmountPoint = GeneralUtility::intExplode(',', $alternativeWebmountPoint);
1218 $this->setWebmounts($alternativeWebmountPoint);
1219 return;
1220 }
1221
1222 $alternativeWebmountPoints = trim($this->getTSConfig()['options.']['pageTree.']['altElementBrowserMountPoints'] ?? '');
1223 $appendAlternativeWebmountPoints = $this->getTSConfig()['options.']['pageTree.']['altElementBrowserMountPoints.']['append'] ?? '';
1224 if ($alternativeWebmountPoints) {
1225 $alternativeWebmountPoints = GeneralUtility::intExplode(',', $alternativeWebmountPoints);
1226 $this->setWebmounts($alternativeWebmountPoints, $appendAlternativeWebmountPoints);
1227 }
1228 }
1229
1230 /**
1231 * Returns TRUE or FALSE, depending if an alert popup (a javascript confirmation) should be shown
1232 * call like $GLOBALS['BE_USER']->jsConfirmation($BITMASK).
1233 *
1234 * @param int $bitmask Bitmask, one of \TYPO3\CMS\Core\Type\Bitmask\JsConfirmation
1235 * @return bool TRUE if the confirmation should be shown
1236 * @see JsConfirmation
1237 */
1238 public function jsConfirmation($bitmask)
1239 {
1240 try {
1241 $alertPopupsSetting = trim((string)($this->getTSConfig()['options.']['alertPopups'] ?? ''));
1242 $alertPopup = JsConfirmation::cast($alertPopupsSetting === '' ? null : (int)$alertPopupsSetting);
1243 } catch (InvalidEnumerationValueException $e) {
1244 $alertPopup = new JsConfirmation();
1245 }
1246
1247 return JsConfirmation::cast($bitmask)->matches($alertPopup);
1248 }
1249
1250 /**
1251 * Initializes a lot of stuff like the access-lists, database-mountpoints and filemountpoints
1252 * This method is called by ->backendCheckLogin() (from extending BackendUserAuthentication)
1253 * if the backend user login has verified OK.
1254 * Generally this is required initialization of a backend user.
1255 *
1256 * @internal
1257 * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
1258 */
1259 public function fetchGroupData()
1260 {
1261 if ($this->user['uid']) {
1262 // Get lists for the be_user record and set them as default/primary values.
1263 // Enabled Backend Modules
1264 $this->dataLists['modList'] = $this->user['userMods'];
1265 // Add Allowed Languages
1266 $this->dataLists['allowed_languages'] = $this->user['allowed_languages'];
1267 // Set user value for workspace permissions.
1268 $this->dataLists['workspace_perms'] = $this->user['workspace_perms'];
1269 // Database mountpoints
1270 $this->dataLists['webmount_list'] = $this->user['db_mountpoints'];
1271 // File mountpoints
1272 $this->dataLists['filemount_list'] = $this->user['file_mountpoints'];
1273 // Fileoperation permissions
1274 $this->dataLists['file_permissions'] = $this->user['file_permissions'];
1275 // Setting default User TSconfig:
1276 $this->TSdataArray[] = $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig'];
1277 // Default TSconfig for admin-users
1278 if ($this->isAdmin()) {
1279 $this->TSdataArray[] = 'admPanel.enable.all = 1';
1280 if (ExtensionManagementUtility::isLoaded('sys_note')) {
1281 $this->TSdataArray[] = '
1282 // Setting defaults for sys_note author / email...
1283 TCAdefaults.sys_note.author = ' . $this->user['realName'] . '
1284 TCAdefaults.sys_note.email = ' . $this->user['email'] . '
1285 ';
1286 }
1287 }
1288 // BE_GROUPS:
1289 // Get the groups...
1290 if (!empty($this->user[$this->usergroup_column])) {
1291 // Fetch groups will add a lot of information to the internal arrays: modules, accesslists, TSconfig etc.
1292 // Refer to fetchGroups() function.
1293 $this->fetchGroups($this->user[$this->usergroup_column]);
1294 }
1295
1296 // Populating the $this->userGroupsUID -array with the groups in the order in which they were LAST included.!!
1297 $this->userGroupsUID = array_reverse(array_unique(array_reverse($this->includeGroupArray)));
1298 // Finally this is the list of group_uid's in the order they are parsed (including subgroups!)
1299 // and without duplicates (duplicates are presented with their last entrance in the list,
1300 // which thus reflects the order of the TypoScript in TSconfig)
1301 $this->groupList = implode(',', $this->userGroupsUID);
1302 $this->setCachedList($this->groupList);
1303
1304 // Add the TSconfig for this specific user:
1305 $this->TSdataArray[] = $this->user['TSconfig'];
1306 // Check include lines.
1307 $this->TSdataArray = \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines_array($this->TSdataArray);
1308 // Imploding with "[global]" will make sure that non-ended confinements with braces are ignored.
1309 $this->userTS_text = implode(LF . '[GLOBAL]' . LF, $this->TSdataArray);
1310 // Parsing the user TSconfig (or getting from cache)
1311 $hash = md5('userTS:' . $this->userTS_text);
1312 $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
1313 $parseObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::class);
1314 $parseObj->parse($this->userTS_text);
1315 $this->userTS = $parseObj->setup;
1316 $cache->set($hash, $this->userTS, ['ident_BE_USER_TSconfig'], 0);
1317 // Update UC:
1318 $this->userTSUpdated = true;
1319 // Processing webmounts
1320 // Admin's always have the root mounted
1321 if ($this->isAdmin() && !($this->getTSConfig()['options.']['dontMountAdminMounts'] ?? false)) {
1322 $this->dataLists['webmount_list'] = '0,' . $this->dataLists['webmount_list'];
1323 }
1324 // The lists are cleaned for duplicates
1325 $this->groupData['webmounts'] = GeneralUtility::uniqueList($this->dataLists['webmount_list']);
1326 $this->groupData['pagetypes_select'] = GeneralUtility::uniqueList($this->dataLists['pagetypes_select']);
1327 $this->groupData['tables_select'] = GeneralUtility::uniqueList($this->dataLists['tables_modify'] . ',' . $this->dataLists['tables_select']);
1328 $this->groupData['tables_modify'] = GeneralUtility::uniqueList($this->dataLists['tables_modify']);
1329 $this->groupData['non_exclude_fields'] = GeneralUtility::uniqueList($this->dataLists['non_exclude_fields']);
1330 $this->groupData['explicit_allowdeny'] = GeneralUtility::uniqueList($this->dataLists['explicit_allowdeny']);
1331 $this->groupData['allowed_languages'] = GeneralUtility::uniqueList($this->dataLists['allowed_languages']);
1332 $this->groupData['custom_options'] = GeneralUtility::uniqueList($this->dataLists['custom_options']);
1333 $this->groupData['modules'] = GeneralUtility::uniqueList($this->dataLists['modList']);
1334 $this->groupData['file_permissions'] = GeneralUtility::uniqueList($this->dataLists['file_permissions']);
1335 $this->groupData['workspace_perms'] = $this->dataLists['workspace_perms'];
1336
1337 if (!empty(trim($this->groupData['webmounts']))) {
1338 // Checking read access to web mounts if there are mounts points (not empty string, false or 0)
1339 $webmounts = explode(',', $this->groupData['webmounts']);
1340 // Selecting all web mounts with permission clause for reading
1341 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1342 $queryBuilder->getRestrictions()
1343 ->removeAll()
1344 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1345
1346 $MProws = $queryBuilder->select('uid')
1347 ->from('pages')
1348 // @todo DOCTRINE: check how to make getPagePermsClause() portable
1349 ->where(
1350 $this->getPagePermsClause(Permission::PAGE_SHOW),
1351 $queryBuilder->expr()->in(
1352 'uid',
1353 $queryBuilder->createNamedParameter(
1354 GeneralUtility::intExplode(',', $this->groupData['webmounts']),
1355 Connection::PARAM_INT_ARRAY
1356 )
1357 )
1358 )
1359 ->execute()
1360 ->fetchAll();
1361 $MProws = array_column(($MProws ?: []), 'uid', 'uid');
1362 foreach ($webmounts as $idx => $mountPointUid) {
1363 // If the mount ID is NOT found among selected pages, unset it:
1364 if ($mountPointUid > 0 && !isset($MProws[$mountPointUid])) {
1365 unset($webmounts[$idx]);
1366 }
1367 }
1368 // Implode mounts in the end.
1369 $this->groupData['webmounts'] = implode(',', $webmounts);
1370 }
1371 // Setting up workspace situation (after webmounts are processed!):
1372 $this->workspaceInit();
1373 }
1374 }
1375
1376 /**
1377 * Fetches the group records, subgroups and fills internal arrays.
1378 * Function is called recursively to fetch subgroups
1379 *
1380 * @param string $grList Commalist of be_groups uid numbers
1381 * @param string $idList List of already processed be_groups-uids so the function will not fall into an eternal recursion.
1382 * @internal
1383 */
1384 public function fetchGroups($grList, $idList = '')
1385 {
1386 // Fetching records of the groups in $grList (which are not blocked by lockedToDomain either):
1387 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->usergroup_table);
1388 $expressionBuilder = $queryBuilder->expr();
1389 $constraints = $expressionBuilder->andX(
1390 $expressionBuilder->eq(
1391 'pid',
1392 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1393 ),
1394 $expressionBuilder->in(
1395 'uid',
1396 $queryBuilder->createNamedParameter(
1397 GeneralUtility::intExplode(',', $grList),
1398 Connection::PARAM_INT_ARRAY
1399 )
1400 ),
1401 $expressionBuilder->orX(
1402 $expressionBuilder->eq('lockToDomain', $queryBuilder->quote('')),
1403 $expressionBuilder->isNull('lockToDomain'),
1404 $expressionBuilder->eq(
1405 'lockToDomain',
1406 $queryBuilder->createNamedParameter(GeneralUtility::getIndpEnv('HTTP_HOST'), \PDO::PARAM_STR)
1407 )
1408 )
1409 );
1410 // Hook for manipulation of the WHERE sql sentence which controls which BE-groups are included
1411 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroupQuery'] ?? [] as $className) {
1412 $hookObj = GeneralUtility::makeInstance($className);
1413 if (method_exists($hookObj, 'fetchGroupQuery_processQuery')) {
1414 $constraints = $hookObj->fetchGroupQuery_processQuery($this, $grList, $idList, (string)$constraints);
1415 }
1416 }
1417 $res = $queryBuilder->select('*')
1418 ->from($this->usergroup_table)
1419 ->where($constraints)
1420 ->execute();
1421 // The userGroups array is filled
1422 while ($row = $res->fetch(\PDO::FETCH_ASSOC)) {
1423 $this->userGroups[$row['uid']] = $row;
1424 }
1425 // Traversing records in the correct order
1426 foreach (explode(',', $grList) as $uid) {
1427 // Get row:
1428 $row = $this->userGroups[$uid];
1429 // Must be an array and $uid should not be in the idList, because then it is somewhere previously in the grouplist
1430 if (is_array($row) && !GeneralUtility::inList($idList, $uid)) {
1431 // Include sub groups
1432 if (trim($row['subgroup'])) {
1433 // Make integer list
1434 $theList = implode(',', GeneralUtility::intExplode(',', $row['subgroup']));
1435 // Call recursively, pass along list of already processed groups so they are not recursed again.
1436 $this->fetchGroups($theList, $idList . ',' . $uid);
1437 }
1438 // Add the group uid, current list, TSconfig to the internal arrays.
1439 $this->includeGroupArray[] = $uid;
1440 $this->TSdataArray[] = $row['TSconfig'];
1441 // Mount group database-mounts
1442 if (($this->user['options'] & Permission::PAGE_SHOW) == 1) {
1443 $this->dataLists['webmount_list'] .= ',' . $row['db_mountpoints'];
1444 }
1445 // Mount group file-mounts
1446 if (($this->user['options'] & Permission::PAGE_EDIT) == 2) {
1447 $this->dataLists['filemount_list'] .= ',' . $row['file_mountpoints'];
1448 }
1449 // The lists are made: groupMods, tables_select, tables_modify, pagetypes_select, non_exclude_fields, explicit_allowdeny, allowed_languages, custom_options
1450 $this->dataLists['modList'] .= ',' . $row['groupMods'];
1451 $this->dataLists['tables_select'] .= ',' . $row['tables_select'];
1452 $this->dataLists['tables_modify'] .= ',' . $row['tables_modify'];
1453 $this->dataLists['pagetypes_select'] .= ',' . $row['pagetypes_select'];
1454 $this->dataLists['non_exclude_fields'] .= ',' . $row['non_exclude_fields'];
1455 $this->dataLists['explicit_allowdeny'] .= ',' . $row['explicit_allowdeny'];
1456 $this->dataLists['allowed_languages'] .= ',' . $row['allowed_languages'];
1457 $this->dataLists['custom_options'] .= ',' . $row['custom_options'];
1458 $this->dataLists['file_permissions'] .= ',' . $row['file_permissions'];
1459 // Setting workspace permissions:
1460 $this->dataLists['workspace_perms'] |= $row['workspace_perms'];
1461 // If this function is processing the users OWN group-list (not subgroups) AND
1462 // if the ->firstMainGroup is not set, then the ->firstMainGroup will be set.
1463 if ($idList === '' && !$this->firstMainGroup) {
1464 $this->firstMainGroup = $uid;
1465 }
1466 }
1467 }
1468 // HOOK: fetchGroups_postProcessing
1469 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroups_postProcessing'] ?? [] as $_funcRef) {
1470 $_params = [];
1471 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1472 }
1473 }
1474
1475 /**
1476 * Updates the field be_users.usergroup_cached_list if the groupList of the user
1477 * has changed/is different from the current list.
1478 * The field "usergroup_cached_list" contains the list of groups which the user is a member of.
1479 * After authentication (where these functions are called...) one can depend on this list being
1480 * a representation of the exact groups/subgroups which the BE_USER has membership with.
1481 *
1482 * @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.
1483 * @internal
1484 */
1485 public function setCachedList($cList)
1486 {
1487 if ((string)$cList != (string)$this->user['usergroup_cached_list']) {
1488 GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users')->update(
1489 'be_users',
1490 ['usergroup_cached_list' => $cList],
1491 ['uid' => (int)$this->user['uid']]
1492 );
1493 }
1494 }
1495
1496 /**
1497 * Sets up all file storages for a user.
1498 * Needs to be called AFTER the groups have been loaded.
1499 */
1500 protected function initializeFileStorages()
1501 {
1502 $this->fileStorages = [];
1503 /** @var \TYPO3\CMS\Core\Resource\StorageRepository $storageRepository */
1504 $storageRepository = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\StorageRepository::class);
1505 // Admin users have all file storages visible, without any filters
1506 if ($this->isAdmin()) {
1507 $storageObjects = $storageRepository->findAll();
1508 foreach ($storageObjects as $storageObject) {
1509 $this->fileStorages[$storageObject->getUid()] = $storageObject;
1510 }
1511 } else {
1512 // Regular users only have storages that are defined in their filemounts
1513 // Permissions and file mounts for the storage are added in StoragePermissionAspect
1514 foreach ($this->getFileMountRecords() as $row) {
1515 if (!array_key_exists((int)$row['base'], $this->fileStorages)) {
1516 $storageObject = $storageRepository->findByUid($row['base']);
1517 if ($storageObject) {
1518 $this->fileStorages[$storageObject->getUid()] = $storageObject;
1519 }
1520 }
1521 }
1522 }
1523
1524 // This has to be called always in order to set certain filters
1525 $this->evaluateUserSpecificFileFilterSettings();
1526 }
1527
1528 /**
1529 * Returns an array of category mount points. The category permissions from BE Groups
1530 * are also taken into consideration and are merged into User permissions.
1531 *
1532 * @return array
1533 */
1534 public function getCategoryMountPoints()
1535 {
1536 $categoryMountPoints = '';
1537
1538 // Category mounts of the groups
1539 if (is_array($this->userGroups)) {
1540 foreach ($this->userGroups as $group) {
1541 if ($group['category_perms']) {
1542 $categoryMountPoints .= ',' . $group['category_perms'];
1543 }
1544 }
1545 }
1546
1547 // Category mounts of the user record
1548 if ($this->user['category_perms']) {
1549 $categoryMountPoints .= ',' . $this->user['category_perms'];
1550 }
1551
1552 // Make the ids unique
1553 $categoryMountPoints = GeneralUtility::trimExplode(',', $categoryMountPoints);
1554 $categoryMountPoints = array_filter($categoryMountPoints); // remove empty value
1555 $categoryMountPoints = array_unique($categoryMountPoints); // remove unique value
1556
1557 return $categoryMountPoints;
1558 }
1559
1560 /**
1561 * Returns an array of file mount records, taking workspaces and user home and group home directories into account
1562 * Needs to be called AFTER the groups have been loaded.
1563 *
1564 * @return array
1565 * @internal
1566 */
1567 public function getFileMountRecords()
1568 {
1569 $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
1570 $fileMountRecordCache = $runtimeCache->get('backendUserAuthenticationFileMountRecords') ?: [];
1571
1572 if (!empty($fileMountRecordCache)) {
1573 return $fileMountRecordCache;
1574 }
1575
1576 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
1577
1578 // Processing file mounts (both from the user and the groups)
1579 $fileMounts = array_unique(GeneralUtility::intExplode(',', $this->dataLists['filemount_list'], true));
1580
1581 // Limit file mounts if set in workspace record
1582 if ($this->workspace > 0 && !empty($this->workspaceRec['file_mountpoints'])) {
1583 $workspaceFileMounts = GeneralUtility::intExplode(',', $this->workspaceRec['file_mountpoints'], true);
1584 $fileMounts = array_intersect($fileMounts, $workspaceFileMounts);
1585 }
1586
1587 if (!empty($fileMounts)) {
1588 $orderBy = $GLOBALS['TCA']['sys_filemounts']['ctrl']['default_sortby'] ?? 'sorting';
1589
1590 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_filemounts');
1591 $queryBuilder->getRestrictions()
1592 ->removeAll()
1593 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1594 ->add(GeneralUtility::makeInstance(HiddenRestriction::class))
1595 ->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
1596
1597 $queryBuilder->select('*')
1598 ->from('sys_filemounts')
1599 ->where(
1600 $queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($fileMounts, Connection::PARAM_INT_ARRAY))
1601 );
1602
1603 foreach (QueryHelper::parseOrderBy($orderBy) as $fieldAndDirection) {
1604 $queryBuilder->addOrderBy(...$fieldAndDirection);
1605 }
1606
1607 $fileMountRecords = $queryBuilder->execute()->fetchAll(\PDO::FETCH_ASSOC);
1608 if ($fileMountRecords !== false) {
1609 foreach ($fileMountRecords as $fileMount) {
1610 $fileMountRecordCache[$fileMount['base'] . $fileMount['path']] = $fileMount;
1611 }
1612 }
1613 }
1614
1615 // Read-only file mounts
1616 $readOnlyMountPoints = \trim($this->getTSConfig()['options.']['folderTree.']['altElementBrowserMountPoints'] ?? '');
1617 if ($readOnlyMountPoints) {
1618 // We cannot use the API here but need to fetch the default storage record directly
1619 // to not instantiate it (which directly applies mount points) before all mount points are resolved!
1620 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_file_storage');
1621 $defaultStorageRow = $queryBuilder->select('uid')
1622 ->from('sys_file_storage')
1623 ->where(
1624 $queryBuilder->expr()->eq('is_default', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT))
1625 )
1626 ->setMaxResults(1)
1627 ->execute()
1628 ->fetch(\PDO::FETCH_ASSOC);
1629
1630 $readOnlyMountPointArray = GeneralUtility::trimExplode(',', $readOnlyMountPoints);
1631 foreach ($readOnlyMountPointArray as $readOnlyMountPoint) {
1632 $readOnlyMountPointConfiguration = GeneralUtility::trimExplode(':', $readOnlyMountPoint);
1633 if (count($readOnlyMountPointConfiguration) === 2) {
1634 // A storage is passed in the configuration
1635 $storageUid = (int)$readOnlyMountPointConfiguration[0];
1636 $path = $readOnlyMountPointConfiguration[1];
1637 } else {
1638 if (empty($defaultStorageRow)) {
1639 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);
1640 }
1641 // Backwards compatibility: If no storage is passed, we use the default storage
1642 $storageUid = $defaultStorageRow['uid'];
1643 $path = $readOnlyMountPointConfiguration[0];
1644 }
1645 $fileMountRecordCache[$storageUid . $path] = [
1646 'base' => $storageUid,
1647 'title' => $path,
1648 'path' => $path,
1649 'read_only' => true
1650 ];
1651 }
1652 }
1653
1654 // Personal or Group filemounts are not accessible if file mount list is set in workspace record
1655 if ($this->workspace <= 0 || empty($this->workspaceRec['file_mountpoints'])) {
1656 // If userHomePath is set, we attempt to mount it
1657 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath']) {
1658 list($userHomeStorageUid, $userHomeFilter) = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath'], 2);
1659 $userHomeStorageUid = (int)$userHomeStorageUid;
1660 $userHomeFilter = '/' . ltrim($userHomeFilter, '/');
1661 if ($userHomeStorageUid > 0) {
1662 // Try and mount with [uid]_[username]
1663 $path = $userHomeFilter . $this->user['uid'] . '_' . $this->user['username'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1664 $fileMountRecordCache[$userHomeStorageUid . $path] = [
1665 'base' => $userHomeStorageUid,
1666 'title' => $this->user['username'],
1667 'path' => $path,
1668 'read_only' => false,
1669 'user_mount' => true
1670 ];
1671 // Try and mount with only [uid]
1672 $path = $userHomeFilter . $this->user['uid'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1673 $fileMountRecordCache[$userHomeStorageUid . $path] = [
1674 'base' => $userHomeStorageUid,
1675 'title' => $this->user['username'],
1676 'path' => $path,
1677 'read_only' => false,
1678 'user_mount' => true
1679 ];
1680 }
1681 }
1682
1683 // Mount group home-dirs
1684 if ((is_array($this->user) && $this->user['options'] & Permission::PAGE_EDIT) == 2 && $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'] != '') {
1685 // If groupHomePath is set, we attempt to mount it
1686 list($groupHomeStorageUid, $groupHomeFilter) = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'], 2);
1687 $groupHomeStorageUid = (int)$groupHomeStorageUid;
1688 $groupHomeFilter = '/' . ltrim($groupHomeFilter, '/');
1689 if ($groupHomeStorageUid > 0) {
1690 foreach ($this->userGroups as $groupData) {
1691 $path = $groupHomeFilter . $groupData['uid'];
1692 $fileMountRecordCache[$groupHomeStorageUid . $path] = [
1693 'base' => $groupHomeStorageUid,
1694 'title' => $groupData['title'],
1695 'path' => $path,
1696 'read_only' => false,
1697 'user_mount' => true
1698 ];
1699 }
1700 }
1701 }
1702 }
1703
1704 $runtimeCache->set('backendUserAuthenticationFileMountRecords', $fileMountRecordCache);
1705 return $fileMountRecordCache;
1706 }
1707
1708 /**
1709 * Returns an array with the filemounts for the user.
1710 * Each filemount is represented with an array of a "name", "path" and "type".
1711 * If no filemounts an empty array is returned.
1712 *
1713 * @return \TYPO3\CMS\Core\Resource\ResourceStorage[]
1714 */
1715 public function getFileStorages()
1716 {
1717 // Initializing file mounts after the groups are fetched
1718 if ($this->fileStorages === null) {
1719 $this->initializeFileStorages();
1720 }
1721 return $this->fileStorages;
1722 }
1723
1724 /**
1725 * Adds filters based on what the user has set
1726 * this should be done in this place, and called whenever needed,
1727 * but only when needed
1728 */
1729 public function evaluateUserSpecificFileFilterSettings()
1730 {
1731 // Add the option for also displaying the non-hidden files
1732 if ($this->uc['showHiddenFilesAndFolders']) {
1733 \TYPO3\CMS\Core\Resource\Filter\FileNameFilter::setShowHiddenFilesAndFolders(true);
1734 }
1735 }
1736
1737 /**
1738 * Returns the information about file permissions.
1739 * Previously, this was stored in the DB field fileoper_perms now it is file_permissions.
1740 * Besides it can be handled via userTSconfig
1741 *
1742 * permissions.file.default {
1743 * addFile = 1
1744 * readFile = 1
1745 * writeFile = 1
1746 * copyFile = 1
1747 * moveFile = 1
1748 * renameFile = 1
1749 * deleteFile = 1
1750 *
1751 * addFolder = 1
1752 * readFolder = 1
1753 * writeFolder = 1
1754 * copyFolder = 1
1755 * moveFolder = 1
1756 * renameFolder = 1
1757 * deleteFolder = 1
1758 * recursivedeleteFolder = 1
1759 * }
1760 *
1761 * # overwrite settings for a specific storageObject
1762 * permissions.file.storage.StorageUid {
1763 * readFile = 1
1764 * recursivedeleteFolder = 0
1765 * }
1766 *
1767 * Please note that these permissions only apply, if the storage has the
1768 * capabilities (browseable, writable), and if the driver allows for writing etc
1769 *
1770 * @return array
1771 */
1772 public function getFilePermissions()
1773 {
1774 if (!isset($this->filePermissions)) {
1775 $filePermissions = [
1776 // File permissions
1777 'addFile' => false,
1778 'readFile' => false,
1779 'writeFile' => false,
1780 'copyFile' => false,
1781 'moveFile' => false,
1782 'renameFile' => false,
1783 'deleteFile' => false,
1784 // Folder permissions
1785 'addFolder' => false,
1786 'readFolder' => false,
1787 'writeFolder' => false,
1788 'copyFolder' => false,
1789 'moveFolder' => false,
1790 'renameFolder' => false,
1791 'deleteFolder' => false,
1792 'recursivedeleteFolder' => false
1793 ];
1794 if ($this->isAdmin()) {
1795 $filePermissions = array_map('is_bool', $filePermissions);
1796 } else {
1797 $userGroupRecordPermissions = GeneralUtility::trimExplode(',', $this->groupData['file_permissions'] ?? '', true);
1798 array_walk(
1799 $userGroupRecordPermissions,
1800 function ($permission) use (&$filePermissions) {
1801 $filePermissions[$permission] = true;
1802 }
1803 );
1804
1805 // Finally overlay any userTSconfig
1806 $permissionsTsConfig = $this->getTSConfig()['permissions.']['file.']['default.'] ?? [];
1807 if (!empty($permissionsTsConfig)) {
1808 array_walk(
1809 $permissionsTsConfig,
1810 function ($value, $permission) use (&$filePermissions) {
1811 $filePermissions[$permission] = (bool)$value;
1812 }
1813 );
1814 }
1815 }
1816 $this->filePermissions = $filePermissions;
1817 }
1818 return $this->filePermissions;
1819 }
1820
1821 /**
1822 * Gets the file permissions for a storage
1823 * by merging any storage-specific permissions for a
1824 * storage with the default settings.
1825 * Admin users will always get the default settings.
1826 *
1827 * @param \TYPO3\CMS\Core\Resource\ResourceStorage $storageObject
1828 * @return array
1829 */
1830 public function getFilePermissionsForStorage(\TYPO3\CMS\Core\Resource\ResourceStorage $storageObject)
1831 {
1832 $finalUserPermissions = $this->getFilePermissions();
1833 if (!$this->isAdmin()) {
1834 $storageFilePermissions = $this->getTSConfig()['permissions.']['file.']['storage.'][$storageObject->getUid() . '.'] ?? [];
1835 if (!empty($storageFilePermissions)) {
1836 array_walk(
1837 $storageFilePermissions,
1838 function ($value, $permission) use (&$finalUserPermissions) {
1839 $finalUserPermissions[$permission] = (bool)$value;
1840 }
1841 );
1842 }
1843 }
1844 return $finalUserPermissions;
1845 }
1846
1847 /**
1848 * Returns a \TYPO3\CMS\Core\Resource\Folder object that is used for uploading
1849 * files by default.
1850 * This is used for RTE and its magic images, as well as uploads
1851 * in the TCEforms fields.
1852 *
1853 * The default upload folder for a user is the defaultFolder on the first
1854 * filestorage/filemount that the user can access and to which files are allowed to be added
1855 * however, you can set the users' upload folder like this:
1856 *
1857 * options.defaultUploadFolder = 3:myfolder/yourfolder/
1858 *
1859 * @param int $pid PageUid
1860 * @param string $table Table name
1861 * @param string $field Field name
1862 * @return \TYPO3\CMS\Core\Resource\Folder|bool The default upload folder for this user
1863 */
1864 public function getDefaultUploadFolder($pid = null, $table = null, $field = null)
1865 {
1866 $uploadFolder = $this->getTSConfig()['options.']['defaultUploadFolder'] ?? '';
1867 if ($uploadFolder) {
1868 $uploadFolder = \TYPO3\CMS\Core\Resource\ResourceFactory::getInstance()->getFolderObjectFromCombinedIdentifier($uploadFolder);
1869 } else {
1870 foreach ($this->getFileStorages() as $storage) {
1871 if ($storage->isDefault() && $storage->isWritable()) {
1872 try {
1873 $uploadFolder = $storage->getDefaultFolder();
1874 if ($uploadFolder->checkActionPermission('write')) {
1875 break;
1876 }
1877 $uploadFolder = null;
1878 } catch (\TYPO3\CMS\Core\Resource\Exception $folderAccessException) {
1879 // If the folder is not accessible (no permissions / does not exist) we skip this one.
1880 }
1881 break;
1882 }
1883 }
1884 if (!$uploadFolder instanceof \TYPO3\CMS\Core\Resource\Folder) {
1885 /** @var ResourceStorage $storage */
1886 foreach ($this->getFileStorages() as $storage) {
1887 if ($storage->isWritable()) {
1888 try {
1889 $uploadFolder = $storage->getDefaultFolder();
1890 if ($uploadFolder->checkActionPermission('write')) {
1891 break;
1892 }
1893 $uploadFolder = null;
1894 } catch (\TYPO3\CMS\Core\Resource\Exception $folderAccessException) {
1895 // If the folder is not accessible (no permissions / does not exist) try the next one.
1896 }
1897 }
1898 }
1899 }
1900 }
1901
1902 // HOOK: getDefaultUploadFolder
1903 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getDefaultUploadFolder'] ?? [] as $_funcRef) {
1904 $_params = [
1905 'uploadFolder' => $uploadFolder,
1906 'pid' => $pid,
1907 'table' => $table,
1908 'field' => $field,
1909 ];
1910 $uploadFolder = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1911 }
1912
1913 if ($uploadFolder instanceof \TYPO3\CMS\Core\Resource\Folder) {
1914 return $uploadFolder;
1915 }
1916 return false;
1917 }
1918
1919 /**
1920 * Returns a \TYPO3\CMS\Core\Resource\Folder object that could be used for uploading
1921 * temporary files in user context. The folder _temp_ below the default upload folder
1922 * of the user is used.
1923 *
1924 * @return \TYPO3\CMS\Core\Resource\Folder|null
1925 * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::getDefaultUploadFolder()
1926 */
1927 public function getDefaultUploadTemporaryFolder()
1928 {
1929 $defaultTemporaryFolder = null;
1930 $defaultFolder = $this->getDefaultUploadFolder();
1931
1932 if ($defaultFolder !== false) {
1933 $tempFolderName = '_temp_';
1934 $createFolder = !$defaultFolder->hasFolder($tempFolderName);
1935 if ($createFolder === true) {
1936 try {
1937 $defaultTemporaryFolder = $defaultFolder->createFolder($tempFolderName);
1938 } catch (\TYPO3\CMS\Core\Resource\Exception $folderAccessException) {
1939 }
1940 } else {
1941 $defaultTemporaryFolder = $defaultFolder->getSubfolder($tempFolderName);
1942 }
1943 }
1944
1945 return $defaultTemporaryFolder;
1946 }
1947
1948 /**
1949 * Initializing workspace.
1950 * Called from within this function, see fetchGroupData()
1951 *
1952 * @see fetchGroupData()
1953 */
1954 public function workspaceInit()
1955 {
1956 // Initializing workspace by evaluating and setting the workspace, possibly updating it in the user record!
1957 $this->setWorkspace($this->user['workspace_id']);
1958 // Limiting the DB mountpoints if there any selected in the workspace record
1959 $this->initializeDbMountpointsInWorkspace();
1960 $allowed_languages = $this->getTSConfig()['options.']['workspaces.']['allowed_languages.'][$this->workspace] ?? '';
1961 if (!empty($allowed_languages)) {
1962 $this->groupData['allowed_languages'] = GeneralUtility::uniqueList($allowed_languages);
1963 }
1964 }
1965
1966 /**
1967 * Limiting the DB mountpoints if there any selected in the workspace record
1968 */
1969 protected function initializeDbMountpointsInWorkspace()
1970 {
1971 $dbMountpoints = trim($this->workspaceRec['db_mountpoints'] ?? '');
1972 if ($this->workspace > 0 && $dbMountpoints != '') {
1973 $filteredDbMountpoints = [];
1974 // Notice: We cannot call $this->getPagePermsClause(1);
1975 // as usual because the group-list is not available at this point.
1976 // But bypassing is fine because all we want here is check if the
1977 // workspace mounts are inside the current webmounts rootline.
1978 // The actual permission checking on page level is done elsewhere
1979 // as usual anyway before the page tree is rendered.
1980 $readPerms = '1=1';
1981 // Traverse mount points of the
1982 $dbMountpoints = GeneralUtility::intExplode(',', $dbMountpoints);
1983 foreach ($dbMountpoints as $mpId) {
1984 if ($this->isInWebMount($mpId, $readPerms)) {
1985 $filteredDbMountpoints[] = $mpId;
1986 }
1987 }
1988 // Re-insert webmounts:
1989 $filteredDbMountpoints = array_unique($filteredDbMountpoints);
1990 $this->groupData['webmounts'] = implode(',', $filteredDbMountpoints);
1991 }
1992 }
1993
1994 /**
1995 * Checking if a workspace is allowed for backend user
1996 *
1997 * @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)
1998 * @param string $fields List of fields to select. Default fields are all
1999 * @return array Output will also show how access was granted. Admin users will have a true output regardless of input.
2000 */
2001 public function checkWorkspace($wsRec, $fields = '*')
2002 {
2003 $retVal = false;
2004 // If not array, look up workspace record:
2005 if (!is_array($wsRec)) {
2006 switch ((string)$wsRec) {
2007 case '0':
2008 $wsRec = ['uid' => $wsRec];
2009 break;
2010 default:
2011 if (ExtensionManagementUtility::isLoaded('workspaces')) {
2012 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
2013 $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
2014 $wsRec = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields))
2015 ->from('sys_workspace')
2016 ->where($queryBuilder->expr()->eq(
2017 'uid',
2018 $queryBuilder->createNamedParameter($wsRec, \PDO::PARAM_INT)
2019 ))
2020 ->orderBy('title')
2021 ->setMaxResults(1)
2022 ->execute()
2023 ->fetch(\PDO::FETCH_ASSOC);
2024 }
2025 }
2026 }
2027 // If wsRec is set to an array, evaluate it:
2028 if (is_array($wsRec)) {
2029 if ($this->isAdmin()) {
2030 return array_merge($wsRec, ['_ACCESS' => 'admin']);
2031 }
2032 switch ((string)$wsRec['uid']) {
2033 case '0':
2034 $retVal = $this->groupData['workspace_perms'] & Permission::PAGE_SHOW
2035 ? array_merge($wsRec, ['_ACCESS' => 'online'])
2036 : false;
2037 break;
2038 default:
2039 // Checking if the guy is admin:
2040 if (GeneralUtility::inList($wsRec['adminusers'], 'be_users_' . $this->user['uid'])) {
2041 return array_merge($wsRec, ['_ACCESS' => 'owner']);
2042 }
2043 // Checking if he is owner through a user group of his:
2044 foreach ($this->userGroupsUID as $groupUid) {
2045 if (GeneralUtility::inList($wsRec['adminusers'], 'be_groups_' . $groupUid)) {
2046 return array_merge($wsRec, ['_ACCESS' => 'owner']);
2047 }
2048 }
2049 // Checking if he is member as user:
2050 if (GeneralUtility::inList($wsRec['members'], 'be_users_' . $this->user['uid'])) {
2051 return array_merge($wsRec, ['_ACCESS' => 'member']);
2052 }
2053 // Checking if he is member through a user group of his:
2054 foreach ($this->userGroupsUID as $groupUid) {
2055 if (GeneralUtility::inList($wsRec['members'], 'be_groups_' . $groupUid)) {
2056 return array_merge($wsRec, ['_ACCESS' => 'member']);
2057 }
2058 }
2059 }
2060 }
2061 return $retVal;
2062 }
2063
2064 /**
2065 * Uses checkWorkspace() to check if current workspace is available for user.
2066 * This function caches the result and so can be called many times with no performance loss.
2067 *
2068 * @return array See checkWorkspace()
2069 * @see checkWorkspace()
2070 */
2071 public function checkWorkspaceCurrent()
2072 {
2073 if (!isset($this->checkWorkspaceCurrent_cache)) {
2074 $this->checkWorkspaceCurrent_cache = $this->checkWorkspace($this->workspace);
2075 }
2076 return $this->checkWorkspaceCurrent_cache;
2077 }
2078
2079 /**
2080 * Setting workspace ID
2081 *
2082 * @param int $workspaceId ID of workspace to set for backend user. If not valid the default workspace for BE user is found and set.
2083 */
2084 public function setWorkspace($workspaceId)
2085 {
2086 // Check workspace validity and if not found, revert to default workspace.
2087 if (!$this->setTemporaryWorkspace($workspaceId)) {
2088 $this->setDefaultWorkspace();
2089 }
2090 // Unset access cache:
2091 $this->checkWorkspaceCurrent_cache = null;
2092 // If ID is different from the stored one, change it:
2093 if ((int)$this->workspace !== (int)$this->user['workspace_id']) {
2094 $this->user['workspace_id'] = $this->workspace;
2095 GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users')->update(
2096 'be_users',
2097 ['workspace_id' => $this->user['workspace_id']],
2098 ['uid' => (int)$this->user['uid']]
2099 );
2100 $this->writelog(4, 0, 0, 0, 'User changed workspace to "' . $this->workspace . '"', []);
2101 }
2102 }
2103
2104 /**
2105 * Sets a temporary workspace in the context of the current backend user.
2106 *
2107 * @param int $workspaceId
2108 * @return bool
2109 */
2110 public function setTemporaryWorkspace($workspaceId)
2111 {
2112 $result = false;
2113 $workspaceRecord = $this->checkWorkspace($workspaceId);
2114
2115 if ($workspaceRecord) {
2116 $this->workspaceRec = $workspaceRecord;
2117 $this->workspace = (int)$workspaceId;
2118 $result = true;
2119 }
2120
2121 return $result;
2122 }
2123
2124 /**
2125 * Sets the default workspace in the context of the current backend user.
2126 */
2127 public function setDefaultWorkspace()
2128 {
2129 $this->workspace = (int)$this->getDefaultWorkspace();
2130 $this->workspaceRec = $this->checkWorkspace($this->workspace);
2131 }
2132
2133 /**
2134 * Return default workspace ID for user,
2135 * if EXT:workspaces is not installed the user will be pushed to the
2136 * Live workspace, if he has access to. If no workspace is available for the user, the workspace ID is set to "-99"
2137 *
2138 * @return int Default workspace id.
2139 */
2140 public function getDefaultWorkspace()
2141 {
2142 if (!ExtensionManagementUtility::isLoaded('workspaces')) {
2143 return 0;
2144 }
2145 // Online is default
2146 if ($this->checkWorkspace(0)) {
2147 return 0;
2148 }
2149 // Otherwise -99 is the fallback
2150 $defaultWorkspace = -99;
2151 // Traverse all workspaces
2152 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
2153 $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
2154 $result = $queryBuilder->select('*')
2155 ->from('sys_workspace')
2156 ->orderBy('title')
2157 ->execute();
2158 while ($workspaceRecord = $result->fetch()) {
2159 if ($this->checkWorkspace($workspaceRecord)) {
2160 $defaultWorkspace = (int)$workspaceRecord['uid'];
2161 break;
2162 }
2163 }
2164 return $defaultWorkspace;
2165 }
2166
2167 /**
2168 * Writes an entry in the logfile/table
2169 * Documentation in "TYPO3 Core API"
2170 *
2171 * @param int $type Denotes which module that has submitted the entry. See "TYPO3 Core API". Use "4" for extensions.
2172 * @param int $action Denotes which specific operation that wrote the entry. Use "0" when no sub-categorizing applies
2173 * @param int $error Flag. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin)
2174 * @param int $details_nr The message number. Specific for each $type and $action. This will make it possible to translate errormessages to other languages
2175 * @param string $details Default text that follows the message (in english!). Possibly translated by identification through type/action/details_nr
2176 * @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
2177 * @param string $tablename Table name. Special field used by tce_main.php.
2178 * @param int|string $recuid Record UID. Special field used by tce_main.php.
2179 * @param int|string $recpid Record PID. Special field used by tce_main.php. OBSOLETE
2180 * @param int $event_pid The page_uid (pid) where the event occurred. Used to select log-content for specific pages.
2181 * @param string $NEWid Special field used by tce_main.php. NEWid string of newly created records.
2182 * @param int $userId Alternative Backend User ID (used for logging login actions where this is not yet known).
2183 * @return int Log entry ID.
2184 */
2185 public function writelog($type, $action, $error, $details_nr, $details, $data, $tablename = '', $recuid = '', $recpid = '', $event_pid = -1, $NEWid = '', $userId = 0)
2186 {
2187 if (!$userId && !empty($this->user['uid'])) {
2188 $userId = $this->user['uid'];
2189 }
2190
2191 if (!empty($this->user['ses_backuserid'])) {
2192 if (empty($data)) {
2193 $data = [];
2194 }
2195 $data['originalUser'] = $this->user['ses_backuserid'];
2196 }
2197
2198 $fields = [
2199 'userid' => (int)$userId,
2200 'type' => (int)$type,
2201 'action' => (int)$action,
2202 'error' => (int)$error,
2203 'details_nr' => (int)$details_nr,
2204 'details' => $details,
2205 'log_data' => serialize($data),
2206 'tablename' => $tablename,
2207 'recuid' => (int)$recuid,
2208 'IP' => (string)GeneralUtility::getIndpEnv('REMOTE_ADDR'),
2209 'tstamp' => $GLOBALS['EXEC_TIME'] ?? time(),
2210 'event_pid' => (int)$event_pid,
2211 'NEWid' => $NEWid,
2212 'workspace' => $this->workspace
2213 ];
2214
2215 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_log');
2216 $connection->insert(
2217 'sys_log',
2218 $fields,
2219 [
2220 \PDO::PARAM_INT,
2221 \PDO::PARAM_INT,
2222 \PDO::PARAM_INT,
2223 \PDO::PARAM_INT,
2224 \PDO::PARAM_INT,
2225 \PDO::PARAM_STR,
2226 \PDO::PARAM_STR,
2227 \PDO::PARAM_STR,
2228 \PDO::PARAM_INT,
2229 \PDO::PARAM_STR,
2230 \PDO::PARAM_INT,
2231 \PDO::PARAM_INT,
2232 \PDO::PARAM_STR,
2233 \PDO::PARAM_STR,
2234 ]
2235 );
2236
2237 return (int)$connection->lastInsertId('sys_log');
2238 }
2239
2240 /**
2241 * Sends a warning to $email if there has been a certain amount of failed logins during a period.
2242 * If a login fails, this function is called. It will look up the sys_log to see if there
2243 * have been more than $max failed logins the last $secondsBack seconds (default 3600).
2244 * If so, an email with a warning is sent to $email.
2245 *
2246 * @param string $email Email address
2247 * @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.
2248 * @param int $max Max allowed failures before a warning mail is sent
2249 * @internal
2250 */
2251 public function checkLogFailures($email, $secondsBack = 3600, $max = 3)
2252 {
2253 if ($email) {
2254 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
2255
2256 // Get last flag set in the log for sending
2257 $theTimeBack = $GLOBALS['EXEC_TIME'] - $secondsBack;
2258 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
2259 $queryBuilder->select('tstamp')
2260 ->from('sys_log')
2261 ->where(
2262 $queryBuilder->expr()->eq(
2263 'type',
2264 $queryBuilder->createNamedParameter(255, \PDO::PARAM_INT)
2265 ),
2266 $queryBuilder->expr()->eq(
2267 'action',
2268 $queryBuilder->createNamedParameter(4, \PDO::PARAM_INT)
2269 ),
2270 $queryBuilder->expr()->gt(
2271 'tstamp',
2272 $queryBuilder->createNamedParameter($theTimeBack, \PDO::PARAM_INT)
2273 )
2274 )
2275 ->orderBy('tstamp', 'DESC')
2276 ->setMaxResults(1);
2277 if ($testRow = $queryBuilder->execute()->fetch(\PDO::FETCH_ASSOC)) {
2278 $theTimeBack = $testRow['tstamp'];
2279 }
2280
2281 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
2282 $result = $queryBuilder->select('*')
2283 ->from('sys_log')
2284 ->where(
2285 $queryBuilder->expr()->eq(
2286 'type',
2287 $queryBuilder->createNamedParameter(255, \PDO::PARAM_INT)
2288 ),
2289 $queryBuilder->expr()->eq(
2290 'action',
2291 $queryBuilder->createNamedParameter(3, \PDO::PARAM_INT)
2292 ),
2293 $queryBuilder->expr()->neq(
2294 'error',
2295 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
2296 ),
2297 $queryBuilder->expr()->gt(
2298 'tstamp',
2299 $queryBuilder->createNamedParameter($theTimeBack, \PDO::PARAM_INT)
2300 )
2301 )
2302 ->orderBy('tstamp')
2303 ->execute();
2304
2305 $rowCount = $queryBuilder
2306 ->count('uid')
2307 ->execute()
2308 ->fetchColumn(0);
2309 // Check for more than $max number of error failures with the last period.
2310 if ($rowCount > $max) {
2311 // OK, so there were more than the max allowed number of login failures - so we will send an email then.
2312 $subject = 'TYPO3 Login Failure Warning (at ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . ')';
2313 $email_body = 'There have been some attempts (' . $rowCount . ') to login at the TYPO3
2314 site "' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . '" (' . GeneralUtility::getIndpEnv('HTTP_HOST') . ').
2315
2316 This is a dump of the failures:
2317
2318 ';
2319 while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
2320 $theData = unserialize($row['log_data']);
2321 $email_body .= date(
2322 $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
2323 $row['tstamp']
2324 ) . ': ' . @sprintf($row['details'], (string)$theData[0], (string)$theData[1], (string)$theData[2]);
2325 $email_body .= LF;
2326 }
2327 $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
2328 $mail->setTo($email)->subject($subject)->text($email_body);
2329 $mail->send();
2330 // Logout written to log
2331 $this->writelog(255, 4, 0, 3, 'Failure warning (%s failures within %s seconds) sent by email to %s', [$rowCount, $secondsBack, $email]);
2332 }
2333 }
2334 }
2335
2336 /**
2337 * Getter for the cookie name
2338 *
2339 * @static
2340 * @return string returns the configured cookie name
2341 */
2342 public static function getCookieName()
2343 {
2344 $configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']);
2345 if (empty($configuredCookieName)) {
2346 $configuredCookieName = 'be_typo_user';
2347 }
2348 return $configuredCookieName;
2349 }
2350
2351 /**
2352 * If TYPO3_CONF_VARS['BE']['enabledBeUserIPLock'] is enabled and
2353 * an IP-list is found in the User TSconfig objString "options.lockToIP",
2354 * then make an IP comparison with REMOTE_ADDR and check if the IP address matches
2355 *
2356 * @return bool TRUE, if IP address validates OK (or no check is done at all because no restriction is set)
2357 */
2358 public function checkLockToIP()
2359 {
2360 $isValid = true;
2361 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['enabledBeUserIPLock']) {
2362 $IPList = trim($this->getTSConfig()['options.']['lockToIP'] ?? '');
2363 if (!empty($IPList)) {
2364 $isValid = GeneralUtility::cmpIP(GeneralUtility::getIndpEnv('REMOTE_ADDR'), $IPList);
2365 }
2366 }
2367 return $isValid;
2368 }
2369
2370 /**
2371 * Check if user is logged in and if so, call ->fetchGroupData() to load group information and
2372 * access lists of all kind, further check IP, set the ->uc array.
2373 * If no user is logged in the default behaviour is to exit with an error message.
2374 * This function is called right after ->start() in fx. the TYPO3 Bootstrap.
2375 *
2376 * @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.
2377 * @throws \RuntimeException
2378 */
2379 public function backendCheckLogin($proceedIfNoUserIsLoggedIn = false)
2380 {
2381 if (empty($this->user['uid'])) {
2382 if ($proceedIfNoUserIsLoggedIn === false) {
2383 $url = GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir;
2384 \TYPO3\CMS\Core\Utility\HttpUtility::redirect($url);
2385 }
2386 } else {
2387 // ...and if that's the case, call these functions
2388 $this->fetchGroupData();
2389 // The groups are fetched and ready for permission checking in this initialization.
2390 // Tables.php must be read before this because stuff like the modules has impact in this
2391 if ($this->checkLockToIP()) {
2392 if ($this->isUserAllowedToLogin()) {
2393 // Setting the UC array. It's needed with fetchGroupData first, due to default/overriding of values.
2394 $this->backendSetUC();
2395 if ($this->loginSessionStarted) {
2396 // Process hooks
2397 $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['backendUserLogin'];
2398 foreach ($hooks ?? [] as $_funcRef) {
2399 $_params = ['user' => $this->user];
2400 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2401 }
2402 }
2403 } else {
2404 throw new \RuntimeException('Login Error: TYPO3 is in maintenance mode at the moment. Only administrators are allowed access.', 1294585860);
2405 }
2406 } else {
2407 throw new \RuntimeException('Login Error: IP locking prevented you from being authorized. Can\'t proceed, sorry.', 1294585861);
2408 }
2409 }
2410 }
2411
2412 /**
2413 * Initialize the internal ->uc array for the backend user
2414 * Will make the overrides if necessary, and write the UC back to the be_users record if changes has happened
2415 *
2416 * @internal
2417 */
2418 public function backendSetUC()
2419 {
2420 // UC - user configuration is a serialized array inside the user object
2421 // If there is a saved uc we implement that instead of the default one.
2422 $this->unpack_uc();
2423 // Setting defaults if uc is empty
2424 $updated = false;
2425 $originalUc = [];
2426 if (is_array($this->uc) && isset($this->uc['ucSetByInstallTool'])) {
2427 $originalUc = $this->uc;
2428 unset($originalUc['ucSetByInstallTool'], $this->uc);
2429 }
2430 if (!is_array($this->uc)) {
2431 $this->uc = array_merge(
2432 $this->uc_default,
2433 (array)$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUC'],
2434 GeneralUtility::removeDotsFromTS((array)($this->getTSConfig()['setup.']['default.'] ?? [])),
2435 $originalUc
2436 );
2437 $this->overrideUC();
2438 $updated = true;
2439 }
2440 // If TSconfig is updated, update the defaultUC.
2441 if ($this->userTSUpdated) {
2442 $this->overrideUC();
2443 $updated = true;
2444 }
2445 // Setting default lang from be_user record.
2446 if (!isset($this->uc['lang'])) {
2447 $this->uc['lang'] = $this->user['lang'];
2448 $updated = true;
2449 }
2450 // Setting the time of the first login:
2451 if (!isset($this->uc['firstLoginTimeStamp'])) {
2452 $this->uc['firstLoginTimeStamp'] = $GLOBALS['EXEC_TIME'];
2453 $updated = true;
2454 }
2455 // Saving if updated.
2456 if ($updated) {
2457 $this->writeUC();
2458 }
2459 }
2460
2461 /**
2462 * Override: Call this function every time the uc is updated.
2463 * That is 1) by reverting to default values, 2) in the setup-module, 3) userTS changes (userauthgroup)
2464 *
2465 * @internal
2466 */
2467 public function overrideUC()
2468 {
2469 $this->uc = array_merge((array)$this->uc, (array)($this->getTSConfig()['setup.']['override.'] ?? []));
2470 }
2471
2472 /**
2473 * Clears the user[uc] and ->uc to blank strings. Then calls ->backendSetUC() to fill it again with reset contents
2474 *
2475 * @internal
2476 */
2477 public function resetUC()
2478 {
2479 $this->user['uc'] = '';
2480 $this->uc = '';
2481 $this->backendSetUC();
2482 }
2483
2484 /**
2485 * Determines whether a backend user is allowed to access the backend.
2486 *
2487 * The conditions are:
2488 * + backend user is a regular user and adminOnly is not defined
2489 * + backend user is an admin user
2490 * + backend user is used in CLI context and adminOnly is explicitly set to "2" (see CommandLineUserAuthentication)
2491 * + backend user is being controlled by an admin user
2492 *
2493 * @return bool Whether a backend user is allowed to access the backend
2494 */
2495 protected function isUserAllowedToLogin()
2496 {
2497 $isUserAllowedToLogin = false;
2498 $adminOnlyMode = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'];
2499 // Backend user is allowed if adminOnly is not set or user is an admin:
2500 if (!$adminOnlyMode || $this->isAdmin()) {
2501 $isUserAllowedToLogin = true;
2502 } elseif ($this->user['ses_backuserid']) {
2503 $backendUserId = (int)$this->user['ses_backuserid'];
2504 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
2505 $isUserAllowedToLogin = (bool)$queryBuilder->count('uid')
2506 ->from('be_users')
2507 ->where(
2508 $queryBuilder->expr()->eq(
2509 'uid',
2510 $queryBuilder->createNamedParameter($backendUserId, \PDO::PARAM_INT)
2511 ),
2512 $queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT))
2513 )
2514 ->execute()
2515 ->fetchColumn(0);
2516 }
2517 return $isUserAllowedToLogin;
2518 }
2519
2520 /**
2521 * Logs out the current user and clears the form protection tokens.
2522 */
2523 public function logoff()
2524 {
2525 if (isset($GLOBALS['BE_USER'])
2526 && $GLOBALS['BE_USER'] instanceof self
2527 && isset($GLOBALS['BE_USER']->user['uid'])
2528 ) {
2529 FormProtectionFactory::get()->clean();
2530 // Release the locked records
2531 $this->releaseLockedRecords((int)$GLOBALS['BE_USER']->user['uid']);
2532
2533 if ($this->isSystemMaintainer()) {
2534 // If user is system maintainer, destroy its possibly valid install tool session.
2535 $session = new SessionService();
2536 if ($session->hasSession()) {
2537 $session->destroySession();
2538 }
2539 }
2540 }
2541 parent::logoff();
2542 }
2543
2544 /**
2545 * Remove any "locked records" added for editing for the given user (= current backend user)
2546 * @param int $userId
2547 */
2548 protected function releaseLockedRecords(int $userId)
2549 {
2550 if ($userId > 0) {
2551 GeneralUtility::makeInstance(ConnectionPool::class)
2552 ->getConnectionForTable('sys_lockedrecords')
2553 ->delete(
2554 'sys_lockedrecords',
2555 ['userid' => $userId]
2556 );
2557 }
2558 }
2559 }