[TASK] Respect alternative web mounts in suggest wizard
[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 * Checks for alternative web mount points for the element browser.
1327 *
1328 * If there is a temporary mount point active in the page tree it will be used.
1329 *
1330 * If the User TSconfig options.pageTree.altElementBrowserMountPoints is not empty the pages configured
1331 * there are used as web mounts If options.pageTree.altElementBrowserMountPoints.append is enabled,
1332 * they are appended to the existing webmounts.
1333 *
1334 * @internal - do not use in your own extension
1335 */
1336 public function initializeWebmountsForElementBrowser()
1337 {
1338 $alternativeWebmountPoint = (int)$this->getSessionData('pageTree_temporaryMountPoint');
1339 if ($alternativeWebmountPoint) {
1340 $alternativeWebmountPoint = GeneralUtility::intExplode(',', $alternativeWebmountPoint);
1341 $this->setWebmounts($alternativeWebmountPoint);
1342 return;
1343 }
1344
1345 $alternativeWebmountPoints = trim($this->getTSConfig()['options.']['pageTree.']['altElementBrowserMountPoints'] ?? '');
1346 $appendAlternativeWebmountPoints = $this->getTSConfig()['options.']['pageTree.']['altElementBrowserMountPoints.']['append'] ?? '';
1347 if ($alternativeWebmountPoints) {
1348 $alternativeWebmountPoints = GeneralUtility::intExplode(',', $alternativeWebmountPoints);
1349 $this->setWebmounts($alternativeWebmountPoints, $appendAlternativeWebmountPoints);
1350 }
1351 }
1352
1353 /**
1354 * Returns TRUE or FALSE, depending if an alert popup (a javascript confirmation) should be shown
1355 * call like $GLOBALS['BE_USER']->jsConfirmation($BITMASK).
1356 *
1357 * @param int $bitmask Bitmask, one of \TYPO3\CMS\Core\Type\Bitmask\JsConfirmation
1358 * @return bool TRUE if the confirmation should be shown
1359 * @see JsConfirmation
1360 */
1361 public function jsConfirmation($bitmask)
1362 {
1363 try {
1364 $alertPopupsSetting = trim((string)($this->getTSConfig()['options.']['alertPopups'] ?? ''));
1365 $alertPopup = JsConfirmation::cast($alertPopupsSetting === '' ? null : (int)$alertPopupsSetting);
1366 } catch (InvalidEnumerationValueException $e) {
1367 $alertPopup = new JsConfirmation();
1368 }
1369
1370 return JsConfirmation::cast($bitmask)->matches($alertPopup);
1371 }
1372
1373 /**
1374 * Initializes a lot of stuff like the access-lists, database-mountpoints and filemountpoints
1375 * This method is called by ->backendCheckLogin() (from extending BackendUserAuthentication)
1376 * if the backend user login has verified OK.
1377 * Generally this is required initialization of a backend user.
1378 *
1379 * @internal
1380 * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
1381 */
1382 public function fetchGroupData()
1383 {
1384 if ($this->user['uid']) {
1385 // Get lists for the be_user record and set them as default/primary values.
1386 // Enabled Backend Modules
1387 $this->dataLists['modList'] = $this->user['userMods'];
1388 // Add Allowed Languages
1389 $this->dataLists['allowed_languages'] = $this->user['allowed_languages'];
1390 // Set user value for workspace permissions.
1391 $this->dataLists['workspace_perms'] = $this->user['workspace_perms'];
1392 // Database mountpoints
1393 $this->dataLists['webmount_list'] = $this->user['db_mountpoints'];
1394 // File mountpoints
1395 $this->dataLists['filemount_list'] = $this->user['file_mountpoints'];
1396 // Fileoperation permissions
1397 $this->dataLists['file_permissions'] = $this->user['file_permissions'];
1398 // Setting default User TSconfig:
1399 $this->TSdataArray[] = $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig'];
1400 // Default TSconfig for admin-users
1401 if ($this->isAdmin()) {
1402 $this->TSdataArray[] = 'admPanel.enable.all = 1';
1403 if (ExtensionManagementUtility::isLoaded('sys_note')) {
1404 $this->TSdataArray[] = '
1405 // Setting defaults for sys_note author / email...
1406 TCAdefaults.sys_note.author = ' . $this->user['realName'] . '
1407 TCAdefaults.sys_note.email = ' . $this->user['email'] . '
1408 ';
1409 }
1410 }
1411 // BE_GROUPS:
1412 // Get the groups...
1413 if (!empty($this->user[$this->usergroup_column])) {
1414 // Fetch groups will add a lot of information to the internal arrays: modules, accesslists, TSconfig etc.
1415 // Refer to fetchGroups() function.
1416 $this->fetchGroups($this->user[$this->usergroup_column]);
1417 }
1418
1419 // Populating the $this->userGroupsUID -array with the groups in the order in which they were LAST included.!!
1420 $this->userGroupsUID = array_reverse(array_unique(array_reverse($this->includeGroupArray)));
1421 // Finally this is the list of group_uid's in the order they are parsed (including subgroups!)
1422 // and without duplicates (duplicates are presented with their last entrance in the list,
1423 // which thus reflects the order of the TypoScript in TSconfig)
1424 $this->groupList = implode(',', $this->userGroupsUID);
1425 $this->setCachedList($this->groupList);
1426
1427 // Add the TSconfig for this specific user:
1428 $this->TSdataArray[] = $this->user['TSconfig'];
1429 // Check include lines.
1430 $this->TSdataArray = \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines_array($this->TSdataArray);
1431 // Imploding with "[global]" will make sure that non-ended confinements with braces are ignored.
1432 $this->userTS_text = implode(LF . '[GLOBAL]' . LF, $this->TSdataArray);
1433 if (!$this->userTS_dontGetCached) {
1434 // @deprecated: Property userTS_dontGetCached is deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0
1435 // Perform TS-Config parsing with condition matching
1436 $parseObj = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Configuration\TsConfigParser::class);
1437 $res = $parseObj->parseTSconfig($this->userTS_text, 'userTS');
1438 if ($res) {
1439 $this->userTS = $res['TSconfig'];
1440 $this->userTSUpdated = (bool)$res['cached'];
1441 }
1442 } else {
1443 // Parsing the user TSconfig (or getting from cache)
1444 $hash = md5('userTS:' . $this->userTS_text);
1445 $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_hash');
1446 $cachedContent = $cache->get($hash);
1447 if (is_array($cachedContent) && !$this->userTS_dontGetCached) {
1448 // @deprecated: Property userTS_dontGetCached is deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0
1449 $this->userTS = $cachedContent;
1450 } else {
1451 $parseObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::class);
1452 $parseObj->parse($this->userTS_text);
1453 $this->userTS = $parseObj->setup;
1454 $cache->set($hash, $this->userTS, ['ident_BE_USER_TSconfig'], 0);
1455 // Update UC:
1456 $this->userTSUpdated = true;
1457 }
1458 }
1459 // Processing webmounts
1460 // Admin's always have the root mounted
1461 if ($this->isAdmin() && !($this->getTSConfig()['options.']['dontMountAdminMounts'] ?? false)) {
1462 $this->dataLists['webmount_list'] = '0,' . $this->dataLists['webmount_list'];
1463 }
1464 // The lists are cleaned for duplicates
1465 $this->groupData['webmounts'] = GeneralUtility::uniqueList($this->dataLists['webmount_list']);
1466 $this->groupData['pagetypes_select'] = GeneralUtility::uniqueList($this->dataLists['pagetypes_select']);
1467 $this->groupData['tables_select'] = GeneralUtility::uniqueList($this->dataLists['tables_modify'] . ',' . $this->dataLists['tables_select']);
1468 $this->groupData['tables_modify'] = GeneralUtility::uniqueList($this->dataLists['tables_modify']);
1469 $this->groupData['non_exclude_fields'] = GeneralUtility::uniqueList($this->dataLists['non_exclude_fields']);
1470 $this->groupData['explicit_allowdeny'] = GeneralUtility::uniqueList($this->dataLists['explicit_allowdeny']);
1471 $this->groupData['allowed_languages'] = GeneralUtility::uniqueList($this->dataLists['allowed_languages']);
1472 $this->groupData['custom_options'] = GeneralUtility::uniqueList($this->dataLists['custom_options']);
1473 $this->groupData['modules'] = GeneralUtility::uniqueList($this->dataLists['modList']);
1474 $this->groupData['file_permissions'] = GeneralUtility::uniqueList($this->dataLists['file_permissions']);
1475 $this->groupData['workspace_perms'] = $this->dataLists['workspace_perms'];
1476
1477 if (!empty(trim($this->groupData['webmounts']))) {
1478 // Checking read access to web mounts if there are mounts points (not empty string, false or 0)
1479 $webmounts = explode(',', $this->groupData['webmounts']);
1480 // Selecting all web mounts with permission clause for reading
1481 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1482 $queryBuilder->getRestrictions()
1483 ->removeAll()
1484 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1485
1486 $MProws = $queryBuilder->select('uid')
1487 ->from('pages')
1488 // @todo DOCTRINE: check how to make getPagePermsClause() portable
1489 ->where(
1490 $this->getPagePermsClause(Permission::PAGE_SHOW),
1491 $queryBuilder->expr()->in(
1492 'uid',
1493 $queryBuilder->createNamedParameter(
1494 GeneralUtility::intExplode(',', $this->groupData['webmounts']),
1495 Connection::PARAM_INT_ARRAY
1496 )
1497 )
1498 )
1499 ->execute()
1500 ->fetchAll();
1501 $MProws = array_column(($MProws ?: []), 'uid', 'uid');
1502 foreach ($webmounts as $idx => $mountPointUid) {
1503 // If the mount ID is NOT found among selected pages, unset it:
1504 if ($mountPointUid > 0 && !isset($MProws[$mountPointUid])) {
1505 unset($webmounts[$idx]);
1506 }
1507 }
1508 // Implode mounts in the end.
1509 $this->groupData['webmounts'] = implode(',', $webmounts);
1510 }
1511 // Setting up workspace situation (after webmounts are processed!):
1512 $this->workspaceInit();
1513 }
1514 }
1515
1516 /**
1517 * Fetches the group records, subgroups and fills internal arrays.
1518 * Function is called recursively to fetch subgroups
1519 *
1520 * @param string $grList Commalist of be_groups uid numbers
1521 * @param string $idList List of already processed be_groups-uids so the function will not fall into an eternal recursion.
1522 * @internal
1523 */
1524 public function fetchGroups($grList, $idList = '')
1525 {
1526 // Fetching records of the groups in $grList (which are not blocked by lockedToDomain either):
1527 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->usergroup_table);
1528 $expressionBuilder = $queryBuilder->expr();
1529 $constraints = $expressionBuilder->andX(
1530 $expressionBuilder->eq(
1531 'pid',
1532 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1533 ),
1534 $expressionBuilder->in(
1535 'uid',
1536 $queryBuilder->createNamedParameter(
1537 GeneralUtility::intExplode(',', $grList),
1538 Connection::PARAM_INT_ARRAY
1539 )
1540 ),
1541 $expressionBuilder->orX(
1542 $expressionBuilder->eq('lockToDomain', $queryBuilder->quote('')),
1543 $expressionBuilder->isNull('lockToDomain'),
1544 $expressionBuilder->eq(
1545 'lockToDomain',
1546 $queryBuilder->createNamedParameter(GeneralUtility::getIndpEnv('HTTP_HOST'), \PDO::PARAM_STR)
1547 )
1548 )
1549 );
1550 // Hook for manipulation of the WHERE sql sentence which controls which BE-groups are included
1551 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroupQuery'] ?? [] as $className) {
1552 $hookObj = GeneralUtility::makeInstance($className);
1553 if (method_exists($hookObj, 'fetchGroupQuery_processQuery')) {
1554 $constraints = $hookObj->fetchGroupQuery_processQuery($this, $grList, $idList, (string)$constraints);
1555 }
1556 }
1557 $res = $queryBuilder->select('*')
1558 ->from($this->usergroup_table)
1559 ->where($constraints)
1560 ->execute();
1561 // The userGroups array is filled
1562 while ($row = $res->fetch(\PDO::FETCH_ASSOC)) {
1563 $this->userGroups[$row['uid']] = $row;
1564 }
1565 // Traversing records in the correct order
1566 foreach (explode(',', $grList) as $uid) {
1567 // Get row:
1568 $row = $this->userGroups[$uid];
1569 // Must be an array and $uid should not be in the idList, because then it is somewhere previously in the grouplist
1570 if (is_array($row) && !GeneralUtility::inList($idList, $uid)) {
1571 // Include sub groups
1572 if (trim($row['subgroup'])) {
1573 // Make integer list
1574 $theList = implode(',', GeneralUtility::intExplode(',', $row['subgroup']));
1575 // Call recursively, pass along list of already processed groups so they are not recursed again.
1576 $this->fetchGroups($theList, $idList . ',' . $uid);
1577 }
1578 // Add the group uid, current list, TSconfig to the internal arrays.
1579 $this->includeGroupArray[] = $uid;
1580 $this->TSdataArray[] = $row['TSconfig'];
1581 // Mount group database-mounts
1582 if (($this->user['options'] & Permission::PAGE_SHOW) == 1) {
1583 $this->dataLists['webmount_list'] .= ',' . $row['db_mountpoints'];
1584 }
1585 // Mount group file-mounts
1586 if (($this->user['options'] & Permission::PAGE_EDIT) == 2) {
1587 $this->dataLists['filemount_list'] .= ',' . $row['file_mountpoints'];
1588 }
1589 // The lists are made: groupMods, tables_select, tables_modify, pagetypes_select, non_exclude_fields, explicit_allowdeny, allowed_languages, custom_options
1590 $this->dataLists['modList'] .= ',' . $row['groupMods'];
1591 $this->dataLists['tables_select'] .= ',' . $row['tables_select'];
1592 $this->dataLists['tables_modify'] .= ',' . $row['tables_modify'];
1593 $this->dataLists['pagetypes_select'] .= ',' . $row['pagetypes_select'];
1594 $this->dataLists['non_exclude_fields'] .= ',' . $row['non_exclude_fields'];
1595 $this->dataLists['explicit_allowdeny'] .= ',' . $row['explicit_allowdeny'];
1596 $this->dataLists['allowed_languages'] .= ',' . $row['allowed_languages'];
1597 $this->dataLists['custom_options'] .= ',' . $row['custom_options'];
1598 $this->dataLists['file_permissions'] .= ',' . $row['file_permissions'];
1599 // Setting workspace permissions:
1600 $this->dataLists['workspace_perms'] |= $row['workspace_perms'];
1601 // If this function is processing the users OWN group-list (not subgroups) AND
1602 // if the ->firstMainGroup is not set, then the ->firstMainGroup will be set.
1603 if ($idList === '' && !$this->firstMainGroup) {
1604 $this->firstMainGroup = $uid;
1605 }
1606 }
1607 }
1608 // HOOK: fetchGroups_postProcessing
1609 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroups_postProcessing'] ?? [] as $_funcRef) {
1610 $_params = [];
1611 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1612 }
1613 }
1614
1615 /**
1616 * Updates the field be_users.usergroup_cached_list if the groupList of the user
1617 * has changed/is different from the current list.
1618 * The field "usergroup_cached_list" contains the list of groups which the user is a member of.
1619 * After authentication (where these functions are called...) one can depend on this list being
1620 * a representation of the exact groups/subgroups which the BE_USER has membership with.
1621 *
1622 * @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.
1623 * @internal
1624 */
1625 public function setCachedList($cList)
1626 {
1627 if ((string)$cList != (string)$this->user['usergroup_cached_list']) {
1628 GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users')->update(
1629 'be_users',
1630 ['usergroup_cached_list' => $cList],
1631 ['uid' => (int)$this->user['uid']]
1632 );
1633 }
1634 }
1635
1636 /**
1637 * Sets up all file storages for a user.
1638 * Needs to be called AFTER the groups have been loaded.
1639 */
1640 protected function initializeFileStorages()
1641 {
1642 $this->fileStorages = [];
1643 /** @var \TYPO3\CMS\Core\Resource\StorageRepository $storageRepository */
1644 $storageRepository = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\StorageRepository::class);
1645 // Admin users have all file storages visible, without any filters
1646 if ($this->isAdmin()) {
1647 $storageObjects = $storageRepository->findAll();
1648 foreach ($storageObjects as $storageObject) {
1649 $this->fileStorages[$storageObject->getUid()] = $storageObject;
1650 }
1651 } else {
1652 // Regular users only have storages that are defined in their filemounts
1653 // Permissions and file mounts for the storage are added in StoragePermissionAspect
1654 foreach ($this->getFileMountRecords() as $row) {
1655 if (!array_key_exists((int)$row['base'], $this->fileStorages)) {
1656 $storageObject = $storageRepository->findByUid($row['base']);
1657 if ($storageObject) {
1658 $this->fileStorages[$storageObject->getUid()] = $storageObject;
1659 }
1660 }
1661 }
1662 }
1663
1664 // This has to be called always in order to set certain filters
1665 $this->evaluateUserSpecificFileFilterSettings();
1666 }
1667
1668 /**
1669 * Returns an array of category mount points. The category permissions from BE Groups
1670 * are also taken into consideration and are merged into User permissions.
1671 *
1672 * @return array
1673 */
1674 public function getCategoryMountPoints()
1675 {
1676 $categoryMountPoints = '';
1677
1678 // Category mounts of the groups
1679 if (is_array($this->userGroups)) {
1680 foreach ($this->userGroups as $group) {
1681 if ($group['category_perms']) {
1682 $categoryMountPoints .= ',' . $group['category_perms'];
1683 }
1684 }
1685 }
1686
1687 // Category mounts of the user record
1688 if ($this->user['category_perms']) {
1689 $categoryMountPoints .= ',' . $this->user['category_perms'];
1690 }
1691
1692 // Make the ids unique
1693 $categoryMountPoints = GeneralUtility::trimExplode(',', $categoryMountPoints);
1694 $categoryMountPoints = array_filter($categoryMountPoints); // remove empty value
1695 $categoryMountPoints = array_unique($categoryMountPoints); // remove unique value
1696
1697 return $categoryMountPoints;
1698 }
1699
1700 /**
1701 * Returns an array of file mount records, taking workspaces and user home and group home directories into account
1702 * Needs to be called AFTER the groups have been loaded.
1703 *
1704 * @return array
1705 * @internal
1706 */
1707 public function getFileMountRecords()
1708 {
1709 $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
1710 $fileMountRecordCache = $runtimeCache->get('backendUserAuthenticationFileMountRecords') ?: [];
1711
1712 if (!empty($fileMountRecordCache)) {
1713 return $fileMountRecordCache;
1714 }
1715
1716 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
1717
1718 // Processing file mounts (both from the user and the groups)
1719 $fileMounts = array_unique(GeneralUtility::intExplode(',', $this->dataLists['filemount_list'], true));
1720
1721 // Limit file mounts if set in workspace record
1722 if ($this->workspace > 0 && !empty($this->workspaceRec['file_mountpoints'])) {
1723 $workspaceFileMounts = GeneralUtility::intExplode(',', $this->workspaceRec['file_mountpoints'], true);
1724 $fileMounts = array_intersect($fileMounts, $workspaceFileMounts);
1725 }
1726
1727 if (!empty($fileMounts)) {
1728 $orderBy = $GLOBALS['TCA']['sys_filemounts']['ctrl']['default_sortby'] ?? 'sorting';
1729
1730 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_filemounts');
1731 $queryBuilder->getRestrictions()
1732 ->removeAll()
1733 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1734 ->add(GeneralUtility::makeInstance(HiddenRestriction::class))
1735 ->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
1736
1737 $queryBuilder->select('*')
1738 ->from('sys_filemounts')
1739 ->where(
1740 $queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($fileMounts, Connection::PARAM_INT_ARRAY))
1741 );
1742
1743 foreach (QueryHelper::parseOrderBy($orderBy) as $fieldAndDirection) {
1744 $queryBuilder->addOrderBy(...$fieldAndDirection);
1745 }
1746
1747 $fileMountRecords = $queryBuilder->execute()->fetchAll(\PDO::FETCH_ASSOC);
1748 if ($fileMountRecords !== false) {
1749 foreach ($fileMountRecords as $fileMount) {
1750 $fileMountRecordCache[$fileMount['base'] . $fileMount['path']] = $fileMount;
1751 }
1752 }
1753 }
1754
1755 // Read-only file mounts
1756 $readOnlyMountPoints = \trim($this->getTSConfig()['options.']['folderTree.']['altElementBrowserMountPoints'] ?? '');
1757 if ($readOnlyMountPoints) {
1758 // We cannot use the API here but need to fetch the default storage record directly
1759 // to not instantiate it (which directly applies mount points) before all mount points are resolved!
1760 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_file_storage');
1761 $defaultStorageRow = $queryBuilder->select('uid')
1762 ->from('sys_file_storage')
1763 ->where(
1764 $queryBuilder->expr()->eq('is_default', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT))
1765 )
1766 ->setMaxResults(1)
1767 ->execute()
1768 ->fetch(\PDO::FETCH_ASSOC);
1769
1770 $readOnlyMountPointArray = GeneralUtility::trimExplode(',', $readOnlyMountPoints);
1771 foreach ($readOnlyMountPointArray as $readOnlyMountPoint) {
1772 $readOnlyMountPointConfiguration = GeneralUtility::trimExplode(':', $readOnlyMountPoint);
1773 if (count($readOnlyMountPointConfiguration) === 2) {
1774 // A storage is passed in the configuration
1775 $storageUid = (int)$readOnlyMountPointConfiguration[0];
1776 $path = $readOnlyMountPointConfiguration[1];
1777 } else {
1778 if (empty($defaultStorageRow)) {
1779 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);
1780 }
1781 // Backwards compatibility: If no storage is passed, we use the default storage
1782 $storageUid = $defaultStorageRow['uid'];
1783 $path = $readOnlyMountPointConfiguration[0];
1784 }
1785 $fileMountRecordCache[$storageUid . $path] = [
1786 'base' => $storageUid,
1787 'title' => $path,
1788 'path' => $path,
1789 'read_only' => true
1790 ];
1791 }
1792 }
1793
1794 // Personal or Group filemounts are not accessible if file mount list is set in workspace record
1795 if ($this->workspace <= 0 || empty($this->workspaceRec['file_mountpoints'])) {
1796 // If userHomePath is set, we attempt to mount it
1797 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath']) {
1798 list($userHomeStorageUid, $userHomeFilter) = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath'], 2);
1799 $userHomeStorageUid = (int)$userHomeStorageUid;
1800 $userHomeFilter = '/' . ltrim($userHomeFilter, '/');
1801 if ($userHomeStorageUid > 0) {
1802 // Try and mount with [uid]_[username]
1803 $path = $userHomeFilter . $this->user['uid'] . '_' . $this->user['username'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1804 $fileMountRecordCache[$userHomeStorageUid . $path] = [
1805 'base' => $userHomeStorageUid,
1806 'title' => $this->user['username'],
1807 'path' => $path,
1808 'read_only' => false,
1809 'user_mount' => true
1810 ];
1811 // Try and mount with only [uid]
1812 $path = $userHomeFilter . $this->user['uid'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1813 $fileMountRecordCache[$userHomeStorageUid . $path] = [
1814 'base' => $userHomeStorageUid,
1815 'title' => $this->user['username'],
1816 'path' => $path,
1817 'read_only' => false,
1818 'user_mount' => true
1819 ];
1820 }
1821 }
1822
1823 // Mount group home-dirs
1824 if ((is_array($this->user) && $this->user['options'] & Permission::PAGE_EDIT) == 2 && $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'] != '') {
1825 // If groupHomePath is set, we attempt to mount it
1826 list($groupHomeStorageUid, $groupHomeFilter) = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'], 2);
1827 $groupHomeStorageUid = (int)$groupHomeStorageUid;
1828 $groupHomeFilter = '/' . ltrim($groupHomeFilter, '/');
1829 if ($groupHomeStorageUid > 0) {
1830 foreach ($this->userGroups as $groupData) {
1831 $path = $groupHomeFilter . $groupData['uid'];
1832 $fileMountRecordCache[$groupHomeStorageUid . $path] = [
1833 'base' => $groupHomeStorageUid,
1834 'title' => $groupData['title'],
1835 'path' => $path,
1836 'read_only' => false,
1837 'user_mount' => true
1838 ];
1839 }
1840 }
1841 }
1842 }
1843
1844 $runtimeCache->set('backendUserAuthenticationFileMountRecords', $fileMountRecordCache);
1845 return $fileMountRecordCache;
1846 }
1847
1848 /**
1849 * Returns an array with the filemounts for the user.
1850 * Each filemount is represented with an array of a "name", "path" and "type".
1851 * If no filemounts an empty array is returned.
1852 *
1853 * @return \TYPO3\CMS\Core\Resource\ResourceStorage[]
1854 */
1855 public function getFileStorages()
1856 {
1857 // Initializing file mounts after the groups are fetched
1858 if ($this->fileStorages === null) {
1859 $this->initializeFileStorages();
1860 }
1861 return $this->fileStorages;
1862 }
1863
1864 /**
1865 * Adds filters based on what the user has set
1866 * this should be done in this place, and called whenever needed,
1867 * but only when needed
1868 */
1869 public function evaluateUserSpecificFileFilterSettings()
1870 {
1871 // Add the option for also displaying the non-hidden files
1872 if ($this->uc['showHiddenFilesAndFolders']) {
1873 \TYPO3\CMS\Core\Resource\Filter\FileNameFilter::setShowHiddenFilesAndFolders(true);
1874 }
1875 }
1876
1877 /**
1878 * Returns the information about file permissions.
1879 * Previously, this was stored in the DB field fileoper_perms now it is file_permissions.
1880 * Besides it can be handled via userTSconfig
1881 *
1882 * permissions.file.default {
1883 * addFile = 1
1884 * readFile = 1
1885 * writeFile = 1
1886 * copyFile = 1
1887 * moveFile = 1
1888 * renameFile = 1
1889 * deleteFile = 1
1890 *
1891 * addFolder = 1
1892 * readFolder = 1
1893 * writeFolder = 1
1894 * copyFolder = 1
1895 * moveFolder = 1
1896 * renameFolder = 1
1897 * deleteFolder = 1
1898 * recursivedeleteFolder = 1
1899 * }
1900 *
1901 * # overwrite settings for a specific storageObject
1902 * permissions.file.storage.StorageUid {
1903 * readFile = 1
1904 * recursivedeleteFolder = 0
1905 * }
1906 *
1907 * Please note that these permissions only apply, if the storage has the
1908 * capabilities (browseable, writable), and if the driver allows for writing etc
1909 *
1910 * @return array
1911 */
1912 public function getFilePermissions()
1913 {
1914 if (!isset($this->filePermissions)) {
1915 $filePermissions = [
1916 // File permissions
1917 'addFile' => false,
1918 'readFile' => false,
1919 'writeFile' => false,
1920 'copyFile' => false,
1921 'moveFile' => false,
1922 'renameFile' => false,
1923 'deleteFile' => false,
1924 // Folder permissions
1925 'addFolder' => false,
1926 'readFolder' => false,
1927 'writeFolder' => false,
1928 'copyFolder' => false,
1929 'moveFolder' => false,
1930 'renameFolder' => false,
1931 'deleteFolder' => false,
1932 'recursivedeleteFolder' => false
1933 ];
1934 if ($this->isAdmin()) {
1935 $filePermissions = array_map('is_bool', $filePermissions);
1936 } else {
1937 $userGroupRecordPermissions = GeneralUtility::trimExplode(',', $this->groupData['file_permissions'] ?? '', true);
1938 array_walk(
1939 $userGroupRecordPermissions,
1940 function ($permission) use (&$filePermissions) {
1941 $filePermissions[$permission] = true;
1942 }
1943 );
1944
1945 // Finally overlay any userTSconfig
1946 $permissionsTsConfig = $this->getTSConfig()['permissions.']['file.']['default.'] ?? [];
1947 if (!empty($permissionsTsConfig)) {
1948 array_walk(
1949 $permissionsTsConfig,
1950 function ($value, $permission) use (&$filePermissions) {
1951 $filePermissions[$permission] = (bool)$value;
1952 }
1953 );
1954 }
1955 }
1956 $this->filePermissions = $filePermissions;
1957 }
1958 return $this->filePermissions;
1959 }
1960
1961 /**
1962 * Gets the file permissions for a storage
1963 * by merging any storage-specific permissions for a
1964 * storage with the default settings.
1965 * Admin users will always get the default settings.
1966 *
1967 * @param \TYPO3\CMS\Core\Resource\ResourceStorage $storageObject
1968 * @return array
1969 */
1970 public function getFilePermissionsForStorage(\TYPO3\CMS\Core\Resource\ResourceStorage $storageObject)
1971 {
1972 $finalUserPermissions = $this->getFilePermissions();
1973 if (!$this->isAdmin()) {
1974 $storageFilePermissions = $this->getTSConfig()['permissions.']['file.']['storage.'][$storageObject->getUid() . '.'] ?? [];
1975 if (!empty($storageFilePermissions)) {
1976 array_walk(
1977 $storageFilePermissions,
1978 function ($value, $permission) use (&$finalUserPermissions) {
1979 $finalUserPermissions[$permission] = (bool)$value;
1980 }
1981 );
1982 }
1983 }
1984 return $finalUserPermissions;
1985 }
1986
1987 /**
1988 * Returns a \TYPO3\CMS\Core\Resource\Folder object that is used for uploading
1989 * files by default.
1990 * This is used for RTE and its magic images, as well as uploads
1991 * in the TCEforms fields.
1992 *
1993 * The default upload folder for a user is the defaultFolder on the first
1994 * filestorage/filemount that the user can access and to which files are allowed to be added
1995 * however, you can set the users' upload folder like this:
1996 *
1997 * options.defaultUploadFolder = 3:myfolder/yourfolder/
1998 *
1999 * @param int $pid PageUid
2000 * @param string $table Table name
2001 * @param string $field Field name
2002 * @return \TYPO3\CMS\Core\Resource\Folder|bool The default upload folder for this user
2003 */
2004 public function getDefaultUploadFolder($pid = null, $table = null, $field = null)
2005 {
2006 $uploadFolder = $this->getTSConfig()['options.']['defaultUploadFolder'] ?? '';
2007 if ($uploadFolder) {
2008 $uploadFolder = \TYPO3\CMS\Core\Resource\ResourceFactory::getInstance()->getFolderObjectFromCombinedIdentifier($uploadFolder);
2009 } else {
2010 foreach ($this->getFileStorages() as $storage) {
2011 if ($storage->isDefault() && $storage->isWritable()) {
2012 try {
2013 $uploadFolder = $storage->getDefaultFolder();
2014 if ($uploadFolder->checkActionPermission('write')) {
2015 break;
2016 }
2017 $uploadFolder = null;
2018 } catch (\TYPO3\CMS\Core\Resource\Exception $folderAccessException) {
2019 // If the folder is not accessible (no permissions / does not exist) we skip this one.
2020 }
2021 break;
2022 }
2023 }
2024 if (!$uploadFolder instanceof \TYPO3\CMS\Core\Resource\Folder) {
2025 /** @var ResourceStorage $storage */
2026 foreach ($this->getFileStorages() as $storage) {
2027 if ($storage->isWritable()) {
2028 try {
2029 $uploadFolder = $storage->getDefaultFolder();
2030 if ($uploadFolder->checkActionPermission('write')) {
2031 break;
2032 }
2033 $uploadFolder = null;
2034 } catch (\TYPO3\CMS\Core\Resource\Exception $folderAccessException) {
2035 // If the folder is not accessible (no permissions / does not exist) try the next one.
2036 }
2037 }
2038 }
2039 }
2040 }
2041
2042 // HOOK: getDefaultUploadFolder
2043 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getDefaultUploadFolder'] ?? [] as $_funcRef) {
2044 $_params = [
2045 'uploadFolder' => $uploadFolder,
2046 'pid' => $pid,
2047 'table' => $table,
2048 'field' => $field,
2049 ];
2050 $uploadFolder = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2051 }
2052
2053 if ($uploadFolder instanceof \TYPO3\CMS\Core\Resource\Folder) {
2054 return $uploadFolder;
2055 }
2056 return false;
2057 }
2058
2059 /**
2060 * Returns a \TYPO3\CMS\Core\Resource\Folder object that could be used for uploading
2061 * temporary files in user context. The folder _temp_ below the default upload folder
2062 * of the user is used.
2063 *
2064 * @return \TYPO3\CMS\Core\Resource\Folder|null
2065 * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::getDefaultUploadFolder();
2066 */
2067 public function getDefaultUploadTemporaryFolder()
2068 {
2069 $defaultTemporaryFolder = null;
2070 $defaultFolder = $this->getDefaultUploadFolder();
2071
2072 if ($defaultFolder !== false) {
2073 $tempFolderName = '_temp_';
2074 $createFolder = !$defaultFolder->hasFolder($tempFolderName);
2075 if ($createFolder === true) {
2076 try {
2077 $defaultTemporaryFolder = $defaultFolder->createFolder($tempFolderName);
2078 } catch (\TYPO3\CMS\Core\Resource\Exception $folderAccessException) {
2079 }
2080 } else {
2081 $defaultTemporaryFolder = $defaultFolder->getSubfolder($tempFolderName);
2082 }
2083 }
2084
2085 return $defaultTemporaryFolder;
2086 }
2087
2088 /**
2089 * Creates a TypoScript comment with the string text inside.
2090 *
2091 * @param string $str The text to wrap in comment prefixes and delimiters.
2092 * @return string TypoScript comment with the string text inside.
2093 * @deprecated since TYPO3 v9, will be removed with TYPO3 v10.0
2094 */
2095 public function addTScomment($str)
2096 {
2097 trigger_error('BackendUserAuthentication->addTScomment() will be removed in TYPO3 v10.0.', E_USER_DEPRECATED);
2098 $delimiter = '# ***********************************************';
2099 $out = $delimiter . LF;
2100 $lines = GeneralUtility::trimExplode(LF, $str);
2101 foreach ($lines as $v) {
2102 $out .= '# ' . $v . LF;
2103 }
2104 $out .= $delimiter . LF;
2105 return $out;
2106 }
2107
2108 /**
2109 * Initializing workspace.
2110 * Called from within this function, see fetchGroupData()
2111 *
2112 * @see fetchGroupData()
2113 */
2114 public function workspaceInit()
2115 {
2116 // Initializing workspace by evaluating and setting the workspace, possibly updating it in the user record!
2117 $this->setWorkspace($this->user['workspace_id']);
2118 // Limiting the DB mountpoints if there any selected in the workspace record
2119 $this->initializeDbMountpointsInWorkspace();
2120 $allowed_languages = $this->getTSConfig()['options.']['workspaces.']['allowed_languages.'][$this->workspace] ?? '';
2121 if (!empty($allowed_languages)) {
2122 $this->groupData['allowed_languages'] = GeneralUtility::uniqueList($allowed_languages);
2123 }
2124 }
2125
2126 /**
2127 * Limiting the DB mountpoints if there any selected in the workspace record
2128 */
2129 protected function initializeDbMountpointsInWorkspace()
2130 {
2131 $dbMountpoints = trim($this->workspaceRec['db_mountpoints'] ?? '');
2132 if ($this->workspace > 0 && $dbMountpoints != '') {
2133 $filteredDbMountpoints = [];
2134 // Notice: We cannot call $this->getPagePermsClause(1);
2135 // as usual because the group-list is not available at this point.
2136 // But bypassing is fine because all we want here is check if the
2137 // workspace mounts are inside the current webmounts rootline.
2138 // The actual permission checking on page level is done elsewhere
2139 // as usual anyway before the page tree is rendered.
2140 $readPerms = '1=1';
2141 // Traverse mount points of the
2142 $dbMountpoints = GeneralUtility::intExplode(',', $dbMountpoints);
2143 foreach ($dbMountpoints as $mpId) {
2144 if ($this->isInWebMount($mpId, $readPerms)) {
2145 $filteredDbMountpoints[] = $mpId;
2146 }
2147 }
2148 // Re-insert webmounts:
2149 $filteredDbMountpoints = array_unique($filteredDbMountpoints);
2150 $this->groupData['webmounts'] = implode(',', $filteredDbMountpoints);
2151 }
2152 }
2153
2154 /**
2155 * Checking if a workspace is allowed for backend user
2156 *
2157 * @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)
2158 * @param string $fields List of fields to select. Default fields are: uid,title,adminusers,members,reviewers,publish_access,stagechg_notification
2159 * @return array Output will also show how access was granted. Admin users will have a true output regardless of input.
2160 */
2161 public function checkWorkspace($wsRec, $fields = 'uid,title,adminusers,members,reviewers,publish_access,stagechg_notification')
2162 {
2163 $retVal = false;
2164 // If not array, look up workspace record:
2165 if (!is_array($wsRec)) {
2166 switch ((string)$wsRec) {
2167 case '0':
2168 $wsRec = ['uid' => $wsRec];
2169 break;
2170 default:
2171 if (ExtensionManagementUtility::isLoaded('workspaces')) {
2172 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
2173 $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
2174 $wsRec = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields))
2175 ->from('sys_workspace')
2176 ->where($queryBuilder->expr()->eq(
2177 'uid',
2178 $queryBuilder->createNamedParameter($wsRec, \PDO::PARAM_INT)
2179 ))
2180 ->orderBy('title')
2181 ->setMaxResults(1)
2182 ->execute()
2183 ->fetch(\PDO::FETCH_ASSOC);
2184 }
2185 }
2186 }
2187 // If wsRec is set to an array, evaluate it:
2188 if (is_array($wsRec)) {
2189 if ($this->isAdmin()) {
2190 return array_merge($wsRec, ['_ACCESS' => 'admin']);
2191 }
2192 switch ((string)$wsRec['uid']) {
2193 case '0':
2194 $retVal = $this->groupData['workspace_perms'] & Permission::PAGE_SHOW
2195 ? array_merge($wsRec, ['_ACCESS' => 'online'])
2196 : false;
2197 break;
2198 default:
2199 // Checking if the guy is admin:
2200 if (GeneralUtility::inList($wsRec['adminusers'], 'be_users_' . $this->user['uid'])) {
2201 return array_merge($wsRec, ['_ACCESS' => 'owner']);
2202 }
2203 // Checking if he is owner through a user group of his:
2204 foreach ($this->userGroupsUID as $groupUid) {
2205 if (GeneralUtility::inList($wsRec['adminusers'], 'be_groups_' . $groupUid)) {
2206 return array_merge($wsRec, ['_ACCESS' => 'owner']);
2207 }
2208 }
2209 // Checking if he is reviewer user:
2210 if (GeneralUtility::inList($wsRec['reviewers'], 'be_users_' . $this->user['uid'])) {
2211 return array_merge($wsRec, ['_ACCESS' => 'reviewer']);
2212 }
2213 // Checking if he is reviewer through a user group of his:
2214 foreach ($this->userGroupsUID as $groupUid) {
2215 if (GeneralUtility::inList($wsRec['reviewers'], 'be_groups_' . $groupUid)) {
2216 return array_merge($wsRec, ['_ACCESS' => 'reviewer']);
2217 }
2218 }
2219 // Checking if he is member as user:
2220 if (GeneralUtility::inList($wsRec['members'], 'be_users_' . $this->user['uid'])) {
2221 return array_merge($wsRec, ['_ACCESS' => 'member']);
2222 }
2223 // Checking if he is member through a user group of his:
2224 foreach ($this->userGroupsUID as $groupUid) {
2225 if (GeneralUtility::inList($wsRec['members'], 'be_groups_' . $groupUid)) {
2226 return array_merge($wsRec, ['_ACCESS' => 'member']);
2227 }
2228 }
2229 }
2230 }
2231 return $retVal;
2232 }
2233
2234 /**
2235 * Uses checkWorkspace() to check if current workspace is available for user.
2236 * This function caches the result and so can be called many times with no performance loss.
2237 *
2238 * @return array See checkWorkspace()
2239 * @see checkWorkspace()
2240 */
2241 public function checkWorkspaceCurrent()
2242 {
2243 if (!isset($this->checkWorkspaceCurrent_cache)) {
2244 $this->checkWorkspaceCurrent_cache = $this->checkWorkspace($this->workspace);
2245 }
2246 return $this->checkWorkspaceCurrent_cache;
2247 }
2248
2249 /**
2250 * Setting workspace ID
2251 *
2252 * @param int $workspaceId ID of workspace to set for backend user. If not valid the default workspace for BE user is found and set.
2253 */
2254 public function setWorkspace($workspaceId)
2255 {
2256 // Check workspace validity and if not found, revert to default workspace.
2257 if (!$this->setTemporaryWorkspace($workspaceId)) {
2258 $this->setDefaultWorkspace();
2259 }
2260 // Unset access cache:
2261 $this->checkWorkspaceCurrent_cache = null;
2262 // If ID is different from the stored one, change it:
2263 if ((int)$this->workspace !== (int)$this->user['workspace_id']) {
2264 $this->user['workspace_id'] = $this->workspace;
2265 GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users')->update(
2266 'be_users',
2267 ['workspace_id' => $this->user['workspace_id']],
2268 ['uid' => (int)$this->user['uid']]
2269 );
2270 $this->writelog(4, 0, 0, 0, 'User changed workspace to "' . $this->workspace . '"', []);
2271 }
2272 }
2273
2274 /**
2275 * Sets a temporary workspace in the context of the current backend user.
2276 *
2277 * @param int $workspaceId
2278 * @return bool
2279 */
2280 public function setTemporaryWorkspace($workspaceId)
2281 {
2282 $result = false;
2283 $workspaceRecord = $this->checkWorkspace($workspaceId, '*');
2284
2285 if ($workspaceRecord) {
2286 $this->workspaceRec = $workspaceRecord;
2287 $this->workspace = (int)$workspaceId;
2288 $result = true;
2289 }
2290
2291 return $result;
2292 }
2293
2294 /**
2295 * Sets the default workspace in the context of the current backend user.
2296 */
2297 public function setDefaultWorkspace()
2298 {
2299 $this->workspace = (int)$this->getDefaultWorkspace();
2300 $this->workspaceRec = $this->checkWorkspace($this->workspace, '*');
2301 }
2302
2303 /**
2304 * Return default workspace ID for user,
2305 * if EXT:workspaces is not installed the user will be pushed to the
2306 * Live workspace, if he has access to. If no workspace is available for the user, the workspace ID is set to "-99"
2307 *
2308 * @return int Default workspace id.
2309 */
2310 public function getDefaultWorkspace()
2311 {
2312 if (!ExtensionManagementUtility::isLoaded('workspaces')) {
2313 return 0;
2314 }
2315 // Online is default
2316 if ($this->checkWorkspace(0)) {
2317 return 0;
2318 }
2319 // Otherwise -99 is the fallback
2320 $defaultWorkspace = -99;
2321 // Traverse all workspaces
2322 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
2323 $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
2324 $result = $queryBuilder->select('*')
2325 ->from('sys_workspace')
2326 ->orderBy('title')
2327 ->execute();
2328 while ($workspaceRecord = $result->fetch()) {
2329 if ($this->checkWorkspace($workspaceRecord)) {
2330 $defaultWorkspace = (int)$workspaceRecord['uid'];
2331 break;
2332 }
2333 }
2334 return $defaultWorkspace;
2335 }
2336
2337 /**
2338 * Writes an entry in the logfile/table
2339 * Documentation in "TYPO3 Core API"
2340 *
2341 * @param int $type Denotes which module that has submitted the entry. See "TYPO3 Core API". Use "4" for extensions.
2342 * @param int $action Denotes which specific operation that wrote the entry. Use "0" when no sub-categorizing applies
2343 * @param int $error Flag. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin)
2344 * @param int $details_nr The message number. Specific for each $type and $action. This will make it possible to translate errormessages to other languages
2345 * @param string $details Default text that follows the message (in english!). Possibly translated by identification through type/action/details_nr
2346 * @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
2347 * @param string $tablename Table name. Special field used by tce_main.php.
2348 * @param int|string $recuid Record UID. Special field used by tce_main.php.
2349 * @param int|string $recpid Record PID. Special field used by tce_main.php. OBSOLETE
2350 * @param int $event_pid The page_uid (pid) where the event occurred. Used to select log-content for specific pages.
2351 * @param string $NEWid Special field used by tce_main.php. NEWid string of newly created records.
2352 * @param int $userId Alternative Backend User ID (used for logging login actions where this is not yet known).
2353 * @return int Log entry ID.
2354 */
2355 public function writelog($type, $action, $error, $details_nr, $details, $data, $tablename = '', $recuid = '', $recpid = '', $event_pid = -1, $NEWid = '', $userId = 0)
2356 {
2357 if (!$userId && !empty($this->user['uid'])) {
2358 $userId = $this->user['uid'];
2359 }
2360
2361 if (!empty($this->user['ses_backuserid'])) {
2362 if (empty($data)) {
2363 $data = [];
2364 }
2365 $data['originalUser'] = $this->user['ses_backuserid'];
2366 }
2367
2368 $fields = [
2369 'userid' => (int)$userId,
2370 'type' => (int)$type,
2371 'action' => (int)$action,
2372 'error' => (int)$error,
2373 'details_nr' => (int)$details_nr,
2374 'details' => $details,
2375 'log_data' => serialize($data),
2376 'tablename' => $tablename,
2377 'recuid' => (int)$recuid,
2378 'IP' => (string)GeneralUtility::getIndpEnv('REMOTE_ADDR'),
2379 'tstamp' => $GLOBALS['EXEC_TIME'] ?? time(),
2380 'event_pid' => (int)$event_pid,
2381 'NEWid' => $NEWid,
2382 'workspace' => $this->workspace
2383 ];
2384
2385 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_log');
2386 $connection->insert(
2387 'sys_log',
2388 $fields,
2389 [
2390 \PDO::PARAM_INT,
2391 \PDO::PARAM_INT,
2392 \PDO::PARAM_INT,
2393 \PDO::PARAM_INT,
2394 \PDO::PARAM_INT,
2395 \PDO::PARAM_STR,
2396 \PDO::PARAM_STR,
2397 \PDO::PARAM_STR,
2398 \PDO::PARAM_INT,
2399 \PDO::PARAM_STR,
2400 \PDO::PARAM_INT,
2401 \PDO::PARAM_INT,
2402 \PDO::PARAM_STR,
2403 \PDO::PARAM_STR,
2404 ]
2405 );
2406
2407 return (int)$connection->lastInsertId('sys_log');
2408 }
2409
2410 /**
2411 * Simple logging function
2412 *
2413 * @param string $message Log message
2414 * @param string $extKey Option extension key / module name
2415 * @param int $error Error level. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin)
2416 * @return int Log entry UID
2417 * @deprecated since TYPO3 v9, will be removed with TYPO3 v10.0
2418 */
2419 public function simplelog($message, $extKey = '', $error = 0)
2420 {
2421 trigger_error('BackendUserAuthentication->simplelog() will be removed in TYPO3 v10.0. Use writelog, or better, PSR-3 based logging instead.', E_USER_DEPRECATED);
2422 return $this->writelog(4, 0, $error, 0, ($extKey ? '[' . $extKey . '] ' : '') . $message, []);
2423 }
2424
2425 /**
2426 * Sends a warning to $email if there has been a certain amount of failed logins during a period.
2427 * If a login fails, this function is called. It will look up the sys_log to see if there
2428 * have been more than $max failed logins the last $secondsBack seconds (default 3600).
2429 * If so, an email with a warning is sent to $email.
2430 *
2431 * @param string $email Email address
2432 * @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.
2433 * @param int $max Max allowed failures before a warning mail is sent
2434 * @internal
2435 */
2436 public function checkLogFailures($email, $secondsBack = 3600, $max = 3)
2437 {
2438 if ($email) {
2439 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
2440
2441 // Get last flag set in the log for sending
2442 $theTimeBack = $GLOBALS['EXEC_TIME'] - $secondsBack;
2443 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
2444 $queryBuilder->select('tstamp')
2445 ->from('sys_log')
2446 ->where(
2447 $queryBuilder->expr()->eq(
2448 'type',
2449 $queryBuilder->createNamedParameter(255, \PDO::PARAM_INT)
2450 ),
2451 $queryBuilder->expr()->eq(
2452 'action',
2453 $queryBuilder->createNamedParameter(4, \PDO::PARAM_INT)
2454 ),
2455 $queryBuilder->expr()->gt(
2456 'tstamp',
2457 $queryBuilder->createNamedParameter($theTimeBack, \PDO::PARAM_INT)
2458 )
2459 )
2460 ->orderBy('tstamp', 'DESC')
2461 ->setMaxResults(1);
2462 if ($testRow = $queryBuilder->execute()->fetch(\PDO::FETCH_ASSOC)) {
2463 $theTimeBack = $testRow['tstamp'];
2464 }
2465
2466 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
2467 $result = $queryBuilder->select('*')
2468 ->from('sys_log')
2469 ->where(
2470 $queryBuilder->expr()->eq(
2471 'type',
2472 $queryBuilder->createNamedParameter(255, \PDO::PARAM_INT)
2473 ),
2474 $queryBuilder->expr()->eq(
2475 'action',
2476 $queryBuilder->createNamedParameter(3, \PDO::PARAM_INT)
2477 ),
2478 $queryBuilder->expr()->neq(
2479 'error',
2480 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
2481 ),
2482 $queryBuilder->expr()->gt(
2483 'tstamp',
2484 $queryBuilder->createNamedParameter($theTimeBack, \PDO::PARAM_INT)
2485 )
2486 )
2487 ->orderBy('tstamp')
2488 ->execute();
2489
2490 $rowCount = $queryBuilder
2491 ->count('uid')
2492 ->execute()
2493 ->fetchColumn(0);
2494 // Check for more than $max number of error failures with the last period.
2495 if ($rowCount > $max) {
2496 // OK, so there were more than the max allowed number of login failures - so we will send an email then.
2497 $subject = 'TYPO3 Login Failure Warning (at ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . ')';
2498 $email_body = 'There have been some attempts (' . $rowCount . ') to login at the TYPO3
2499 site "' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . '" (' . GeneralUtility::getIndpEnv('HTTP_HOST') . ').
2500
2501 This is a dump of the failures:
2502
2503 ';
2504 while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
2505 $theData = unserialize($row['log_data']);
2506 $email_body .= date(
2507 $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
2508 $row['tstamp']
2509 ) . ': ' . @sprintf($row['details'], (string)$theData[0], (string)$theData[1], (string)$theData[2]);
2510 $email_body .= LF;
2511 }
2512 /** @var \TYPO3\CMS\Core\Mail\MailMessage $mail */
2513 $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
2514 $mail->setTo($email)->setSubject($subject)->setBody($email_body);
2515 $mail->send();
2516 // Logout written to log
2517 $this->writelog(255, 4, 0, 3, 'Failure warning (%s failures within %s seconds) sent by email to %s', [$rowCount, $secondsBack, $email]);
2518 }
2519 }
2520 }
2521
2522 /**
2523 * Getter for the cookie name
2524 *
2525 * @static
2526 * @return string returns the configured cookie name
2527 */
2528 public static function getCookieName()
2529 {
2530 $configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']);
2531 if (empty($configuredCookieName)) {
2532 $configuredCookieName = 'be_typo_user';
2533 }
2534 return $configuredCookieName;
2535 }
2536
2537 /**
2538 * If TYPO3_CONF_VARS['BE']['enabledBeUserIPLock'] is enabled and
2539 * an IP-list is found in the User TSconfig objString "options.lockToIP",
2540 * then make an IP comparison with REMOTE_ADDR and check if the IP address matches
2541 *
2542 * @return bool TRUE, if IP address validates OK (or no check is done at all because no restriction is set)
2543 */
2544 public function checkLockToIP()
2545 {
2546 $isValid = true;
2547 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['enabledBeUserIPLock']) {
2548 $IPList = trim($this->getTSConfig()['options.']['lockToIP'] ?? '');
2549 if (!empty($IPList)) {
2550 $isValid = GeneralUtility::cmpIP(GeneralUtility::getIndpEnv('REMOTE_ADDR'), $IPList);
2551 }
2552 }
2553 return $isValid;
2554 }
2555
2556 /**
2557 * Check if user is logged in and if so, call ->fetchGroupData() to load group information and
2558 * access lists of all kind, further check IP, set the ->uc array and send login-notification email if required.
2559 * If no user is logged in the default behaviour is to exit with an error message.
2560 * This function is called right after ->start() in fx. the TYPO3 Bootstrap.
2561 *
2562 * @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.
2563 * @throws \RuntimeException
2564 */
2565 public function backendCheckLogin($proceedIfNoUserIsLoggedIn = false)
2566 {
2567 if (empty($this->user['uid'])) {
2568 if ($proceedIfNoUserIsLoggedIn === false) {
2569 $url = GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir;
2570 \TYPO3\CMS\Core\Utility\HttpUtility::redirect($url);
2571 }
2572 } else {
2573 // ...and if that's the case, call these functions
2574 $this->fetchGroupData();
2575 // The groups are fetched and ready for permission checking in this initialization.
2576 // Tables.php must be read before this because stuff like the modules has impact in this
2577 if ($this->checkLockToIP()) {
2578 if ($this->isUserAllowedToLogin()) {
2579 // Setting the UC array. It's needed with fetchGroupData first, due to default/overriding of values.
2580 $this->backendSetUC();
2581 if ($this->loginSessionStarted) {
2582 // Process hooks
2583 $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['backendUserLogin'];
2584 foreach ($hooks ?? [] as $_funcRef) {
2585 $_params = ['user' => $this->user];
2586 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2587 }
2588 // Email at login, if feature is enabled in configuration
2589 $this->emailAtLogin();
2590 }
2591 } else {
2592 throw new \RuntimeException('Login Error: TYPO3 is in maintenance mode at the moment. Only administrators are allowed access.', 1294585860);
2593 }
2594 } else {
2595 throw new \RuntimeException('Login Error: IP locking prevented you from being authorized. Can\'t proceed, sorry.', 1294585861);
2596 }
2597 }
2598 }
2599
2600 /**
2601 * Initialize the internal ->uc array for the backend user
2602 * Will make the overrides if necessary, and write the UC back to the be_users record if changes has happened
2603 *
2604 * @internal
2605 */
2606 public function backendSetUC()
2607 {
2608 // UC - user configuration is a serialized array inside the user object
2609 // If there is a saved uc we implement that instead of the default one.
2610 $this->unpack_uc();
2611 // Setting defaults if uc is empty
2612 $updated = false;
2613 $originalUc = [];
2614 if (is_array($this->uc) && isset($this->uc['ucSetByInstallTool'])) {
2615 $originalUc = $this->uc;
2616 unset($originalUc['ucSetByInstallTool'], $this->uc);
2617 }
2618 if (!is_array($this->uc)) {
2619 $this->uc = array_merge(
2620 $this->uc_default,
2621 (array)$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUC'],
2622 GeneralUtility::removeDotsFromTS((array)($this->getTSConfig()['setup.']['default.'] ?? [])),
2623 $originalUc
2624 );
2625 $this->overrideUC();
2626 $updated = true;
2627 }
2628 // If TSconfig is updated, update the defaultUC.
2629 if ($this->userTSUpdated) {
2630 $this->overrideUC();
2631 $updated = true;
2632 }
2633 // Setting default lang from be_user record.
2634 if (!isset($this->uc['lang'])) {
2635 $this->uc['lang'] = $this->user['lang'];
2636 $updated = true;
2637 }
2638 // Setting the time of the first login:
2639 if (!isset($this->uc['firstLoginTimeStamp'])) {
2640 $this->uc['firstLoginTimeStamp'] = $GLOBALS['EXEC_TIME'];
2641 $updated = true;
2642 }
2643 // Saving if updated.
2644 if ($updated) {
2645 $this->writeUC();
2646 }
2647 }
2648
2649 /**
2650 * Override: Call this function every time the uc is updated.
2651 * That is 1) by reverting to default values, 2) in the setup-module, 3) userTS changes (userauthgroup)
2652 *
2653 * @internal
2654 */
2655 public function overrideUC()
2656 {
2657 $this->uc = array_merge((array)$this->uc, (array)($this->getTSConfig()['setup.']['override.'] ?? []));
2658 }
2659
2660 /**
2661 * Clears the user[uc] and ->uc to blank strings. Then calls ->backendSetUC() to fill it again with reset contents
2662 *
2663 * @internal
2664 */
2665 public function resetUC()
2666 {
2667 $this->user['uc'] = '';
2668 $this->uc = '';
2669 $this->backendSetUC();
2670 }
2671
2672 /**
2673 * Sends an email notification to warning_email_address and/or the logged-in user's email address.
2674 *
2675 * @internal
2676 */
2677 private function emailAtLogin()
2678 {
2679 // Send notify-mail
2680 $subject = 'At "' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . '"' . ' from ' . GeneralUtility::getIndpEnv('REMOTE_ADDR');
2681 $msg = sprintf(
2682 'User "%s" logged in from %s at "%s" (%s)',
2683 $this->user['username'],
2684 GeneralUtility::getIndpEnv('REMOTE_ADDR'),
2685 $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'],
2686 GeneralUtility::getIndpEnv('HTTP_HOST')
2687 );
2688 // Warning email address
2689 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr']) {
2690 $warn = 0;
2691 $prefix = '';
2692 if ((int)$GLOBALS['TYPO3_CONF_VARS']['BE']['warning_mode'] & 1) {
2693 // first bit: All logins
2694 $warn = 1;
2695 $prefix = $this->isAdmin() ? '[AdminLoginWarning]' : '[LoginWarning]';
2696 }
2697 if ($this->isAdmin() && (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['warning_mode'] & 2) {
2698 // second bit: Only admin-logins
2699 $warn = 1;
2700 $prefix = '[AdminLoginWarning]';
2701 }
2702 if ($warn) {
2703 /** @var \TYPO3\CMS\Core\Mail\MailMessage $mail */
2704 $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
2705 $mail->setTo($GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr'])->setSubject($prefix . ' ' . $subject)->setBody($msg);
2706 $mail->send();
2707 }
2708 }
2709 // Trigger an email to the current BE user, if this has been enabled in the user configuration
2710 if ($this->uc['emailMeAtLogin'] && strstr($this->user['email'], '@')) {
2711 /** @var \TYPO3\CMS\Core\Mail\MailMessage $mail */
2712 $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
2713 $mail->setTo($this->user['email'])->setSubject($subject)->setBody($msg);
2714 $mail->send();
2715 }
2716 }
2717
2718 /**
2719 * Determines whether a backend user is allowed to access the backend.
2720 *
2721 * The conditions are:
2722 * + backend user is a regular user and adminOnly is not defined
2723 * + backend user is an admin user
2724 * + backend user is used in CLI context and adminOnly is explicitly set to "2" (see CommandLineUserAuthentication)
2725 * + backend user is being controlled by an admin user
2726 *
2727 * @return bool Whether a backend user is allowed to access the backend
2728 */
2729 protected function isUserAllowedToLogin()
2730 {
2731 $isUserAllowedToLogin = false;
2732 $adminOnlyMode = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'];
2733 // Backend user is allowed if adminOnly is not set or user is an admin:
2734 if (!$adminOnlyMode || $this->isAdmin()) {
2735 $isUserAllowedToLogin = true;
2736 } elseif ($this->user['ses_backuserid']) {
2737 $backendUserId = (int)$this->user['ses_backuserid'];
2738 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
2739 $isUserAllowedToLogin = (bool)$queryBuilder->count('uid')
2740 ->from('be_users')
2741 ->where(
2742 $queryBuilder->expr()->eq(
2743 'uid',
2744 $queryBuilder->createNamedParameter($backendUserId, \PDO::PARAM_INT)
2745 ),
2746 $queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT))
2747 )
2748 ->execute()
2749 ->fetchColumn(0);
2750 }
2751 return $isUserAllowedToLogin;
2752 }
2753
2754 /**
2755 * Logs out the current user and clears the form protection tokens.
2756 */
2757 public function logoff()
2758 {
2759 if (isset($GLOBALS['BE_USER'])
2760 && $GLOBALS['BE_USER'] instanceof self
2761 && isset($GLOBALS['BE_USER']->user['uid'])
2762 ) {
2763 FormProtectionFactory::get()->clean();
2764 // Release the locked records
2765 $this->releaseLockedRecords((int)$GLOBALS['BE_USER']->user['uid']);
2766
2767 if ($this->isSystemMaintainer()) {
2768 // If user is system maintainer, destroy its possibly valid install tool session.
2769 $session = new SessionService();
2770 if ($session->hasSession()) {
2771 $session->destroySession();
2772 }
2773 }
2774 }
2775 parent::logoff();
2776 }
2777
2778 /**
2779 * Remove any "locked records" added for editing for the given user (= current backend user)
2780 * @param int $userId
2781 */
2782 protected function releaseLockedRecords(int $userId)
2783 {
2784 if ($userId > 0) {
2785 GeneralUtility::makeInstance(ConnectionPool::class)
2786 ->getConnectionForTable('sys_lockedrecords')
2787 ->delete(
2788 'sys_lockedrecords',
2789 ['userid' => $userId]
2790 );
2791 }
2792 }
2793 }