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