[TASK] Deprecate various methods inside BE_USER
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Authentication / BackendUserAuthentication.php
1 <?php
2 namespace TYPO3\CMS\Core\Authentication;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Cache\CacheManager;
19 use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
20 use TYPO3\CMS\Core\Database\Connection;
21 use TYPO3\CMS\Core\Database\ConnectionPool;
22 use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
23 use TYPO3\CMS\Core\Database\Query\QueryHelper;
24 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
25 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
26 use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
27 use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction;
28 use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
29 use TYPO3\CMS\Core\Resource\ResourceStorage;
30 use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
31 use TYPO3\CMS\Core\Type\Bitmask\Permission;
32 use TYPO3\CMS\Core\Type\Exception\InvalidEnumerationValueException;
33 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
34 use TYPO3\CMS\Core\Utility\GeneralUtility;
35 use TYPO3\CMS\Install\Service\SessionService;
36
37 /**
38 * TYPO3 backend user authentication
39 * Contains most of the functions used for checking permissions, authenticating users,
40 * setting up the user, and API for user from outside.
41 * This class contains the configuration of the database fields used plus some
42 * functions for the authentication process of backend users.
43 */
44 class BackendUserAuthentication extends AbstractUserAuthentication
45 {
46 use PublicPropertyDeprecationTrait;
47
48 public const ROLE_SYSTEMMAINTAINER = 'systemMaintainer';
49
50 /**
51 * Properties which have been moved to protected status from public
52 *
53 * @var array
54 */
55 protected $deprecatedPublicProperties = [
56 'TSdataArray' => 'Using $TSdataArray of class BackendUserAuthentication from the outside is discouraged. This property is for class internal use only.',
57 'userTS' => 'Using $userTS of class BackendUserAuthentication from the outside is discouraged. Use getTSConfig() instead.',
58 'userTSUpdated' => 'Using $userTSUpdated of class BackendUserAuthentication from the outside is discouraged. This property is for class internal use only.',
59 'userTS_text' => 'Using $userTS_text of class BackendUserAuthentication from the outside is discouraged. This property is for class internal use only.',
60 'userTS_dontGetCached' => 'Using $userTS_dontGetCached of class BackendUserAuthentication is deprecated. The property will be removed in v10.',
61 '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 v9, will be removed in v10. 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 $recordLocalizationAccess = $this->checkLanguageAccess(0);
759 if ($recordLocalizationAccess && BackendUtility::isTableLocalizable($table)) {
760 $pointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
761 $pointerValue = $record[$pointerField] > 0 ? $record[$pointerField] : $record['uid'];
762 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
763 $queryBuilder->getRestrictions()
764 ->removeAll()
765 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
766 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
767 $recordLocalization = $queryBuilder->select('*')
768 ->from($table)
769 ->where(
770 $queryBuilder->expr()->eq(
771 $pointerField,
772 $queryBuilder->createNamedParameter($pointerValue, \PDO::PARAM_INT)
773 )
774 )
775 ->setMaxResults(1)
776 ->execute()
777 ->fetch();
778
779 if (is_array($recordLocalization)) {
780 $languageAccess = $this->checkLanguageAccess(
781 $recordLocalization[$GLOBALS['TCA'][$table]['ctrl']['languageField']]
782 );
783 $recordLocalizationAccess = $recordLocalizationAccess && $languageAccess;
784 }
785 }
786 return $recordLocalizationAccess;
787 }
788
789 /**
790 * Checking if a user has editing access to a record from a $GLOBALS['TCA'] table.
791 * The checks does not take page permissions and other "environmental" things into account.
792 * It only deal with record internals; If any values in the record fields disallows it.
793 * For instance languages settings, authMode selector boxes are evaluated (and maybe more in the future).
794 * It will check for workspace dependent access.
795 * The function takes an ID (int) or row (array) as second argument.
796 *
797 * @param string $table Table name
798 * @param mixed $idOrRow If integer, then this is the ID of the record. If Array this just represents fields in the record.
799 * @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.
800 * @param bool $deletedRecord Set, if testing a deleted record array.
801 * @param bool $checkFullLanguageAccess Set, whenever access to all translations of the record is required
802 * @return bool TRUE if OK, otherwise FALSE
803 */
804 public function recordEditAccessInternals($table, $idOrRow, $newRecord = false, $deletedRecord = false, $checkFullLanguageAccess = false)
805 {
806 if (!isset($GLOBALS['TCA'][$table])) {
807 return false;
808 }
809 // Always return TRUE for Admin users.
810 if ($this->isAdmin()) {
811 return true;
812 }
813 // Fetching the record if the $idOrRow variable was not an array on input:
814 if (!is_array($idOrRow)) {
815 if ($deletedRecord) {
816 $idOrRow = BackendUtility::getRecord($table, $idOrRow, '*', '', false);
817 } else {
818 $idOrRow = BackendUtility::getRecord($table, $idOrRow);
819 }
820 if (!is_array($idOrRow)) {
821 $this->errorMsg = 'ERROR: Record could not be fetched.';
822 return false;
823 }
824 }
825 // Checking languages:
826 if ($table === 'pages' && $checkFullLanguageAccess && !$this->checkFullLanguagesAccess($table, $idOrRow)) {
827 return false;
828 }
829 if ($GLOBALS['TCA'][$table]['ctrl']['languageField']) {
830 // Language field must be found in input row - otherwise it does not make sense.
831 if (isset($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
832 if (!$this->checkLanguageAccess($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
833 $this->errorMsg = 'ERROR: Language was not allowed.';
834 return false;
835 }
836 if (
837 $checkFullLanguageAccess && $idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']] == 0
838 && !$this->checkFullLanguagesAccess($table, $idOrRow)
839 ) {
840 $this->errorMsg = 'ERROR: Related/affected language was not allowed.';
841 return false;
842 }
843 } else {
844 $this->errorMsg = 'ERROR: The "languageField" field named "'
845 . $GLOBALS['TCA'][$table]['ctrl']['languageField'] . '" was not found in testing record!';
846 return false;
847 }
848 }
849 // Checking authMode fields:
850 if (is_array($GLOBALS['TCA'][$table]['columns'])) {
851 foreach ($GLOBALS['TCA'][$table]['columns'] as $fieldName => $fieldValue) {
852 if (isset($idOrRow[$fieldName])) {
853 if (
854 $fieldValue['config']['type'] === 'select' && $fieldValue['config']['authMode']
855 && $fieldValue['config']['authMode_enforce'] === 'strict'
856 ) {
857 if (!$this->checkAuthMode($table, $fieldName, $idOrRow[$fieldName], $fieldValue['config']['authMode'])) {
858 $this->errorMsg = 'ERROR: authMode "' . $fieldValue['config']['authMode']
859 . '" failed for field "' . $fieldName . '" with value "'
860 . $idOrRow[$fieldName] . '" evaluated';
861 return false;
862 }
863 }
864 }
865 }
866 }
867 // Checking "editlock" feature (doesn't apply to new records)
868 if (!$newRecord && $GLOBALS['TCA'][$table]['ctrl']['editlock']) {
869 if (isset($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['editlock']])) {
870 if ($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['editlock']]) {
871 $this->errorMsg = 'ERROR: Record was locked for editing. Only admin users can change this state.';
872 return false;
873 }
874 } else {
875 $this->errorMsg = 'ERROR: The "editLock" field named "' . $GLOBALS['TCA'][$table]['ctrl']['editlock']
876 . '" was not found in testing record!';
877 return false;
878 }
879 }
880 // Checking record permissions
881 // THIS is where we can include a check for "perms_" fields for other records than pages...
882 // Process any hooks
883 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['recordEditAccessInternals'] ?? [] as $funcRef) {
884 $params = [
885 'table' => $table,
886 'idOrRow' => $idOrRow,
887 'newRecord' => $newRecord
888 ];
889 if (!GeneralUtility::callUserFunction($funcRef, $params, $this)) {
890 return false;
891 }
892 }
893 // Finally, return TRUE if all is well.
894 return true;
895 }
896
897 /**
898 * Checks a type of permission against the compiled permission integer,
899 * $compiledPermissions, and in relation to table, $tableName
900 *
901 * @param int $compiledPermissions Could typically be the "compiled permissions" integer returned by ->calcPerms
902 * @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)
903 * @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)
904 * @return bool
905 * @deprecated since TYPO3 v9.5, will be removed in TYPO3 v10.0.
906 */
907 public function isPSet($compiledPermissions, $tableName, $actionType = '')
908 {
909 trigger_error('BackendUserAuthentication->isPSet() will be removed in TYPO3 v10.0. Use doesUserHaveAccess() and calcPerms()', E_USER_DEPRECATED);
910 if ($this->isAdmin()) {
911 $result = true;
912 } elseif ($tableName === 'pages') {
913 switch ($actionType) {
914 case 'edit':
915 $result = ($compiledPermissions & Permission::PAGE_EDIT) !== 0;
916 break;
917 case 'new':
918 // Create new page OR page content
919 $result = ($compiledPermissions & Permission::PAGE_NEW + Permission::CONTENT_EDIT) !== 0;
920 break;
921 case 'delete':
922 $result = ($compiledPermissions & Permission::PAGE_DELETE) !== 0;
923 break;
924 case 'editcontent':
925 $result = ($compiledPermissions & Permission::CONTENT_EDIT) !== 0;
926 break;
927 default:
928 $result = false;
929 }
930 } else {
931 $result = ($compiledPermissions & Permission::CONTENT_EDIT) !== 0;
932 }
933 return $result;
934 }
935
936 /**
937 * Returns TRUE if the BE_USER is allowed to *create* shortcuts in the backend modules
938 *
939 * @return bool
940 */
941 public function mayMakeShortcut()
942 {
943 return $this->getTSConfig()['options.']['enableBookmarks'] ?? false
944 && !($this->getTSConfig()['options.']['mayNotCreateEditBookmarks'] ?? false);
945 }
946
947 /**
948 * Checking if editing of an existing record is allowed in current workspace if that is offline.
949 * Rules for editing in offline mode:
950 * - record supports versioning and is an offline version from workspace and has the corrent stage
951 * - or record (any) is in a branch where there is a page which is a version from the workspace
952 * and where the stage is not preventing records
953 *
954 * @param string $table Table of record
955 * @param array|int $recData Integer (record uid) or array where fields are at least: pid, t3ver_wsid, t3ver_stage (if versioningWS is set)
956 * @return string String error code, telling the failure state. FALSE=All ok
957 */
958 public function workspaceCannotEditRecord($table, $recData)
959 {
960 // Only test offline spaces:
961 if ($this->workspace !== 0) {
962 if (!is_array($recData)) {
963 $recData = BackendUtility::getRecord(
964 $table,
965 $recData,
966 'pid' . ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] ? ',t3ver_wsid,t3ver_stage' : '')
967 );
968 }
969 if (is_array($recData)) {
970 // We are testing a "version" (identified by a pid of -1): it can be edited provided
971 // that workspace matches and versioning is enabled for the table.
972 if ((int)$recData['pid'] === -1) {
973 // No versioning, basic error, inconsistency even! Such records should not have a pid of -1!
974 if (!$GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
975 return 'Versioning disabled for table';
976 }
977 if ((int)$recData['t3ver_wsid'] !== $this->workspace) {
978 // So does workspace match?
979 return 'Workspace ID of record didn\'t match current workspace';
980 }
981 // So is the user allowed to "use" the edit stage within the workspace?
982 return $this->workspaceCheckStageForCurrent(0)
983 ? false
984 : 'User\'s access level did not allow for editing';
985 }
986 // We are testing a "live" record:
987 // For "Live" records, check that PID for table allows editing
988 if ($res = $this->workspaceAllowLiveRecordsInPID($recData['pid'], $table)) {
989 // Live records are OK in this branch, but what about the stage of branch point, if any:
990 // OK
991 return $res > 0
992 ? false
993 : 'Stage for versioning root point and users access level did not allow for editing';
994 }
995 // If not offline and not in versionized branch, output error:
996 return 'Online record was not in versionized branch!';
997 }
998 return 'No record';
999 }
1000 // OK because workspace is 0
1001 return false;
1002 }
1003
1004 /**
1005 * Evaluates if a user is allowed to edit the offline version
1006 *
1007 * @param string $table Table of record
1008 * @param array|int $recData Integer (record uid) or array where fields are at least: pid, t3ver_wsid, t3ver_stage (if versioningWS is set)
1009 * @return string String error code, telling the failure state. FALSE=All ok
1010 * @see workspaceCannotEditRecord()
1011 * @internal this method will be moved to EXT:workspaces
1012 */
1013 public function workspaceCannotEditOfflineVersion($table, $recData)
1014 {
1015 if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1016 if (!is_array($recData)) {
1017 $recData = BackendUtility::getRecord($table, $recData, 'uid,pid,t3ver_wsid,t3ver_stage');
1018 }
1019 if (is_array($recData)) {
1020 if ((int)$recData['pid'] === -1) {
1021 return $this->workspaceCannotEditRecord($table, $recData);
1022 }
1023 return 'Not an offline version';
1024 }
1025 return 'No record';
1026 }
1027 return 'Table does not support versioning.';
1028 }
1029
1030 /**
1031 * Check if "live" records from $table may be created or edited in this PID.
1032 * If the answer is FALSE it means the only valid way to create or edit records in the PID is by versioning
1033 * 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
1034 * of versioning because the element was within a versionized branch
1035 * but NOT ok in terms of the state the root point had!
1036 *
1037 * @param int $pid PID value to check for. OBSOLETE!
1038 * @param string $table Table name
1039 * @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.
1040 */
1041 public function workspaceAllowLiveRecordsInPID($pid, $table)
1042 {
1043 // Always for Live workspace AND if live-edit is enabled
1044 // and tables are completely without versioning it is ok as well.
1045 if (
1046 $this->workspace === 0
1047 || $this->workspaceRec['live_edit'] && !$GLOBALS['TCA'][$table]['ctrl']['versioningWS']
1048 || $GLOBALS['TCA'][$table]['ctrl']['versioningWS_alwaysAllowLiveEdit']
1049 ) {
1050 // OK to create for this table.
1051 return 2;
1052 }
1053 // If the answer is FALSE it means the only valid way to create or edit records in the PID is by versioning
1054 return false;
1055 }
1056
1057 /**
1058 * Evaluates if a record from $table can be created in $pid
1059 *
1060 * @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!
1061 * @param string $table Table name
1062 * @return bool TRUE if OK.
1063 */
1064 public function workspaceCreateNewRecord($pid, $table)
1065 {
1066 if ($res = $this->workspaceAllowLiveRecordsInPID($pid, $table)) {
1067 // If LIVE records cannot be created in the current PID due to workspace restrictions, prepare creation of placeholder-record
1068 if ($res < 0) {
1069 // Stage for versioning root point and users access level did not allow for editing
1070 return false;
1071 }
1072 } elseif (!$GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
1073 // So, if no live records were allowed, we have to create a new version of this record:
1074 return false;
1075 }
1076 return true;
1077 }
1078
1079 /**
1080 * Evaluates if auto creation of a version of a record is allowed.
1081 *
1082 * @param string $table Table of the record
1083 * @param int $id UID of record
1084 * @param int $recpid PID of record
1085 * @return bool TRUE if ok.
1086 */
1087 public function workspaceAllowAutoCreation($table, $id, $recpid)
1088 {
1089 // Auto-creation of version: In offline workspace, test if versioning is
1090 // enabled and look for workspace version of input record.
1091 // If there is no versionized record found we will create one and save to that.
1092 if (
1093 $this->workspace !== 0
1094 && $GLOBALS['TCA'][$table]['ctrl']['versioningWS'] && $recpid >= 0
1095 && !BackendUtility::getWorkspaceVersionOfRecord($this->workspace, $table, $id, 'uid')
1096 ) {
1097 // There must be no existing version of this record in workspace.
1098 return true;
1099 }
1100 return false;
1101 }
1102
1103 /**
1104 * Checks if an element stage allows access for the user in the current workspace
1105 * In live workspace (= 0) access is always granted for any stage.
1106 * Admins are always allowed.
1107 * An option for custom workspaces allows members to also edit when the stage is "Review"
1108 *
1109 * @param int $stage Stage id from an element: -1,0 = editing, 1 = reviewer, >1 = owner
1110 * @return bool TRUE if user is allowed access
1111 */
1112 public function workspaceCheckStageForCurrent($stage)
1113 {
1114 // Always allow for admins
1115 if ($this->isAdmin()) {
1116 return true;
1117 }
1118 if ($this->workspace !== 0 && ExtensionManagementUtility::isLoaded('workspaces')) {
1119 $stage = (int)$stage;
1120 $stat = $this->checkWorkspaceCurrent();
1121 // Check if custom staging is activated
1122 $workspaceRec = BackendUtility::getRecord('sys_workspace', $stat['uid']);
1123 if ($workspaceRec['custom_stages'] > 0 && $stage !== 0 && $stage !== -10) {
1124 // Get custom stage record
1125 $workspaceStageRec = BackendUtility::getRecord('sys_workspace_stage', $stage);
1126 // Check if the user is responsible for the current stage
1127 if (
1128 $stat['_ACCESS'] === 'owner'
1129 || $stat['_ACCESS'] === 'member'
1130 && GeneralUtility::inList($workspaceStageRec['responsible_persons'], 'be_users_' . $this->user['uid'])
1131 ) {
1132 return true;
1133 }
1134 // Check if the user is in a group which is responsible for the current stage
1135 foreach ($this->userGroupsUID as $groupUid) {
1136 if (
1137 $stat['_ACCESS'] === 'owner'
1138 || $stat['_ACCESS'] === 'member'
1139 && GeneralUtility::inList($workspaceStageRec['responsible_persons'], 'be_groups_' . $groupUid)
1140 ) {
1141 return true;
1142 }
1143 }
1144 } elseif ($stage == -10 || $stage == -20) {
1145 if ($stat['_ACCESS'] === 'owner') {
1146 return true;
1147 }
1148 return false;
1149 } else {
1150 $memberStageLimit = $this->workspaceRec['review_stage_edit'] ? 1 : 0;
1151 if (
1152 $stat['_ACCESS'] === 'owner'
1153 || $stat['_ACCESS'] === 'reviewer' && $stage <= 1
1154 || $stat['_ACCESS'] === 'member' && $stage <= $memberStageLimit
1155 ) {
1156 return true;
1157 }
1158 }
1159 } else {
1160 // Always OK for live workspace.
1161 return true;
1162 }
1163 return false;
1164 }
1165
1166 /**
1167 * Returns TRUE if the user has access to publish content from the workspace ID given.
1168 * Admin-users are always granted access to do this
1169 * If the workspace ID is 0 (live) all users have access also
1170 * For custom workspaces it depends on whether the user is owner OR like with
1171 * draft workspace if the user has access to Live workspace.
1172 *
1173 * @param int $wsid Workspace UID; 0,1+
1174 * @return bool Returns TRUE if the user has access to publish content from the workspace ID given.
1175 * @internal this method will be moved to EXT:workspaces
1176 */
1177 public function workspacePublishAccess($wsid)
1178 {
1179 if ($this->isAdmin()) {
1180 return true;
1181 }
1182 // If no access to workspace, of course you cannot publish!
1183 $retVal = false;
1184 $wsAccess = $this->checkWorkspace($wsid);
1185 if ($wsAccess) {
1186 switch ($wsAccess['uid']) {
1187 case 0:
1188 // Live workspace
1189 // If access to Live workspace, no problem.
1190 $retVal = true;
1191 break;
1192 default:
1193 // Custom workspace
1194 $retVal = $wsAccess['_ACCESS'] === 'owner' || $this->checkWorkspace(0) && !($wsAccess['publish_access'] & Permission::PAGE_EDIT);
1195 // Either be an adminuser OR have access to online
1196 // workspace which is OK as well as long as publishing
1197 // access is not limited by workspace option.
1198 }
1199 }
1200 return $retVal;
1201 }
1202
1203 /**
1204 * Workspace swap-mode access?
1205 *
1206 * @return bool Returns TRUE if records can be swapped in the current workspace, otherwise FALSE
1207 * @internal this method will be moved to EXT:workspaces
1208 */
1209 public function workspaceSwapAccess()
1210 {
1211 if ($this->workspace > 0 && (int)$this->workspaceRec['swap_modes'] === 2) {
1212 return false;
1213 }
1214 return true;
1215 }
1216
1217 /**
1218 * Returns full parsed user TSconfig array, merged with TSconfig from groups.
1219 *
1220 * Example:
1221 * [
1222 * 'options.' => [
1223 * 'fooEnabled' => '0',
1224 * 'fooEnabled.' => [
1225 * 'tt_content' => 1,
1226 * ],
1227 * ],
1228 * ]
1229 *
1230 * @param string $objectString @deprecated
1231 * @param array|string $config @deprecated
1232 * @return array Parsed and merged user TSconfig array
1233 */
1234 public function getTSConfig($objectString = null, $config = null)
1235 {
1236 if ($objectString === null && $config === null) {
1237 return $this->userTS;
1238 }
1239
1240 trigger_error('Handing over arguments to BackendUserAuthentication->getTSConfig() is deprecated, they will be removed in v10.', E_USER_DEPRECATED);
1241
1242 if (!is_array($config)) {
1243 // Getting Root-ts if not sent
1244 $config = $this->userTS;
1245 }
1246 $TSConf = ['value' => null, 'properties' => null];
1247 $parts = GeneralUtility::trimExplode('.', $objectString, true, 2);
1248 $key = $parts[0];
1249 if ($key !== '') {
1250 if (count($parts) > 1 && $parts[1] !== '') {
1251 // Go on, get the next level
1252 if (is_array($config[$key . '.'] ?? false)) {
1253 $TSConf = $this->getTSConfig($parts[1], $config[$key . '.']);
1254 }
1255 } else {
1256 $TSConf['value'] = $config[$key] ?? null;
1257 $TSConf['properties'] = $config[$key . '.'] ?? null;
1258 }
1259 }
1260 return $TSConf;
1261 }
1262
1263 /**
1264 * Returns the "value" of the $objectString from the BE_USERS "User TSconfig" array
1265 *
1266 * @param string $objectString Object string, eg. "somestring.someproperty.somesubproperty
1267 * @return string The value for that object string (object path)
1268 * @see getTSConfig()
1269 * @deprecated since TYPO3 v9, will be removed with TYPO3 v10
1270 */
1271 public function getTSConfigVal($objectString)
1272 {
1273 trigger_error('BackendUserAuthentication->getTSConfigVal() will be removed in TYPO3 v10. Use getTSConfig() instead.', E_USER_DEPRECATED);
1274 $TSConf = $this->getTSConfig($objectString);
1275 return $TSConf['value'];
1276 }
1277
1278 /**
1279 * Returns the "properties" of the $objectString from the BE_USERS "User TSconfig" array
1280 *
1281 * @param string $objectString Object string, eg. "somestring.someproperty.somesubproperty
1282 * @return array The properties for that object string (object path) - if any
1283 * @see getTSConfig()
1284 * @deprecated since TYPO3 v9, will be removed with TYPO3 v10
1285 */
1286 public function getTSConfigProp($objectString)
1287 {
1288 trigger_error('BackendUserAuthentication->getTSConfigProp() will be removed in TYPO3 v10. Use getTSConfig() instead.', E_USER_DEPRECATED);
1289 $TSConf = $this->getTSConfig($objectString);
1290 return $TSConf['properties'];
1291 }
1292
1293 /**
1294 * Returns an array with the webmounts.
1295 * If no webmounts, and empty array is returned.
1296 * NOTICE: Deleted pages WILL NOT be filtered out! So if a mounted page has been deleted
1297 * it is STILL coming out as a webmount. This is not checked due to performance.
1298 *
1299 * @return array
1300 */
1301 public function returnWebmounts()
1302 {
1303 return (string)$this->groupData['webmounts'] != '' ? explode(',', $this->groupData['webmounts']) : [];
1304 }
1305
1306 /**
1307 * Initializes the given mount points for the current Backend user.
1308 *
1309 * @param array $mountPointUids Page UIDs that should be used as web mountpoints
1310 * @param bool $append If TRUE the given mount point will be appended. Otherwise the current mount points will be replaced.
1311 */
1312 public function setWebmounts(array $mountPointUids, $append = false)
1313 {
1314 if (empty($mountPointUids)) {
1315 return;
1316 }
1317 if ($append) {
1318 $currentWebMounts = GeneralUtility::intExplode(',', $this->groupData['webmounts']);
1319 $mountPointUids = array_merge($currentWebMounts, $mountPointUids);
1320 }
1321 $this->groupData['webmounts'] = implode(',', array_unique($mountPointUids));
1322 }
1323
1324 /**
1325 * Returns TRUE or FALSE, depending if an alert popup (a javascript confirmation) should be shown
1326 * call like $GLOBALS['BE_USER']->jsConfirmation($BITMASK).
1327 *
1328 * @param int $bitmask Bitmask, one of \TYPO3\CMS\Core\Type\Bitmask\JsConfirmation
1329 * @return bool TRUE if the confirmation should be shown
1330 * @see JsConfirmation
1331 */
1332 public function jsConfirmation($bitmask)
1333 {
1334 try {
1335 $alertPopupsSetting = trim((string)($this->getTSConfig()['options.']['alertPopups'] ?? ''));
1336 $alertPopup = JsConfirmation::cast($alertPopupsSetting === '' ? null : (int)$alertPopupsSetting);
1337 } catch (InvalidEnumerationValueException $e) {
1338 $alertPopup = new JsConfirmation();
1339 }
1340
1341 return JsConfirmation::cast($bitmask)->matches($alertPopup);
1342 }
1343
1344 /**
1345 * Initializes a lot of stuff like the access-lists, database-mountpoints and filemountpoints
1346 * This method is called by ->backendCheckLogin() (from extending BackendUserAuthentication)
1347 * if the backend user login has verified OK.
1348 * Generally this is required initialization of a backend user.
1349 *
1350 * @access private
1351 * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
1352 */
1353 public function fetchGroupData()
1354 {
1355 if ($this->user['uid']) {
1356 // Get lists for the be_user record and set them as default/primary values.
1357 // Enabled Backend Modules
1358 $this->dataLists['modList'] = $this->user['userMods'];
1359 // Add Allowed Languages
1360 $this->dataLists['allowed_languages'] = $this->user['allowed_languages'];
1361 // Set user value for workspace permissions.
1362 $this->dataLists['workspace_perms'] = $this->user['workspace_perms'];
1363 // Database mountpoints
1364 $this->dataLists['webmount_list'] = $this->user['db_mountpoints'];
1365 // File mountpoints
1366 $this->dataLists['filemount_list'] = $this->user['file_mountpoints'];
1367 // Fileoperation permissions
1368 $this->dataLists['file_permissions'] = $this->user['file_permissions'];
1369 // Setting default User TSconfig:
1370 $this->TSdataArray[] = $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig'];
1371 // Default TSconfig for admin-users
1372 if ($this->isAdmin()) {
1373 $this->TSdataArray[] = 'admPanel.enable.all = 1';
1374 if (ExtensionManagementUtility::isLoaded('sys_note')) {
1375 $this->TSdataArray[] = '
1376 // Setting defaults for sys_note author / email...
1377 TCAdefaults.sys_note.author = ' . $this->user['realName'] . '
1378 TCAdefaults.sys_note.email = ' . $this->user['email'] . '
1379 ';
1380 }
1381 }
1382 // BE_GROUPS:
1383 // Get the groups...
1384 if (!empty($this->user[$this->usergroup_column])) {
1385 // Fetch groups will add a lot of information to the internal arrays: modules, accesslists, TSconfig etc.
1386 // Refer to fetchGroups() function.
1387 $this->fetchGroups($this->user[$this->usergroup_column]);
1388 }
1389
1390 // Populating the $this->userGroupsUID -array with the groups in the order in which they were LAST included.!!
1391 $this->userGroupsUID = array_reverse(array_unique(array_reverse($this->includeGroupArray)));
1392 // Finally this is the list of group_uid's in the order they are parsed (including subgroups!)
1393 // and without duplicates (duplicates are presented with their last entrance in the list,
1394 // which thus reflects the order of the TypoScript in TSconfig)
1395 $this->groupList = implode(',', $this->userGroupsUID);
1396 $this->setCachedList($this->groupList);
1397
1398 // Add the TSconfig for this specific user:
1399 $this->TSdataArray[] = $this->user['TSconfig'];
1400 // Check include lines.
1401 $this->TSdataArray = \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::checkIncludeLines_array($this->TSdataArray);
1402 // Imploding with "[global]" will make sure that non-ended confinements with braces are ignored.
1403 $this->userTS_text = implode(LF . '[GLOBAL]' . LF, $this->TSdataArray);
1404 if (!$this->userTS_dontGetCached) {
1405 // @deprecated: Property userTS_dontGetCached is deprecated since v9 and will be removed in v10
1406 // Perform TS-Config parsing with condition matching
1407 $parseObj = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Configuration\TsConfigParser::class);
1408 $res = $parseObj->parseTSconfig($this->userTS_text, 'userTS');
1409 if ($res) {
1410 $this->userTS = $res['TSconfig'];
1411 $this->userTSUpdated = (bool)$res['cached'];
1412 }
1413 } else {
1414 // Parsing the user TSconfig (or getting from cache)
1415 $hash = md5('userTS:' . $this->userTS_text);
1416 $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_hash');
1417 $cachedContent = $cache->get($hash);
1418 if (is_array($cachedContent) && !$this->userTS_dontGetCached) {
1419 // @deprecated: Property userTS_dontGetCached is deprecated since v9 and will be removed in v10
1420 $this->userTS = $cachedContent;
1421 } else {
1422 $parseObj = GeneralUtility::makeInstance(\TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::class);
1423 $parseObj->parse($this->userTS_text);
1424 $this->userTS = $parseObj->setup;
1425 $cache->set($hash, $this->userTS, ['ident_BE_USER_TSconfig'], 0);
1426 // Update UC:
1427 $this->userTSUpdated = true;
1428 }
1429 }
1430 // Processing webmounts
1431 // Admin's always have the root mounted
1432 if ($this->isAdmin() && !($this->getTSConfig()['options.']['dontMountAdminMounts'] ?? false)) {
1433 $this->dataLists['webmount_list'] = '0,' . $this->dataLists['webmount_list'];
1434 }
1435 // The lists are cleaned for duplicates
1436 $this->groupData['webmounts'] = GeneralUtility::uniqueList($this->dataLists['webmount_list']);
1437 $this->groupData['pagetypes_select'] = GeneralUtility::uniqueList($this->dataLists['pagetypes_select']);
1438 $this->groupData['tables_select'] = GeneralUtility::uniqueList($this->dataLists['tables_modify'] . ',' . $this->dataLists['tables_select']);
1439 $this->groupData['tables_modify'] = GeneralUtility::uniqueList($this->dataLists['tables_modify']);
1440 $this->groupData['non_exclude_fields'] = GeneralUtility::uniqueList($this->dataLists['non_exclude_fields']);
1441 $this->groupData['explicit_allowdeny'] = GeneralUtility::uniqueList($this->dataLists['explicit_allowdeny']);
1442 $this->groupData['allowed_languages'] = GeneralUtility::uniqueList($this->dataLists['allowed_languages']);
1443 $this->groupData['custom_options'] = GeneralUtility::uniqueList($this->dataLists['custom_options']);
1444 $this->groupData['modules'] = GeneralUtility::uniqueList($this->dataLists['modList']);
1445 $this->groupData['file_permissions'] = GeneralUtility::uniqueList($this->dataLists['file_permissions']);
1446 $this->groupData['workspace_perms'] = $this->dataLists['workspace_perms'];
1447
1448 if (!empty(trim($this->groupData['webmounts']))) {
1449 // Checking read access to web mounts if there are mounts points (not empty string, false or 0)
1450 $webmounts = explode(',', $this->groupData['webmounts']);
1451 // Selecting all web mounts with permission clause for reading
1452 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1453 $queryBuilder->getRestrictions()
1454 ->removeAll()
1455 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1456
1457 $MProws = $queryBuilder->select('uid')
1458 ->from('pages')
1459 // @todo DOCTRINE: check how to make getPagePermsClause() portable
1460 ->where(
1461 $this->getPagePermsClause(Permission::PAGE_SHOW),
1462 $queryBuilder->expr()->in(
1463 'uid',
1464 $queryBuilder->createNamedParameter(
1465 GeneralUtility::intExplode(',', $this->groupData['webmounts']),
1466 Connection::PARAM_INT_ARRAY
1467 )
1468 )
1469 )
1470 ->execute()
1471 ->fetchAll();
1472 $MProws = array_column(($MProws ?: []), 'uid', 'uid');
1473 foreach ($webmounts as $idx => $mountPointUid) {
1474 // If the mount ID is NOT found among selected pages, unset it:
1475 if ($mountPointUid > 0 && !isset($MProws[$mountPointUid])) {
1476 unset($webmounts[$idx]);
1477 }
1478 }
1479 // Implode mounts in the end.
1480 $this->groupData['webmounts'] = implode(',', $webmounts);
1481 }
1482 // Setting up workspace situation (after webmounts are processed!):
1483 $this->workspaceInit();
1484 }
1485 }
1486
1487 /**
1488 * Fetches the group records, subgroups and fills internal arrays.
1489 * Function is called recursively to fetch subgroups
1490 *
1491 * @param string $grList Commalist of be_groups uid numbers
1492 * @param string $idList List of already processed be_groups-uids so the function will not fall into an eternal recursion.
1493 * @access private
1494 */
1495 public function fetchGroups($grList, $idList = '')
1496 {
1497 // Fetching records of the groups in $grList (which are not blocked by lockedToDomain either):
1498 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->usergroup_table);
1499 $expressionBuilder = $queryBuilder->expr();
1500 $constraints = $expressionBuilder->andX(
1501 $expressionBuilder->eq(
1502 'pid',
1503 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1504 ),
1505 $expressionBuilder->in(
1506 'uid',
1507 $queryBuilder->createNamedParameter(
1508 GeneralUtility::intExplode(',', $grList),
1509 Connection::PARAM_INT_ARRAY
1510 )
1511 ),
1512 $expressionBuilder->orX(
1513 $expressionBuilder->eq('lockToDomain', $queryBuilder->quote('')),
1514 $expressionBuilder->isNull('lockToDomain'),
1515 $expressionBuilder->eq(
1516 'lockToDomain',
1517 $queryBuilder->createNamedParameter(GeneralUtility::getIndpEnv('HTTP_HOST'), \PDO::PARAM_STR)
1518 )
1519 )
1520 );
1521 // Hook for manipulation of the WHERE sql sentence which controls which BE-groups are included
1522 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroupQuery'] ?? [] as $className) {
1523 $hookObj = GeneralUtility::makeInstance($className);
1524 if (method_exists($hookObj, 'fetchGroupQuery_processQuery')) {
1525 $constraints = $hookObj->fetchGroupQuery_processQuery($this, $grList, $idList, (string)$constraints);
1526 }
1527 }
1528 $res = $queryBuilder->select('*')
1529 ->from($this->usergroup_table)
1530 ->where($constraints)
1531 ->execute();
1532 // The userGroups array is filled
1533 while ($row = $res->fetch(\PDO::FETCH_ASSOC)) {
1534 $this->userGroups[$row['uid']] = $row;
1535 }
1536 // Traversing records in the correct order
1537 foreach (explode(',', $grList) as $uid) {
1538 // Get row:
1539 $row = $this->userGroups[$uid];
1540 // Must be an array and $uid should not be in the idList, because then it is somewhere previously in the grouplist
1541 if (is_array($row) && !GeneralUtility::inList($idList, $uid)) {
1542 // Include sub groups
1543 if (trim($row['subgroup'])) {
1544 // Make integer list
1545 $theList = implode(',', GeneralUtility::intExplode(',', $row['subgroup']));
1546 // Call recursively, pass along list of already processed groups so they are not recursed again.
1547 $this->fetchGroups($theList, $idList . ',' . $uid);
1548 }
1549 // Add the group uid, current list, TSconfig to the internal arrays.
1550 $this->includeGroupArray[] = $uid;
1551 $this->TSdataArray[] = $row['TSconfig'];
1552 // Mount group database-mounts
1553 if (($this->user['options'] & Permission::PAGE_SHOW) == 1) {
1554 $this->dataLists['webmount_list'] .= ',' . $row['db_mountpoints'];
1555 }
1556 // Mount group file-mounts
1557 if (($this->user['options'] & Permission::PAGE_EDIT) == 2) {
1558 $this->dataLists['filemount_list'] .= ',' . $row['file_mountpoints'];
1559 }
1560 // The lists are made: groupMods, tables_select, tables_modify, pagetypes_select, non_exclude_fields, explicit_allowdeny, allowed_languages, custom_options
1561 $this->dataLists['modList'] .= ',' . $row['groupMods'];
1562 $this->dataLists['tables_select'] .= ',' . $row['tables_select'];
1563 $this->dataLists['tables_modify'] .= ',' . $row['tables_modify'];
1564 $this->dataLists['pagetypes_select'] .= ',' . $row['pagetypes_select'];
1565 $this->dataLists['non_exclude_fields'] .= ',' . $row['non_exclude_fields'];
1566 $this->dataLists['explicit_allowdeny'] .= ',' . $row['explicit_allowdeny'];
1567 $this->dataLists['allowed_languages'] .= ',' . $row['allowed_languages'];
1568 $this->dataLists['custom_options'] .= ',' . $row['custom_options'];
1569 $this->dataLists['file_permissions'] .= ',' . $row['file_permissions'];
1570 // Setting workspace permissions:
1571 $this->dataLists['workspace_perms'] |= $row['workspace_perms'];
1572 // If this function is processing the users OWN group-list (not subgroups) AND
1573 // if the ->firstMainGroup is not set, then the ->firstMainGroup will be set.
1574 if ($idList === '' && !$this->firstMainGroup) {
1575 $this->firstMainGroup = $uid;
1576 }
1577 }
1578 }
1579 // HOOK: fetchGroups_postProcessing
1580 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroups_postProcessing'] ?? [] as $_funcRef) {
1581 $_params = [];
1582 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1583 }
1584 }
1585
1586 /**
1587 * Updates the field be_users.usergroup_cached_list if the groupList of the user
1588 * has changed/is different from the current list.
1589 * The field "usergroup_cached_list" contains the list of groups which the user is a member of.
1590 * After authentication (where these functions are called...) one can depend on this list being
1591 * a representation of the exact groups/subgroups which the BE_USER has membership with.
1592 *
1593 * @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.
1594 * @access private
1595 */
1596 public function setCachedList($cList)
1597 {
1598 if ((string)$cList != (string)$this->user['usergroup_cached_list']) {
1599 GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users')->update(
1600 'be_users',
1601 ['usergroup_cached_list' => $cList],
1602 ['uid' => (int)$this->user['uid']]
1603 );
1604 }
1605 }
1606
1607 /**
1608 * Sets up all file storages for a user.
1609 * Needs to be called AFTER the groups have been loaded.
1610 */
1611 protected function initializeFileStorages()
1612 {
1613 $this->fileStorages = [];
1614 /** @var \TYPO3\CMS\Core\Resource\StorageRepository $storageRepository */
1615 $storageRepository = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\StorageRepository::class);
1616 // Admin users have all file storages visible, without any filters
1617 if ($this->isAdmin()) {
1618 $storageObjects = $storageRepository->findAll();
1619 foreach ($storageObjects as $storageObject) {
1620 $this->fileStorages[$storageObject->getUid()] = $storageObject;
1621 }
1622 } else {
1623 // Regular users only have storages that are defined in their filemounts
1624 // Permissions and file mounts for the storage are added in StoragePermissionAspect
1625 foreach ($this->getFileMountRecords() as $row) {
1626 if (!array_key_exists((int)$row['base'], $this->fileStorages)) {
1627 $storageObject = $storageRepository->findByUid($row['base']);
1628 if ($storageObject) {
1629 $this->fileStorages[$storageObject->getUid()] = $storageObject;
1630 }
1631 }
1632 }
1633 }
1634
1635 // This has to be called always in order to set certain filters
1636 $this->evaluateUserSpecificFileFilterSettings();
1637 }
1638
1639 /**
1640 * Returns an array of category mount points. The category permissions from BE Groups
1641 * are also taken into consideration and are merged into User permissions.
1642 *
1643 * @return array
1644 */
1645 public function getCategoryMountPoints()
1646 {
1647 $categoryMountPoints = '';
1648
1649 // Category mounts of the groups
1650 if (is_array($this->userGroups)) {
1651 foreach ($this->userGroups as $group) {
1652 if ($group['category_perms']) {
1653 $categoryMountPoints .= ',' . $group['category_perms'];
1654 }
1655 }
1656 }
1657
1658 // Category mounts of the user record
1659 if ($this->user['category_perms']) {
1660 $categoryMountPoints .= ',' . $this->user['category_perms'];
1661 }
1662
1663 // Make the ids unique
1664 $categoryMountPoints = GeneralUtility::trimExplode(',', $categoryMountPoints);
1665 $categoryMountPoints = array_filter($categoryMountPoints); // remove empty value
1666 $categoryMountPoints = array_unique($categoryMountPoints); // remove unique value
1667
1668 return $categoryMountPoints;
1669 }
1670
1671 /**
1672 * Returns an array of file mount records, taking workspaces and user home and group home directories into account
1673 * Needs to be called AFTER the groups have been loaded.
1674 *
1675 * @return array
1676 * @internal
1677 */
1678 public function getFileMountRecords()
1679 {
1680 $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
1681 $fileMountRecordCache = $runtimeCache->get('backendUserAuthenticationFileMountRecords') ?: [];
1682
1683 if (!empty($fileMountRecordCache)) {
1684 return $fileMountRecordCache;
1685 }
1686
1687 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
1688
1689 // Processing file mounts (both from the user and the groups)
1690 $fileMounts = array_unique(GeneralUtility::intExplode(',', $this->dataLists['filemount_list'], true));
1691
1692 // Limit file mounts if set in workspace record
1693 if ($this->workspace > 0 && !empty($this->workspaceRec['file_mountpoints'])) {
1694 $workspaceFileMounts = GeneralUtility::intExplode(',', $this->workspaceRec['file_mountpoints'], true);
1695 $fileMounts = array_intersect($fileMounts, $workspaceFileMounts);
1696 }
1697
1698 if (!empty($fileMounts)) {
1699 $orderBy = $GLOBALS['TCA']['sys_filemounts']['ctrl']['default_sortby'] ?? 'sorting';
1700
1701 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_filemounts');
1702 $queryBuilder->getRestrictions()
1703 ->removeAll()
1704 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1705 ->add(GeneralUtility::makeInstance(HiddenRestriction::class))
1706 ->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
1707
1708 $queryBuilder->select('*')
1709 ->from('sys_filemounts')
1710 ->where(
1711 $queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($fileMounts, Connection::PARAM_INT_ARRAY))
1712 );
1713
1714 foreach (QueryHelper::parseOrderBy($orderBy) as $fieldAndDirection) {
1715 $queryBuilder->addOrderBy(...$fieldAndDirection);
1716 }
1717
1718 $fileMountRecords = $queryBuilder->execute()->fetchAll(\PDO::FETCH_ASSOC);
1719 if ($fileMountRecords !== false) {
1720 foreach ($fileMountRecords as $fileMount) {
1721 $fileMountRecordCache[$fileMount['base'] . $fileMount['path']] = $fileMount;
1722 }
1723 }
1724 }
1725
1726 // Read-only file mounts
1727 $readOnlyMountPoints = \trim($this->getTSConfig()['options.']['folderTree.']['altElementBrowserMountPoints'] ?? '');
1728 if ($readOnlyMountPoints) {
1729 // We cannot use the API here but need to fetch the default storage record directly
1730 // to not instantiate it (which directly applies mount points) before all mount points are resolved!
1731 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_file_storage');
1732 $defaultStorageRow = $queryBuilder->select('uid')
1733 ->from('sys_file_storage')
1734 ->where(
1735 $queryBuilder->expr()->eq('is_default', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT))
1736 )
1737 ->setMaxResults(1)
1738 ->execute()
1739 ->fetch(\PDO::FETCH_ASSOC);
1740
1741 $readOnlyMountPointArray = GeneralUtility::trimExplode(',', $readOnlyMountPoints);
1742 foreach ($readOnlyMountPointArray as $readOnlyMountPoint) {
1743 $readOnlyMountPointConfiguration = GeneralUtility::trimExplode(':', $readOnlyMountPoint);
1744 if (count($readOnlyMountPointConfiguration) === 2) {
1745 // A storage is passed in the configuration
1746 $storageUid = (int)$readOnlyMountPointConfiguration[0];
1747 $path = $readOnlyMountPointConfiguration[1];
1748 } else {
1749 if (empty($defaultStorageRow)) {
1750 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);
1751 }
1752 // Backwards compatibility: If no storage is passed, we use the default storage
1753 $storageUid = $defaultStorageRow['uid'];
1754 $path = $readOnlyMountPointConfiguration[0];
1755 }
1756 $fileMountRecordCache[$storageUid . $path] = [
1757 'base' => $storageUid,
1758 'title' => $path,
1759 'path' => $path,
1760 'read_only' => true
1761 ];
1762 }
1763 }
1764
1765 // Personal or Group filemounts are not accessible if file mount list is set in workspace record
1766 if ($this->workspace <= 0 || empty($this->workspaceRec['file_mountpoints'])) {
1767 // If userHomePath is set, we attempt to mount it
1768 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath']) {
1769 list($userHomeStorageUid, $userHomeFilter) = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath'], 2);
1770 $userHomeStorageUid = (int)$userHomeStorageUid;
1771 $userHomeFilter = '/' . ltrim($userHomeFilter, '/');
1772 if ($userHomeStorageUid > 0) {
1773 // Try and mount with [uid]_[username]
1774 $path = $userHomeFilter . $this->user['uid'] . '_' . $this->user['username'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1775 $fileMountRecordCache[$userHomeStorageUid . $path] = [
1776 'base' => $userHomeStorageUid,
1777 'title' => $this->user['username'],
1778 'path' => $path,
1779 'read_only' => false,
1780 'user_mount' => true
1781 ];
1782 // Try and mount with only [uid]
1783 $path = $userHomeFilter . $this->user['uid'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1784 $fileMountRecordCache[$userHomeStorageUid . $path] = [
1785 'base' => $userHomeStorageUid,
1786 'title' => $this->user['username'],
1787 'path' => $path,
1788 'read_only' => false,
1789 'user_mount' => true
1790 ];
1791 }
1792 }
1793
1794 // Mount group home-dirs
1795 if ((is_array($this->user) && $this->user['options'] & Permission::PAGE_EDIT) == 2 && $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'] != '') {
1796 // If groupHomePath is set, we attempt to mount it
1797 list($groupHomeStorageUid, $groupHomeFilter) = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'], 2);
1798 $groupHomeStorageUid = (int)$groupHomeStorageUid;
1799 $groupHomeFilter = '/' . ltrim($groupHomeFilter, '/');
1800 if ($groupHomeStorageUid > 0) {
1801 foreach ($this->userGroups as $groupData) {
1802 $path = $groupHomeFilter . $groupData['uid'];
1803 $fileMountRecordCache[$groupHomeStorageUid . $path] = [
1804 'base' => $groupHomeStorageUid,
1805 'title' => $groupData['title'],
1806 'path' => $path,
1807 'read_only' => false,
1808 'user_mount' => true
1809 ];
1810 }
1811 }
1812 }
1813 }
1814
1815 $runtimeCache->set('backendUserAuthenticationFileMountRecords', $fileMountRecordCache);
1816 return $fileMountRecordCache;
1817 }
1818
1819 /**
1820 * Returns an array with the filemounts for the user.
1821 * Each filemount is represented with an array of a "name", "path" and "type".
1822 * If no filemounts an empty array is returned.
1823 *
1824 * @api
1825 * @return \TYPO3\CMS\Core\Resource\ResourceStorage[]
1826 */
1827 public function getFileStorages()
1828 {
1829 // Initializing file mounts after the groups are fetched
1830 if ($this->fileStorages === null) {
1831 $this->initializeFileStorages();
1832 }
1833 return $this->fileStorages;
1834 }
1835
1836 /**
1837 * Adds filters based on what the user has set
1838 * this should be done in this place, and called whenever needed,
1839 * but only when needed
1840 */
1841 public function evaluateUserSpecificFileFilterSettings()
1842 {
1843 // Add the option for also displaying the non-hidden files
1844 if ($this->uc['showHiddenFilesAndFolders']) {
1845 \TYPO3\CMS\Core\Resource\Filter\FileNameFilter::setShowHiddenFilesAndFolders(true);
1846 }
1847 }
1848
1849 /**
1850 * Returns the information about file permissions.
1851 * Previously, this was stored in the DB field fileoper_perms now it is file_permissions.
1852 * Besides it can be handled via userTSconfig
1853 *
1854 * permissions.file.default {
1855 * addFile = 1
1856 * readFile = 1
1857 * writeFile = 1
1858 * copyFile = 1
1859 * moveFile = 1
1860 * renameFile = 1
1861 * deleteFile = 1
1862 *
1863 * addFolder = 1
1864 * readFolder = 1
1865 * writeFolder = 1
1866 * copyFolder = 1
1867 * moveFolder = 1
1868 * renameFolder = 1
1869 * deleteFolder = 1
1870 * recursivedeleteFolder = 1
1871 * }
1872 *
1873 * # overwrite settings for a specific storageObject
1874 * permissions.file.storage.StorageUid {
1875 * readFile = 1
1876 * recursivedeleteFolder = 0
1877 * }
1878 *
1879 * Please note that these permissions only apply, if the storage has the
1880 * capabilities (browseable, writable), and if the driver allows for writing etc
1881 *
1882 * @api
1883 * @return array
1884 */
1885 public function getFilePermissions()
1886 {
1887 if (!isset($this->filePermissions)) {
1888 $filePermissions = [
1889 // File permissions
1890 'addFile' => false,
1891 'readFile' => false,
1892 'writeFile' => false,
1893 'copyFile' => false,
1894 'moveFile' => false,
1895 'renameFile' => false,
1896 'deleteFile' => false,
1897 // Folder permissions
1898 'addFolder' => false,
1899 'readFolder' => false,
1900 'writeFolder' => false,
1901 'copyFolder' => false,
1902 'moveFolder' => false,
1903 'renameFolder' => false,
1904 'deleteFolder' => false,
1905 'recursivedeleteFolder' => false
1906 ];
1907 if ($this->isAdmin()) {
1908 $filePermissions = array_map('is_bool', $filePermissions);
1909 } else {
1910 $userGroupRecordPermissions = GeneralUtility::trimExplode(',', $this->groupData['file_permissions'] ?? '', true);
1911 array_walk(
1912 $userGroupRecordPermissions,
1913 function ($permission) use (&$filePermissions) {
1914 $filePermissions[$permission] = true;
1915 }
1916 );
1917
1918 // Finally overlay any userTSconfig
1919 $permissionsTsConfig = $this->getTSConfig()['permissions.']['file.']['default.'] ?? [];
1920 if (!empty($permissionsTsConfig)) {
1921 array_walk(
1922 $permissionsTsConfig,
1923 function ($value, $permission) use (&$filePermissions) {
1924 $filePermissions[$permission] = (bool)$value;
1925 }
1926 );
1927 }
1928 }
1929 $this->filePermissions = $filePermissions;
1930 }
1931 return $this->filePermissions;
1932 }
1933
1934 /**
1935 * Gets the file permissions for a storage
1936 * by merging any storage-specific permissions for a
1937 * storage with the default settings.
1938 * Admin users will always get the default settings.
1939 *
1940 * @api
1941 * @param \TYPO3\CMS\Core\Resource\ResourceStorage $storageObject
1942 * @return array
1943 */
1944 public function getFilePermissionsForStorage(\TYPO3\CMS\Core\Resource\ResourceStorage $storageObject)
1945 {
1946 $finalUserPermissions = $this->getFilePermissions();
1947 if (!$this->isAdmin()) {
1948 $storageFilePermissions = $this->getTSConfig()['permissions.']['file.']['storage.'][$storageObject->getUid() . '.'] ?? [];
1949 if (!empty($storageFilePermissions)) {
1950 array_walk(
1951 $storageFilePermissions,
1952 function ($value, $permission) use (&$finalUserPermissions) {
1953 $finalUserPermissions[$permission] = (bool)$value;
1954 }
1955 );
1956 }
1957 }
1958 return $finalUserPermissions;
1959 }
1960
1961 /**
1962 * Returns a \TYPO3\CMS\Core\Resource\Folder object that is used for uploading
1963 * files by default.
1964 * This is used for RTE and its magic images, as well as uploads
1965 * in the TCEforms fields.
1966 *
1967 * The default upload folder for a user is the defaultFolder on the first
1968 * filestorage/filemount that the user can access and to which files are allowed to be added
1969 * however, you can set the users' upload folder like this:
1970 *
1971 * options.defaultUploadFolder = 3:myfolder/yourfolder/
1972 *
1973 * @param int $pid PageUid
1974 * @param string $table Table name
1975 * @param string $field Field name
1976 * @return \TYPO3\CMS\Core\Resource\Folder|bool The default upload folder for this user
1977 */
1978 public function getDefaultUploadFolder($pid = null, $table = null, $field = null)
1979 {
1980 $uploadFolder = $this->getTSConfig()['options.']['defaultUploadFolder'] ?? '';
1981 if ($uploadFolder) {
1982 $uploadFolder = \TYPO3\CMS\Core\Resource\ResourceFactory::getInstance()->getFolderObjectFromCombinedIdentifier($uploadFolder);
1983 } else {
1984 foreach ($this->getFileStorages() as $storage) {
1985 if ($storage->isDefault() && $storage->isWritable()) {
1986 try {
1987 $uploadFolder = $storage->getDefaultFolder();
1988 if ($uploadFolder->checkActionPermission('write')) {
1989 break;
1990 }
1991 $uploadFolder = null;
1992 } catch (\TYPO3\CMS\Core\Resource\Exception $folderAccessException) {
1993 // If the folder is not accessible (no permissions / does not exist) we skip this one.
1994 }
1995 break;
1996 }
1997 }
1998 if (!$uploadFolder instanceof \TYPO3\CMS\Core\Resource\Folder) {
1999 /** @var ResourceStorage $storage */
2000 foreach ($this->getFileStorages() as $storage) {
2001 if ($storage->isWritable()) {
2002 try {
2003 $uploadFolder = $storage->getDefaultFolder();
2004 if ($uploadFolder->checkActionPermission('write')) {
2005 break;
2006 }
2007 $uploadFolder = null;
2008 } catch (\TYPO3\CMS\Core\Resource\Exception $folderAccessException) {
2009 // If the folder is not accessible (no permissions / does not exist) try the next one.
2010 }
2011 }
2012 }
2013 }
2014 }
2015
2016 // HOOK: getDefaultUploadFolder
2017 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getDefaultUploadFolder'] ?? [] as $_funcRef) {
2018 $_params = [
2019 'uploadFolder' => $uploadFolder,
2020 'pid' => $pid,
2021 'table' => $table,
2022 'field' => $field,
2023 ];
2024 $uploadFolder = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2025 }
2026
2027 if ($uploadFolder instanceof \TYPO3\CMS\Core\Resource\Folder) {
2028 return $uploadFolder;
2029 }
2030 return false;
2031 }
2032
2033 /**
2034 * Returns a \TYPO3\CMS\Core\Resource\Folder object that could be used for uploading
2035 * temporary files in user context. The folder _temp_ below the default upload folder
2036 * of the user is used.
2037 *
2038 * @return \TYPO3\CMS\Core\Resource\Folder|null
2039 * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::getDefaultUploadFolder();
2040 */
2041 public function getDefaultUploadTemporaryFolder()
2042 {
2043 $defaultTemporaryFolder = null;
2044 $defaultFolder = $this->getDefaultUploadFolder();
2045
2046 if ($defaultFolder !== false) {
2047 $tempFolderName = '_temp_';
2048 $createFolder = !$defaultFolder->hasFolder($tempFolderName);
2049 if ($createFolder === true) {
2050 try {
2051 $defaultTemporaryFolder = $defaultFolder->createFolder($tempFolderName);
2052 } catch (\TYPO3\CMS\Core\Resource\Exception $folderAccessException) {
2053 }
2054 } else {
2055 $defaultTemporaryFolder = $defaultFolder->getSubfolder($tempFolderName);
2056 }
2057 }
2058
2059 return $defaultTemporaryFolder;
2060 }
2061
2062 /**
2063 * Creates a TypoScript comment with the string text inside.
2064 *
2065 * @param string $str The text to wrap in comment prefixes and delimiters.
2066 * @return string TypoScript comment with the string text inside.
2067 * @deprecated since TYPO3 v9, will be removed with TYPO3 v10
2068 */
2069 public function addTScomment($str)
2070 {
2071 trigger_error('BackendUserAuthentication->addTScomment() will be removed in TYPO3 v10.', E_USER_DEPRECATED);
2072 $delimiter = '# ***********************************************';
2073 $out = $delimiter . LF;
2074 $lines = GeneralUtility::trimExplode(LF, $str);
2075 foreach ($lines as $v) {
2076 $out .= '# ' . $v . LF;
2077 }
2078 $out .= $delimiter . LF;
2079 return $out;
2080 }
2081
2082 /**
2083 * Initializing workspace.
2084 * Called from within this function, see fetchGroupData()
2085 *
2086 * @see fetchGroupData()
2087 */
2088 public function workspaceInit()
2089 {
2090 // Initializing workspace by evaluating and setting the workspace, possibly updating it in the user record!
2091 $this->setWorkspace($this->user['workspace_id']);
2092 // Limiting the DB mountpoints if there any selected in the workspace record
2093 $this->initializeDbMountpointsInWorkspace();
2094 $allowed_languages = $this->getTSConfig()['options.']['workspaces.']['allowed_languages.'][$this->workspace] ?? '';
2095 if (!empty($allowed_languages)) {
2096 $this->groupData['allowed_languages'] = GeneralUtility::uniqueList($allowed_languages);
2097 }
2098 }
2099
2100 /**
2101 * Limiting the DB mountpoints if there any selected in the workspace record
2102 */
2103 protected function initializeDbMountpointsInWorkspace()
2104 {
2105 $dbMountpoints = trim($this->workspaceRec['db_mountpoints'] ?? '');
2106 if ($this->workspace > 0 && $dbMountpoints != '') {
2107 $filteredDbMountpoints = [];
2108 // Notice: We cannot call $this->getPagePermsClause(1);
2109 // as usual because the group-list is not available at this point.
2110 // But bypassing is fine because all we want here is check if the
2111 // workspace mounts are inside the current webmounts rootline.
2112 // The actual permission checking on page level is done elsewhere
2113 // as usual anyway before the page tree is rendered.
2114 $readPerms = '1=1';
2115 // Traverse mount points of the
2116 $dbMountpoints = GeneralUtility::intExplode(',', $dbMountpoints);
2117 foreach ($dbMountpoints as $mpId) {
2118 if ($this->isInWebMount($mpId, $readPerms)) {
2119 $filteredDbMountpoints[] = $mpId;
2120 }
2121 }
2122 // Re-insert webmounts:
2123 $filteredDbMountpoints = array_unique($filteredDbMountpoints);
2124 $this->groupData['webmounts'] = implode(',', $filteredDbMountpoints);
2125 }
2126 }
2127
2128 /**
2129 * Checking if a workspace is allowed for backend user
2130 *
2131 * @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)
2132 * @param string $fields List of fields to select. Default fields are: uid,title,adminusers,members,reviewers,publish_access,stagechg_notification
2133 * @return array Output will also show how access was granted. Admin users will have a true output regardless of input.
2134 */
2135 public function checkWorkspace($wsRec, $fields = 'uid,title,adminusers,members,reviewers,publish_access,stagechg_notification')
2136 {
2137 $retVal = false;
2138 // If not array, look up workspace record:
2139 if (!is_array($wsRec)) {
2140 switch ((string)$wsRec) {
2141 case '0':
2142 $wsRec = ['uid' => $wsRec];
2143 break;
2144 default:
2145 if (ExtensionManagementUtility::isLoaded('workspaces')) {
2146 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
2147 $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
2148 $wsRec = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields))
2149 ->from('sys_workspace')
2150 ->where($queryBuilder->expr()->eq(
2151 'uid',
2152 $queryBuilder->createNamedParameter($wsRec, \PDO::PARAM_INT)
2153 ))
2154 ->orderBy('title')
2155 ->setMaxResults(1)
2156 ->execute()
2157 ->fetch(\PDO::FETCH_ASSOC);
2158 }
2159 }
2160 }
2161 // If wsRec is set to an array, evaluate it:
2162 if (is_array($wsRec)) {
2163 if ($this->isAdmin()) {
2164 return array_merge($wsRec, ['_ACCESS' => 'admin']);
2165 }
2166 switch ((string)$wsRec['uid']) {
2167 case '0':
2168 $retVal = $this->groupData['workspace_perms'] & Permission::PAGE_SHOW
2169 ? array_merge($wsRec, ['_ACCESS' => 'online'])
2170 : false;
2171 break;
2172 default:
2173 // Checking if the guy is admin:
2174 if (GeneralUtility::inList($wsRec['adminusers'], 'be_users_' . $this->user['uid'])) {
2175 return array_merge($wsRec, ['_ACCESS' => 'owner']);
2176 }
2177 // Checking if he is owner through a user group of his:
2178 foreach ($this->userGroupsUID as $groupUid) {
2179 if (GeneralUtility::inList($wsRec['adminusers'], 'be_groups_' . $groupUid)) {
2180 return array_merge($wsRec, ['_ACCESS' => 'owner']);
2181 }
2182 }
2183 // Checking if he is reviewer user:
2184 if (GeneralUtility::inList($wsRec['reviewers'], 'be_users_' . $this->user['uid'])) {
2185 return array_merge($wsRec, ['_ACCESS' => 'reviewer']);
2186 }
2187 // Checking if he is reviewer through a user group of his:
2188 foreach ($this->userGroupsUID as $groupUid) {
2189 if (GeneralUtility::inList($wsRec['reviewers'], 'be_groups_' . $groupUid)) {
2190 return array_merge($wsRec, ['_ACCESS' => 'reviewer']);
2191 }
2192 }
2193 // Checking if he is member as user:
2194 if (GeneralUtility::inList($wsRec['members'], 'be_users_' . $this->user['uid'])) {
2195 return array_merge($wsRec, ['_ACCESS' => 'member']);
2196 }
2197 // Checking if he is member through a user group of his:
2198 foreach ($this->userGroupsUID as $groupUid) {
2199 if (GeneralUtility::inList($wsRec['members'], 'be_groups_' . $groupUid)) {
2200 return array_merge($wsRec, ['_ACCESS' => 'member']);
2201 }
2202 }
2203 }
2204 }
2205 return $retVal;
2206 }
2207
2208 /**
2209 * Uses checkWorkspace() to check if current workspace is available for user.
2210 * This function caches the result and so can be called many times with no performance loss.
2211 *
2212 * @return array See checkWorkspace()
2213 * @see checkWorkspace()
2214 */
2215 public function checkWorkspaceCurrent()
2216 {
2217 if (!isset($this->checkWorkspaceCurrent_cache)) {
2218 $this->checkWorkspaceCurrent_cache = $this->checkWorkspace($this->workspace);
2219 }
2220 return $this->checkWorkspaceCurrent_cache;
2221 }
2222
2223 /**
2224 * Setting workspace ID
2225 *
2226 * @param int $workspaceId ID of workspace to set for backend user. If not valid the default workspace for BE user is found and set.
2227 */
2228 public function setWorkspace($workspaceId)
2229 {
2230 // Check workspace validity and if not found, revert to default workspace.
2231 if (!$this->setTemporaryWorkspace($workspaceId)) {
2232 $this->setDefaultWorkspace();
2233 }
2234 // Unset access cache:
2235 $this->checkWorkspaceCurrent_cache = null;
2236 // If ID is different from the stored one, change it:
2237 if ((int)$this->workspace !== (int)$this->user['workspace_id']) {
2238 $this->user['workspace_id'] = $this->workspace;
2239 GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users')->update(
2240 'be_users',
2241 ['workspace_id' => $this->user['workspace_id']],
2242 ['uid' => (int)$this->user['uid']]
2243 );
2244 $this->writelog(4, 0, 0, 0, 'User changed workspace to "' . $this->workspace . '"', []);
2245 }
2246 }
2247
2248 /**
2249 * Sets a temporary workspace in the context of the current backend user.
2250 *
2251 * @param int $workspaceId
2252 * @return bool
2253 */
2254 public function setTemporaryWorkspace($workspaceId)
2255 {
2256 $result = false;
2257 $workspaceRecord = $this->checkWorkspace($workspaceId, '*');
2258
2259 if ($workspaceRecord) {
2260 $this->workspaceRec = $workspaceRecord;
2261 $this->workspace = (int)$workspaceId;
2262 $result = true;
2263 }
2264
2265 return $result;
2266 }
2267
2268 /**
2269 * Sets the default workspace in the context of the current backend user.
2270 */
2271 public function setDefaultWorkspace()
2272 {
2273 $this->workspace = (int)$this->getDefaultWorkspace();
2274 $this->workspaceRec = $this->checkWorkspace($this->workspace, '*');
2275 }
2276
2277 /**
2278 * Return default workspace ID for user,
2279 * if EXT:workspaces is not installed the user will be pushed to the
2280 * Live workspace, if he has access to. If no workspace is available for the user, the workspace ID is set to "-99"
2281 *
2282 * @return int Default workspace id.
2283 */
2284 public function getDefaultWorkspace()
2285 {
2286 if (!ExtensionManagementUtility::isLoaded('workspaces')) {
2287 return 0;
2288 }
2289 // Online is default
2290 if ($this->checkWorkspace(0)) {
2291 return 0;
2292 }
2293 // Otherwise -99 is the fallback
2294 $defaultWorkspace = -99;
2295 // Traverse all workspaces
2296 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
2297 $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
2298 $result = $queryBuilder->select('*')
2299 ->from('sys_workspace')
2300 ->orderBy('title')
2301 ->execute();
2302 while ($workspaceRecord = $result->fetch()) {
2303 if ($this->checkWorkspace($workspaceRecord)) {
2304 $defaultWorkspace = (int)$workspaceRecord['uid'];
2305 break;
2306 }
2307 }
2308 return $defaultWorkspace;
2309 }
2310
2311 /**
2312 * Writes an entry in the logfile/table
2313 * Documentation in "TYPO3 Core API"
2314 *
2315 * @param int $type Denotes which module that has submitted the entry. See "TYPO3 Core API". Use "4" for extensions.
2316 * @param int $action Denotes which specific operation that wrote the entry. Use "0" when no sub-categorizing applies
2317 * @param int $error Flag. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin)
2318 * @param int $details_nr The message number. Specific for each $type and $action. This will make it possible to translate errormessages to other languages
2319 * @param string $details Default text that follows the message (in english!). Possibly translated by identification through type/action/details_nr
2320 * @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
2321 * @param string $tablename Table name. Special field used by tce_main.php.
2322 * @param int|string $recuid Record UID. Special field used by tce_main.php.
2323 * @param int|string $recpid Record PID. Special field used by tce_main.php. OBSOLETE
2324 * @param int $event_pid The page_uid (pid) where the event occurred. Used to select log-content for specific pages.
2325 * @param string $NEWid Special field used by tce_main.php. NEWid string of newly created records.
2326 * @param int $userId Alternative Backend User ID (used for logging login actions where this is not yet known).
2327 * @return int Log entry ID.
2328 */
2329 public function writelog($type, $action, $error, $details_nr, $details, $data, $tablename = '', $recuid = '', $recpid = '', $event_pid = -1, $NEWid = '', $userId = 0)
2330 {
2331 if (!$userId && !empty($this->user['uid'])) {
2332 $userId = $this->user['uid'];
2333 }
2334
2335 if (!empty($this->user['ses_backuserid'])) {
2336 if (empty($data)) {
2337 $data = [];
2338 }
2339 $data['originalUser'] = $this->user['ses_backuserid'];
2340 }
2341
2342 $fields = [
2343 'userid' => (int)$userId,
2344 'type' => (int)$type,
2345 'action' => (int)$action,
2346 'error' => (int)$error,
2347 'details_nr' => (int)$details_nr,
2348 'details' => $details,
2349 'log_data' => serialize($data),
2350 'tablename' => $tablename,
2351 'recuid' => (int)$recuid,
2352 'IP' => (string)GeneralUtility::getIndpEnv('REMOTE_ADDR'),
2353 'tstamp' => $GLOBALS['EXEC_TIME'] ?? time(),
2354 'event_pid' => (int)$event_pid,
2355 'NEWid' => $NEWid,
2356 'workspace' => $this->workspace
2357 ];
2358
2359 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_log');
2360 $connection->insert(
2361 'sys_log',
2362 $fields,
2363 [
2364 \PDO::PARAM_INT,
2365 \PDO::PARAM_INT,
2366 \PDO::PARAM_INT,
2367 \PDO::PARAM_INT,
2368 \PDO::PARAM_INT,
2369 \PDO::PARAM_STR,
2370 \PDO::PARAM_STR,
2371 \PDO::PARAM_STR,
2372 \PDO::PARAM_INT,
2373 \PDO::PARAM_STR,
2374 \PDO::PARAM_INT,
2375 \PDO::PARAM_INT,
2376 \PDO::PARAM_STR,
2377 \PDO::PARAM_STR,
2378 ]
2379 );
2380
2381 return (int)$connection->lastInsertId('sys_log');
2382 }
2383
2384 /**
2385 * Simple logging function
2386 *
2387 * @param string $message Log message
2388 * @param string $extKey Option extension key / module name
2389 * @param int $error Error level. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin)
2390 * @return int Log entry UID
2391 * @deprecated since TYPO3 v9, will be removed with TYPO3 v10
2392 */
2393 public function simplelog($message, $extKey = '', $error = 0)
2394 {
2395 trigger_error('BackendUserAuthentication->simplelog() will be removed in TYPO3 v10. Use writelog, or better, PSR-3 based logging instead.', E_USER_DEPRECATED);
2396 return $this->writelog(4, 0, $error, 0, ($extKey ? '[' . $extKey . '] ' : '') . $message, []);
2397 }
2398
2399 /**
2400 * Sends a warning to $email if there has been a certain amount of failed logins during a period.
2401 * If a login fails, this function is called. It will look up the sys_log to see if there
2402 * have been more than $max failed logins the last $secondsBack seconds (default 3600).
2403 * If so, an email with a warning is sent to $email.
2404 *
2405 * @param string $email Email address
2406 * @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.
2407 * @param int $max Max allowed failures before a warning mail is sent
2408 * @access private
2409 */
2410 public function checkLogFailures($email, $secondsBack = 3600, $max = 3)
2411 {
2412 if ($email) {
2413 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
2414
2415 // Get last flag set in the log for sending
2416 $theTimeBack = $GLOBALS['EXEC_TIME'] - $secondsBack;
2417 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
2418 $queryBuilder->select('tstamp')
2419 ->from('sys_log')
2420 ->where(
2421 $queryBuilder->expr()->eq(
2422 'type',
2423 $queryBuilder->createNamedParameter(255, \PDO::PARAM_INT)
2424 ),
2425 $queryBuilder->expr()->eq(
2426 'action',
2427 $queryBuilder->createNamedParameter(4, \PDO::PARAM_INT)
2428 ),
2429 $queryBuilder->expr()->gt(
2430 'tstamp',
2431 $queryBuilder->createNamedParameter($theTimeBack, \PDO::PARAM_INT)
2432 )
2433 )
2434 ->orderBy('tstamp', 'DESC')
2435 ->setMaxResults(1);
2436 if ($testRow = $queryBuilder->execute()->fetch(\PDO::FETCH_ASSOC)) {
2437 $theTimeBack = $testRow['tstamp'];
2438 }
2439
2440 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
2441 $result = $queryBuilder->select('*')
2442 ->from('sys_log')
2443 ->where(
2444 $queryBuilder->expr()->eq(
2445 'type',
2446 $queryBuilder->createNamedParameter(255, \PDO::PARAM_INT)
2447 ),
2448 $queryBuilder->expr()->eq(
2449 'action',
2450 $queryBuilder->createNamedParameter(3, \PDO::PARAM_INT)
2451 ),
2452 $queryBuilder->expr()->neq(
2453 'error',
2454 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
2455 ),
2456 $queryBuilder->expr()->gt(
2457 'tstamp',
2458 $queryBuilder->createNamedParameter($theTimeBack, \PDO::PARAM_INT)
2459 )
2460 )
2461 ->orderBy('tstamp')
2462 ->execute();
2463
2464 $rowCount = $queryBuilder
2465 ->count('uid')
2466 ->execute()
2467 ->fetchColumn(0);
2468 // Check for more than $max number of error failures with the last period.
2469 if ($rowCount > $max) {
2470 // OK, so there were more than the max allowed number of login failures - so we will send an email then.
2471 $subject = 'TYPO3 Login Failure Warning (at ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . ')';
2472 $email_body = 'There have been some attempts (' . $rowCount . ') to login at the TYPO3
2473 site "' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . '" (' . GeneralUtility::getIndpEnv('HTTP_HOST') . ').
2474
2475 This is a dump of the failures:
2476
2477 ';
2478 while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
2479 $theData = unserialize($row['log_data']);
2480 $email_body .= date(
2481 $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
2482 $row['tstamp']
2483 ) . ': ' . @sprintf($row['details'], (string)$theData[0], (string)$theData[1], (string)$theData[2]);
2484 $email_body .= LF;
2485 }
2486 /** @var \TYPO3\CMS\Core\Mail\MailMessage $mail */
2487 $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
2488 $mail->setTo($email)->setSubject($subject)->setBody($email_body);
2489 $mail->send();
2490 // Logout written to log
2491 $this->writelog(255, 4, 0, 3, 'Failure warning (%s failures within %s seconds) sent by email to %s', [$rowCount, $secondsBack, $email]);
2492 }
2493 }
2494 }
2495
2496 /**
2497 * Getter for the cookie name
2498 *
2499 * @static
2500 * @return string returns the configured cookie name
2501 */
2502 public static function getCookieName()
2503 {
2504 $configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']);
2505 if (empty($configuredCookieName)) {
2506 $configuredCookieName = 'be_typo_user';
2507 }
2508 return $configuredCookieName;
2509 }
2510
2511 /**
2512 * If TYPO3_CONF_VARS['BE']['enabledBeUserIPLock'] is enabled and
2513 * an IP-list is found in the User TSconfig objString "options.lockToIP",
2514 * then make an IP comparison with REMOTE_ADDR and check if the IP address matches
2515 *
2516 * @return bool TRUE, if IP address validates OK (or no check is done at all because no restriction is set)
2517 */
2518 public function checkLockToIP()
2519 {
2520 $isValid = true;
2521 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['enabledBeUserIPLock']) {
2522 $IPList = trim($this->getTSConfig()['options.']['lockToIP'] ?? '');
2523 if (!empty($IPList)) {
2524 $isValid = GeneralUtility::cmpIP(GeneralUtility::getIndpEnv('REMOTE_ADDR'), $IPList);
2525 }
2526 }
2527 return $isValid;
2528 }
2529
2530 /**
2531 * Check if user is logged in and if so, call ->fetchGroupData() to load group information and
2532 * access lists of all kind, further check IP, set the ->uc array and send login-notification email if required.
2533 * If no user is logged in the default behaviour is to exit with an error message.
2534 * This function is called right after ->start() in fx. the TYPO3 Bootstrap.
2535 *
2536 * @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.
2537 * @throws \RuntimeException
2538 */
2539 public function backendCheckLogin($proceedIfNoUserIsLoggedIn = false)
2540 {
2541 if (empty($this->user['uid'])) {
2542 if ($proceedIfNoUserIsLoggedIn === false) {
2543 $url = GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir;
2544 \TYPO3\CMS\Core\Utility\HttpUtility::redirect($url);
2545 }
2546 } else {
2547 // ...and if that's the case, call these functions
2548 $this->fetchGroupData();
2549 // The groups are fetched and ready for permission checking in this initialization.
2550 // Tables.php must be read before this because stuff like the modules has impact in this
2551 if ($this->checkLockToIP()) {
2552 if ($this->isUserAllowedToLogin()) {
2553 // Setting the UC array. It's needed with fetchGroupData first, due to default/overriding of values.
2554 $this->backendSetUC();
2555 if ($this->loginSessionStarted) {
2556 // Process hooks
2557 $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['backendUserLogin'];
2558 foreach ($hooks ?? [] as $_funcRef) {
2559 $_params = ['user' => $this->user];
2560 GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2561 }
2562 // Email at login, if feature is enabled in configuration
2563 $this->emailAtLogin();
2564 }
2565 } else {
2566 throw new \RuntimeException('Login Error: TYPO3 is in maintenance mode at the moment. Only administrators are allowed access.', 1294585860);
2567 }
2568 } else {
2569 throw new \RuntimeException('Login Error: IP locking prevented you from being authorized. Can\'t proceed, sorry.', 1294585861);
2570 }
2571 }
2572 }
2573
2574 /**
2575 * Initialize the internal ->uc array for the backend user
2576 * Will make the overrides if necessary, and write the UC back to the be_users record if changes has happened
2577 *
2578 * @internal
2579 */
2580 public function backendSetUC()
2581 {
2582 // UC - user configuration is a serialized array inside the user object
2583 // If there is a saved uc we implement that instead of the default one.
2584 $this->unpack_uc();
2585 // Setting defaults if uc is empty
2586 $updated = false;
2587 $originalUc = [];
2588 if (is_array($this->uc) && isset($this->uc['ucSetByInstallTool'])) {
2589 $originalUc = $this->uc;
2590 unset($originalUc['ucSetByInstallTool'], $this->uc);
2591 }
2592 if (!is_array($this->uc)) {
2593 $this->uc = array_merge(
2594 $this->uc_default,
2595 (array)$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUC'],
2596 GeneralUtility::removeDotsFromTS((array)($this->getTSConfig()['setup.']['default.'] ?? [])),
2597 $originalUc
2598 );
2599 $this->overrideUC();
2600 $updated = true;
2601 }
2602 // If TSconfig is updated, update the defaultUC.
2603 if ($this->userTSUpdated) {
2604 $this->overrideUC();
2605 $updated = true;
2606 }
2607 // Setting default lang from be_user record.
2608 if (!isset($this->uc['lang'])) {
2609 $this->uc['lang'] = $this->user['lang'];
2610 $updated = true;
2611 }
2612 // Setting the time of the first login:
2613 if (!isset($this->uc['firstLoginTimeStamp'])) {
2614 $this->uc['firstLoginTimeStamp'] = $GLOBALS['EXEC_TIME'];
2615 $updated = true;
2616 }
2617 // Saving if updated.
2618 if ($updated) {
2619 $this->writeUC();
2620 }
2621 }
2622
2623 /**
2624 * Override: Call this function every time the uc is updated.
2625 * That is 1) by reverting to default values, 2) in the setup-module, 3) userTS changes (userauthgroup)
2626 *
2627 * @internal
2628 */
2629 public function overrideUC()
2630 {
2631 $this->uc = array_merge((array)$this->uc, (array)($this->getTSConfig()['setup.']['override.'] ?? []));
2632 }
2633
2634 /**
2635 * Clears the user[uc] and ->uc to blank strings. Then calls ->backendSetUC() to fill it again with reset contents
2636 *
2637 * @internal
2638 */
2639 public function resetUC()
2640 {
2641 $this->user['uc'] = '';
2642 $this->uc = '';
2643 $this->backendSetUC();
2644 }
2645
2646 /**
2647 * Sends an email notification to warning_email_address and/or the logged-in user's email address.
2648 *
2649 * @access private
2650 */
2651 private function emailAtLogin()
2652 {
2653 // Send notify-mail
2654 $subject = 'At "' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . '"' . ' from ' . GeneralUtility::getIndpEnv('REMOTE_ADDR');
2655 $msg = sprintf(
2656 'User "%s" logged in from %s at "%s" (%s)',
2657 $this->user['username'],
2658 GeneralUtility::getIndpEnv('REMOTE_ADDR'),
2659 $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'],
2660 GeneralUtility::getIndpEnv('HTTP_HOST')
2661 );
2662 // Warning email address
2663 if ($GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr']) {
2664 $warn = 0;
2665 $prefix = '';
2666 if ((int)$GLOBALS['TYPO3_CONF_VARS']['BE']['warning_mode'] & 1) {
2667 // first bit: All logins
2668 $warn = 1;
2669 $prefix = $this->isAdmin() ? '[AdminLoginWarning]' : '[LoginWarning]';
2670 }
2671 if ($this->isAdmin() && (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['warning_mode'] & 2) {
2672 // second bit: Only admin-logins
2673 $warn = 1;
2674 $prefix = '[AdminLoginWarning]';
2675 }
2676 if ($warn) {
2677 /** @var \TYPO3\CMS\Core\Mail\MailMessage $mail */
2678 $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
2679 $mail->setTo($GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr'])->setSubject($prefix . ' ' . $subject)->setBody($msg);
2680 $mail->send();
2681 }
2682 }
2683 // Trigger an email to the current BE user, if this has been enabled in the user configuration
2684 if ($this->uc['emailMeAtLogin'] && strstr($this->user['email'], '@')) {
2685 /** @var \TYPO3\CMS\Core\Mail\MailMessage $mail */
2686 $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
2687 $mail->setTo($this->user['email'])->setSubject($subject)->setBody($msg);
2688 $mail->send();
2689 }
2690 }
2691
2692 /**
2693 * Determines whether a backend user is allowed to access the backend.
2694 *
2695 * The conditions are:
2696 * + backend user is a regular user and adminOnly is not defined
2697 * + backend user is an admin user
2698 * + backend user is used in CLI context and adminOnly is explicitly set to "2" (see CommandLineUserAuthentication)
2699 * + backend user is being controlled by an admin user
2700 *
2701 * @return bool Whether a backend user is allowed to access the backend
2702 */
2703 protected function isUserAllowedToLogin()
2704 {
2705 $isUserAllowedToLogin = false;
2706 $adminOnlyMode = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'];
2707 // Backend user is allowed if adminOnly is not set or user is an admin:
2708 if (!$adminOnlyMode || $this->isAdmin()) {
2709 $isUserAllowedToLogin = true;
2710 } elseif ($this->user['ses_backuserid']) {
2711 $backendUserId = (int)$this->user['ses_backuserid'];
2712 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
2713 $isUserAllowedToLogin = (bool)$queryBuilder->count('uid')
2714 ->from('be_users')
2715 ->where(
2716 $queryBuilder->expr()->eq(
2717 'uid',
2718 $queryBuilder->createNamedParameter($backendUserId, \PDO::PARAM_INT)
2719 ),
2720 $queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT))
2721 )
2722 ->execute()
2723 ->fetchColumn(0);
2724 }
2725 return $isUserAllowedToLogin;
2726 }
2727
2728 /**
2729 * Logs out the current user and clears the form protection tokens.
2730 */
2731 public function logoff()
2732 {
2733 if (isset($GLOBALS['BE_USER'])
2734 && $GLOBALS['BE_USER'] instanceof self
2735 && isset($GLOBALS['BE_USER']->user['uid'])
2736 ) {
2737 FormProtectionFactory::get()->clean();
2738 // Release the locked records
2739 $this->releaseLockedRecords((int)$GLOBALS['BE_USER']->user['uid']);
2740
2741 if ($this->isSystemMaintainer()) {
2742 // If user is system maintainer, destroy its possibly valid install tool session.
2743 $session = new SessionService();
2744 if ($session->hasSession()) {
2745 $session->destroySession();
2746 }
2747 }
2748 }
2749 parent::logoff();
2750 }
2751
2752 /**
2753 * Remove any "locked records" added for editing for the given user (= current backend user)
2754 * @param int $userId
2755 */
2756 protected function releaseLockedRecords(int $userId)
2757 {
2758 if ($userId > 0) {
2759 GeneralUtility::makeInstance(ConnectionPool::class)
2760 ->getConnectionForTable('sys_lockedrecords')
2761 ->delete(
2762 'sys_lockedrecords',
2763 ['userid' => $userId]
2764 );
2765 }
2766 }
2767 }