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