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