BackendUserAuthentication::writelog()   B
last analyzed

Complexity

Conditions 5
Paths 6

Size

Total Lines 60
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 45
dl 0
loc 60
rs 8.8888
c 0
b 0
f 0
cc 5
nc 6
nop 12

How to fix   Long Method    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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