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