BackendUserAuthentication::logoff()   A
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 17
rs 9.6111
c 0
b 0
f 0
cc 5
nc 3
nop 0
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Core\Authentication;
17
18
use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
19
use TYPO3\CMS\Backend\Utility\BackendUtility;
20
use TYPO3\CMS\Core\Cache\CacheManager;
21
use TYPO3\CMS\Core\Core\Environment;
22
use TYPO3\CMS\Core\Database\Connection;
23
use TYPO3\CMS\Core\Database\ConnectionPool;
24
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
25
use TYPO3\CMS\Core\Database\Query\QueryHelper;
26
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
27
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
28
use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction;
29
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
30
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
31
use TYPO3\CMS\Core\Http\ImmediateResponseException;
32
use TYPO3\CMS\Core\Http\RedirectResponse;
33
use TYPO3\CMS\Core\Resource\Exception;
34
use TYPO3\CMS\Core\Resource\Filter\FileNameFilter;
35
use TYPO3\CMS\Core\Resource\Folder;
36
use TYPO3\CMS\Core\Resource\ResourceFactory;
37
use TYPO3\CMS\Core\Resource\ResourceStorage;
38
use TYPO3\CMS\Core\Resource\StorageRepository;
39
use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction;
40
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
41
use TYPO3\CMS\Core\SysLog\Type;
42
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
43
use TYPO3\CMS\Core\Type\Bitmask\BackendGroupMountOption;
44
use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
45
use TYPO3\CMS\Core\Type\Bitmask\Permission;
46
use TYPO3\CMS\Core\Type\Exception\InvalidEnumerationValueException;
47
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
48
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
49
use TYPO3\CMS\Core\Utility\GeneralUtility;
50
use TYPO3\CMS\Core\Utility\StringUtility;
51
use TYPO3\CMS\Install\Service\SessionService;
52
53
/**
54
 * TYPO3 backend user authentication
55
 * Contains most of the functions used for checking permissions, authenticating users,
56
 * setting up the user, and API for user from outside.
57
 * This class contains the configuration of the database fields used plus some
58
 * functions for the authentication process of backend users.
59
 */
60
class BackendUserAuthentication extends AbstractUserAuthentication
61
{
62
    public const ROLE_SYSTEMMAINTAINER = 'systemMaintainer';
63
64
    /**
65
     * Should be set to the usergroup-column (id-list) in the user-record
66
     * @var string
67
     */
68
    public $usergroup_column = 'usergroup';
69
70
    /**
71
     * The name of the group-table
72
     * @var string
73
     */
74
    public $usergroup_table = 'be_groups';
75
76
    /**
77
     * holds lists of eg. tables, fields and other values related to the permission-system. See fetchGroupData
78
     * @var array
79
     * @internal
80
     */
81
    public $groupData = [
82
        'allowed_languages' => '',
83
        'tables_select' => '',
84
        'tables_modify' => '',
85
        'pagetypes_select' => '',
86
        'non_exclude_fields' => '',
87
        'explicit_allowdeny' => '',
88
        'custom_options' => '',
89
        'file_permissions' => '',
90
    ];
91
92
    /**
93
     * This array holds the uid's of the groups in the listed order
94
     * @var array
95
     */
96
    public $userGroupsUID = [];
97
98
    /**
99
     * User workspace.
100
     * -99 is ERROR (none available)
101
     * 0 is online
102
     * >0 is custom workspaces
103
     * @var int
104
     */
105
    public $workspace = -99;
106
107
    /**
108
     * Custom workspace record if any
109
     * @var array
110
     */
111
    public $workspaceRec = [];
112
113
    /**
114
     * @var array Parsed user TSconfig
115
     */
116
    protected $userTS = [];
117
118
    /**
119
     * @var bool True if the user TSconfig was parsed and needs to be cached.
120
     */
121
    protected $userTSUpdated = false;
122
123
    /**
124
     * Contains last error message
125
     * @internal should only be used from within TYPO3 Core
126
     * @var string
127
     */
128
    public $errorMsg = '';
129
130
    /**
131
     * Cache for checkWorkspaceCurrent()
132
     * @var array|null
133
     */
134
    protected $checkWorkspaceCurrent_cache;
135
136
    /**
137
     * @var \TYPO3\CMS\Core\Resource\ResourceStorage[]
138
     */
139
    protected $fileStorages;
140
141
    /**
142
     * @var array
143
     */
144
    protected $filePermissions;
145
146
    /**
147
     * Table in database with user data
148
     * @var string
149
     */
150
    public $user_table = 'be_users';
151
152
    /**
153
     * Column for login-name
154
     * @var string
155
     */
156
    public $username_column = 'username';
157
158
    /**
159
     * Column for password
160
     * @var string
161
     */
162
    public $userident_column = 'password';
163
164
    /**
165
     * Column for user-id
166
     * @var string
167
     */
168
    public $userid_column = 'uid';
169
170
    /**
171
     * @var string
172
     */
173
    public $lastLogin_column = 'lastlogin';
174
175
    /**
176
     * @var array
177
     */
178
    public $enablecolumns = [
179
        'rootLevel' => 1,
180
        'deleted' => 'deleted',
181
        'disabled' => 'disable',
182
        'starttime' => 'starttime',
183
        'endtime' => 'endtime',
184
    ];
185
186
    /**
187
     * Form field with login-name
188
     * @var string
189
     */
190
    public $formfield_uname = 'username';
191
192
    /**
193
     * Form field with password
194
     * @var string
195
     */
196
    public $formfield_uident = 'userident';
197
198
    /**
199
     * Form field with status: *'login', 'logout'
200
     * @var string
201
     */
202
    public $formfield_status = 'login_status';
203
204
    /**
205
     * Decides if the writelog() function is called at login and logout
206
     * @var bool
207
     */
208
    public $writeStdLog = true;
209
210
    /**
211
     * If the writelog() functions is called if a login-attempt has be tried without success
212
     * @var bool
213
     */
214
    public $writeAttemptLog = true;
215
216
    /**
217
     * @var int
218
     * @internal should only be used from within TYPO3 Core
219
     */
220
    public $firstMainGroup = 0;
221
222
    /**
223
     * User Config
224
     * @var array|string
225
     */
226
    public $uc;
227
228
    /**
229
     * User Config Default values:
230
     * The array may contain other fields for configuration.
231
     * For this, see "setup" extension and "TSconfig" document (User TSconfig, "setup.[xxx]....")
232
     * Reserved keys for other storage of session data:
233
     * moduleData
234
     * moduleSessionID
235
     * @var array
236
     * @internal should only be used from within TYPO3 Core
237
     */
238
    public $uc_default = [
239
        'interfaceSetup' => '',
240
        // serialized content that is used to store interface pane and menu positions. Set by the logout.php-script
241
        'moduleData' => [],
242
        // user-data for the modules
243
        'emailMeAtLogin' => 0,
244
        'titleLen' => 50,
245
        'edit_RTE' => '1',
246
        'edit_docModuleUpload' => '1',
247
        'resizeTextareas_MaxHeight' => 500,
248
    ];
249
250
    /**
251
     * Login type, used for services.
252
     * @var string
253
     */
254
    public $loginType = 'BE';
255
256
    /**
257
     * Constructor
258
     */
259
    public function __construct()
260
    {
261
        $this->name = self::getCookieName();
262
        parent::__construct();
263
    }
264
265
    /**
266
     * Returns TRUE if user is admin
267
     * Basically this function evaluates if the ->user[admin] field has bit 0 set. If so, user is admin.
268
     *
269
     * @return bool
270
     */
271
    public function isAdmin()
272
    {
273
        return is_array($this->user) && (($this->user['admin'] ?? 0) & 1) == 1;
274
    }
275
276
    /**
277
     * Returns TRUE if the current user is a member of group $groupId
278
     * $groupId must be set. $this->userGroupsUID must contain groups
279
     * Will return TRUE also if the user is a member of a group through subgroups.
280
     *
281
     * @param int $groupId Group ID to look for in $this->userGroupsUID
282
     * @return bool
283
     * @internal should only be used from within TYPO3 Core, use Context API for quicker access
284
     */
285
    public function isMemberOfGroup($groupId)
286
    {
287
        $groupId = (int)$groupId;
288
        if (!empty($this->userGroupsUID) && $groupId) {
289
            return in_array($groupId, $this->userGroupsUID, true);
290
        }
291
        return false;
292
    }
293
294
    /**
295
     * Checks if the permissions is granted based on a page-record ($row) and $perms (binary and'ed)
296
     *
297
     * Bits for permissions, see $perms variable:
298
     *
299
     * 1  - Show:             See/Copy page and the pagecontent.
300
     * 2  - Edit page:        Change/Move the page, eg. change title, startdate, hidden.
301
     * 4  - Delete page:      Delete the page and pagecontent.
302
     * 8  - New pages:        Create new pages under the page.
303
     * 16 - Edit pagecontent: Change/Add/Delete/Move pagecontent.
304
     *
305
     * @param array $row Is the pagerow for which the permissions is checked
306
     * @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.
307
     * @return bool
308
     */
309
    public function doesUserHaveAccess($row, $perms)
310
    {
311
        $userPerms = $this->calcPerms($row);
312
        return ($userPerms & $perms) == $perms;
313
    }
314
315
    /**
316
     * Checks if the page id or page record ($idOrRow) is found within the webmounts set up for the user.
317
     * This should ALWAYS be checked for any page id a user works with, whether it's about reading, writing or whatever.
318
     * The point is that this will add the security that a user can NEVER touch parts outside his mounted
319
     * pages in the page tree. This is otherwise possible if the raw page permissions allows for it.
320
     * So this security check just makes it easier to make safe user configurations.
321
     * If the user is admin then it returns "1" right away
322
     * Otherwise the function will return the uid of the webmount which was first found in the rootline of the input page $id
323
     *
324
     * @param int|array $idOrRow Page ID or full page record to check
325
     * @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!)
326
     * @param bool|int|null $exitOnError If set, then the function will exit with an error message. @deprecated will be removed in TYPO3 v12.0.
327
     * @throws \RuntimeException
328
     * @return int|null The page UID of a page in the rootline that matched a mount point
329
     */
330
    public function isInWebMount($idOrRow, $readPerms = '', $exitOnError = null)
331
    {
332
        if ($exitOnError !== null) {
333
            trigger_error('Calling BackendUserAuthentication->isInWebMount() with the third argument $exitOnError will have no effect anymore in TYPO3 v12.0.', E_USER_DEPRECATED);
334
        } else {
335
            $exitOnError = 0;
336
        }
337
        if ($this->isAdmin()) {
338
            return 1;
339
        }
340
        $checkRec = [];
341
        $fetchPageFromDatabase = true;
342
        if (is_array($idOrRow)) {
343
            if (!isset($idOrRow['uid'])) {
344
                throw new \RuntimeException('The given page record is invalid. Missing uid.', 1578950324);
345
            }
346
            $checkRec = $idOrRow;
347
            $id = (int)$idOrRow['uid'];
348
            // ensure the required fields are present on the record
349
            if (isset($checkRec['t3ver_oid'], $checkRec[$GLOBALS['TCA']['pages']['ctrl']['languageField']], $checkRec[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']])) {
350
                $fetchPageFromDatabase = false;
351
            }
352
        } else {
353
            $id = (int)$idOrRow;
354
        }
355
        if ($fetchPageFromDatabase) {
356
            // Check if input id is an offline version page in which case we will map id to the online version:
357
            $checkRec = BackendUtility::getRecord(
358
                'pages',
359
                $id,
360
                't3ver_oid,'
361
                . $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'] . ','
362
                . $GLOBALS['TCA']['pages']['ctrl']['languageField']
363
            );
364
        }
365
        if ((int)($checkRec['t3ver_oid'] ?? 0) > 0) {
366
            $id = (int)$checkRec['t3ver_oid'];
367
        }
368
        // if current rec is a translation then get uid from l10n_parent instead
369
        // because web mounts point to pages in default language and rootline returns uids of default languages
370
        if ((int)($checkRec[$GLOBALS['TCA']['pages']['ctrl']['languageField'] ?? null] ?? 0) !== 0
371
            && (int)($checkRec[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'] ?? null] ?? 0) !== 0
372
        ) {
373
            $id = (int)$checkRec[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']];
374
        }
375
        if (!$readPerms) {
376
            $readPerms = $this->getPagePermsClause(Permission::PAGE_SHOW);
377
        }
378
        if ($id > 0) {
379
            $wM = $this->returnWebmounts();
380
            $rL = BackendUtility::BEgetRootLine($id, ' AND ' . $readPerms, true);
381
            foreach ($rL as $v) {
382
                if ($v['uid'] && in_array($v['uid'], $wM)) {
383
                    return $v['uid'];
384
                }
385
            }
386
        }
387
        // @deprecated will be removed in TYPO3 v12.0.
388
        if ($exitOnError) {
389
            throw new \RuntimeException('Access Error: This page is not within your DB-mounts', 1294586445);
390
        }
391
        return null;
392
    }
393
394
    /**
395
     * Checks access to a backend module with the $MCONF passed as first argument
396
     *
397
     * @param array $conf $MCONF array of a backend module!
398
     * @throws \RuntimeException
399
     * @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
400
     */
401
    public function modAccess($conf)
402
    {
403
        $moduleName = $conf['name'] ?? '';
404
        if (!BackendUtility::isModuleSetInTBE_MODULES($moduleName)) {
405
            throw new \RuntimeException('Fatal Error: This module "' . $moduleName . '" is not enabled in TBE_MODULES', 1294586446);
406
        }
407
        // Workspaces check:
408
        if (
409
            !empty($conf['workspaces'])
410
            && ExtensionManagementUtility::isLoaded('workspaces')
411
            && ($this->workspace !== 0 || !GeneralUtility::inList($conf['workspaces'], 'online'))
412
            && ($this->workspace <= 0 || !GeneralUtility::inList($conf['workspaces'], 'custom'))
413
        ) {
414
            throw new \RuntimeException('Workspace Error: This module "' . $moduleName . '" is not available under the current workspace', 1294586447);
415
        }
416
        // Throws exception if conf[access] is set to system maintainer and the user is no system maintainer
417
        if (str_contains($conf['access'] ?? '', self::ROLE_SYSTEMMAINTAINER) && !$this->isSystemMaintainer()) {
418
            throw new \RuntimeException('This module "' . $moduleName . '" is only available as system maintainer', 1504804727);
419
        }
420
        // Returns TRUE if conf[access] is not set at all or if the user is admin
421
        if (!($conf['access'] ?? false) || $this->isAdmin()) {
422
            return true;
423
        }
424
        // If $conf['access'] is set but not with 'admin' then we return TRUE, if the module is found in the modList
425
        $acs = false;
426
        if ($moduleName && !str_contains($conf['access'] ?? '', 'admin')) {
427
            $acs = $this->check('modules', $moduleName);
428
        }
429
        if (!$acs) {
430
            throw new \RuntimeException('Access Error: You don\'t have access to this module.', 1294586448);
431
        }
432
        // User has access (Otherwise an exception would haven been thrown)
433
        return true;
434
    }
435
436
    /**
437
     * Checks if the user is in the valid list of allowed system maintainers. if the list is not set,
438
     * then all admins are system maintainers. If the list is empty, no one is system maintainer (good for production
439
     * systems). If the currently logged in user is in "switch user" mode, this method will return false.
440
     *
441
     * @return bool
442
     */
443
    public function isSystemMaintainer(): bool
444
    {
445
        if (!$this->isAdmin()) {
446
            return false;
447
        }
448
449
        if ($GLOBALS['BE_USER']->getOriginalUserIdWhenInSwitchUserMode()) {
450
            return false;
451
        }
452
        if (Environment::getContext()->isDevelopment()) {
453
            return true;
454
        }
455
        $systemMaintainers = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? [];
456
        $systemMaintainers = array_map('intval', $systemMaintainers);
457
        if (!empty($systemMaintainers)) {
458
            return in_array((int)$this->user['uid'], $systemMaintainers, true);
459
        }
460
        // No system maintainers set up yet, so any admin is allowed to access the modules
461
        // but explicitly no system maintainers allowed (empty string in TYPO3_CONF_VARS).
462
        // @todo: this needs to be adjusted once system maintainers can log into the install tool with their credentials
463
        if (isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'])
464
            && empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'])) {
465
            return false;
466
        }
467
        return true;
468
    }
469
470
    /**
471
     * Returns a WHERE-clause for the pages-table where user permissions according to input argument, $perms, is validated.
472
     * $perms is the "mask" used to select. Fx. if $perms is 1 then you'll get all pages that a user can actually see!
473
     * 2^0 = show (1)
474
     * 2^1 = edit (2)
475
     * 2^2 = delete (4)
476
     * 2^3 = new (8)
477
     * If the user is 'admin' " 1=1" is returned (no effect)
478
     * 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)
479
     * The 95% use of this function is "->getPagePermsClause(1)" which will
480
     * return WHERE clauses for *selecting* pages in backend listings - in other words this will check read permissions.
481
     *
482
     * @param int $perms Permission mask to use, see function description
483
     * @return string Part of where clause. Prefix " AND " to this.
484
     * @internal should only be used from within TYPO3 Core, use PagePermissionDatabaseRestriction instead.
485
     */
486
    public function getPagePermsClause($perms)
487
    {
488
        if (is_array($this->user)) {
489
            if ($this->isAdmin()) {
490
                return ' 1=1';
491
            }
492
            // Make sure it's integer.
493
            $perms = (int)$perms;
494
            $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
495
                ->getQueryBuilderForTable('pages')
496
                ->expr();
497
498
            // User
499
            $constraint = $expressionBuilder->orX(
500
                $expressionBuilder->comparison(
501
                    $expressionBuilder->bitAnd('pages.perms_everybody', $perms),
502
                    ExpressionBuilder::EQ,
503
                    $perms
504
                ),
505
                $expressionBuilder->andX(
506
                    $expressionBuilder->eq('pages.perms_userid', (int)$this->user['uid']),
507
                    $expressionBuilder->comparison(
508
                        $expressionBuilder->bitAnd('pages.perms_user', $perms),
509
                        ExpressionBuilder::EQ,
510
                        $perms
511
                    )
512
                )
513
            );
514
515
            // Group (if any is set)
516
            if (!empty($this->userGroupsUID)) {
517
                $constraint->add(
518
                    $expressionBuilder->andX(
519
                        $expressionBuilder->in(
520
                            'pages.perms_groupid',
521
                            $this->userGroupsUID
522
                        ),
523
                        $expressionBuilder->comparison(
524
                            $expressionBuilder->bitAnd('pages.perms_group', $perms),
525
                            ExpressionBuilder::EQ,
526
                            $perms
527
                        )
528
                    )
529
                );
530
            }
531
532
            $constraint = ' (' . (string)$constraint . ')';
533
534
            // ****************
535
            // getPagePermsClause-HOOK
536
            // ****************
537
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getPagePermsClause'] ?? [] as $_funcRef) {
538
                $_params = ['currentClause' => $constraint, 'perms' => $perms];
539
                $constraint = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
540
            }
541
            return $constraint;
542
        }
543
        return ' 1=0';
544
    }
545
546
    /**
547
     * Returns a combined binary representation of the current users permissions for the page-record, $row.
548
     * The perms for user, group and everybody is OR'ed together (provided that the page-owner is the user
549
     * and for the groups that the user is a member of the group.
550
     * If the user is admin, 31 is returned	(full permissions for all five flags)
551
     *
552
     * @param array $row Input page row with all perms_* fields available.
553
     * @return int Bitwise representation of the users permissions in relation to input page row, $row
554
     */
555
    public function calcPerms($row)
556
    {
557
        // Return 31 for admin users.
558
        if ($this->isAdmin()) {
559
            return Permission::ALL;
560
        }
561
        // Return 0 if page is not within the allowed web mount
562
        if (!$this->isInWebMount($row)) {
0 ignored issues
show
Deprecated Code introduced by
The function TYPO3\CMS\Core\Authentic...ication::isInWebMount() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

562
        if (!/** @scrutinizer ignore-deprecated */ $this->isInWebMount($row)) {
Loading history...
Bug Best Practice introduced by
The expression $this->isInWebMount($row) of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
563
            return Permission::NOTHING;
564
        }
565
        $out = Permission::NOTHING;
566
        if (
567
            isset($row['perms_userid']) && isset($row['perms_user']) && isset($row['perms_groupid'])
568
            && isset($row['perms_group']) && isset($row['perms_everybody']) && !empty($this->userGroupsUID)
569
        ) {
570
            if ($this->user['uid'] == $row['perms_userid']) {
571
                $out |= $row['perms_user'];
572
            }
573
            if ($this->isMemberOfGroup($row['perms_groupid'])) {
574
                $out |= $row['perms_group'];
575
            }
576
            $out |= $row['perms_everybody'];
577
        }
578
        // ****************
579
        // CALCPERMS hook
580
        // ****************
581
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['calcPerms'] ?? [] as $_funcRef) {
582
            $_params = [
583
                'row' => $row,
584
                'outputPermissions' => $out,
585
            ];
586
            $out = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
587
        }
588
        return $out;
589
    }
590
591
    /**
592
     * Returns TRUE if the RTE (Rich Text Editor) is enabled for the user.
593
     *
594
     * @return bool
595
     * @internal should only be used from within TYPO3 Core
596
     */
597
    public function isRTE(): bool
598
    {
599
        return (bool)($this->uc['edit_RTE'] ?? false);
600
    }
601
602
    /**
603
     * Returns TRUE if the $value is found in the list in a $this->groupData[] index pointed to by $type (array key).
604
     * Can thus be users to check for modules, exclude-fields, select/modify permissions for tables etc.
605
     * If user is admin TRUE is also returned
606
     * Please see the document Inside TYPO3 for examples.
607
     *
608
     * @param string $type The type value; "webmounts", "filemounts", "pagetypes_select", "tables_select", "tables_modify", "non_exclude_fields", "modules", "available_widgets", "mfa_providers"
609
     * @param string $value String to search for in the groupData-list
610
     * @return bool TRUE if permission is granted (that is, the value was found in the groupData list - or the BE_USER is "admin")
611
     */
612
    public function check($type, $value)
613
    {
614
        return isset($this->groupData[$type])
615
            && ($this->isAdmin() || GeneralUtility::inList($this->groupData[$type], $value));
616
    }
617
618
    /**
619
     * Checking the authMode of a select field with authMode set
620
     *
621
     * @param string $table Table name
622
     * @param string $field Field name (must be configured in TCA and of type "select" with authMode set!)
623
     * @param string $value Value to evaluation (single value, must not contain any of the chars ":,|")
624
     * @param string $authMode Auth mode keyword (explicitAllow, explicitDeny, individual)
625
     * @return bool Whether access is granted or not
626
     */
627
    public function checkAuthMode($table, $field, $value, $authMode)
628
    {
629
        // Admin users can do anything:
630
        if ($this->isAdmin()) {
631
            return true;
632
        }
633
        // Allow all blank values:
634
        if ((string)$value === '') {
635
            return true;
636
        }
637
        // Allow dividers:
638
        if ($value === '--div--') {
639
            return true;
640
        }
641
        // Certain characters are not allowed in the value
642
        if (preg_match('/[:|,]/', $value)) {
643
            return false;
644
        }
645
        // Initialize:
646
        $testValue = $table . ':' . $field . ':' . $value;
647
        $out = true;
648
        // Checking value:
649
        switch ((string)$authMode) {
650
            case 'explicitAllow':
651
                if (!GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':ALLOW')) {
652
                    $out = false;
653
                }
654
                break;
655
            case 'explicitDeny':
656
                if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) {
657
                    $out = false;
658
                }
659
                break;
660
            case 'individual':
661
                if (is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
662
                    $items = $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'];
663
                    if (is_array($items)) {
664
                        foreach ($items as $iCfg) {
665
                            if ((string)$iCfg[1] === (string)$value && $iCfg[4]) {
666
                                switch ((string)$iCfg[4]) {
667
                                    case 'EXPL_ALLOW':
668
                                        if (!GeneralUtility::inList(
669
                                            $this->groupData['explicit_allowdeny'],
670
                                            $testValue . ':ALLOW'
671
                                        )) {
672
                                            $out = false;
673
                                        }
674
                                        break;
675
                                    case 'EXPL_DENY':
676
                                        if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) {
677
                                            $out = false;
678
                                        }
679
                                        break;
680
                                }
681
                                break;
682
                            }
683
                        }
684
                    }
685
                }
686
                break;
687
        }
688
        return $out;
689
    }
690
691
    /**
692
     * Checking if a language value (-1, 0 and >0 for sys_language records) is allowed to be edited by the user.
693
     *
694
     * @param int $langValue Language value to evaluate
695
     * @return bool Returns TRUE if the language value is allowed, otherwise FALSE.
696
     */
697
    public function checkLanguageAccess($langValue)
698
    {
699
        // The users language list must be non-blank - otherwise all languages are allowed.
700
        if (trim($this->groupData['allowed_languages']) !== '') {
701
            $langValue = (int)$langValue;
702
            // Language must either be explicitly allowed OR the lang Value be "-1" (all languages)
703
            if ($langValue != -1 && !$this->check('allowed_languages', (string)$langValue)) {
704
                return false;
705
            }
706
        }
707
        return true;
708
    }
709
710
    /**
711
     * Check if user has access to all existing localizations for a certain record
712
     *
713
     * @param string $table The table
714
     * @param array $record The current record
715
     * @return bool
716
     */
717
    public function checkFullLanguagesAccess($table, $record)
718
    {
719
        if (!$this->checkLanguageAccess(0)) {
720
            return false;
721
        }
722
723
        if (BackendUtility::isTableLocalizable($table)) {
724
            $pointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
725
            $pointerValue = $record[$pointerField] > 0 ? $record[$pointerField] : $record['uid'];
726
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
727
            $queryBuilder->getRestrictions()
728
                ->removeAll()
729
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
730
                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->workspace));
731
            $recordLocalizations = $queryBuilder->select('*')
732
                ->from($table)
733
                ->where(
734
                    $queryBuilder->expr()->eq(
735
                        $pointerField,
736
                        $queryBuilder->createNamedParameter($pointerValue, \PDO::PARAM_INT)
737
                    )
738
                )
739
                ->execute()
740
                ->fetchAllAssociative();
741
742
            foreach ($recordLocalizations as $recordLocalization) {
743
                if (!$this->checkLanguageAccess($recordLocalization[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
744
                    return false;
745
                }
746
            }
747
        }
748
        return true;
749
    }
750
751
    /**
752
     * Checking if a user has editing access to a record from a $GLOBALS['TCA'] table.
753
     * The checks does not take page permissions and other "environmental" things into account.
754
     * It only deal with record internals; If any values in the record fields disallows it.
755
     * For instance languages settings, authMode selector boxes are evaluated (and maybe more in the future).
756
     * It will check for workspace dependent access.
757
     * The function takes an ID (int) or row (array) as second argument.
758
     *
759
     * @param string $table Table name
760
     * @param int|array $idOrRow If integer, then this is the ID of the record. If Array this just represents fields in the record.
761
     * @param bool $newRecord Set, if testing a new (non-existing) record array. Will disable certain checks that doesn't make much sense in that context.
762
     * @param bool $deletedRecord Set, if testing a deleted record array.
763
     * @param bool $checkFullLanguageAccess Set, whenever access to all translations of the record is required
764
     * @return bool TRUE if OK, otherwise FALSE
765
     * @internal should only be used from within TYPO3 Core
766
     */
767
    public function recordEditAccessInternals($table, $idOrRow, $newRecord = false, $deletedRecord = false, $checkFullLanguageAccess = false): bool
768
    {
769
        if (!isset($GLOBALS['TCA'][$table])) {
770
            return false;
771
        }
772
        // Always return TRUE for Admin users.
773
        if ($this->isAdmin()) {
774
            return true;
775
        }
776
        // Fetching the record if the $idOrRow variable was not an array on input:
777
        if (!is_array($idOrRow)) {
778
            if ($deletedRecord) {
779
                $idOrRow = BackendUtility::getRecord($table, $idOrRow, '*', '', false);
780
            } else {
781
                $idOrRow = BackendUtility::getRecord($table, $idOrRow);
782
            }
783
            if (!is_array($idOrRow)) {
784
                $this->errorMsg = 'ERROR: Record could not be fetched.';
785
                return false;
786
            }
787
        }
788
        // Checking languages:
789
        if ($table === 'pages' && $checkFullLanguageAccess && !$this->checkFullLanguagesAccess($table, $idOrRow)) {
790
            return false;
791
        }
792
        if ($GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? false) {
793
            // Language field must be found in input row - otherwise it does not make sense.
794
            if (isset($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
795
                if (!$this->checkLanguageAccess($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
796
                    $this->errorMsg = 'ERROR: Language was not allowed.';
797
                    return false;
798
                }
799
                if (
800
                    $checkFullLanguageAccess && $idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']] == 0
801
                    && !$this->checkFullLanguagesAccess($table, $idOrRow)
802
                ) {
803
                    $this->errorMsg = 'ERROR: Related/affected language was not allowed.';
804
                    return false;
805
                }
806
            } else {
807
                $this->errorMsg = 'ERROR: The "languageField" field named "'
808
                    . $GLOBALS['TCA'][$table]['ctrl']['languageField'] . '" was not found in testing record!';
809
                return false;
810
            }
811
        }
812
        // Checking authMode fields:
813
        if (is_array($GLOBALS['TCA'][$table]['columns'])) {
814
            foreach ($GLOBALS['TCA'][$table]['columns'] as $fieldName => $fieldValue) {
815
                if (isset($idOrRow[$fieldName])
816
                    && ($fieldValue['config']['type'] ?? '') === 'select'
817
                    && ($fieldValue['config']['authMode'] ?? false)
818
                    && ($fieldValue['config']['authMode_enforce'] ?? '') === 'strict'
819
                    && !$this->checkAuthMode($table, $fieldName, $idOrRow[$fieldName], $fieldValue['config']['authMode'])) {
820
                    $this->errorMsg = 'ERROR: authMode "' . $fieldValue['config']['authMode']
821
                            . '" failed for field "' . $fieldName . '" with value "'
822
                            . $idOrRow[$fieldName] . '" evaluated';
823
                    return false;
824
                }
825
            }
826
        }
827
        // Checking "editlock" feature (doesn't apply to new records)
828
        if (!$newRecord && ($GLOBALS['TCA'][$table]['ctrl']['editlock'] ?? false)) {
829
            if (isset($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['editlock']])) {
830
                if ($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['editlock']]) {
831
                    $this->errorMsg = 'ERROR: Record was locked for editing. Only admin users can change this state.';
832
                    return false;
833
                }
834
            } else {
835
                $this->errorMsg = 'ERROR: The "editLock" field named "' . $GLOBALS['TCA'][$table]['ctrl']['editlock']
836
                    . '" was not found in testing record!';
837
                return false;
838
            }
839
        }
840
        // Checking record permissions
841
        // THIS is where we can include a check for "perms_" fields for other records than pages...
842
        // Process any hooks
843
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['recordEditAccessInternals'] ?? [] as $funcRef) {
844
            $params = [
845
                'table' => $table,
846
                'idOrRow' => $idOrRow,
847
                'newRecord' => $newRecord,
848
            ];
849
            if (!GeneralUtility::callUserFunction($funcRef, $params, $this)) {
850
                return false;
851
            }
852
        }
853
        // Finally, return TRUE if all is well.
854
        return true;
855
    }
856
857
    /**
858
     * Returns TRUE if the BE_USER is allowed to *create* shortcuts in the backend modules
859
     *
860
     * @return bool
861
     */
862
    public function mayMakeShortcut()
863
    {
864
        return ($this->getTSConfig()['options.']['enableBookmarks'] ?? false)
865
            && !($this->getTSConfig()['options.']['mayNotCreateEditBookmarks'] ?? false);
866
    }
867
868
    /**
869
     * Checks if a record is allowed to be edited in the current workspace.
870
     * This is not bound to an actual record, but to the mere fact if the user is in a workspace
871
     * and depending on the table settings.
872
     *
873
     * @param string $table
874
     * @return bool
875
     * @internal should only be used from within TYPO3 Core
876
     */
877
    public function workspaceAllowsLiveEditingInTable(string $table): bool
878
    {
879
        // In live workspace the record can be added/modified
880
        if ($this->workspace === 0) {
881
            return true;
882
        }
883
        // Workspace setting allows to "live edit" records of tables without versioning
884
        if (($this->workspaceRec['live_edit'] ?? false)
885
            && !BackendUtility::isTableWorkspaceEnabled($table)
886
        ) {
887
            return true;
888
        }
889
        // Always for Live workspace AND if live-edit is enabled
890
        // and tables are completely without versioning it is ok as well.
891
        if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS_alwaysAllowLiveEdit'] ?? false) {
892
            return true;
893
        }
894
        // If the answer is FALSE it means the only valid way to create or edit records by creating records in the workspace
895
        return false;
896
    }
897
898
    /**
899
     * Evaluates if a record from $table can be created. If the table is not set up for versioning,
900
     * and the "live edit" flag of the page is set, return false. In live workspace this is always true,
901
     * as all records can be created in live workspace
902
     *
903
     * @param string $table Table name
904
     * @return bool
905
     * @internal should only be used from within TYPO3 Core
906
     */
907
    public function workspaceCanCreateNewRecord(string $table): bool
908
    {
909
        // If LIVE records cannot be created due to workspace restrictions, prepare creation of placeholder-record
910
        if (!$this->workspaceAllowsLiveEditingInTable($table) && !BackendUtility::isTableWorkspaceEnabled($table)) {
911
            return false;
912
        }
913
        return true;
914
    }
915
916
    /**
917
     * Checks if an element stage allows access for the user in the current workspace
918
     * In live workspace (= 0) access is always granted for any stage.
919
     * Admins are always allowed.
920
     * An option for custom workspaces allows members to also edit when the stage is "Review"
921
     *
922
     * @param int $stage Stage id from an element: -1,0 = editing, 1 = reviewer, >1 = owner
923
     * @return bool TRUE if user is allowed access
924
     * @internal should only be used from within TYPO3 Core
925
     */
926
    public function workspaceCheckStageForCurrent($stage)
927
    {
928
        // Always allow for admins
929
        if ($this->isAdmin()) {
930
            return true;
931
        }
932
        // Always OK for live workspace
933
        if ($this->workspace === 0 || !ExtensionManagementUtility::isLoaded('workspaces')) {
934
            return true;
935
        }
936
        $stage = (int)$stage;
937
        $stat = $this->checkWorkspaceCurrent();
938
        $accessType = $stat['_ACCESS'];
939
        // Workspace owners are always allowed for stage change
940
        if ($accessType === 'owner') {
941
            return true;
942
        }
943
944
        // Check if custom staging is activated
945
        $workspaceRec = BackendUtility::getRecord('sys_workspace', $stat['uid']);
946
        if ($workspaceRec['custom_stages'] > 0 && $stage !== 0 && $stage !== -10) {
947
            // Get custom stage record
948
            $workspaceStageRec = BackendUtility::getRecord('sys_workspace_stage', $stage);
949
            // Check if the user is responsible for the current stage
950
            if (
951
                $accessType === 'member'
952
                && GeneralUtility::inList($workspaceStageRec['responsible_persons'] ?? '', 'be_users_' . $this->user['uid'])
953
            ) {
954
                return true;
955
            }
956
            // Check if the user is in a group which is responsible for the current stage
957
            foreach ($this->userGroupsUID as $groupUid) {
958
                if (
959
                    $accessType === 'member'
960
                    && GeneralUtility::inList($workspaceStageRec['responsible_persons'] ?? '', 'be_groups_' . $groupUid)
961
                ) {
962
                    return true;
963
                }
964
            }
965
        } elseif ($stage === -10 || $stage === -20) {
966
            // Nobody is allowed to do that except the owner (which was checked above)
967
            return false;
968
        } elseif (
969
            $accessType === 'reviewer' && $stage <= 1
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($accessType === 'review...'member' && $stage <= 0, Probably Intended Meaning: $accessType === 'reviewe...member' && $stage <= 0)
Loading history...
970
            || $accessType === 'member' && $stage <= 0
971
        ) {
972
            return true;
973
        }
974
        return false;
975
    }
976
977
    /**
978
     * Returns TRUE if the user has access to publish content from the workspace ID given.
979
     * Admin-users are always granted access to do this
980
     * If the workspace ID is 0 (live) all users have access also
981
     * For custom workspaces it depends on whether the user is owner OR like with
982
     * draft workspace if the user has access to Live workspace.
983
     *
984
     * @param int $wsid Workspace UID; 0,1+
985
     * @return bool Returns TRUE if the user has access to publish content from the workspace ID given.
986
     * @internal this method will be moved to EXT:workspaces
987
     */
988
    public function workspacePublishAccess($wsid)
989
    {
990
        if ($this->isAdmin()) {
991
            return true;
992
        }
993
        $wsAccess = $this->checkWorkspace($wsid);
994
        // If no access to workspace, of course you cannot publish!
995
        if ($wsAccess === false) {
0 ignored issues
show
introduced by
The condition $wsAccess === false is always false.
Loading history...
996
            return false;
997
        }
998
        if ((int)$wsAccess['uid'] === 0) {
999
            // If access to Live workspace, no problem.
1000
            return true;
1001
        }
1002
        // Custom workspaces
1003
        // 1. Owners can always publish
1004
        if ($wsAccess['_ACCESS'] === 'owner') {
1005
            return true;
1006
        }
1007
        // 2. User has access to online workspace which is OK as well as long as publishing
1008
        // access is not limited by workspace option.
1009
        return $this->checkWorkspace(0) && !($wsAccess['publish_access'] & 2);
1010
    }
1011
1012
    /**
1013
     * Returns full parsed user TSconfig array, merged with TSconfig from groups.
1014
     *
1015
     * Example:
1016
     * [
1017
     *     'options.' => [
1018
     *         'fooEnabled' => '0',
1019
     *         'fooEnabled.' => [
1020
     *             'tt_content' => 1,
1021
     *         ],
1022
     *     ],
1023
     * ]
1024
     *
1025
     * @return array Parsed and merged user TSconfig array
1026
     */
1027
    public function getTSConfig()
1028
    {
1029
        return $this->userTS;
1030
    }
1031
1032
    /**
1033
     * Returns an array with the webmounts.
1034
     * If no webmounts, and empty array is returned.
1035
     * Webmounts permissions are checked in fetchGroupData()
1036
     *
1037
     * @return array of web mounts uids (may include '0')
1038
     */
1039
    public function returnWebmounts()
1040
    {
1041
        return (string)$this->groupData['webmounts'] != '' ? explode(',', $this->groupData['webmounts']) : [];
1042
    }
1043
1044
    /**
1045
     * Initializes the given mount points for the current Backend user.
1046
     *
1047
     * @param array $mountPointUids Page UIDs that should be used as web mountpoints
1048
     * @param bool $append If TRUE the given mount point will be appended. Otherwise the current mount points will be replaced.
1049
     */
1050
    public function setWebmounts(array $mountPointUids, $append = false)
1051
    {
1052
        if (empty($mountPointUids)) {
1053
            return;
1054
        }
1055
        if ($append) {
1056
            $currentWebMounts = GeneralUtility::intExplode(',', $this->groupData['webmounts']);
1057
            $mountPointUids = array_merge($currentWebMounts, $mountPointUids);
1058
        }
1059
        $this->groupData['webmounts'] = implode(',', array_unique($mountPointUids));
1060
    }
1061
1062
    /**
1063
     * Checks for alternative web mount points for the element browser.
1064
     *
1065
     * If there is a temporary mount point active in the page tree it will be used.
1066
     *
1067
     * If the User TSconfig options.pageTree.altElementBrowserMountPoints is not empty the pages configured
1068
     * there are used as web mounts If options.pageTree.altElementBrowserMountPoints.append is enabled,
1069
     * they are appended to the existing webmounts.
1070
     *
1071
     * @internal - do not use in your own extension
1072
     */
1073
    public function initializeWebmountsForElementBrowser()
1074
    {
1075
        $alternativeWebmountPoint = (int)$this->getSessionData('pageTree_temporaryMountPoint');
1076
        if ($alternativeWebmountPoint) {
1077
            $alternativeWebmountPoint = GeneralUtility::intExplode(',', (string)$alternativeWebmountPoint);
1078
            $this->setWebmounts($alternativeWebmountPoint);
1079
            return;
1080
        }
1081
1082
        $alternativeWebmountPoints = trim($this->getTSConfig()['options.']['pageTree.']['altElementBrowserMountPoints'] ?? '');
1083
        $appendAlternativeWebmountPoints = $this->getTSConfig()['options.']['pageTree.']['altElementBrowserMountPoints.']['append'] ?? '';
1084
        if ($alternativeWebmountPoints) {
1085
            $alternativeWebmountPoints = GeneralUtility::intExplode(',', $alternativeWebmountPoints);
1086
            $this->setWebmounts($alternativeWebmountPoints, $appendAlternativeWebmountPoints);
0 ignored issues
show
Bug introduced by
It seems like $appendAlternativeWebmountPoints can also be of type string; however, parameter $append of TYPO3\CMS\Core\Authentic...ication::setWebmounts() does only seem to accept boolean, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1086
            $this->setWebmounts($alternativeWebmountPoints, /** @scrutinizer ignore-type */ $appendAlternativeWebmountPoints);
Loading history...
1087
        }
1088
    }
1089
1090
    /**
1091
     * Returns TRUE or FALSE, depending if an alert popup (a javascript confirmation) should be shown
1092
     * call like $GLOBALS['BE_USER']->jsConfirmation($BITMASK).
1093
     *
1094
     * @param int $bitmask Bitmask, one of \TYPO3\CMS\Core\Type\Bitmask\JsConfirmation
1095
     * @return bool TRUE if the confirmation should be shown
1096
     * @see JsConfirmation
1097
     */
1098
    public function jsConfirmation($bitmask)
1099
    {
1100
        try {
1101
            $alertPopupsSetting = trim((string)($this->getTSConfig()['options.']['alertPopups'] ?? ''));
1102
            $alertPopup = JsConfirmation::cast($alertPopupsSetting === '' ? null : (int)$alertPopupsSetting);
1103
        } catch (InvalidEnumerationValueException $e) {
1104
            $alertPopup = new JsConfirmation();
1105
        }
1106
1107
        return JsConfirmation::cast($bitmask)->matches($alertPopup);
1108
    }
1109
1110
    /**
1111
     * Initializes a lot of stuff like the access-lists, database-mountpoints and filemountpoints
1112
     * This method is called by ->backendCheckLogin() (from extending BackendUserAuthentication)
1113
     * if the backend user login has verified OK.
1114
     * Generally this is required initialization of a backend user.
1115
     *
1116
     * @internal
1117
     * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
1118
     */
1119
    public function fetchGroupData()
1120
    {
1121
        if ($this->user['uid']) {
1122
            // Get lists for the be_user record and set them as default/primary values.
1123
            // Enabled Backend Modules
1124
            $this->groupData['modules'] = $this->user['userMods'] ?? '';
1125
            // Add available widgets
1126
            $this->groupData['available_widgets'] = $this->user['available_widgets'] ?? '';
1127
            // Add allowed mfa providers
1128
            $this->groupData['mfa_providers'] = $this->user['mfa_providers'] ?? '';
1129
            // Add Allowed Languages
1130
            $this->groupData['allowed_languages'] = $this->user['allowed_languages'] ?? '';
1131
            // Set user value for workspace permissions.
1132
            $this->groupData['workspace_perms'] = $this->user['workspace_perms'] ?? 0;
1133
            // Database mountpoints
1134
            $this->groupData['webmounts'] = $this->user['db_mountpoints'] ?? '';
1135
            // File mountpoints
1136
            $this->groupData['filemounts'] = $this->user['file_mountpoints'] ?? '';
1137
            // Fileoperation permissions
1138
            $this->groupData['file_permissions'] = $this->user['file_permissions'] ?? '';
1139
1140
            // Get the groups and accumulate their permission settings
1141
            $mountOptions = new BackendGroupMountOption($this->user['options']);
1142
            $groupResolver = GeneralUtility::makeInstance(GroupResolver::class);
1143
            $resolvedGroups = $groupResolver->resolveGroupsForUser($this->user, $this->usergroup_table);
1144
            foreach ($resolvedGroups as $groupInfo) {
1145
                $groupInfo += [
1146
                    'uid' => 0,
1147
                    'db_mountpoints' => '',
1148
                    'file_mountpoints' => '',
1149
                    'groupMods' => '',
1150
                    'availableWidgets' => '',
1151
                    'mfa_providers' => '',
1152
                    'tables_select' => '',
1153
                    'tables_modify' => '',
1154
                    'pagetypes_select' => '',
1155
                    'non_exclude_fields' => '',
1156
                    'explicit_allowdeny' => '',
1157
                    'allowed_languages' => '',
1158
                    'custom_options' => '',
1159
                    'file_permissions' => '',
1160
                    'workspace_perms' => 0, // Bitflag.
1161
                ];
1162
                // Add the group uid to internal arrays.
1163
                $this->userGroupsUID[] = (int)$groupInfo['uid'];
1164
                $this->userGroups[(int)$groupInfo['uid']] = $groupInfo;
1165
                // Mount group database-mounts
1166
                if ($mountOptions->shouldUserIncludePageMountsFromAssociatedGroups()) {
1167
                    $this->groupData['webmounts'] .= ',' . $groupInfo['db_mountpoints'];
1168
                }
1169
                // Mount group file-mounts
1170
                if ($mountOptions->shouldUserIncludePageMountsFromAssociatedGroups()) {
1171
                    $this->groupData['filemounts'] .= ',' . $groupInfo['file_mountpoints'];
1172
                }
1173
                // Gather permission detail fields
1174
                $this->groupData['modules'] .= ',' . $groupInfo['groupMods'];
1175
                $this->groupData['available_widgets'] .= ',' . $groupInfo['availableWidgets'];
1176
                $this->groupData['mfa_providers'] .= ',' . $groupInfo['mfa_providers'];
1177
                $this->groupData['tables_select'] .= ',' . $groupInfo['tables_select'];
1178
                $this->groupData['tables_modify'] .= ',' . $groupInfo['tables_modify'];
1179
                $this->groupData['pagetypes_select'] .= ',' . $groupInfo['pagetypes_select'];
1180
                $this->groupData['non_exclude_fields'] .= ',' . $groupInfo['non_exclude_fields'];
1181
                $this->groupData['explicit_allowdeny'] .= ',' . $groupInfo['explicit_allowdeny'];
1182
                $this->groupData['allowed_languages'] .= ',' . $groupInfo['allowed_languages'];
1183
                $this->groupData['custom_options'] .= ',' . $groupInfo['custom_options'];
1184
                $this->groupData['file_permissions'] .= ',' . $groupInfo['file_permissions'];
1185
                // Setting workspace permissions:
1186
                $this->groupData['workspace_perms'] |= $groupInfo['workspace_perms'];
1187
                if (!$this->firstMainGroup) {
1188
                    $this->firstMainGroup = (int)$groupInfo['uid'];
1189
                }
1190
            }
1191
1192
            // Populating the $this->userGroupsUID -array with the groups in the order in which they were LAST included.!!
1193
            // Finally this is the list of group_uid's in the order they are parsed (including subgroups!)
1194
            // and without duplicates (duplicates are presented with their last entrance in the list,
1195
            // which thus reflects the order of the TypoScript in TSconfig)
1196
            $this->userGroupsUID = array_reverse(array_unique(array_reverse($this->userGroupsUID)));
1197
1198
            $this->prepareUserTsConfig();
1199
1200
            // Processing webmounts
1201
            // Admin's always have the root mounted
1202
            if ($this->isAdmin() && !($this->getTSConfig()['options.']['dontMountAdminMounts'] ?? false)) {
1203
                $this->groupData['webmounts'] = '0,' . $this->groupData['webmounts'];
1204
            }
1205
            // The lists are cleaned for duplicates
1206
            $this->groupData['webmounts'] = StringUtility::uniqueList($this->groupData['webmounts'] ?? '');
1207
            $this->groupData['pagetypes_select'] = StringUtility::uniqueList($this->groupData['pagetypes_select'] ?? '');
1208
            $this->groupData['tables_select'] = StringUtility::uniqueList(($this->groupData['tables_modify'] ?? '') . ',' . ($this->groupData['tables_select'] ?? ''));
1209
            $this->groupData['tables_modify'] = StringUtility::uniqueList($this->groupData['tables_modify'] ?? '');
1210
            $this->groupData['non_exclude_fields'] = StringUtility::uniqueList($this->groupData['non_exclude_fields'] ?? '');
1211
            $this->groupData['explicit_allowdeny'] = StringUtility::uniqueList($this->groupData['explicit_allowdeny'] ?? '');
1212
            $this->groupData['allowed_languages'] = StringUtility::uniqueList($this->groupData['allowed_languages'] ?? '');
1213
            $this->groupData['custom_options'] = StringUtility::uniqueList($this->groupData['custom_options'] ?? '');
1214
            $this->groupData['modules'] = StringUtility::uniqueList($this->groupData['modules'] ?? '');
1215
            $this->groupData['available_widgets'] = StringUtility::uniqueList($this->groupData['available_widgets'] ?? '');
1216
            $this->groupData['mfa_providers'] = StringUtility::uniqueList($this->groupData['mfa_providers'] ?? '');
1217
            $this->groupData['file_permissions'] = StringUtility::uniqueList($this->groupData['file_permissions'] ?? '');
1218
1219
            // Check if the user access to all web mounts set
1220
            if (!empty(trim($this->groupData['webmounts']))) {
1221
                $validWebMounts = $this->filterValidWebMounts($this->groupData['webmounts']);
1222
                $this->groupData['webmounts'] = implode(',', $validWebMounts);
1223
            }
1224
            // Setting up workspace situation (after webmounts are processed!):
1225
            $this->workspaceInit();
1226
        }
1227
    }
1228
1229
    /**
1230
     * Checking read access to web mounts, but keeps "0" or empty strings.
1231
     * In any case, checks if the list of pages is visible for the backend user but also
1232
     * if the page is not deleted.
1233
     *
1234
     * @param string $listOfWebMounts a comma-separated list of webmounts, could also be empty, or contain "0"
1235
     * @return array a list of all valid web mounts the user has access to
1236
     */
1237
    protected function filterValidWebMounts(string $listOfWebMounts): array
1238
    {
1239
        // Checking read access to web mounts if there are mounts points (not empty string, false or 0)
1240
        $allWebMounts = explode(',', $listOfWebMounts);
1241
        // Selecting all web mounts with permission clause for reading
1242
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1243
        $queryBuilder->getRestrictions()
1244
            ->removeAll()
1245
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1246
1247
        $readablePagesOfWebMounts = $queryBuilder->select('uid')
1248
            ->from('pages')
1249
            // @todo DOCTRINE: check how to make getPagePermsClause() portable
1250
            ->where(
1251
                $this->getPagePermsClause(Permission::PAGE_SHOW),
1252
                $queryBuilder->expr()->in(
1253
                    'uid',
1254
                    $queryBuilder->createNamedParameter(
1255
                        GeneralUtility::intExplode(',', $listOfWebMounts),
1256
                        Connection::PARAM_INT_ARRAY
1257
                    )
1258
                )
1259
            )
1260
            ->execute()
1261
            ->fetchAllAssociative();
1262
        $readablePagesOfWebMounts = array_column(($readablePagesOfWebMounts ?: []), 'uid', 'uid');
1263
        foreach ($allWebMounts as $key => $mountPointUid) {
1264
            // If the mount ID is NOT found among selected pages, unset it:
1265
            if ($mountPointUid > 0 && !isset($readablePagesOfWebMounts[$mountPointUid])) {
1266
                unset($allWebMounts[$key]);
1267
            }
1268
        }
1269
        return $allWebMounts;
1270
    }
1271
1272
    /**
1273
     * This method parses the UserTSconfig from the current user and all their groups.
1274
     * If the contents are the same, parsing is skipped. No matching is applied here currently.
1275
     */
1276
    protected function prepareUserTsConfig(): void
1277
    {
1278
        $collectedUserTSconfig = [
1279
            'default' => $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig'],
1280
        ];
1281
        // Default TSconfig for admin-users
1282
        if ($this->isAdmin()) {
1283
            $collectedUserTSconfig[] = 'admPanel.enable.all = 1';
1284
        }
1285
        // Setting defaults for sys_note author / email
1286
        $collectedUserTSconfig[] = '
1287
TCAdefaults.sys_note.author = ' . $this->user['realName'] . '
1288
TCAdefaults.sys_note.email = ' . $this->user['email'];
1289
1290
        // Loop through all groups and add their 'TSconfig' fields
1291
        foreach ($this->userGroupsUID as $groupId) {
1292
            $collectedUserTSconfig['group_' . $groupId] = $this->userGroups[$groupId]['TSconfig'] ?? '';
1293
        }
1294
1295
        $collectedUserTSconfig[] = $this->user['TSconfig'];
1296
        // Check external files
1297
        $collectedUserTSconfig = TypoScriptParser::checkIncludeLines_array($collectedUserTSconfig);
1298
        // Imploding with "[global]" will make sure that non-ended confinements with braces are ignored.
1299
        $userTS_text = implode("\n[GLOBAL]\n", $collectedUserTSconfig);
1300
        // Parsing the user TSconfig (or getting from cache)
1301
        $hash = md5('userTS:' . $userTS_text);
1302
        $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
1303
        if (!($this->userTS = $cache->get($hash))) {
1304
            $parseObj = GeneralUtility::makeInstance(TypoScriptParser::class);
1305
            $conditionMatcher = GeneralUtility::makeInstance(ConditionMatcher::class);
1306
            $parseObj->parse($userTS_text, $conditionMatcher);
1307
            $this->userTS = $parseObj->setup;
1308
            $cache->set($hash, $this->userTS, ['UserTSconfig'], 0);
1309
            // Ensure to update UC later
1310
            $this->userTSUpdated = true;
1311
        }
1312
    }
1313
1314
    /**
1315
     * Sets up all file storages for a user.
1316
     * Needs to be called AFTER the groups have been loaded.
1317
     */
1318
    protected function initializeFileStorages()
1319
    {
1320
        $this->fileStorages = [];
1321
        /** @var \TYPO3\CMS\Core\Resource\StorageRepository $storageRepository */
1322
        $storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
1323
        // Admin users have all file storages visible, without any filters
1324
        if ($this->isAdmin()) {
1325
            $storageObjects = $storageRepository->findAll();
1326
            foreach ($storageObjects as $storageObject) {
1327
                $this->fileStorages[$storageObject->getUid()] = $storageObject;
1328
            }
1329
        } else {
1330
            // Regular users only have storages that are defined in their filemounts
1331
            // Permissions and file mounts for the storage are added in StoragePermissionAspect
1332
            foreach ($this->getFileMountRecords() as $row) {
1333
                if (!array_key_exists((int)$row['base'], $this->fileStorages)) {
1334
                    $storageObject = $storageRepository->findByUid($row['base']);
1335
                    if ($storageObject) {
1336
                        $this->fileStorages[$storageObject->getUid()] = $storageObject;
1337
                    }
1338
                }
1339
            }
1340
        }
1341
1342
        // This has to be called always in order to set certain filters
1343
        $this->evaluateUserSpecificFileFilterSettings();
1344
    }
1345
1346
    /**
1347
     * Returns an array of category mount points. The category permissions from BE Groups
1348
     * are also taken into consideration and are merged into User permissions.
1349
     *
1350
     * @return array
1351
     */
1352
    public function getCategoryMountPoints()
1353
    {
1354
        $categoryMountPoints = '';
1355
1356
        // Category mounts of the groups
1357
        if (is_array($this->userGroups)) {
0 ignored issues
show
introduced by
The condition is_array($this->userGroups) is always true.
Loading history...
1358
            foreach ($this->userGroups as $group) {
1359
                if ($group['category_perms']) {
1360
                    $categoryMountPoints .= ',' . $group['category_perms'];
1361
                }
1362
            }
1363
        }
1364
1365
        // Category mounts of the user record
1366
        if ($this->user['category_perms']) {
1367
            $categoryMountPoints .= ',' . $this->user['category_perms'];
1368
        }
1369
1370
        // Make the ids unique
1371
        $categoryMountPoints = GeneralUtility::trimExplode(',', $categoryMountPoints);
1372
        $categoryMountPoints = array_filter($categoryMountPoints); // remove empty value
1373
        $categoryMountPoints = array_unique($categoryMountPoints); // remove unique value
1374
1375
        return $categoryMountPoints;
1376
    }
1377
1378
    /**
1379
     * Returns an array of file mount records, taking workspaces and user home and group home directories into account
1380
     * Needs to be called AFTER the groups have been loaded.
1381
     *
1382
     * @return array
1383
     * @internal
1384
     */
1385
    public function getFileMountRecords()
1386
    {
1387
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
1388
        $fileMountRecordCache = $runtimeCache->get('backendUserAuthenticationFileMountRecords') ?: [];
1389
1390
        if (!empty($fileMountRecordCache)) {
1391
            return $fileMountRecordCache;
1392
        }
1393
1394
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
1395
1396
        // Processing file mounts (both from the user and the groups)
1397
        $fileMounts = array_unique(GeneralUtility::intExplode(',', $this->groupData['filemounts'] ?? '', true));
1398
1399
        // Limit file mounts if set in workspace record
1400
        if ($this->workspace > 0 && !empty($this->workspaceRec['file_mountpoints'])) {
1401
            $workspaceFileMounts = GeneralUtility::intExplode(',', $this->workspaceRec['file_mountpoints'], true);
1402
            $fileMounts = array_intersect($fileMounts, $workspaceFileMounts);
1403
        }
1404
1405
        if (!empty($fileMounts)) {
1406
            $orderBy = $GLOBALS['TCA']['sys_filemounts']['ctrl']['default_sortby'] ?? 'sorting';
1407
1408
            $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_filemounts');
1409
            $queryBuilder->getRestrictions()
1410
                ->removeAll()
1411
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1412
                ->add(GeneralUtility::makeInstance(HiddenRestriction::class))
1413
                ->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
1414
1415
            $queryBuilder->select('*')
1416
                ->from('sys_filemounts')
1417
                ->where(
1418
                    $queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($fileMounts, Connection::PARAM_INT_ARRAY))
1419
                );
1420
1421
            foreach (QueryHelper::parseOrderBy($orderBy) as $fieldAndDirection) {
1422
                $queryBuilder->addOrderBy(...$fieldAndDirection);
1423
            }
1424
1425
            $fileMountRecords = $queryBuilder->execute()->fetchAllAssociative();
1426
            if ($fileMountRecords !== false) {
1427
                foreach ($fileMountRecords as $fileMount) {
1428
                    $fileMountRecordCache[$fileMount['base'] . $fileMount['path']] = $fileMount;
1429
                }
1430
            }
1431
        }
1432
1433
        // Read-only file mounts
1434
        $readOnlyMountPoints = \trim($this->getTSConfig()['options.']['folderTree.']['altElementBrowserMountPoints'] ?? '');
1435
        if ($readOnlyMountPoints) {
1436
            // We cannot use the API here but need to fetch the default storage record directly
1437
            // to not instantiate it (which directly applies mount points) before all mount points are resolved!
1438
            $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_file_storage');
1439
            $defaultStorageRow = $queryBuilder->select('uid')
1440
                ->from('sys_file_storage')
1441
                ->where(
1442
                    $queryBuilder->expr()->eq('is_default', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT))
1443
                )
1444
                ->setMaxResults(1)
1445
                ->execute()
1446
                ->fetchAssociative();
1447
1448
            $readOnlyMountPointArray = GeneralUtility::trimExplode(',', $readOnlyMountPoints);
1449
            foreach ($readOnlyMountPointArray as $readOnlyMountPoint) {
1450
                $readOnlyMountPointConfiguration = GeneralUtility::trimExplode(':', $readOnlyMountPoint);
1451
                if (count($readOnlyMountPointConfiguration) === 2) {
1452
                    // A storage is passed in the configuration
1453
                    $storageUid = (int)$readOnlyMountPointConfiguration[0];
1454
                    $path = $readOnlyMountPointConfiguration[1];
1455
                } else {
1456
                    if (empty($defaultStorageRow)) {
1457
                        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);
1458
                    }
1459
                    // Backwards compatibility: If no storage is passed, we use the default storage
1460
                    $storageUid = $defaultStorageRow['uid'];
1461
                    $path = $readOnlyMountPointConfiguration[0];
1462
                }
1463
                $fileMountRecordCache[$storageUid . $path] = [
1464
                    'base' => $storageUid,
1465
                    'title' => $path,
1466
                    'path' => $path,
1467
                    'read_only' => true,
1468
                ];
1469
            }
1470
        }
1471
1472
        // Personal or Group filemounts are not accessible if file mount list is set in workspace record
1473
        if ($this->workspace <= 0 || empty($this->workspaceRec['file_mountpoints'])) {
1474
            // If userHomePath is set, we attempt to mount it
1475
            if ($GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath']) {
1476
                [$userHomeStorageUid, $userHomeFilter] = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath'], 2);
1477
                $userHomeStorageUid = (int)$userHomeStorageUid;
1478
                $userHomeFilter = '/' . ltrim($userHomeFilter, '/');
1479
                if ($userHomeStorageUid > 0) {
1480
                    // Try and mount with [uid]_[username]
1481
                    $path = $userHomeFilter . $this->user['uid'] . '_' . $this->user['username'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1482
                    $fileMountRecordCache[$userHomeStorageUid . $path] = [
1483
                        'base' => $userHomeStorageUid,
1484
                        'title' => $this->user['username'],
1485
                        'path' => $path,
1486
                        'read_only' => false,
1487
                        'user_mount' => true,
1488
                    ];
1489
                    // Try and mount with only [uid]
1490
                    $path = $userHomeFilter . $this->user['uid'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1491
                    $fileMountRecordCache[$userHomeStorageUid . $path] = [
1492
                        'base' => $userHomeStorageUid,
1493
                        'title' => $this->user['username'],
1494
                        'path' => $path,
1495
                        'read_only' => false,
1496
                        'user_mount' => true,
1497
                    ];
1498
                }
1499
            }
1500
1501
            // Mount group home-dirs
1502
            $mountOptions = new BackendGroupMountOption((int)$this->user['options']);
1503
            if ($GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'] !== '' && $mountOptions->shouldUserIncludeFileMountsFromAssociatedGroups()) {
1504
                // If groupHomePath is set, we attempt to mount it
1505
                [$groupHomeStorageUid, $groupHomeFilter] = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'], 2);
1506
                $groupHomeStorageUid = (int)$groupHomeStorageUid;
1507
                $groupHomeFilter = '/' . ltrim($groupHomeFilter, '/');
1508
                if ($groupHomeStorageUid > 0) {
1509
                    foreach ($this->userGroups as $groupData) {
1510
                        $path = $groupHomeFilter . $groupData['uid'];
1511
                        $fileMountRecordCache[$groupHomeStorageUid . $path] = [
1512
                            'base' => $groupHomeStorageUid,
1513
                            'title' => $groupData['title'],
1514
                            'path' => $path,
1515
                            'read_only' => false,
1516
                            'user_mount' => true,
1517
                        ];
1518
                    }
1519
                }
1520
            }
1521
        }
1522
1523
        $runtimeCache->set('backendUserAuthenticationFileMountRecords', $fileMountRecordCache);
1524
        return $fileMountRecordCache;
1525
    }
1526
1527
    /**
1528
     * Returns an array with the filemounts for the user.
1529
     * Each filemount is represented with an array of a "name", "path" and "type".
1530
     * If no filemounts an empty array is returned.
1531
     *
1532
     * @return \TYPO3\CMS\Core\Resource\ResourceStorage[]
1533
     */
1534
    public function getFileStorages()
1535
    {
1536
        // Initializing file mounts after the groups are fetched
1537
        if ($this->fileStorages === null) {
1538
            $this->initializeFileStorages();
1539
        }
1540
        return $this->fileStorages;
1541
    }
1542
1543
    /**
1544
     * Adds filters based on what the user has set
1545
     * this should be done in this place, and called whenever needed,
1546
     * but only when needed
1547
     */
1548
    public function evaluateUserSpecificFileFilterSettings()
1549
    {
1550
        // Add the option for also displaying the non-hidden files
1551
        if ($this->uc['showHiddenFilesAndFolders'] ?? false) {
1552
            FileNameFilter::setShowHiddenFilesAndFolders(true);
1553
        }
1554
    }
1555
1556
    /**
1557
     * Returns the information about file permissions.
1558
     * Previously, this was stored in the DB field fileoper_perms now it is file_permissions.
1559
     * Besides it can be handled via userTSconfig
1560
     *
1561
     * permissions.file.default {
1562
     * addFile = 1
1563
     * readFile = 1
1564
     * writeFile = 1
1565
     * copyFile = 1
1566
     * moveFile = 1
1567
     * renameFile = 1
1568
     * deleteFile = 1
1569
     *
1570
     * addFolder = 1
1571
     * readFolder = 1
1572
     * writeFolder = 1
1573
     * copyFolder = 1
1574
     * moveFolder = 1
1575
     * renameFolder = 1
1576
     * deleteFolder = 1
1577
     * recursivedeleteFolder = 1
1578
     * }
1579
     *
1580
     * # overwrite settings for a specific storageObject
1581
     * permissions.file.storage.StorageUid {
1582
     * readFile = 1
1583
     * recursivedeleteFolder = 0
1584
     * }
1585
     *
1586
     * Please note that these permissions only apply, if the storage has the
1587
     * capabilities (browseable, writable), and if the driver allows for writing etc
1588
     *
1589
     * @return array
1590
     */
1591
    public function getFilePermissions()
1592
    {
1593
        if (!isset($this->filePermissions)) {
1594
            $filePermissions = [
1595
                // File permissions
1596
                'addFile' => false,
1597
                'readFile' => false,
1598
                'writeFile' => false,
1599
                'copyFile' => false,
1600
                'moveFile' => false,
1601
                'renameFile' => false,
1602
                'deleteFile' => false,
1603
                // Folder permissions
1604
                'addFolder' => false,
1605
                'readFolder' => false,
1606
                'writeFolder' => false,
1607
                'copyFolder' => false,
1608
                'moveFolder' => false,
1609
                'renameFolder' => false,
1610
                'deleteFolder' => false,
1611
                'recursivedeleteFolder' => false,
1612
            ];
1613
            if ($this->isAdmin()) {
1614
                $filePermissions = array_map('is_bool', $filePermissions);
1615
            } else {
1616
                $userGroupRecordPermissions = GeneralUtility::trimExplode(',', $this->groupData['file_permissions'] ?? '', true);
1617
                array_walk(
1618
                    $userGroupRecordPermissions,
1619
                    static function ($permission) use (&$filePermissions) {
1620
                        $filePermissions[$permission] = true;
1621
                    }
1622
                );
1623
1624
                // Finally overlay any userTSconfig
1625
                $permissionsTsConfig = $this->getTSConfig()['permissions.']['file.']['default.'] ?? [];
1626
                if (!empty($permissionsTsConfig)) {
1627
                    array_walk(
1628
                        $permissionsTsConfig,
1629
                        static function ($value, $permission) use (&$filePermissions) {
1630
                            $filePermissions[$permission] = (bool)$value;
1631
                        }
1632
                    );
1633
                }
1634
            }
1635
            $this->filePermissions = $filePermissions;
1636
        }
1637
        return $this->filePermissions;
1638
    }
1639
1640
    /**
1641
     * Gets the file permissions for a storage
1642
     * by merging any storage-specific permissions for a
1643
     * storage with the default settings.
1644
     * Admin users will always get the default settings.
1645
     *
1646
     * @param \TYPO3\CMS\Core\Resource\ResourceStorage $storageObject
1647
     * @return array
1648
     */
1649
    public function getFilePermissionsForStorage(ResourceStorage $storageObject)
1650
    {
1651
        $finalUserPermissions = $this->getFilePermissions();
1652
        if (!$this->isAdmin()) {
1653
            $storageFilePermissions = $this->getTSConfig()['permissions.']['file.']['storage.'][$storageObject->getUid() . '.'] ?? [];
1654
            if (!empty($storageFilePermissions)) {
1655
                array_walk(
1656
                    $storageFilePermissions,
1657
                    static function ($value, $permission) use (&$finalUserPermissions) {
1658
                        $finalUserPermissions[$permission] = (bool)$value;
1659
                    }
1660
                );
1661
            }
1662
        }
1663
        return $finalUserPermissions;
1664
    }
1665
1666
    /**
1667
     * Returns a \TYPO3\CMS\Core\Resource\Folder object that is used for uploading
1668
     * files by default.
1669
     * This is used for RTE and its magic images, as well as uploads
1670
     * in the TCEforms fields.
1671
     *
1672
     * The default upload folder for a user is the defaultFolder on the first
1673
     * filestorage/filemount that the user can access and to which files are allowed to be added
1674
     * however, you can set the users' upload folder like this:
1675
     *
1676
     * options.defaultUploadFolder = 3:myfolder/yourfolder/
1677
     *
1678
     * @param int $pid PageUid
1679
     * @param string $table Table name
1680
     * @param string $field Field name
1681
     * @return \TYPO3\CMS\Core\Resource\Folder|bool The default upload folder for this user
1682
     */
1683
    public function getDefaultUploadFolder($pid = null, $table = null, $field = null)
1684
    {
1685
        $uploadFolder = $this->getTSConfig()['options.']['defaultUploadFolder'] ?? '';
1686
        if ($uploadFolder) {
1687
            try {
1688
                $uploadFolder = GeneralUtility::makeInstance(ResourceFactory::class)->getFolderObjectFromCombinedIdentifier($uploadFolder);
1689
            } catch (Exception\FolderDoesNotExistException $e) {
1690
                $uploadFolder = null;
1691
            }
1692
        }
1693
        if (empty($uploadFolder)) {
1694
            foreach ($this->getFileStorages() as $storage) {
1695
                if ($storage->isDefault() && $storage->isWritable()) {
1696
                    try {
1697
                        $uploadFolder = $storage->getDefaultFolder();
1698
                        if ($uploadFolder->checkActionPermission('write')) {
1699
                            break;
1700
                        }
1701
                        $uploadFolder = null;
1702
                    } catch (Exception $folderAccessException) {
1703
                        // If the folder is not accessible (no permissions / does not exist) we skip this one.
1704
                    }
1705
                    break;
1706
                }
1707
            }
1708
            if (!$uploadFolder instanceof Folder) {
1709
                /** @var ResourceStorage $storage */
1710
                foreach ($this->getFileStorages() as $storage) {
1711
                    if ($storage->isWritable()) {
1712
                        try {
1713
                            $uploadFolder = $storage->getDefaultFolder();
1714
                            if ($uploadFolder->checkActionPermission('write')) {
1715
                                break;
1716
                            }
1717
                            $uploadFolder = null;
1718
                        } catch (Exception $folderAccessException) {
1719
                            // If the folder is not accessible (no permissions / does not exist) try the next one.
1720
                        }
1721
                    }
1722
                }
1723
            }
1724
        }
1725
1726
        // HOOK: getDefaultUploadFolder
1727
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getDefaultUploadFolder'] ?? [] as $_funcRef) {
1728
            $_params = [
1729
                'uploadFolder' => $uploadFolder,
1730
                'pid' => $pid,
1731
                'table' => $table,
1732
                'field' => $field,
1733
            ];
1734
            $uploadFolder = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1735
        }
1736
1737
        if ($uploadFolder instanceof Folder) {
1738
            return $uploadFolder;
1739
        }
1740
        return false;
1741
    }
1742
1743
    /**
1744
     * Returns a \TYPO3\CMS\Core\Resource\Folder object that could be used for uploading
1745
     * temporary files in user context. The folder _temp_ below the default upload folder
1746
     * of the user is used.
1747
     *
1748
     * @return \TYPO3\CMS\Core\Resource\Folder|null
1749
     * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::getDefaultUploadFolder()
1750
     */
1751
    public function getDefaultUploadTemporaryFolder()
1752
    {
1753
        $defaultTemporaryFolder = null;
1754
        $defaultFolder = $this->getDefaultUploadFolder();
1755
1756
        if ($defaultFolder !== false) {
1757
            $tempFolderName = '_temp_';
1758
            $createFolder = !$defaultFolder->hasFolder($tempFolderName);
1759
            if ($createFolder === true) {
1760
                try {
1761
                    $defaultTemporaryFolder = $defaultFolder->createFolder($tempFolderName);
1762
                } catch (Exception $folderAccessException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1763
                }
1764
            } else {
1765
                $defaultTemporaryFolder = $defaultFolder->getSubfolder($tempFolderName);
1766
            }
1767
        }
1768
1769
        return $defaultTemporaryFolder;
1770
    }
1771
1772
    /**
1773
     * Initializing workspace.
1774
     * Called from within this function, see fetchGroupData()
1775
     *
1776
     * @see fetchGroupData()
1777
     * @internal should only be used from within TYPO3 Core
1778
     */
1779
    public function workspaceInit()
1780
    {
1781
        // Initializing workspace by evaluating and setting the workspace, possibly updating it in the user record!
1782
        $this->setWorkspace($this->user['workspace_id']);
1783
        // Limiting the DB mountpoints if there any selected in the workspace record
1784
        $this->initializeDbMountpointsInWorkspace();
1785
        $allowed_languages = (string)($this->getTSConfig()['options.']['workspaces.']['allowed_languages.'][$this->workspace] ?? '');
1786
        if ($allowed_languages !== '') {
1787
            $this->groupData['allowed_languages'] = StringUtility::uniqueList($allowed_languages);
1788
        }
1789
    }
1790
1791
    /**
1792
     * Limiting the DB mountpoints if there any selected in the workspace record
1793
     */
1794
    protected function initializeDbMountpointsInWorkspace()
1795
    {
1796
        $dbMountpoints = trim($this->workspaceRec['db_mountpoints'] ?? '');
1797
        if ($this->workspace > 0 && $dbMountpoints != '') {
1798
            $filteredDbMountpoints = [];
1799
            // Notice: We cannot call $this->getPagePermsClause(1);
1800
            // as usual because the group-list is not available at this point.
1801
            // But bypassing is fine because all we want here is check if the
1802
            // workspace mounts are inside the current webmounts rootline.
1803
            // The actual permission checking on page level is done elsewhere
1804
            // as usual anyway before the page tree is rendered.
1805
            $readPerms = '1=1';
1806
            // Traverse mount points of the workspace, add them,
1807
            // but make sure they match against the users' DB mounts
1808
1809
            $workspaceWebMounts = GeneralUtility::intExplode(',', $dbMountpoints);
1810
            $webMountsOfUser = GeneralUtility::intExplode(',', $this->groupData['webmounts']);
1811
            $webMountsOfUser = array_combine($webMountsOfUser, $webMountsOfUser) ?: [];
1812
1813
            $entryPointRootLineUids = [];
1814
            foreach ($webMountsOfUser as $webMountPageId) {
1815
                $rootLine = BackendUtility::BEgetRootLine($webMountPageId, '', true);
1816
                $entryPointRootLineUids[$webMountPageId] = array_map('intval', array_column($rootLine, 'uid'));
1817
            }
1818
            foreach ($entryPointRootLineUids as $webMountOfUser => $uidsOfRootLine) {
1819
                // Remove the DB mounts of the user if the DB mount is not in the list of
1820
                // workspace mounts
1821
                foreach ($workspaceWebMounts as $webmountOfWorkspace) {
1822
                    // This workspace DB mount is somewhere in the rootline of the users' web mount,
1823
                    // so this is "OK" to be included
1824
                    if (in_array($webmountOfWorkspace, $uidsOfRootLine, true)) {
1825
                        continue;
1826
                    }
1827
                    // Remove the user's DB Mount (possible via array_combine, see above)
1828
                    unset($webMountsOfUser[$webMountOfUser]);
1829
                }
1830
            }
1831
            $dbMountpoints = array_merge($workspaceWebMounts, $webMountsOfUser);
1832
            $dbMountpoints = array_unique($dbMountpoints);
1833
            foreach ($dbMountpoints as $mpId) {
1834
                if ($this->isInWebMount($mpId, $readPerms)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->isInWebMount($mpId, $readPerms) of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
Deprecated Code introduced by
The function TYPO3\CMS\Core\Authentic...ication::isInWebMount() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

1834
                if (/** @scrutinizer ignore-deprecated */ $this->isInWebMount($mpId, $readPerms)) {
Loading history...
1835
                    $filteredDbMountpoints[] = $mpId;
1836
                }
1837
            }
1838
            // Re-insert webmounts
1839
            $this->groupData['webmounts'] = implode(',', $filteredDbMountpoints);
1840
        }
1841
    }
1842
1843
    /**
1844
     * Checking if a workspace is allowed for backend user
1845
     *
1846
     * @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)
1847
     * @param string $fields List of fields to select. Default fields are all
1848
     * @return array Output will also show how access was granted. Admin users will have a true output regardless of input.
1849
     * @internal should only be used from within TYPO3 Core
1850
     */
1851
    public function checkWorkspace($wsRec, $fields = '*')
1852
    {
1853
        $retVal = false;
1854
        // If not array, look up workspace record:
1855
        if (!is_array($wsRec)) {
1856
            switch ((string)$wsRec) {
1857
                case '0':
1858
                    $wsRec = ['uid' => $wsRec];
1859
                    break;
1860
                default:
1861
                    if (ExtensionManagementUtility::isLoaded('workspaces')) {
1862
                        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
1863
                        $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
1864
                        $wsRec = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields))
1865
                            ->from('sys_workspace')
1866
                            ->where($queryBuilder->expr()->eq(
1867
                                'uid',
1868
                                $queryBuilder->createNamedParameter($wsRec, \PDO::PARAM_INT)
1869
                            ))
1870
                            ->orderBy('title')
1871
                            ->setMaxResults(1)
1872
                            ->execute()
1873
                            ->fetchAssociative();
1874
                    }
1875
            }
1876
        }
1877
        // If wsRec is set to an array, evaluate it:
1878
        if (is_array($wsRec)) {
1879
            if ($this->isAdmin()) {
1880
                return array_merge($wsRec, ['_ACCESS' => 'admin']);
1881
            }
1882
            switch ((string)$wsRec['uid']) {
1883
                    case '0':
1884
                        $retVal = (($this->groupData['workspace_perms'] ?? 0) & 1)
1885
                            ? array_merge($wsRec, ['_ACCESS' => 'online'])
1886
                            : false;
1887
                        break;
1888
                    default:
1889
                        // Checking if the guy is admin:
1890
                        if (GeneralUtility::inList($wsRec['adminusers'], 'be_users_' . $this->user['uid'])) {
1891
                            return array_merge($wsRec, ['_ACCESS' => 'owner']);
1892
                        }
1893
                        // Checking if he is owner through a user group of his:
1894
                        foreach ($this->userGroupsUID as $groupUid) {
1895
                            if (GeneralUtility::inList($wsRec['adminusers'], 'be_groups_' . $groupUid)) {
1896
                                return array_merge($wsRec, ['_ACCESS' => 'owner']);
1897
                            }
1898
                        }
1899
                        // Checking if he is member as user:
1900
                        if (GeneralUtility::inList($wsRec['members'], 'be_users_' . $this->user['uid'])) {
1901
                            return array_merge($wsRec, ['_ACCESS' => 'member']);
1902
                        }
1903
                        // Checking if he is member through a user group of his:
1904
                        foreach ($this->userGroupsUID as $groupUid) {
1905
                            if (GeneralUtility::inList($wsRec['members'], 'be_groups_' . $groupUid)) {
1906
                                return array_merge($wsRec, ['_ACCESS' => 'member']);
1907
                            }
1908
                        }
1909
                }
1910
        }
1911
        return $retVal;
1912
    }
1913
1914
    /**
1915
     * Uses checkWorkspace() to check if current workspace is available for user.
1916
     * This function caches the result and so can be called many times with no performance loss.
1917
     *
1918
     * @return array See checkWorkspace()
1919
     * @see checkWorkspace()
1920
     * @internal should only be used from within TYPO3 Core
1921
     */
1922
    public function checkWorkspaceCurrent()
1923
    {
1924
        if (!isset($this->checkWorkspaceCurrent_cache)) {
1925
            $this->checkWorkspaceCurrent_cache = $this->checkWorkspace($this->workspace);
1926
        }
1927
        return $this->checkWorkspaceCurrent_cache;
1928
    }
1929
1930
    /**
1931
     * Setting workspace ID
1932
     *
1933
     * @param int $workspaceId ID of workspace to set for backend user. If not valid the default workspace for BE user is found and set.
1934
     * @internal should only be used from within TYPO3 Core
1935
     */
1936
    public function setWorkspace($workspaceId)
1937
    {
1938
        // Check workspace validity and if not found, revert to default workspace.
1939
        if (!$this->setTemporaryWorkspace($workspaceId)) {
1940
            $this->setDefaultWorkspace();
1941
        }
1942
        // Unset access cache:
1943
        $this->checkWorkspaceCurrent_cache = null;
1944
        // If ID is different from the stored one, change it:
1945
        if ((int)$this->workspace !== (int)$this->user['workspace_id']) {
1946
            $this->user['workspace_id'] = $this->workspace;
1947
            GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users')->update(
1948
                'be_users',
1949
                ['workspace_id' => $this->user['workspace_id']],
1950
                ['uid' => (int)$this->user['uid']]
1951
            );
1952
            $this->writelog(SystemLogType::EXTENSION, SystemLogGenericAction::UNDEFINED, SystemLogErrorClassification::MESSAGE, 0, 'User changed workspace to "' . $this->workspace . '"', []);
1953
        }
1954
    }
1955
1956
    /**
1957
     * Sets a temporary workspace in the context of the current backend user.
1958
     *
1959
     * @param int $workspaceId
1960
     * @return bool
1961
     * @internal should only be used from within TYPO3 Core
1962
     */
1963
    public function setTemporaryWorkspace($workspaceId)
1964
    {
1965
        $result = false;
1966
        $workspaceRecord = $this->checkWorkspace($workspaceId);
1967
1968
        if ($workspaceRecord) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $workspaceRecord of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1969
            $this->workspaceRec = $workspaceRecord;
1970
            $this->workspace = (int)$workspaceId;
1971
            $result = true;
1972
        }
1973
1974
        return $result;
1975
    }
1976
1977
    /**
1978
     * Sets the default workspace in the context of the current backend user.
1979
     * @internal should only be used from within TYPO3 Core
1980
     */
1981
    public function setDefaultWorkspace()
1982
    {
1983
        $this->workspace = (int)$this->getDefaultWorkspace();
1984
        $this->workspaceRec = $this->checkWorkspace($this->workspace);
1985
    }
1986
1987
    /**
1988
     * Return default workspace ID for user,
1989
     * if EXT:workspaces is not installed the user will be pushed to the
1990
     * Live workspace, if he has access to. If no workspace is available for the user, the workspace ID is set to "-99"
1991
     *
1992
     * @return int Default workspace id.
1993
     * @internal should only be used from within TYPO3 Core
1994
     */
1995
    public function getDefaultWorkspace()
1996
    {
1997
        if (!ExtensionManagementUtility::isLoaded('workspaces')) {
1998
            return 0;
1999
        }
2000
        // Online is default
2001
        if ($this->checkWorkspace(0)) {
2002
            return 0;
2003
        }
2004
        // Otherwise -99 is the fallback
2005
        $defaultWorkspace = -99;
2006
        // Traverse all workspaces
2007
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
2008
        $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
2009
        $result = $queryBuilder->select('*')
2010
            ->from('sys_workspace')
2011
            ->orderBy('title')
2012
            ->execute();
2013
        while ($workspaceRecord = $result->fetchAssociative()) {
2014
            if ($this->checkWorkspace($workspaceRecord)) {
2015
                $defaultWorkspace = (int)$workspaceRecord['uid'];
2016
                break;
2017
            }
2018
        }
2019
        return $defaultWorkspace;
2020
    }
2021
2022
    /**
2023
     * Writes an entry in the logfile/table
2024
     * Documentation in "TYPO3 Core API"
2025
     *
2026
     * @param int $type Denotes which module that has submitted the entry. See "TYPO3 Core API". Use "4" for extensions.
2027
     * @param int $action Denotes which specific operation that wrote the entry. Use "0" when no sub-categorizing applies
2028
     * @param int $error Flag. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin)
2029
     * @param int $details_nr The message number. Specific for each $type and $action. This will make it possible to translate errormessages to other languages
2030
     * @param string $details Default text that follows the message (in english!). Possibly translated by identification through type/action/details_nr
2031
     * @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
2032
     * @param string $tablename Table name. Special field used by tce_main.php.
2033
     * @param int|string $recuid Record UID. Special field used by tce_main.php.
2034
     * @param int|string $recpid Record PID. Special field used by tce_main.php. OBSOLETE
2035
     * @param int $event_pid The page_uid (pid) where the event occurred. Used to select log-content for specific pages.
2036
     * @param string $NEWid Special field used by tce_main.php. NEWid string of newly created records.
2037
     * @param int $userId Alternative Backend User ID (used for logging login actions where this is not yet known).
2038
     * @return int Log entry ID.
2039
     */
2040
    public function writelog($type, $action, $error, $details_nr, $details, $data, $tablename = '', $recuid = '', $recpid = '', $event_pid = -1, $NEWid = '', $userId = 0)
2041
    {
2042
        if (!$userId && !empty($this->user['uid'])) {
2043
            $userId = $this->user['uid'];
2044
        }
2045
2046
        if ($backuserid = $this->getOriginalUserIdWhenInSwitchUserMode()) {
2047
            if (empty($data)) {
2048
                $data = [];
2049
            }
2050
            $data['originalUser'] = $backuserid;
2051
        }
2052
2053
        // @todo Remove this once this method is properly typed.
2054
        $type = (int)$type;
2055
2056
        $fields = [
2057
            'userid' => (int)$userId,
2058
            'type' => $type,
2059
            'channel' => Type::toChannel($type),
2060
            'level' => Type::toLevel($type),
2061
            'action' => (int)$action,
2062
            'error' => (int)$error,
2063
            'details_nr' => (int)$details_nr,
2064
            'details' => $details,
2065
            'log_data' => serialize($data),
2066
            'tablename' => $tablename,
2067
            'recuid' => (int)$recuid,
2068
            'IP' => (string)GeneralUtility::getIndpEnv('REMOTE_ADDR'),
2069
            'tstamp' => $GLOBALS['EXEC_TIME'] ?? time(),
2070
            'event_pid' => (int)$event_pid,
2071
            'NEWid' => $NEWid,
2072
            'workspace' => $this->workspace,
2073
        ];
2074
2075
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_log');
2076
        $connection->insert(
2077
            'sys_log',
2078
            $fields,
2079
            [
2080
                \PDO::PARAM_INT,
2081
                \PDO::PARAM_INT,
2082
                \PDO::PARAM_STR,
2083
                \PDO::PARAM_STR,
2084
                \PDO::PARAM_INT,
2085
                \PDO::PARAM_INT,
2086
                \PDO::PARAM_INT,
2087
                \PDO::PARAM_STR,
2088
                \PDO::PARAM_STR,
2089
                \PDO::PARAM_STR,
2090
                \PDO::PARAM_INT,
2091
                \PDO::PARAM_STR,
2092
                \PDO::PARAM_INT,
2093
                \PDO::PARAM_INT,
2094
                \PDO::PARAM_STR,
2095
                \PDO::PARAM_STR,
2096
            ]
2097
        );
2098
2099
        return (int)$connection->lastInsertId('sys_log');
2100
    }
2101
2102
    /**
2103
     * Getter for the cookie name
2104
     *
2105
     * @static
2106
     * @return string returns the configured cookie name
2107
     */
2108
    public static function getCookieName()
2109
    {
2110
        $configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']);
2111
        if (empty($configuredCookieName)) {
2112
            $configuredCookieName = 'be_typo_user';
2113
        }
2114
        return $configuredCookieName;
2115
    }
2116
2117
    /**
2118
     * Check if user is logged in and if so, call ->fetchGroupData() to load group information and
2119
     * access lists of all kind, further check IP, set the ->uc array.
2120
     * If no user is logged in the default behaviour is to exit with an error message.
2121
     * This function is called right after ->start() in fx. the TYPO3 Bootstrap.
2122
     *
2123
     * @param bool|null $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.
2124
     * @throws \RuntimeException
2125
     * @todo deprecate
2126
     */
2127
    public function backendCheckLogin($proceedIfNoUserIsLoggedIn = null)
2128
    {
2129
        if (empty($this->user['uid'])) {
2130
            if ($proceedIfNoUserIsLoggedIn === null) {
2131
                $proceedIfNoUserIsLoggedIn = false;
2132
            } else {
2133
                trigger_error('Calling $BE_USER->backendCheckLogin() with a first input argument will not work anymore in TYPO3 v12.0.', E_USER_DEPRECATED);
2134
            }
2135
            // @todo: throw a proper AccessDeniedException in TYPO3 v12.0. and handle this functionality in the calling code
2136
            if ($proceedIfNoUserIsLoggedIn === false) {
2137
                $url = $GLOBALS['TYPO3_REQUEST']->getAttribute('normalizedParams')->getSiteUrl() . TYPO3_mainDir;
2138
                throw new ImmediateResponseException(new RedirectResponse($url, 303), 1607271747);
2139
            }
2140
        } elseif ($this->isUserAllowedToLogin()) {
2141
            $this->initializeBackendLogin();
2142
        } else {
2143
            // @todo: throw a proper AccessDeniedException in TYPO3 v12.0.
2144
            throw new \RuntimeException('Login Error: TYPO3 is in maintenance mode at the moment. Only administrators are allowed access.', 1294585860);
2145
        }
2146
    }
2147
2148
    /**
2149
     * @internal
2150
     */
2151
    public function initializeBackendLogin(): void
2152
    {
2153
        // The groups are fetched and ready for permission checking in this initialization.
2154
        // Tables.php must be read before this because stuff like the modules has impact in this
2155
        $this->fetchGroupData();
2156
        // Setting the UC array. It's needed with fetchGroupData first, due to default/overriding of values.
2157
        $this->backendSetUC();
2158
        if ($this->loginSessionStarted) {
2159
            // Also, if there is a recovery link set, unset it now
2160
            // this will be moved into its own Event at a later stage.
2161
            // If a token was set previously, this is now unset, as it was now possible to log-in
2162
            if ($this->user['password_reset_token'] ?? '') {
2163
                GeneralUtility::makeInstance(ConnectionPool::class)
2164
                    ->getConnectionForTable($this->user_table)
2165
                    ->update($this->user_table, ['password_reset_token' => ''], ['uid' => $this->user['uid']]);
2166
            }
2167
            // Process hooks
2168
            $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['backendUserLogin'];
2169
            foreach ($hooks ?? [] as $_funcRef) {
2170
                $_params = ['user' => $this->user];
2171
                GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2172
            }
2173
        }
2174
    }
2175
2176
    /**
2177
     * Initialize the internal ->uc array for the backend user
2178
     * Will make the overrides if necessary, and write the UC back to the be_users record if changes has happened
2179
     *
2180
     * @internal
2181
     */
2182
    public function backendSetUC()
2183
    {
2184
        // UC - user configuration is a serialized array inside the user object
2185
        // If there is a saved uc we implement that instead of the default one.
2186
        $this->unpack_uc();
0 ignored issues
show
Deprecated Code introduced by
The function TYPO3\CMS\Core\Authentic...entication::unpack_uc() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2186
        /** @scrutinizer ignore-deprecated */ $this->unpack_uc();
Loading history...
2187
        // Setting defaults if uc is empty
2188
        $updated = false;
2189
        if (!is_array($this->uc)) {
2190
            $this->uc = array_merge(
2191
                $this->uc_default,
2192
                (array)$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUC'],
2193
                GeneralUtility::removeDotsFromTS((array)($this->getTSConfig()['setup.']['default.'] ?? []))
2194
            );
2195
            $this->overrideUC();
2196
            $updated = true;
2197
        }
2198
        // If TSconfig is updated, update the defaultUC.
2199
        if ($this->userTSUpdated) {
2200
            $this->overrideUC();
2201
            $updated = true;
2202
        }
2203
        // Setting default lang from be_user record, also update for backwards-compatibility
2204
        // @deprecated This will be removed in TYPO3 v12
2205
        if (!isset($this->uc['lang']) || $this->uc['lang'] !== $this->user['lang']) {
2206
            $this->uc['lang'] = $this->user['lang'];
2207
            $updated = true;
2208
        }
2209
        // Setting the time of the first login:
2210
        if (!isset($this->uc['firstLoginTimeStamp'])) {
2211
            $this->uc['firstLoginTimeStamp'] = $GLOBALS['EXEC_TIME'];
2212
            $updated = true;
2213
        }
2214
        // Saving if updated.
2215
        if ($updated) {
2216
            $this->writeUC();
0 ignored issues
show
Deprecated Code introduced by
The function TYPO3\CMS\Core\Authentic...thentication::writeUC() has been deprecated. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

2216
            /** @scrutinizer ignore-deprecated */ $this->writeUC();
Loading history...
2217
        }
2218
    }
2219
2220
    /**
2221
     * Override: Call this function every time the uc is updated.
2222
     * That is 1) by reverting to default values, 2) in the setup-module, 3) userTS changes (userauthgroup)
2223
     *
2224
     * @internal
2225
     */
2226
    public function overrideUC()
2227
    {
2228
        $this->uc = array_merge((array)$this->uc, (array)($this->getTSConfig()['setup.']['override.'] ?? []));
2229
    }
2230
2231
    /**
2232
     * Clears the user[uc] and ->uc to blank strings. Then calls ->backendSetUC() to fill it again with reset contents
2233
     *
2234
     * @internal
2235
     */
2236
    public function resetUC()
2237
    {
2238
        $this->user['uc'] = '';
2239
        $this->uc = '';
2240
        $this->backendSetUC();
2241
    }
2242
2243
    /**
2244
     * Determines whether a backend user is allowed to access the backend.
2245
     *
2246
     * The conditions are:
2247
     * + backend user is a regular user and adminOnly is not defined
2248
     * + backend user is an admin user
2249
     * + backend user is used in CLI context and adminOnly is explicitly set to "2" (see CommandLineUserAuthentication)
2250
     * + backend user is being controlled by an admin user
2251
     *
2252
     * @return bool Whether a backend user is allowed to access the backend
2253
     * @internal
2254
     */
2255
    public function isUserAllowedToLogin()
2256
    {
2257
        $isUserAllowedToLogin = false;
2258
        $adminOnlyMode = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'];
2259
        // Backend user is allowed if adminOnly is not set or user is an admin:
2260
        if (!$adminOnlyMode || $this->isAdmin()) {
2261
            $isUserAllowedToLogin = true;
2262
        } elseif ($backUserId = $this->getOriginalUserIdWhenInSwitchUserMode()) {
2263
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
2264
            $isUserAllowedToLogin = (bool)$queryBuilder->count('uid')
2265
                ->from('be_users')
2266
                ->where(
2267
                    $queryBuilder->expr()->eq(
2268
                        'uid',
2269
                        $queryBuilder->createNamedParameter($backUserId, \PDO::PARAM_INT)
2270
                    ),
2271
                    $queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT))
2272
                )
2273
                ->execute()
2274
                ->fetchOne();
2275
        }
2276
        return $isUserAllowedToLogin;
2277
    }
2278
2279
    /**
2280
     * Logs out the current user and clears the form protection tokens.
2281
     */
2282
    public function logoff()
2283
    {
2284
        if (isset($GLOBALS['BE_USER'])
2285
            && $GLOBALS['BE_USER'] instanceof self
2286
            && isset($GLOBALS['BE_USER']->user['uid'])
2287
        ) {
2288
            FormProtectionFactory::get()->clean();
2289
            // Release the locked records
2290
            $this->releaseLockedRecords((int)$GLOBALS['BE_USER']->user['uid']);
2291
2292
            if ($this->isSystemMaintainer()) {
2293
                // If user is system maintainer, destroy its possibly valid install tool session.
2294
                $session = new SessionService();
2295
                $session->destroySession();
2296
            }
2297
        }
2298
        parent::logoff();
2299
    }
2300
2301
    /**
2302
     * Remove any "locked records" added for editing for the given user (= current backend user)
2303
     * @param int $userId
2304
     */
2305
    protected function releaseLockedRecords(int $userId)
2306
    {
2307
        if ($userId > 0) {
2308
            GeneralUtility::makeInstance(ConnectionPool::class)
2309
                ->getConnectionForTable('sys_lockedrecords')
2310
                ->delete(
2311
                    'sys_lockedrecords',
2312
                    ['userid' => $userId]
2313
                );
2314
        }
2315
    }
2316
2317
    /**
2318
     * Returns the uid of the backend user to return to.
2319
     * This is set when the current session is a "switch-user" session.
2320
     *
2321
     * @return int|null The user id
2322
     * @internal should only be used from within TYPO3 Core
2323
     */
2324
    public function getOriginalUserIdWhenInSwitchUserMode(): ?int
2325
    {
2326
        $originalUserId = $this->getSessionData('backuserid');
2327
        return $originalUserId ? (int)$originalUserId : null;
2328
    }
2329
2330
    /**
2331
     * @internal
2332
     */
2333
    protected function evaluateMfaRequirements(): void
2334
    {
2335
        // In case the current session is a "switch-user" session, MFA is not required
2336
        if ($this->getOriginalUserIdWhenInSwitchUserMode() !== null) {
2337
            $this->logger->debug('MFA is skipped in switch user mode', [
0 ignored issues
show
Bug introduced by
The method debug() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

2337
            $this->logger->/** @scrutinizer ignore-call */ 
2338
                           debug('MFA is skipped in switch user mode', [

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2338
                $this->userid_column => $this->user[$this->userid_column],
2339
                $this->username_column => $this->user[$this->username_column],
2340
            ]);
2341
            return;
2342
        }
2343
        parent::evaluateMfaRequirements();
2344
    }
2345
2346
    /**
2347
     * Evaluate whether the user is required to set up MFA, based on user TSconfig and global configuration
2348
     *
2349
     * @return bool
2350
     * @internal
2351
     */
2352
    public function isMfaSetupRequired(): bool
2353
    {
2354
        $authConfig = $this->getTSConfig()['auth.']['mfa.'] ?? [];
2355
2356
        if (isset($authConfig['required'])) {
2357
            // user TSconfig overrules global configuration
2358
            return (bool)$authConfig['required'];
2359
        }
2360
2361
        $globalConfig = (int)($GLOBALS['TYPO3_CONF_VARS']['BE']['requireMfa'] ?? 0);
2362
        if ($globalConfig <= 1) {
2363
            // 0 and 1 can directly be used by type-casting to boolean
2364
            return (bool)$globalConfig;
2365
        }
2366
2367
        // check the system maintainer / admin / non-admin options
2368
        $isAdmin = $this->isAdmin();
2369
        return ($globalConfig === 2 && !$isAdmin)
2370
            || ($globalConfig === 3 && $isAdmin)
2371
            || ($globalConfig === 4 && $this->isSystemMaintainer());
2372
    }
2373
}
2374