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