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