Completed
Push — master ( 6a91d9...e837e5 )
by
unknown
220:07 queued 201:54
created

recordEditAccessInternals()   D

Complexity

Conditions 28
Paths 118

Size

Total Lines 91
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 28
eloc 56
nc 118
nop 5
dl 0
loc 91
rs 4.0166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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:

1
<?php
2
namespace TYPO3\CMS\Core\Authentication;
3
4
/*
5
 * This file is part of the TYPO3 CMS project.
6
 *
7
 * It is free software; you can redistribute it and/or modify it under
8
 * the terms of the GNU General Public License, either version 2
9
 * of the License, or any later version.
10
 *
11
 * For the full copyright and license information, please read the
12
 * LICENSE.txt file that was distributed with this source code.
13
 *
14
 * The TYPO3 project - inspiring people to share!
15
 */
16
17
use Doctrine\DBAL\Driver\Statement;
18
use Psr\Http\Message\ServerRequestInterface;
19
use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
20
use TYPO3\CMS\Backend\Utility\BackendUtility;
21
use TYPO3\CMS\Core\Cache\CacheManager;
22
use TYPO3\CMS\Core\Core\Environment;
23
use TYPO3\CMS\Core\Database\Connection;
24
use TYPO3\CMS\Core\Database\ConnectionPool;
25
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
26
use TYPO3\CMS\Core\Database\Query\QueryHelper;
27
use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
28
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
29
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
30
use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction;
31
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
32
use TYPO3\CMS\Core\Mail\FluidEmail;
33
use TYPO3\CMS\Core\Mail\Mailer;
34
use TYPO3\CMS\Core\Resource\ResourceFactory;
35
use TYPO3\CMS\Core\Resource\ResourceStorage;
36
use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction;
37
use TYPO3\CMS\Core\SysLog\Action\Login as SystemLogLoginAction;
38
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
39
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
40
use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
41
use TYPO3\CMS\Core\Type\Bitmask\Permission;
42
use TYPO3\CMS\Core\Type\Exception\InvalidEnumerationValueException;
43
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
44
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
45
use TYPO3\CMS\Core\Utility\GeneralUtility;
46
use TYPO3\CMS\Install\Service\SessionService;
47
48
/**
49
 * TYPO3 backend user authentication
50
 * Contains most of the functions used for checking permissions, authenticating users,
51
 * setting up the user, and API for user from outside.
52
 * This class contains the configuration of the database fields used plus some
53
 * functions for the authentication process of backend users.
54
 */
55
class BackendUserAuthentication extends AbstractUserAuthentication
56
{
57
    public const ROLE_SYSTEMMAINTAINER = 'systemMaintainer';
58
59
    /**
60
     * Should be set to the usergroup-column (id-list) in the user-record
61
     * @var string
62
     */
63
    public $usergroup_column = 'usergroup';
64
65
    /**
66
     * The name of the group-table
67
     * @var string
68
     */
69
    public $usergroup_table = 'be_groups';
70
71
    /**
72
     * holds lists of eg. tables, fields and other values related to the permission-system. See fetchGroupData
73
     * @var array
74
     * @internal
75
     */
76
    public $groupData = [
77
        'filemounts' => []
78
    ];
79
80
    /**
81
     * This array will hold the groups that the user is a member of
82
     * @var array
83
     */
84
    public $userGroups = [];
85
86
    /**
87
     * This array holds the uid's of the groups in the listed order
88
     * @var array
89
     */
90
    public $userGroupsUID = [];
91
92
    /**
93
     * This is $this->userGroupsUID imploded to a comma list... Will correspond to the 'usergroup_cached_list'
94
     * @var string
95
     */
96
    public $groupList = '';
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
     * Used to accumulate data for the user-group.
115
     * DON NOT USE THIS EXTERNALLY!
116
     * Use $this->groupData instead
117
     * @var array
118
     * @internal
119
     */
120
    public $dataLists = [
121
        'webmount_list' => '',
122
        'filemount_list' => '',
123
        'file_permissions' => '',
124
        'modList' => '',
125
        'tables_select' => '',
126
        'tables_modify' => '',
127
        'pagetypes_select' => '',
128
        'non_exclude_fields' => '',
129
        'explicit_allowdeny' => '',
130
        'allowed_languages' => '',
131
        'workspace_perms' => '',
132
        'available_widgets' => '',
133
        'custom_options' => ''
134
    ];
135
136
    /**
137
     * List of group_id's in the order they are processed.
138
     * @var array
139
     */
140
    public $includeGroupArray = [];
141
142
    /**
143
     * @var array Parsed user TSconfig
144
     */
145
    protected $userTS = [];
146
147
    /**
148
     * @var bool True if the user TSconfig was parsed and needs to be cached.
149
     */
150
    protected $userTSUpdated = false;
151
152
    /**
153
     * Contains last error message
154
     * @var string
155
     */
156
    public $errorMsg = '';
157
158
    /**
159
     * Cache for checkWorkspaceCurrent()
160
     * @var array|null
161
     */
162
    protected $checkWorkspaceCurrent_cache;
163
164
    /**
165
     * @var \TYPO3\CMS\Core\Resource\ResourceStorage[]
166
     */
167
    protected $fileStorages;
168
169
    /**
170
     * @var array
171
     */
172
    protected $filePermissions;
173
174
    /**
175
     * Table in database with user data
176
     * @var string
177
     */
178
    public $user_table = 'be_users';
179
180
    /**
181
     * Column for login-name
182
     * @var string
183
     */
184
    public $username_column = 'username';
185
186
    /**
187
     * Column for password
188
     * @var string
189
     */
190
    public $userident_column = 'password';
191
192
    /**
193
     * Column for user-id
194
     * @var string
195
     */
196
    public $userid_column = 'uid';
197
198
    /**
199
     * @var string
200
     */
201
    public $lastLogin_column = 'lastlogin';
202
203
    /**
204
     * @var array
205
     */
206
    public $enablecolumns = [
207
        'rootLevel' => 1,
208
        'deleted' => 'deleted',
209
        'disabled' => 'disable',
210
        'starttime' => 'starttime',
211
        'endtime' => 'endtime'
212
    ];
213
214
    /**
215
     * Form field with login-name
216
     * @var string
217
     */
218
    public $formfield_uname = 'username';
219
220
    /**
221
     * Form field with password
222
     * @var string
223
     */
224
    public $formfield_uident = 'userident';
225
226
    /**
227
     * Form field with status: *'login', 'logout'
228
     * @var string
229
     */
230
    public $formfield_status = 'login_status';
231
232
    /**
233
     * Decides if the writelog() function is called at login and logout
234
     * @var bool
235
     */
236
    public $writeStdLog = true;
237
238
    /**
239
     * If the writelog() functions is called if a login-attempt has be tried without success
240
     * @var bool
241
     */
242
    public $writeAttemptLog = true;
243
244
    /**
245
     * Session timeout (on the server), defaults to 8 hours for backend user
246
     *
247
     * If >0: session-timeout in seconds.
248
     * If <=0: Instant logout after login.
249
     * The value must be at least 180 to avoid side effects.
250
     *
251
     * @var int
252
     */
253
    public $sessionTimeout = 28800;
254
255
    /**
256
     * @var int
257
     */
258
    public $firstMainGroup = 0;
259
260
    /**
261
     * User Config
262
     * @var array
263
     */
264
    public $uc;
265
266
    /**
267
     * User Config Default values:
268
     * The array may contain other fields for configuration.
269
     * For this, see "setup" extension and "TSconfig" document (User TSconfig, "setup.[xxx]....")
270
     * Reserved keys for other storage of session data:
271
     * moduleData
272
     * moduleSessionID
273
     * @var array
274
     */
275
    public $uc_default = [
276
        'interfaceSetup' => '',
277
        // serialized content that is used to store interface pane and menu positions. Set by the logout.php-script
278
        'moduleData' => [],
279
        // user-data for the modules
280
        'thumbnailsByDefault' => 1,
281
        'emailMeAtLogin' => 0,
282
        'startModule' => 'help_AboutAbout',
283
        'titleLen' => 50,
284
        'edit_RTE' => '1',
285
        'edit_docModuleUpload' => '1',
286
        'resizeTextareas' => 1,
287
        'resizeTextareas_MaxHeight' => 500,
288
        'resizeTextareas_Flexible' => 0
289
    ];
290
291
    /**
292
     * Login type, used for services.
293
     * @var string
294
     */
295
    public $loginType = 'BE';
296
297
    /**
298
     * Constructor
299
     */
300
    public function __construct()
301
    {
302
        $this->name = self::getCookieName();
303
        $this->warningEmail = $GLOBALS['TYPO3_CONF_VARS']['BE']['warning_email_addr'];
304
        $this->sessionTimeout = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['sessionTimeout'];
305
        parent::__construct();
306
    }
307
308
    /**
309
     * Returns TRUE if user is admin
310
     * Basically this function evaluates if the ->user[admin] field has bit 0 set. If so, user is admin.
311
     *
312
     * @return bool
313
     */
314
    public function isAdmin()
315
    {
316
        return is_array($this->user) && ($this->user['admin'] & 1) == 1;
317
    }
318
319
    /**
320
     * Returns TRUE if the current user is a member of group $groupId
321
     * $groupId must be set. $this->groupList must contain groups
322
     * Will return TRUE also if the user is a member of a group through subgroups.
323
     *
324
     * @param int $groupId Group ID to look for in $this->groupList
325
     * @return bool
326
     */
327
    public function isMemberOfGroup($groupId)
328
    {
329
        $groupId = (int)$groupId;
330
        if ($this->groupList && $groupId) {
331
            return GeneralUtility::inList($this->groupList, $groupId);
332
        }
333
        return false;
334
    }
335
336
    /**
337
     * Checks if the permissions is granted based on a page-record ($row) and $perms (binary and'ed)
338
     *
339
     * Bits for permissions, see $perms variable:
340
     *
341
     * 1  - Show:             See/Copy page and the pagecontent.
342
     * 2  - Edit page:        Change/Move the page, eg. change title, startdate, hidden.
343
     * 4  - Delete page:      Delete the page and pagecontent.
344
     * 8  - New pages:        Create new pages under the page.
345
     * 16 - Edit pagecontent: Change/Add/Delete/Move pagecontent.
346
     *
347
     * @param array $row Is the pagerow for which the permissions is checked
348
     * @param int $perms Is the binary representation of the permission we are going to check. Every bit in this number represents a permission that must be set. See function explanation.
349
     * @return bool
350
     */
351
    public function doesUserHaveAccess($row, $perms)
352
    {
353
        $userPerms = $this->calcPerms($row);
354
        return ($userPerms & $perms) == $perms;
355
    }
356
357
    /**
358
     * Checks if the page id or page record ($idOrRow) is found within the webmounts set up for the user.
359
     * This should ALWAYS be checked for any page id a user works with, whether it's about reading, writing or whatever.
360
     * The point is that this will add the security that a user can NEVER touch parts outside his mounted
361
     * pages in the page tree. This is otherwise possible if the raw page permissions allows for it.
362
     * So this security check just makes it easier to make safe user configurations.
363
     * If the user is admin OR if this feature is disabled
364
     * (fx. by setting TYPO3_CONF_VARS['BE']['lockBeUserToDBmounts']=0) then it returns "1" right away
365
     * Otherwise the function will return the uid of the webmount which was first found in the rootline of the input page $id
366
     *
367
     * @param int|array $idOrRow Page ID or full page record to check
368
     * @param string $readPerms Content of "->getPagePermsClause(1)" (read-permissions). If not set, they will be internally calculated (but if you have the correct value right away you can save that database lookup!)
369
     * @param bool|int $exitOnError If set, then the function will exit with an error message.
370
     * @throws \RuntimeException
371
     * @return int|null The page UID of a page in the rootline that matched a mount point
372
     */
373
    public function isInWebMount($idOrRow, $readPerms = '', $exitOnError = 0)
374
    {
375
        if (!$GLOBALS['TYPO3_CONF_VARS']['BE']['lockBeUserToDBmounts'] || $this->isAdmin()) {
376
            return 1;
377
        }
378
        $fetchPageFromDatabase = true;
379
        if (is_array($idOrRow)) {
380
            if (empty($idOrRow['uid'])) {
381
                throw new \RuntimeException('The given page record is invalid. Missing uid.', 1578950324);
382
            }
383
            $checkRec = $idOrRow;
384
            $id = (int)$idOrRow['uid'];
385
            // ensure the required fields are present on the record
386
            if (isset($checkRec['t3ver_oid'], $checkRec[$GLOBALS['TCA']['pages']['ctrl']['languageField']], $checkRec[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']])) {
387
                $fetchPageFromDatabase = false;
388
            }
389
        } else {
390
            $id = (int)$idOrRow;
391
        }
392
        if ($fetchPageFromDatabase) {
393
            // Check if input id is an offline version page in which case we will map id to the online version:
394
            $checkRec = BackendUtility::getRecord(
395
                'pages',
396
                $id,
397
                't3ver_oid,'
398
                . $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'] . ','
399
                . $GLOBALS['TCA']['pages']['ctrl']['languageField']
400
            );
401
        }
402
        if ($checkRec['t3ver_oid'] > 0) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $checkRec does not seem to be defined for all execution paths leading up to this point.
Loading history...
403
            $id = (int)$checkRec['t3ver_oid'];
404
        }
405
        // if current rec is a translation then get uid from l10n_parent instead
406
        // because web mounts point to pages in default language and rootline returns uids of default languages
407
        if ((int)$checkRec[$GLOBALS['TCA']['pages']['ctrl']['languageField']] !== 0 && (int)$checkRec[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] !== 0) {
408
            $id = (int)$checkRec[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']];
409
        }
410
        if (!$readPerms) {
411
            $readPerms = $this->getPagePermsClause(Permission::PAGE_SHOW);
412
        }
413
        if ($id > 0) {
414
            $wM = $this->returnWebmounts();
415
            $rL = BackendUtility::BEgetRootLine($id, ' AND ' . $readPerms);
416
            foreach ($rL as $v) {
417
                if ($v['uid'] && in_array($v['uid'], $wM)) {
418
                    return $v['uid'];
419
                }
420
            }
421
        }
422
        if ($exitOnError) {
423
            throw new \RuntimeException('Access Error: This page is not within your DB-mounts', 1294586445);
424
        }
425
        return null;
426
    }
427
428
    /**
429
     * Checks access to a backend module with the $MCONF passed as first argument
430
     *
431
     * @param array $conf $MCONF array of a backend module!
432
     * @throws \RuntimeException
433
     * @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
434
     */
435
    public function modAccess($conf)
436
    {
437
        if (!BackendUtility::isModuleSetInTBE_MODULES($conf['name'])) {
438
            throw new \RuntimeException('Fatal Error: This module "' . $conf['name'] . '" is not enabled in TBE_MODULES', 1294586446);
439
        }
440
        // Workspaces check:
441
        if (
442
            !empty($conf['workspaces'])
443
            && ExtensionManagementUtility::isLoaded('workspaces')
444
            && ($this->workspace !== 0 || !GeneralUtility::inList($conf['workspaces'], 'online'))
445
            && ($this->workspace !== -1 || !GeneralUtility::inList($conf['workspaces'], 'offline'))
446
            && ($this->workspace <= 0 || !GeneralUtility::inList($conf['workspaces'], 'custom'))
447
        ) {
448
            throw new \RuntimeException('Workspace Error: This module "' . $conf['name'] . '" is not available under the current workspace', 1294586447);
449
        }
450
        // Returns false if conf[access] is set to system maintainers and the user is system maintainer
451
        if (strpos($conf['access'], self::ROLE_SYSTEMMAINTAINER) !== false && !$this->isSystemMaintainer()) {
452
            throw new \RuntimeException('This module "' . $conf['name'] . '" is only available as system maintainer', 1504804727);
453
        }
454
        // Returns TRUE if conf[access] is not set at all or if the user is admin
455
        if (!$conf['access'] || $this->isAdmin()) {
456
            return true;
457
        }
458
        // If $conf['access'] is set but not with 'admin' then we return TRUE, if the module is found in the modList
459
        $acs = false;
460
        if (strpos($conf['access'], 'admin') === false && $conf['name']) {
461
            $acs = $this->check('modules', $conf['name']);
462
        }
463
        if (!$acs) {
464
            throw new \RuntimeException('Access Error: You don\'t have access to this module.', 1294586448);
465
        }
466
        return $acs;
467
    }
468
469
    /**
470
     * Checks if the user is in the valid list of allowed system maintainers. if the list is not set,
471
     * then all admins are system maintainers. If the list is empty, no one is system maintainer (good for production
472
     * systems). If the currently logged in user is in "switch user" mode, this method will return false.
473
     *
474
     * @return bool
475
     */
476
    public function isSystemMaintainer(): bool
477
    {
478
        if ((int)$GLOBALS['BE_USER']->user['ses_backuserid'] !== 0) {
479
            return false;
480
        }
481
        if (Environment::getContext()->isDevelopment() && $this->isAdmin()) {
482
            return true;
483
        }
484
        $systemMaintainers = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? [];
485
        $systemMaintainers = array_map('intval', $systemMaintainers);
486
        if (!empty($systemMaintainers)) {
487
            return in_array((int)$this->user['uid'], $systemMaintainers, true);
488
        }
489
        // No system maintainers set up yet, so any admin is allowed to access the modules
490
        // but explicitly no system maintainers allowed (empty string in TYPO3_CONF_VARS).
491
        // @todo: this needs to be adjusted once system maintainers can log into the install tool with their credentials
492
        if (isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'])
493
            && empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'])) {
494
            return false;
495
        }
496
        return $this->isAdmin();
497
    }
498
499
    /**
500
     * If a user has actually logged in and switched to a different user (admins can use the SU switch user method)
501
     * the real UID is sometimes needed (when checking for permissions for example).
502
     */
503
    protected function getRealUserId(): int
504
    {
505
        return (int)($GLOBALS['BE_USER']->user['ses_backuserid'] ?: $this->user['uid']);
506
    }
507
508
    /**
509
     * Returns a WHERE-clause for the pages-table where user permissions according to input argument, $perms, is validated.
510
     * $perms is the "mask" used to select. Fx. if $perms is 1 then you'll get all pages that a user can actually see!
511
     * 2^0 = show (1)
512
     * 2^1 = edit (2)
513
     * 2^2 = delete (4)
514
     * 2^3 = new (8)
515
     * If the user is 'admin' " 1=1" is returned (no effect)
516
     * 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)
517
     * The 95% use of this function is "->getPagePermsClause(1)" which will
518
     * return WHERE clauses for *selecting* pages in backend listings - in other words this will check read permissions.
519
     *
520
     * @param int $perms Permission mask to use, see function description
521
     * @return string Part of where clause. Prefix " AND " to this.
522
     */
523
    public function getPagePermsClause($perms)
524
    {
525
        if (is_array($this->user)) {
526
            if ($this->isAdmin()) {
527
                return ' 1=1';
528
            }
529
            // Make sure it's integer.
530
            $perms = (int)$perms;
531
            $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
532
                ->getQueryBuilderForTable('pages')
533
                ->expr();
534
535
            // User
536
            $constraint = $expressionBuilder->orX(
537
                $expressionBuilder->comparison(
538
                    $expressionBuilder->bitAnd('pages.perms_everybody', $perms),
539
                    ExpressionBuilder::EQ,
540
                    $perms
541
                ),
542
                $expressionBuilder->andX(
543
                    $expressionBuilder->eq('pages.perms_userid', (int)$this->user['uid']),
544
                    $expressionBuilder->comparison(
545
                        $expressionBuilder->bitAnd('pages.perms_user', $perms),
546
                        ExpressionBuilder::EQ,
547
                        $perms
548
                    )
549
                )
550
            );
551
552
            // Group (if any is set)
553
            if ($this->groupList) {
554
                $constraint->add(
555
                    $expressionBuilder->andX(
556
                        $expressionBuilder->in(
557
                            'pages.perms_groupid',
558
                            GeneralUtility::intExplode(',', $this->groupList)
559
                        ),
560
                        $expressionBuilder->comparison(
561
                            $expressionBuilder->bitAnd('pages.perms_group', $perms),
562
                            ExpressionBuilder::EQ,
563
                            $perms
564
                        )
565
                    )
566
                );
567
            }
568
569
            $constraint = ' (' . (string)$constraint . ')';
570
571
            // ****************
572
            // getPagePermsClause-HOOK
573
            // ****************
574
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getPagePermsClause'] ?? [] as $_funcRef) {
575
                $_params = ['currentClause' => $constraint, 'perms' => $perms];
576
                $constraint = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
577
            }
578
            return $constraint;
579
        }
580
        return ' 1=0';
581
    }
582
583
    /**
584
     * Returns a combined binary representation of the current users permissions for the page-record, $row.
585
     * The perms for user, group and everybody is OR'ed together (provided that the page-owner is the user
586
     * and for the groups that the user is a member of the group.
587
     * If the user is admin, 31 is returned	(full permissions for all five flags)
588
     *
589
     * @param array $row Input page row with all perms_* fields available.
590
     * @return int Bitwise representation of the users permissions in relation to input page row, $row
591
     */
592
    public function calcPerms($row)
593
    {
594
        // Return 31 for admin users.
595
        if ($this->isAdmin()) {
596
            return Permission::ALL;
597
        }
598
        // Return 0 if page is not within the allowed web mount
599
        // Always do this for the default language page record
600
        if (!$this->isInWebMount($row[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] ?: $row)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->isInWebMount($row...ointerField']] ?: $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...
601
            return Permission::NOTHING;
602
        }
603
        $out = Permission::NOTHING;
604
        if (
605
            isset($row['perms_userid']) && isset($row['perms_user']) && isset($row['perms_groupid'])
606
            && isset($row['perms_group']) && isset($row['perms_everybody']) && isset($this->groupList)
607
        ) {
608
            if ($this->user['uid'] == $row['perms_userid']) {
609
                $out |= $row['perms_user'];
610
            }
611
            if ($this->isMemberOfGroup($row['perms_groupid'])) {
612
                $out |= $row['perms_group'];
613
            }
614
            $out |= $row['perms_everybody'];
615
        }
616
        // ****************
617
        // CALCPERMS hook
618
        // ****************
619
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['calcPerms'] ?? [] as $_funcRef) {
620
            $_params = [
621
                'row' => $row,
622
                'outputPermissions' => $out
623
            ];
624
            $out = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
625
        }
626
        return $out;
627
    }
628
629
    /**
630
     * Returns TRUE if the RTE (Rich Text Editor) is enabled for the user.
631
     *
632
     * @return bool
633
     */
634
    public function isRTE()
635
    {
636
        return (bool)$this->uc['edit_RTE'];
637
    }
638
639
    /**
640
     * Returns TRUE if the $value is found in the list in a $this->groupData[] index pointed to by $type (array key).
641
     * Can thus be users to check for modules, exclude-fields, select/modify permissions for tables etc.
642
     * If user is admin TRUE is also returned
643
     * Please see the document Inside TYPO3 for examples.
644
     *
645
     * @param string $type The type value; "webmounts", "filemounts", "pagetypes_select", "tables_select", "tables_modify", "non_exclude_fields", "modules", "available_widgets"
646
     * @param string $value String to search for in the groupData-list
647
     * @return bool TRUE if permission is granted (that is, the value was found in the groupData list - or the BE_USER is "admin")
648
     */
649
    public function check($type, $value)
650
    {
651
        return isset($this->groupData[$type])
652
            && ($this->isAdmin() || GeneralUtility::inList($this->groupData[$type], $value));
653
    }
654
655
    /**
656
     * Checking the authMode of a select field with authMode set
657
     *
658
     * @param string $table Table name
659
     * @param string $field Field name (must be configured in TCA and of type "select" with authMode set!)
660
     * @param string $value Value to evaluation (single value, must not contain any of the chars ":,|")
661
     * @param string $authMode Auth mode keyword (explicitAllow, explicitDeny, individual)
662
     * @return bool Whether access is granted or not
663
     */
664
    public function checkAuthMode($table, $field, $value, $authMode)
665
    {
666
        // Admin users can do anything:
667
        if ($this->isAdmin()) {
668
            return true;
669
        }
670
        // Allow all blank values:
671
        if ((string)$value === '') {
672
            return true;
673
        }
674
        // Allow dividers:
675
        if ($value === '--div--') {
676
            return true;
677
        }
678
        // Certain characters are not allowed in the value
679
        if (preg_match('/[:|,]/', $value)) {
680
            return false;
681
        }
682
        // Initialize:
683
        $testValue = $table . ':' . $field . ':' . $value;
684
        $out = true;
685
        // Checking value:
686
        switch ((string)$authMode) {
687
            case 'explicitAllow':
688
                if (!GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':ALLOW')) {
689
                    $out = false;
690
                }
691
                break;
692
            case 'explicitDeny':
693
                if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) {
694
                    $out = false;
695
                }
696
                break;
697
            case 'individual':
698
                if (is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
699
                    $items = $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'];
700
                    if (is_array($items)) {
701
                        foreach ($items as $iCfg) {
702
                            if ((string)$iCfg[1] === (string)$value && $iCfg[4]) {
703
                                switch ((string)$iCfg[4]) {
704
                                    case 'EXPL_ALLOW':
705
                                        if (!GeneralUtility::inList(
706
                                            $this->groupData['explicit_allowdeny'],
707
                                            $testValue . ':ALLOW'
708
                                        )) {
709
                                            $out = false;
710
                                        }
711
                                        break;
712
                                    case 'EXPL_DENY':
713
                                        if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) {
714
                                            $out = false;
715
                                        }
716
                                        break;
717
                                }
718
                                break;
719
                            }
720
                        }
721
                    }
722
                }
723
                break;
724
        }
725
        return $out;
726
    }
727
728
    /**
729
     * Checking if a language value (-1, 0 and >0 for sys_language records) is allowed to be edited by the user.
730
     *
731
     * @param int $langValue Language value to evaluate
732
     * @return bool Returns TRUE if the language value is allowed, otherwise FALSE.
733
     */
734
    public function checkLanguageAccess($langValue)
735
    {
736
        // The users language list must be non-blank - otherwise all languages are allowed.
737
        if (trim($this->groupData['allowed_languages']) !== '') {
738
            $langValue = (int)$langValue;
739
            // Language must either be explicitly allowed OR the lang Value be "-1" (all languages)
740
            if ($langValue != -1 && !$this->check('allowed_languages', $langValue)) {
741
                return false;
742
            }
743
        }
744
        return true;
745
    }
746
747
    /**
748
     * Check if user has access to all existing localizations for a certain record
749
     *
750
     * @param string $table The table
751
     * @param array $record The current record
752
     * @return bool
753
     */
754
    public function checkFullLanguagesAccess($table, $record)
755
    {
756
        if (!$this->checkLanguageAccess(0)) {
757
            return false;
758
        }
759
760
        if (BackendUtility::isTableLocalizable($table)) {
761
            $pointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
762
            $pointerValue = $record[$pointerField] > 0 ? $record[$pointerField] : $record['uid'];
763
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
764
            $queryBuilder->getRestrictions()
765
                ->removeAll()
766
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
767
                ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
768
            $recordLocalizations = $queryBuilder->select('*')
769
                ->from($table)
770
                ->where(
771
                    $queryBuilder->expr()->eq(
772
                        $pointerField,
773
                        $queryBuilder->createNamedParameter($pointerValue, \PDO::PARAM_INT)
774
                    )
775
                )
776
                ->execute()
777
                ->fetchAll();
778
779
            foreach ($recordLocalizations as $recordLocalization) {
780
                if (!$this->checkLanguageAccess($recordLocalization[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
781
                    return false;
782
                }
783
            }
784
        }
785
        return true;
786
    }
787
788
    /**
789
     * Checking if a user has editing access to a record from a $GLOBALS['TCA'] table.
790
     * The checks does not take page permissions and other "environmental" things into account.
791
     * It only deal with record internals; If any values in the record fields disallows it.
792
     * For instance languages settings, authMode selector boxes are evaluated (and maybe more in the future).
793
     * It will check for workspace dependent access.
794
     * The function takes an ID (int) or row (array) as second argument.
795
     *
796
     * @param string $table Table name
797
     * @param mixed $idOrRow If integer, then this is the ID of the record. If Array this just represents fields in the record.
798
     * @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.
799
     * @param bool $deletedRecord Set, if testing a deleted record array.
800
     * @param bool $checkFullLanguageAccess Set, whenever access to all translations of the record is required
801
     * @return bool TRUE if OK, otherwise FALSE
802
     */
803
    public function recordEditAccessInternals($table, $idOrRow, $newRecord = false, $deletedRecord = false, $checkFullLanguageAccess = false)
804
    {
805
        if (!isset($GLOBALS['TCA'][$table])) {
806
            return false;
807
        }
808
        // Always return TRUE for Admin users.
809
        if ($this->isAdmin()) {
810
            return true;
811
        }
812
        // Fetching the record if the $idOrRow variable was not an array on input:
813
        if (!is_array($idOrRow)) {
814
            if ($deletedRecord) {
815
                $idOrRow = BackendUtility::getRecord($table, $idOrRow, '*', '', false);
816
            } else {
817
                $idOrRow = BackendUtility::getRecord($table, $idOrRow);
818
            }
819
            if (!is_array($idOrRow)) {
820
                $this->errorMsg = 'ERROR: Record could not be fetched.';
821
                return false;
822
            }
823
        }
824
        // Checking languages:
825
        if ($table === 'pages' && $checkFullLanguageAccess && !$this->checkFullLanguagesAccess($table, $idOrRow)) {
826
            return false;
827
        }
828
        if ($GLOBALS['TCA'][$table]['ctrl']['languageField']) {
829
            // Language field must be found in input row - otherwise it does not make sense.
830
            if (isset($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
831
                if (!$this->checkLanguageAccess($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
832
                    $this->errorMsg = 'ERROR: Language was not allowed.';
833
                    return false;
834
                }
835
                if (
836
                    $checkFullLanguageAccess && $idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']] == 0
837
                    && !$this->checkFullLanguagesAccess($table, $idOrRow)
838
                ) {
839
                    $this->errorMsg = 'ERROR: Related/affected language was not allowed.';
840
                    return false;
841
                }
842
            } else {
843
                $this->errorMsg = 'ERROR: The "languageField" field named "'
844
                    . $GLOBALS['TCA'][$table]['ctrl']['languageField'] . '" was not found in testing record!';
845
                return false;
846
            }
847
        }
848
        // Checking authMode fields:
849
        if (is_array($GLOBALS['TCA'][$table]['columns'])) {
850
            foreach ($GLOBALS['TCA'][$table]['columns'] as $fieldName => $fieldValue) {
851
                if (isset($idOrRow[$fieldName])) {
852
                    if (
853
                        $fieldValue['config']['type'] === 'select' && $fieldValue['config']['authMode']
854
                        && $fieldValue['config']['authMode_enforce'] === 'strict'
855
                    ) {
856
                        if (!$this->checkAuthMode($table, $fieldName, $idOrRow[$fieldName], $fieldValue['config']['authMode'])) {
857
                            $this->errorMsg = 'ERROR: authMode "' . $fieldValue['config']['authMode']
858
                                . '" failed for field "' . $fieldName . '" with value "'
859
                                . $idOrRow[$fieldName] . '" evaluated';
860
                            return false;
861
                        }
862
                    }
863
                }
864
            }
865
        }
866
        // Checking "editlock" feature (doesn't apply to new records)
867
        if (!$newRecord && $GLOBALS['TCA'][$table]['ctrl']['editlock']) {
868
            if (isset($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['editlock']])) {
869
                if ($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['editlock']]) {
870
                    $this->errorMsg = 'ERROR: Record was locked for editing. Only admin users can change this state.';
871
                    return false;
872
                }
873
            } else {
874
                $this->errorMsg = 'ERROR: The "editLock" field named "' . $GLOBALS['TCA'][$table]['ctrl']['editlock']
875
                    . '" was not found in testing record!';
876
                return false;
877
            }
878
        }
879
        // Checking record permissions
880
        // THIS is where we can include a check for "perms_" fields for other records than pages...
881
        // Process any hooks
882
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['recordEditAccessInternals'] ?? [] as $funcRef) {
883
            $params = [
884
                'table' => $table,
885
                'idOrRow' => $idOrRow,
886
                'newRecord' => $newRecord
887
            ];
888
            if (!GeneralUtility::callUserFunction($funcRef, $params, $this)) {
889
                return false;
890
            }
891
        }
892
        // Finally, return TRUE if all is well.
893
        return true;
894
    }
895
896
    /**
897
     * Returns TRUE if the BE_USER is allowed to *create* shortcuts in the backend modules
898
     *
899
     * @return bool
900
     */
901
    public function mayMakeShortcut()
902
    {
903
        return $this->getTSConfig()['options.']['enableBookmarks'] ?? false
904
            && !($this->getTSConfig()['options.']['mayNotCreateEditBookmarks'] ?? false);
905
    }
906
907
    /**
908
     * Checking if editing of an existing record is allowed in current workspace if that is offline.
909
     * Rules for editing in offline mode:
910
     * - record supports versioning and is an offline version from workspace and has the current stage
911
     * - or record (any) is in a branch where there is a page which is a version from the workspace
912
     *   and where the stage is not preventing records
913
     *
914
     * @param string $table Table of record
915
     * @param array|int $recData Integer (record uid) or array where fields are at least: pid, t3ver_wsid, t3ver_oid, t3ver_stage (if versioningWS is set)
916
     * @return string String error code, telling the failure state. FALSE=All ok
917
     */
918
    public function workspaceCannotEditRecord($table, $recData)
919
    {
920
        // Only test if the user is in a workspace
921
        if ($this->workspace === 0) {
922
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
923
        }
924
        $tableSupportsVersioning = BackendUtility::isTableWorkspaceEnabled($table);
925
        if (!is_array($recData)) {
926
            $recData = BackendUtility::getRecord(
927
                $table,
928
                $recData,
929
                'pid' . ($tableSupportsVersioning ? ',t3ver_oid,t3ver_wsid,t3ver_stage' : '')
930
            );
931
        }
932
        if (is_array($recData)) {
933
            // We are testing a "version" (identified by having a t3ver_oid): it can be edited provided
934
            // that workspace matches and versioning is enabled for the table.
935
            if ($tableSupportsVersioning && (int)($recData['t3ver_oid'] ?? 0) > 0) {
936
                if ((int)$recData['t3ver_wsid'] !== $this->workspace) {
937
                    // So does workspace match?
938
                    return 'Workspace ID of record didn\'t match current workspace';
939
                }
940
                // So is the user allowed to "use" the edit stage within the workspace?
941
                return $this->workspaceCheckStageForCurrent(0)
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->workspaceC... not allow for editing' could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
942
                        ? false
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
943
                        : 'User\'s access level did not allow for editing';
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
944
            }
945
            // Check if we are testing a "live" record
946
            if ($this->workspaceAllowsLiveEditingInTable($table)) {
947
                // Live records are OK in the current workspace
948
                return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
949
            }
950
            // If not offline, output error
951
            return 'Online record was not in a workspace!';
952
        }
953
        return 'No record';
954
    }
955
956
    /**
957
     * Evaluates if a user is allowed to edit the offline version
958
     *
959
     * @param string $table Table of record
960
     * @param array|int $recData Integer (record uid) or array where fields are at least: pid, t3ver_wsid, t3ver_stage (if versioningWS is set)
961
     * @return string String error code, telling the failure state. FALSE=All ok
962
     * @see workspaceCannotEditRecord()
963
     * @internal this method will be moved to EXT:workspaces
964
     */
965
    public function workspaceCannotEditOfflineVersion($table, $recData)
966
    {
967
        if (!BackendUtility::isTableWorkspaceEnabled($table)) {
968
            return 'Table does not support versioning.';
969
        }
970
        if (!is_array($recData)) {
971
            $recData = BackendUtility::getRecord($table, $recData, 'uid,pid,t3ver_oid,t3ver_wsid,t3ver_stage');
972
        }
973
        if (is_array($recData)) {
974
            if ((int)$recData['t3ver_oid'] > 0) {
975
                return $this->workspaceCannotEditRecord($table, $recData);
976
            }
977
            return 'Not an offline version';
978
        }
979
        return 'No record';
980
    }
981
982
    /**
983
     * Check if "live" records from $table may be created or edited in this PID.
984
     * If the answer is FALSE it means the only valid way to create or edit records in the PID is by versioning
985
     * If the answer is 1 or 2 it means it is OK to create a record, if -1 it means that it is OK in terms
986
     * of versioning because the element was within a versionized branch
987
     * but NOT ok in terms of the state the root point had!
988
     *
989
     * Note: this method is not in use anymore and will likely be deprecated in future TYPO3 versions.
990
     *
991
     * @param int $pid PID value to check for. OBSOLETE!
992
     * @param string $table Table name
993
     * @return mixed Returns FALSE if a live record cannot be created and must be versionized in order to do so. 2 means a) Workspace is "Live" or workspace allows "live edit" of records from non-versionized tables (and the $table is not versionizable). 1 and -1 means the pid is inside a versionized branch where -1 means that the branch-point did NOT allow a new record according to its state.
994
     */
995
    public function workspaceAllowLiveRecordsInPID($pid, $table)
0 ignored issues
show
Unused Code introduced by
The parameter $pid is not used and could be removed. ( Ignorable by Annotation )

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

995
    public function workspaceAllowLiveRecordsInPID(/** @scrutinizer ignore-unused */ $pid, $table)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
996
    {
997
        // Always for Live workspace AND if live-edit is enabled
998
        // and tables are completely without versioning it is ok as well.
999
        if (
1000
            $this->workspace === 0
1001
            || $this->workspaceRec['live_edit'] && !BackendUtility::isTableWorkspaceEnabled($table)
1002
            || $GLOBALS['TCA'][$table]['ctrl']['versioningWS_alwaysAllowLiveEdit']
1003
        ) {
1004
            // OK to create for this table.
1005
            return 2;
1006
        }
1007
        // If the answer is FALSE it means the only valid way to create or edit records in the PID is by versioning
1008
        return false;
1009
    }
1010
1011
    /**
1012
     * Checks if a record is allowed to be edited in the current workspace.
1013
     * This is not bound to an actual record, but to the mere fact if the user is in a workspace
1014
     * and depending on the table settings.
1015
     *
1016
     * @param string $table
1017
     * @return bool
1018
     */
1019
    public function workspaceAllowsLiveEditingInTable(string $table): bool
1020
    {
1021
        // In live workspace the record can be added/modified
1022
        if ($this->workspace === 0) {
1023
            return true;
1024
        }
1025
        // Workspace setting allows to "live edit" records of tables without versioning
1026
        if ($this->workspaceRec['live_edit'] && !BackendUtility::isTableWorkspaceEnabled($table)) {
1027
            return true;
1028
        }
1029
        // Always for Live workspace AND if live-edit is enabled
1030
        // and tables are completely without versioning it is ok as well.
1031
        if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS_alwaysAllowLiveEdit']) {
1032
            return true;
1033
        }
1034
        // If the answer is FALSE it means the only valid way to create or edit records by creating records in the workspace
1035
        return false;
1036
    }
1037
1038
    /**
1039
     * Evaluates if a record from $table can be created in $pid
1040
     *
1041
     * Note: this method is not in use anymore and will likely be deprecated in future TYPO3 versions.
1042
     *
1043
     * @param int $pid Page id. This value must be the _ORIG_uid if available: So when you have pages versionized as "page" or "element" you must supply the id of the page version in the workspace!
1044
     * @param string $table Table name
1045
     * @return bool TRUE if OK.
1046
     */
1047
    public function workspaceCreateNewRecord($pid, $table)
0 ignored issues
show
Unused Code introduced by
The parameter $pid is not used and could be removed. ( Ignorable by Annotation )

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

1047
    public function workspaceCreateNewRecord(/** @scrutinizer ignore-unused */ $pid, $table)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1048
    {
1049
        // If LIVE records cannot be created due to workspace restrictions, prepare creation of placeholder-record
1050
        if (!$this->workspaceAllowsLiveEditingInTable($table) && !BackendUtility::isTableWorkspaceEnabled($table)) {
1051
            // So, if no live records were allowed, we have to create a new version of this record
1052
            return false;
1053
        }
1054
        return true;
1055
    }
1056
1057
    /**
1058
     * Evaluates if a record from $table can be created. If the table is not set up for versioning,
1059
     * and the "live edit" flag of the page is set, return false. In live workspace this is always true,
1060
     * as all records can be created in live workspace
1061
     *
1062
     * @param string $table Table name
1063
     * @return bool
1064
     */
1065
    public function workspaceCanCreateNewRecord(string $table): bool
1066
    {
1067
        // If LIVE records cannot be created due to workspace restrictions, prepare creation of placeholder-record
1068
        if (!$this->workspaceAllowsLiveEditingInTable($table) && !BackendUtility::isTableWorkspaceEnabled($table)) {
1069
            return false;
1070
        }
1071
        return true;
1072
    }
1073
1074
    /**
1075
     * Evaluates if auto creation of a version of a record is allowed.
1076
     * Auto-creation of version: In offline workspace, test if versioning is
1077
     * enabled and look for workspace version of input record.
1078
     * If there is no versionized record found we will create one and save to that.
1079
     *
1080
     * @param string $table Table of the record
1081
     * @param int $id UID of record
1082
     * @param int $recpid PID of record
1083
     * @return bool TRUE if ok.
1084
     */
1085
    public function workspaceAllowAutoCreation($table, $id, $recpid)
1086
    {
1087
        // No version can be created in live workspace
1088
        if ($this->workspace === 0) {
1089
            return false;
1090
        }
1091
        // No versioning support for this table, so no version can be created
1092
        if (!BackendUtility::isTableWorkspaceEnabled($table)) {
1093
            return false;
1094
        }
1095
        if ($recpid < 0) {
1096
            return false;
1097
        }
1098
        // There must be no existing version of this record in workspace
1099
        if (BackendUtility::getWorkspaceVersionOfRecord($this->workspace, $table, $id, 'uid')) {
1100
            return false;
1101
        }
1102
        return true;
1103
    }
1104
1105
    /**
1106
     * Checks if an element stage allows access for the user in the current workspace
1107
     * In live workspace (= 0) access is always granted for any stage.
1108
     * Admins are always allowed.
1109
     * An option for custom workspaces allows members to also edit when the stage is "Review"
1110
     *
1111
     * @param int $stage Stage id from an element: -1,0 = editing, 1 = reviewer, >1 = owner
1112
     * @return bool TRUE if user is allowed access
1113
     */
1114
    public function workspaceCheckStageForCurrent($stage)
1115
    {
1116
        // Always allow for admins
1117
        if ($this->isAdmin()) {
1118
            return true;
1119
        }
1120
        // Always OK for live workspace
1121
        if ($this->workspace === 0 || !ExtensionManagementUtility::isLoaded('workspaces')) {
1122
            return true;
1123
        }
1124
        $stage = (int)$stage;
1125
        $stat = $this->checkWorkspaceCurrent();
1126
        $accessType = $stat['_ACCESS'];
1127
        // Workspace owners are always allowed for stage change
1128
        if ($accessType === 'owner') {
1129
            return true;
1130
        }
1131
1132
        // Check if custom staging is activated
1133
        $workspaceRec = BackendUtility::getRecord('sys_workspace', $stat['uid']);
1134
        if ($workspaceRec['custom_stages'] > 0 && $stage !== 0 && $stage !== -10) {
1135
            // Get custom stage record
1136
            $workspaceStageRec = BackendUtility::getRecord('sys_workspace_stage', $stage);
1137
            // Check if the user is responsible for the current stage
1138
            if (
1139
                $accessType === 'member'
1140
                && GeneralUtility::inList($workspaceStageRec['responsible_persons'], 'be_users_' . $this->user['uid'])
1141
            ) {
1142
                return true;
1143
            }
1144
            // Check if the user is in a group which is responsible for the current stage
1145
            foreach ($this->userGroupsUID as $groupUid) {
1146
                if (
1147
                    $accessType === 'member'
1148
                    && GeneralUtility::inList($workspaceStageRec['responsible_persons'], 'be_groups_' . $groupUid)
1149
                ) {
1150
                    return true;
1151
                }
1152
            }
1153
        } elseif ($stage === -10 || $stage === -20) {
1154
            // Nobody is allowed to do that except the owner (which was checked above)
1155
            return false;
1156
        } elseif (
1157
            $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...
1158
            || $accessType === 'member' && $stage <= 0
1159
        ) {
1160
            return true;
1161
        }
1162
        return false;
1163
    }
1164
1165
    /**
1166
     * Returns TRUE if the user has access to publish content from the workspace ID given.
1167
     * Admin-users are always granted access to do this
1168
     * If the workspace ID is 0 (live) all users have access also
1169
     * For custom workspaces it depends on whether the user is owner OR like with
1170
     * draft workspace if the user has access to Live workspace.
1171
     *
1172
     * @param int $wsid Workspace UID; 0,1+
1173
     * @return bool Returns TRUE if the user has access to publish content from the workspace ID given.
1174
     * @internal this method will be moved to EXT:workspaces
1175
     */
1176
    public function workspacePublishAccess($wsid)
1177
    {
1178
        if ($this->isAdmin()) {
1179
            return true;
1180
        }
1181
        $wsAccess = $this->checkWorkspace($wsid);
1182
        // If no access to workspace, of course you cannot publish!
1183
        if ($wsAccess === false) {
0 ignored issues
show
introduced by
The condition $wsAccess === false is always false.
Loading history...
1184
            return false;
1185
        }
1186
        if ((int)$wsAccess['uid'] === 0) {
1187
            // If access to Live workspace, no problem.
1188
            return true;
1189
        }
1190
        // Custom workspaces
1191
        // 1. Owners can always publish
1192
        if ($wsAccess['_ACCESS'] === 'owner') {
1193
            return true;
1194
        }
1195
        // 2. User has access to online workspace which is OK as well as long as publishing
1196
        // access is not limited by workspace option.
1197
        return $this->checkWorkspace(0) && !($wsAccess['publish_access'] & 2);
1198
    }
1199
1200
    /**
1201
     * Workspace swap-mode access?
1202
     *
1203
     * @return bool Returns TRUE if records can be swapped in the current workspace, otherwise FALSE
1204
     * @internal this method will be moved to EXT:workspaces
1205
     */
1206
    public function workspaceSwapAccess()
1207
    {
1208
        // Always possible in live workspace
1209
        if ($this->workspace === 0) {
1210
            return true;
1211
        }
1212
        // In custom workspaces, only possible if swap_modes flag is not "2" (explicitly disabling swapping)
1213
        if ((int)$this->workspaceRec['swap_modes'] !== 2) {
1214
            return true;
1215
        }
1216
        return false;
1217
    }
1218
1219
    /**
1220
     * Returns full parsed user TSconfig array, merged with TSconfig from groups.
1221
     *
1222
     * Example:
1223
     * [
1224
     *     'options.' => [
1225
     *         'fooEnabled' => '0',
1226
     *         'fooEnabled.' => [
1227
     *             'tt_content' => 1,
1228
     *         ],
1229
     *     ],
1230
     * ]
1231
     *
1232
     * @return array Parsed and merged user TSconfig array
1233
     */
1234
    public function getTSConfig()
1235
    {
1236
        return $this->userTS;
1237
    }
1238
1239
    /**
1240
     * Returns an array with the webmounts.
1241
     * If no webmounts, and empty array is returned.
1242
     * NOTICE: Deleted pages WILL NOT be filtered out! So if a mounted page has been deleted
1243
     *         it is STILL coming out as a webmount. This is not checked due to performance.
1244
     *
1245
     * @return array
1246
     */
1247
    public function returnWebmounts()
1248
    {
1249
        return (string)$this->groupData['webmounts'] != '' ? explode(',', $this->groupData['webmounts']) : [];
1250
    }
1251
1252
    /**
1253
     * Initializes the given mount points for the current Backend user.
1254
     *
1255
     * @param array $mountPointUids Page UIDs that should be used as web mountpoints
1256
     * @param bool $append If TRUE the given mount point will be appended. Otherwise the current mount points will be replaced.
1257
     */
1258
    public function setWebmounts(array $mountPointUids, $append = false)
1259
    {
1260
        if (empty($mountPointUids)) {
1261
            return;
1262
        }
1263
        if ($append) {
1264
            $currentWebMounts = GeneralUtility::intExplode(',', $this->groupData['webmounts']);
1265
            $mountPointUids = array_merge($currentWebMounts, $mountPointUids);
1266
        }
1267
        $this->groupData['webmounts'] = implode(',', array_unique($mountPointUids));
1268
    }
1269
1270
    /**
1271
     * Checks for alternative web mount points for the element browser.
1272
     *
1273
     * If there is a temporary mount point active in the page tree it will be used.
1274
     *
1275
     * If the User TSconfig options.pageTree.altElementBrowserMountPoints is not empty the pages configured
1276
     * there are used as web mounts If options.pageTree.altElementBrowserMountPoints.append is enabled,
1277
     * they are appended to the existing webmounts.
1278
     *
1279
     * @internal - do not use in your own extension
1280
     */
1281
    public function initializeWebmountsForElementBrowser()
1282
    {
1283
        $alternativeWebmountPoint = (int)$this->getSessionData('pageTree_temporaryMountPoint');
1284
        if ($alternativeWebmountPoint) {
1285
            $alternativeWebmountPoint = GeneralUtility::intExplode(',', $alternativeWebmountPoint);
1286
            $this->setWebmounts($alternativeWebmountPoint);
1287
            return;
1288
        }
1289
1290
        $alternativeWebmountPoints = trim($this->getTSConfig()['options.']['pageTree.']['altElementBrowserMountPoints'] ?? '');
1291
        $appendAlternativeWebmountPoints = $this->getTSConfig()['options.']['pageTree.']['altElementBrowserMountPoints.']['append'] ?? '';
1292
        if ($alternativeWebmountPoints) {
1293
            $alternativeWebmountPoints = GeneralUtility::intExplode(',', $alternativeWebmountPoints);
1294
            $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

1294
            $this->setWebmounts($alternativeWebmountPoints, /** @scrutinizer ignore-type */ $appendAlternativeWebmountPoints);
Loading history...
1295
        }
1296
    }
1297
1298
    /**
1299
     * Returns TRUE or FALSE, depending if an alert popup (a javascript confirmation) should be shown
1300
     * call like $GLOBALS['BE_USER']->jsConfirmation($BITMASK).
1301
     *
1302
     * @param int $bitmask Bitmask, one of \TYPO3\CMS\Core\Type\Bitmask\JsConfirmation
1303
     * @return bool TRUE if the confirmation should be shown
1304
     * @see JsConfirmation
1305
     */
1306
    public function jsConfirmation($bitmask)
1307
    {
1308
        try {
1309
            $alertPopupsSetting = trim((string)($this->getTSConfig()['options.']['alertPopups'] ?? ''));
1310
            $alertPopup = JsConfirmation::cast($alertPopupsSetting === '' ? null : (int)$alertPopupsSetting);
1311
        } catch (InvalidEnumerationValueException $e) {
1312
            $alertPopup = new JsConfirmation();
1313
        }
1314
1315
        return JsConfirmation::cast($bitmask)->matches($alertPopup);
1316
    }
1317
1318
    /**
1319
     * Initializes a lot of stuff like the access-lists, database-mountpoints and filemountpoints
1320
     * This method is called by ->backendCheckLogin() (from extending BackendUserAuthentication)
1321
     * if the backend user login has verified OK.
1322
     * Generally this is required initialization of a backend user.
1323
     *
1324
     * @internal
1325
     * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
1326
     */
1327
    public function fetchGroupData()
1328
    {
1329
        if ($this->user['uid']) {
1330
            // Get lists for the be_user record and set them as default/primary values.
1331
            // Enabled Backend Modules
1332
            $this->dataLists['modList'] = $this->user['userMods'];
1333
            // Add available widgets
1334
            $this->dataLists['available_widgets'] = $this->user['available_widgets'];
1335
            // Add Allowed Languages
1336
            $this->dataLists['allowed_languages'] = $this->user['allowed_languages'];
1337
            // Set user value for workspace permissions.
1338
            $this->dataLists['workspace_perms'] = $this->user['workspace_perms'];
1339
            // Database mountpoints
1340
            $this->dataLists['webmount_list'] = $this->user['db_mountpoints'];
1341
            // File mountpoints
1342
            $this->dataLists['filemount_list'] = $this->user['file_mountpoints'];
1343
            // Fileoperation permissions
1344
            $this->dataLists['file_permissions'] = $this->user['file_permissions'];
1345
1346
            // BE_GROUPS:
1347
            // Get the groups...
1348
            if (!empty($this->user[$this->usergroup_column])) {
1349
                // Fetch groups will add a lot of information to the internal arrays: modules, accesslists, TSconfig etc.
1350
                // Refer to fetchGroups() function.
1351
                $this->fetchGroups($this->user[$this->usergroup_column]);
1352
            }
1353
            // Populating the $this->userGroupsUID -array with the groups in the order in which they were LAST included.!!
1354
            $this->userGroupsUID = array_reverse(array_unique(array_reverse($this->includeGroupArray)));
1355
            // Finally this is the list of group_uid's in the order they are parsed (including subgroups!)
1356
            // and without duplicates (duplicates are presented with their last entrance in the list,
1357
            // which thus reflects the order of the TypoScript in TSconfig)
1358
            $this->groupList = implode(',', $this->userGroupsUID);
1359
            $this->setCachedList($this->groupList);
1360
1361
            $this->prepareUserTsConfig();
1362
1363
            // Processing webmounts
1364
            // Admin's always have the root mounted
1365
            if ($this->isAdmin() && !($this->getTSConfig()['options.']['dontMountAdminMounts'] ?? false)) {
1366
                $this->dataLists['webmount_list'] = '0,' . $this->dataLists['webmount_list'];
1367
            }
1368
            // The lists are cleaned for duplicates
1369
            $this->groupData['webmounts'] = GeneralUtility::uniqueList($this->dataLists['webmount_list']);
1370
            $this->groupData['pagetypes_select'] = GeneralUtility::uniqueList($this->dataLists['pagetypes_select']);
1371
            $this->groupData['tables_select'] = GeneralUtility::uniqueList($this->dataLists['tables_modify'] . ',' . $this->dataLists['tables_select']);
1372
            $this->groupData['tables_modify'] = GeneralUtility::uniqueList($this->dataLists['tables_modify']);
1373
            $this->groupData['non_exclude_fields'] = GeneralUtility::uniqueList($this->dataLists['non_exclude_fields']);
1374
            $this->groupData['explicit_allowdeny'] = GeneralUtility::uniqueList($this->dataLists['explicit_allowdeny']);
1375
            $this->groupData['allowed_languages'] = GeneralUtility::uniqueList($this->dataLists['allowed_languages']);
1376
            $this->groupData['custom_options'] = GeneralUtility::uniqueList($this->dataLists['custom_options']);
1377
            $this->groupData['modules'] = GeneralUtility::uniqueList($this->dataLists['modList']);
1378
            $this->groupData['available_widgets'] = GeneralUtility::uniqueList($this->dataLists['available_widgets']);
1379
            $this->groupData['file_permissions'] = GeneralUtility::uniqueList($this->dataLists['file_permissions']);
1380
            $this->groupData['workspace_perms'] = $this->dataLists['workspace_perms'];
1381
1382
            if (!empty(trim($this->groupData['webmounts']))) {
1383
                // Checking read access to web mounts if there are mounts points (not empty string, false or 0)
1384
                $webmounts = explode(',', $this->groupData['webmounts']);
1385
                // Selecting all web mounts with permission clause for reading
1386
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1387
                $queryBuilder->getRestrictions()
1388
                    ->removeAll()
1389
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1390
1391
                $MProws = $queryBuilder->select('uid')
1392
                    ->from('pages')
1393
                    // @todo DOCTRINE: check how to make getPagePermsClause() portable
1394
                    ->where(
1395
                        $this->getPagePermsClause(Permission::PAGE_SHOW),
1396
                        $queryBuilder->expr()->in(
1397
                            'uid',
1398
                            $queryBuilder->createNamedParameter(
1399
                                GeneralUtility::intExplode(',', $this->groupData['webmounts']),
1400
                                Connection::PARAM_INT_ARRAY
1401
                            )
1402
                        )
1403
                    )
1404
                    ->execute()
1405
                    ->fetchAll();
1406
                $MProws = array_column(($MProws ?: []), 'uid', 'uid');
1407
                foreach ($webmounts as $idx => $mountPointUid) {
1408
                    // If the mount ID is NOT found among selected pages, unset it:
1409
                    if ($mountPointUid > 0 && !isset($MProws[$mountPointUid])) {
1410
                        unset($webmounts[$idx]);
1411
                    }
1412
                }
1413
                // Implode mounts in the end.
1414
                $this->groupData['webmounts'] = implode(',', $webmounts);
1415
            }
1416
            // Setting up workspace situation (after webmounts are processed!):
1417
            $this->workspaceInit();
1418
        }
1419
    }
1420
1421
    /**
1422
     * This method parses the UserTSconfig from the current user and all their groups.
1423
     * If the contents are the same, parsing is skipped. No matching is applied here currently.
1424
     */
1425
    protected function prepareUserTsConfig(): void
1426
    {
1427
        $collectedUserTSconfig = [
1428
            'default' => $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig']
1429
        ];
1430
        // Default TSconfig for admin-users
1431
        if ($this->isAdmin()) {
1432
            $collectedUserTSconfig[] = 'admPanel.enable.all = 1';
1433
        }
1434
        // Setting defaults for sys_note author / email
1435
        $collectedUserTSconfig[] = '
1436
TCAdefaults.sys_note.author = ' . $this->user['realName'] . '
1437
TCAdefaults.sys_note.email = ' . $this->user['email'];
1438
1439
        // Loop through all groups and add their 'TSconfig' fields
1440
        foreach ($this->includeGroupArray as $groupId) {
1441
            $collectedUserTSconfig['group_' . $groupId] = $this->userGroups[$groupId]['TSconfig'] ?? '';
1442
        }
1443
1444
        $collectedUserTSconfig[] = $this->user['TSconfig'];
1445
        // Check external files
1446
        $collectedUserTSconfig = TypoScriptParser::checkIncludeLines_array($collectedUserTSconfig);
1447
        // Imploding with "[global]" will make sure that non-ended confinements with braces are ignored.
1448
        $userTS_text = implode("\n[GLOBAL]\n", $collectedUserTSconfig);
1449
        // Parsing the user TSconfig (or getting from cache)
1450
        $hash = md5('userTS:' . $userTS_text);
1451
        $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
1452
        if (!($this->userTS = $cache->get($hash))) {
1453
            $parseObj = GeneralUtility::makeInstance(TypoScriptParser::class);
1454
            $conditionMatcher = GeneralUtility::makeInstance(ConditionMatcher::class);
1455
            $parseObj->parse($userTS_text, $conditionMatcher);
1456
            $this->userTS = $parseObj->setup;
1457
            $cache->set($hash, $this->userTS, ['UserTSconfig'], 0);
1458
            // Ensure to update UC later
1459
            $this->userTSUpdated = true;
1460
        }
1461
    }
1462
1463
    /**
1464
     * Fetches the group records, subgroups and fills internal arrays.
1465
     * Function is called recursively to fetch subgroups
1466
     *
1467
     * @param string $grList Commalist of be_groups uid numbers
1468
     * @param string $idList List of already processed be_groups-uids so the function will not fall into an eternal recursion.
1469
     * @internal
1470
     */
1471
    public function fetchGroups($grList, $idList = '')
1472
    {
1473
        // Fetching records of the groups in $grList (which are not blocked by lockedToDomain either):
1474
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->usergroup_table);
1475
        $expressionBuilder = $queryBuilder->expr();
1476
        $constraints = $expressionBuilder->andX(
1477
            $expressionBuilder->eq(
1478
                'pid',
1479
                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1480
            ),
1481
            $expressionBuilder->in(
1482
                'uid',
1483
                $queryBuilder->createNamedParameter(
1484
                    GeneralUtility::intExplode(',', $grList),
1485
                    Connection::PARAM_INT_ARRAY
1486
                )
1487
            ),
1488
            $expressionBuilder->orX(
1489
                $expressionBuilder->eq('lockToDomain', $queryBuilder->quote('')),
1490
                $expressionBuilder->isNull('lockToDomain'),
1491
                $expressionBuilder->eq(
1492
                    'lockToDomain',
1493
                    $queryBuilder->createNamedParameter(GeneralUtility::getIndpEnv('HTTP_HOST'), \PDO::PARAM_STR)
1494
                )
1495
            )
1496
        );
1497
        // Hook for manipulation of the WHERE sql sentence which controls which BE-groups are included
1498
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroupQuery'] ?? [] as $className) {
1499
            $hookObj = GeneralUtility::makeInstance($className);
1500
            if (method_exists($hookObj, 'fetchGroupQuery_processQuery')) {
1501
                $constraints = $hookObj->fetchGroupQuery_processQuery($this, $grList, $idList, (string)$constraints);
1502
            }
1503
        }
1504
        $res = $queryBuilder->select('*')
1505
            ->from($this->usergroup_table)
1506
            ->where($constraints)
1507
            ->execute();
1508
        // The userGroups array is filled
1509
        while ($row = $res->fetch(\PDO::FETCH_ASSOC)) {
1510
            $this->userGroups[$row['uid']] = $row;
1511
        }
1512
        // Traversing records in the correct order
1513
        foreach (explode(',', $grList) as $uid) {
1514
            // Get row:
1515
            $row = $this->userGroups[$uid];
1516
            // Must be an array and $uid should not be in the idList, because then it is somewhere previously in the grouplist
1517
            if (is_array($row) && !GeneralUtility::inList($idList, $uid)) {
1518
                // Include sub groups
1519
                if (trim($row['subgroup'])) {
1520
                    // Make integer list
1521
                    $theList = implode(',', GeneralUtility::intExplode(',', $row['subgroup']));
1522
                    // Call recursively, pass along list of already processed groups so they are not recursed again.
1523
                    $this->fetchGroups($theList, $idList . ',' . $uid);
1524
                }
1525
                // Add the group uid, current list to the internal arrays.
1526
                $this->includeGroupArray[] = $uid;
1527
                // Mount group database-mounts
1528
                if (($this->user['options'] & Permission::PAGE_SHOW) == 1) {
1529
                    $this->dataLists['webmount_list'] .= ',' . $row['db_mountpoints'];
1530
                }
1531
                // Mount group file-mounts
1532
                if (($this->user['options'] & Permission::PAGE_EDIT) == 2) {
1533
                    $this->dataLists['filemount_list'] .= ',' . $row['file_mountpoints'];
1534
                }
1535
                // The lists are made: groupMods, tables_select, tables_modify, pagetypes_select, non_exclude_fields, explicit_allowdeny, allowed_languages, custom_options
1536
                $this->dataLists['modList'] .= ',' . $row['groupMods'];
1537
                $this->dataLists['available_widgets'] .= ',' . $row['availableWidgets'];
1538
                $this->dataLists['tables_select'] .= ',' . $row['tables_select'];
1539
                $this->dataLists['tables_modify'] .= ',' . $row['tables_modify'];
1540
                $this->dataLists['pagetypes_select'] .= ',' . $row['pagetypes_select'];
1541
                $this->dataLists['non_exclude_fields'] .= ',' . $row['non_exclude_fields'];
1542
                $this->dataLists['explicit_allowdeny'] .= ',' . $row['explicit_allowdeny'];
1543
                $this->dataLists['allowed_languages'] .= ',' . $row['allowed_languages'];
1544
                $this->dataLists['custom_options'] .= ',' . $row['custom_options'];
1545
                $this->dataLists['file_permissions'] .= ',' . $row['file_permissions'];
1546
                // Setting workspace permissions:
1547
                $this->dataLists['workspace_perms'] |= $row['workspace_perms'];
1548
                // If this function is processing the users OWN group-list (not subgroups) AND
1549
                // if the ->firstMainGroup is not set, then the ->firstMainGroup will be set.
1550
                if ($idList === '' && !$this->firstMainGroup) {
1551
                    $this->firstMainGroup = $uid;
0 ignored issues
show
Documentation Bug introduced by
The property $firstMainGroup was declared of type integer, but $uid is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
1552
                }
1553
            }
1554
        }
1555
        // HOOK: fetchGroups_postProcessing
1556
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroups_postProcessing'] ?? [] as $_funcRef) {
1557
            $_params = [];
1558
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1559
        }
1560
    }
1561
1562
    /**
1563
     * Updates the field be_users.usergroup_cached_list if the groupList of the user
1564
     * has changed/is different from the current list.
1565
     * The field "usergroup_cached_list" contains the list of groups which the user is a member of.
1566
     * After authentication (where these functions are called...) one can depend on this list being
1567
     * a representation of the exact groups/subgroups which the BE_USER has membership with.
1568
     *
1569
     * @param string $cList The newly compiled group-list which must be compared with the current list in the user record and possibly stored if a difference is detected.
1570
     * @internal
1571
     */
1572
    public function setCachedList($cList)
1573
    {
1574
        if ((string)$cList != (string)$this->user['usergroup_cached_list']) {
1575
            GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users')->update(
1576
                'be_users',
1577
                ['usergroup_cached_list' => $cList],
1578
                ['uid' => (int)$this->user['uid']]
1579
            );
1580
        }
1581
    }
1582
1583
    /**
1584
     * Sets up all file storages for a user.
1585
     * Needs to be called AFTER the groups have been loaded.
1586
     */
1587
    protected function initializeFileStorages()
1588
    {
1589
        $this->fileStorages = [];
1590
        /** @var \TYPO3\CMS\Core\Resource\StorageRepository $storageRepository */
1591
        $storageRepository = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\StorageRepository::class);
1592
        // Admin users have all file storages visible, without any filters
1593
        if ($this->isAdmin()) {
1594
            $storageObjects = $storageRepository->findAll();
1595
            foreach ($storageObjects as $storageObject) {
1596
                $this->fileStorages[$storageObject->getUid()] = $storageObject;
1597
            }
1598
        } else {
1599
            // Regular users only have storages that are defined in their filemounts
1600
            // Permissions and file mounts for the storage are added in StoragePermissionAspect
1601
            foreach ($this->getFileMountRecords() as $row) {
1602
                if (!array_key_exists((int)$row['base'], $this->fileStorages)) {
1603
                    $storageObject = $storageRepository->findByUid($row['base']);
1604
                    if ($storageObject) {
1605
                        $this->fileStorages[$storageObject->getUid()] = $storageObject;
1606
                    }
1607
                }
1608
            }
1609
        }
1610
1611
        // This has to be called always in order to set certain filters
1612
        $this->evaluateUserSpecificFileFilterSettings();
1613
    }
1614
1615
    /**
1616
     * Returns an array of category mount points. The category permissions from BE Groups
1617
     * are also taken into consideration and are merged into User permissions.
1618
     *
1619
     * @return array
1620
     */
1621
    public function getCategoryMountPoints()
1622
    {
1623
        $categoryMountPoints = '';
1624
1625
        // Category mounts of the groups
1626
        if (is_array($this->userGroups)) {
0 ignored issues
show
introduced by
The condition is_array($this->userGroups) is always true.
Loading history...
1627
            foreach ($this->userGroups as $group) {
1628
                if ($group['category_perms']) {
1629
                    $categoryMountPoints .= ',' . $group['category_perms'];
1630
                }
1631
            }
1632
        }
1633
1634
        // Category mounts of the user record
1635
        if ($this->user['category_perms']) {
1636
            $categoryMountPoints .= ',' . $this->user['category_perms'];
1637
        }
1638
1639
        // Make the ids unique
1640
        $categoryMountPoints = GeneralUtility::trimExplode(',', $categoryMountPoints);
1641
        $categoryMountPoints = array_filter($categoryMountPoints); // remove empty value
1642
        $categoryMountPoints = array_unique($categoryMountPoints); // remove unique value
1643
1644
        return $categoryMountPoints;
1645
    }
1646
1647
    /**
1648
     * Returns an array of file mount records, taking workspaces and user home and group home directories into account
1649
     * Needs to be called AFTER the groups have been loaded.
1650
     *
1651
     * @return array
1652
     * @internal
1653
     */
1654
    public function getFileMountRecords()
1655
    {
1656
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
1657
        $fileMountRecordCache = $runtimeCache->get('backendUserAuthenticationFileMountRecords') ?: [];
1658
1659
        if (!empty($fileMountRecordCache)) {
1660
            return $fileMountRecordCache;
1661
        }
1662
1663
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
1664
1665
        // Processing file mounts (both from the user and the groups)
1666
        $fileMounts = array_unique(GeneralUtility::intExplode(',', $this->dataLists['filemount_list'], true));
1667
1668
        // Limit file mounts if set in workspace record
1669
        if ($this->workspace > 0 && !empty($this->workspaceRec['file_mountpoints'])) {
1670
            $workspaceFileMounts = GeneralUtility::intExplode(',', $this->workspaceRec['file_mountpoints'], true);
1671
            $fileMounts = array_intersect($fileMounts, $workspaceFileMounts);
1672
        }
1673
1674
        if (!empty($fileMounts)) {
1675
            $orderBy = $GLOBALS['TCA']['sys_filemounts']['ctrl']['default_sortby'] ?? 'sorting';
1676
1677
            $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_filemounts');
1678
            $queryBuilder->getRestrictions()
1679
                ->removeAll()
1680
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1681
                ->add(GeneralUtility::makeInstance(HiddenRestriction::class))
1682
                ->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
1683
1684
            $queryBuilder->select('*')
1685
                ->from('sys_filemounts')
1686
                ->where(
1687
                    $queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($fileMounts, Connection::PARAM_INT_ARRAY))
1688
                );
1689
1690
            foreach (QueryHelper::parseOrderBy($orderBy) as $fieldAndDirection) {
1691
                $queryBuilder->addOrderBy(...$fieldAndDirection);
1692
            }
1693
1694
            $fileMountRecords = $queryBuilder->execute()->fetchAll(\PDO::FETCH_ASSOC);
1695
            if ($fileMountRecords !== false) {
1696
                foreach ($fileMountRecords as $fileMount) {
1697
                    $fileMountRecordCache[$fileMount['base'] . $fileMount['path']] = $fileMount;
1698
                }
1699
            }
1700
        }
1701
1702
        // Read-only file mounts
1703
        $readOnlyMountPoints = \trim($this->getTSConfig()['options.']['folderTree.']['altElementBrowserMountPoints'] ?? '');
1704
        if ($readOnlyMountPoints) {
1705
            // We cannot use the API here but need to fetch the default storage record directly
1706
            // to not instantiate it (which directly applies mount points) before all mount points are resolved!
1707
            $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_file_storage');
1708
            $defaultStorageRow = $queryBuilder->select('uid')
1709
                ->from('sys_file_storage')
1710
                ->where(
1711
                    $queryBuilder->expr()->eq('is_default', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT))
1712
                )
1713
                ->setMaxResults(1)
1714
                ->execute()
1715
                ->fetch(\PDO::FETCH_ASSOC);
1716
1717
            $readOnlyMountPointArray = GeneralUtility::trimExplode(',', $readOnlyMountPoints);
1718
            foreach ($readOnlyMountPointArray as $readOnlyMountPoint) {
1719
                $readOnlyMountPointConfiguration = GeneralUtility::trimExplode(':', $readOnlyMountPoint);
1720
                if (count($readOnlyMountPointConfiguration) === 2) {
1721
                    // A storage is passed in the configuration
1722
                    $storageUid = (int)$readOnlyMountPointConfiguration[0];
1723
                    $path = $readOnlyMountPointConfiguration[1];
1724
                } else {
1725
                    if (empty($defaultStorageRow)) {
1726
                        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);
1727
                    }
1728
                    // Backwards compatibility: If no storage is passed, we use the default storage
1729
                    $storageUid = $defaultStorageRow['uid'];
1730
                    $path = $readOnlyMountPointConfiguration[0];
1731
                }
1732
                $fileMountRecordCache[$storageUid . $path] = [
1733
                    'base' => $storageUid,
1734
                    'title' => $path,
1735
                    'path' => $path,
1736
                    'read_only' => true
1737
                ];
1738
            }
1739
        }
1740
1741
        // Personal or Group filemounts are not accessible if file mount list is set in workspace record
1742
        if ($this->workspace <= 0 || empty($this->workspaceRec['file_mountpoints'])) {
1743
            // If userHomePath is set, we attempt to mount it
1744
            if ($GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath']) {
1745
                [$userHomeStorageUid, $userHomeFilter] = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath'], 2);
1746
                $userHomeStorageUid = (int)$userHomeStorageUid;
1747
                $userHomeFilter = '/' . ltrim($userHomeFilter, '/');
1748
                if ($userHomeStorageUid > 0) {
1749
                    // Try and mount with [uid]_[username]
1750
                    $path = $userHomeFilter . $this->user['uid'] . '_' . $this->user['username'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1751
                    $fileMountRecordCache[$userHomeStorageUid . $path] = [
1752
                        'base' => $userHomeStorageUid,
1753
                        'title' => $this->user['username'],
1754
                        'path' => $path,
1755
                        'read_only' => false,
1756
                        'user_mount' => true
1757
                    ];
1758
                    // Try and mount with only [uid]
1759
                    $path = $userHomeFilter . $this->user['uid'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1760
                    $fileMountRecordCache[$userHomeStorageUid . $path] = [
1761
                        'base' => $userHomeStorageUid,
1762
                        'title' => $this->user['username'],
1763
                        'path' => $path,
1764
                        'read_only' => false,
1765
                        'user_mount' => true
1766
                    ];
1767
                }
1768
            }
1769
1770
            // Mount group home-dirs
1771
            if ((is_array($this->user) && $this->user['options'] & Permission::PAGE_EDIT) == 2 && $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'] != '') {
1772
                // If groupHomePath is set, we attempt to mount it
1773
                [$groupHomeStorageUid, $groupHomeFilter] = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'], 2);
1774
                $groupHomeStorageUid = (int)$groupHomeStorageUid;
1775
                $groupHomeFilter = '/' . ltrim($groupHomeFilter, '/');
1776
                if ($groupHomeStorageUid > 0) {
1777
                    foreach ($this->userGroups as $groupData) {
1778
                        $path = $groupHomeFilter . $groupData['uid'];
1779
                        $fileMountRecordCache[$groupHomeStorageUid . $path] = [
1780
                            'base' => $groupHomeStorageUid,
1781
                            'title' => $groupData['title'],
1782
                            'path' => $path,
1783
                            'read_only' => false,
1784
                            'user_mount' => true
1785
                        ];
1786
                    }
1787
                }
1788
            }
1789
        }
1790
1791
        $runtimeCache->set('backendUserAuthenticationFileMountRecords', $fileMountRecordCache);
1792
        return $fileMountRecordCache;
1793
    }
1794
1795
    /**
1796
     * Returns an array with the filemounts for the user.
1797
     * Each filemount is represented with an array of a "name", "path" and "type".
1798
     * If no filemounts an empty array is returned.
1799
     *
1800
     * @return \TYPO3\CMS\Core\Resource\ResourceStorage[]
1801
     */
1802
    public function getFileStorages()
1803
    {
1804
        // Initializing file mounts after the groups are fetched
1805
        if ($this->fileStorages === null) {
1806
            $this->initializeFileStorages();
1807
        }
1808
        return $this->fileStorages;
1809
    }
1810
1811
    /**
1812
     * Adds filters based on what the user has set
1813
     * this should be done in this place, and called whenever needed,
1814
     * but only when needed
1815
     */
1816
    public function evaluateUserSpecificFileFilterSettings()
1817
    {
1818
        // Add the option for also displaying the non-hidden files
1819
        if ($this->uc['showHiddenFilesAndFolders']) {
1820
            \TYPO3\CMS\Core\Resource\Filter\FileNameFilter::setShowHiddenFilesAndFolders(true);
1821
        }
1822
    }
1823
1824
    /**
1825
     * Returns the information about file permissions.
1826
     * Previously, this was stored in the DB field fileoper_perms now it is file_permissions.
1827
     * Besides it can be handled via userTSconfig
1828
     *
1829
     * permissions.file.default {
1830
     * addFile = 1
1831
     * readFile = 1
1832
     * writeFile = 1
1833
     * copyFile = 1
1834
     * moveFile = 1
1835
     * renameFile = 1
1836
     * deleteFile = 1
1837
     *
1838
     * addFolder = 1
1839
     * readFolder = 1
1840
     * writeFolder = 1
1841
     * copyFolder = 1
1842
     * moveFolder = 1
1843
     * renameFolder = 1
1844
     * deleteFolder = 1
1845
     * recursivedeleteFolder = 1
1846
     * }
1847
     *
1848
     * # overwrite settings for a specific storageObject
1849
     * permissions.file.storage.StorageUid {
1850
     * readFile = 1
1851
     * recursivedeleteFolder = 0
1852
     * }
1853
     *
1854
     * Please note that these permissions only apply, if the storage has the
1855
     * capabilities (browseable, writable), and if the driver allows for writing etc
1856
     *
1857
     * @return array
1858
     */
1859
    public function getFilePermissions()
1860
    {
1861
        if (!isset($this->filePermissions)) {
1862
            $filePermissions = [
1863
                // File permissions
1864
                'addFile' => false,
1865
                'readFile' => false,
1866
                'writeFile' => false,
1867
                'copyFile' => false,
1868
                'moveFile' => false,
1869
                'renameFile' => false,
1870
                'deleteFile' => false,
1871
                // Folder permissions
1872
                'addFolder' => false,
1873
                'readFolder' => false,
1874
                'writeFolder' => false,
1875
                'copyFolder' => false,
1876
                'moveFolder' => false,
1877
                'renameFolder' => false,
1878
                'deleteFolder' => false,
1879
                'recursivedeleteFolder' => false
1880
            ];
1881
            if ($this->isAdmin()) {
1882
                $filePermissions = array_map('is_bool', $filePermissions);
1883
            } else {
1884
                $userGroupRecordPermissions = GeneralUtility::trimExplode(',', $this->groupData['file_permissions'] ?? '', true);
1885
                array_walk(
1886
                    $userGroupRecordPermissions,
1887
                    function ($permission) use (&$filePermissions) {
1888
                        $filePermissions[$permission] = true;
1889
                    }
1890
                );
1891
1892
                // Finally overlay any userTSconfig
1893
                $permissionsTsConfig = $this->getTSConfig()['permissions.']['file.']['default.'] ?? [];
1894
                if (!empty($permissionsTsConfig)) {
1895
                    array_walk(
1896
                        $permissionsTsConfig,
1897
                        function ($value, $permission) use (&$filePermissions) {
1898
                            $filePermissions[$permission] = (bool)$value;
1899
                        }
1900
                    );
1901
                }
1902
            }
1903
            $this->filePermissions = $filePermissions;
1904
        }
1905
        return $this->filePermissions;
1906
    }
1907
1908
    /**
1909
     * Gets the file permissions for a storage
1910
     * by merging any storage-specific permissions for a
1911
     * storage with the default settings.
1912
     * Admin users will always get the default settings.
1913
     *
1914
     * @param \TYPO3\CMS\Core\Resource\ResourceStorage $storageObject
1915
     * @return array
1916
     */
1917
    public function getFilePermissionsForStorage(\TYPO3\CMS\Core\Resource\ResourceStorage $storageObject)
1918
    {
1919
        $finalUserPermissions = $this->getFilePermissions();
1920
        if (!$this->isAdmin()) {
1921
            $storageFilePermissions = $this->getTSConfig()['permissions.']['file.']['storage.'][$storageObject->getUid() . '.'] ?? [];
1922
            if (!empty($storageFilePermissions)) {
1923
                array_walk(
1924
                    $storageFilePermissions,
1925
                    function ($value, $permission) use (&$finalUserPermissions) {
1926
                        $finalUserPermissions[$permission] = (bool)$value;
1927
                    }
1928
                );
1929
            }
1930
        }
1931
        return $finalUserPermissions;
1932
    }
1933
1934
    /**
1935
     * Returns a \TYPO3\CMS\Core\Resource\Folder object that is used for uploading
1936
     * files by default.
1937
     * This is used for RTE and its magic images, as well as uploads
1938
     * in the TCEforms fields.
1939
     *
1940
     * The default upload folder for a user is the defaultFolder on the first
1941
     * filestorage/filemount that the user can access and to which files are allowed to be added
1942
     * however, you can set the users' upload folder like this:
1943
     *
1944
     * options.defaultUploadFolder = 3:myfolder/yourfolder/
1945
     *
1946
     * @param int $pid PageUid
1947
     * @param string $table Table name
1948
     * @param string $field Field name
1949
     * @return \TYPO3\CMS\Core\Resource\Folder|bool The default upload folder for this user
1950
     */
1951
    public function getDefaultUploadFolder($pid = null, $table = null, $field = null)
1952
    {
1953
        $uploadFolder = $this->getTSConfig()['options.']['defaultUploadFolder'] ?? '';
1954
        if ($uploadFolder) {
1955
            $uploadFolder = GeneralUtility::makeInstance(ResourceFactory::class)->getFolderObjectFromCombinedIdentifier($uploadFolder);
1956
        } else {
1957
            foreach ($this->getFileStorages() as $storage) {
1958
                if ($storage->isDefault() && $storage->isWritable()) {
1959
                    try {
1960
                        $uploadFolder = $storage->getDefaultFolder();
1961
                        if ($uploadFolder->checkActionPermission('write')) {
1962
                            break;
1963
                        }
1964
                        $uploadFolder = null;
1965
                    } catch (\TYPO3\CMS\Core\Resource\Exception $folderAccessException) {
1966
                        // If the folder is not accessible (no permissions / does not exist) we skip this one.
1967
                    }
1968
                    break;
1969
                }
1970
            }
1971
            if (!$uploadFolder instanceof \TYPO3\CMS\Core\Resource\Folder) {
1972
                /** @var ResourceStorage $storage */
1973
                foreach ($this->getFileStorages() as $storage) {
1974
                    if ($storage->isWritable()) {
1975
                        try {
1976
                            $uploadFolder = $storage->getDefaultFolder();
1977
                            if ($uploadFolder->checkActionPermission('write')) {
1978
                                break;
1979
                            }
1980
                            $uploadFolder = null;
1981
                        } catch (\TYPO3\CMS\Core\Resource\Exception $folderAccessException) {
1982
                            // If the folder is not accessible (no permissions / does not exist) try the next one.
1983
                        }
1984
                    }
1985
                }
1986
            }
1987
        }
1988
1989
        // HOOK: getDefaultUploadFolder
1990
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getDefaultUploadFolder'] ?? [] as $_funcRef) {
1991
            $_params = [
1992
                'uploadFolder' => $uploadFolder,
1993
                'pid' => $pid,
1994
                'table' => $table,
1995
                'field' => $field,
1996
            ];
1997
            $uploadFolder = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1998
        }
1999
2000
        if ($uploadFolder instanceof \TYPO3\CMS\Core\Resource\Folder) {
2001
            return $uploadFolder;
2002
        }
2003
        return false;
2004
    }
2005
2006
    /**
2007
     * Returns a \TYPO3\CMS\Core\Resource\Folder object that could be used for uploading
2008
     * temporary files in user context. The folder _temp_ below the default upload folder
2009
     * of the user is used.
2010
     *
2011
     * @return \TYPO3\CMS\Core\Resource\Folder|null
2012
     * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::getDefaultUploadFolder()
2013
     */
2014
    public function getDefaultUploadTemporaryFolder()
2015
    {
2016
        $defaultTemporaryFolder = null;
2017
        $defaultFolder = $this->getDefaultUploadFolder();
2018
2019
        if ($defaultFolder !== false) {
2020
            $tempFolderName = '_temp_';
2021
            $createFolder = !$defaultFolder->hasFolder($tempFolderName);
2022
            if ($createFolder === true) {
2023
                try {
2024
                    $defaultTemporaryFolder = $defaultFolder->createFolder($tempFolderName);
2025
                } catch (\TYPO3\CMS\Core\Resource\Exception $folderAccessException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
2026
                }
2027
            } else {
2028
                $defaultTemporaryFolder = $defaultFolder->getSubfolder($tempFolderName);
2029
            }
2030
        }
2031
2032
        return $defaultTemporaryFolder;
2033
    }
2034
2035
    /**
2036
     * Initializing workspace.
2037
     * Called from within this function, see fetchGroupData()
2038
     *
2039
     * @see fetchGroupData()
2040
     */
2041
    public function workspaceInit()
2042
    {
2043
        // Initializing workspace by evaluating and setting the workspace, possibly updating it in the user record!
2044
        $this->setWorkspace($this->user['workspace_id']);
2045
        // Limiting the DB mountpoints if there any selected in the workspace record
2046
        $this->initializeDbMountpointsInWorkspace();
2047
        $allowed_languages = $this->getTSConfig()['options.']['workspaces.']['allowed_languages.'][$this->workspace] ?? '';
2048
        if (!empty($allowed_languages)) {
2049
            $this->groupData['allowed_languages'] = GeneralUtility::uniqueList($allowed_languages);
2050
        }
2051
    }
2052
2053
    /**
2054
     * Limiting the DB mountpoints if there any selected in the workspace record
2055
     */
2056
    protected function initializeDbMountpointsInWorkspace()
2057
    {
2058
        $dbMountpoints = trim($this->workspaceRec['db_mountpoints'] ?? '');
2059
        if ($this->workspace > 0 && $dbMountpoints != '') {
2060
            $filteredDbMountpoints = [];
2061
            // Notice: We cannot call $this->getPagePermsClause(1);
2062
            // as usual because the group-list is not available at this point.
2063
            // But bypassing is fine because all we want here is check if the
2064
            // workspace mounts are inside the current webmounts rootline.
2065
            // The actual permission checking on page level is done elsewhere
2066
            // as usual anyway before the page tree is rendered.
2067
            $readPerms = '1=1';
2068
            // Traverse mount points of the
2069
            $dbMountpoints = GeneralUtility::intExplode(',', $dbMountpoints);
2070
            foreach ($dbMountpoints as $mpId) {
2071
                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...
Bug introduced by
$mpId of type string is incompatible with the type array|integer expected by parameter $idOrRow of TYPO3\CMS\Core\Authentic...ication::isInWebMount(). ( Ignorable by Annotation )

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

2071
                if ($this->isInWebMount(/** @scrutinizer ignore-type */ $mpId, $readPerms)) {
Loading history...
2072
                    $filteredDbMountpoints[] = $mpId;
2073
                }
2074
            }
2075
            // Re-insert webmounts:
2076
            $filteredDbMountpoints = array_unique($filteredDbMountpoints);
2077
            $this->groupData['webmounts'] = implode(',', $filteredDbMountpoints);
2078
        }
2079
    }
2080
2081
    /**
2082
     * Checking if a workspace is allowed for backend user
2083
     *
2084
     * @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)
2085
     * @param string $fields List of fields to select. Default fields are all
2086
     * @return array Output will also show how access was granted. Admin users will have a true output regardless of input.
2087
     */
2088
    public function checkWorkspace($wsRec, $fields = '*')
2089
    {
2090
        $retVal = false;
2091
        // If not array, look up workspace record:
2092
        if (!is_array($wsRec)) {
2093
            switch ((string)$wsRec) {
2094
                case '0':
2095
                    $wsRec = ['uid' => $wsRec];
2096
                    break;
2097
                default:
2098
                    if (ExtensionManagementUtility::isLoaded('workspaces')) {
2099
                        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
2100
                        $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
2101
                        $wsRec = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields))
2102
                            ->from('sys_workspace')
2103
                            ->where($queryBuilder->expr()->eq(
2104
                                'uid',
2105
                                $queryBuilder->createNamedParameter($wsRec, \PDO::PARAM_INT)
2106
                            ))
2107
                            ->orderBy('title')
2108
                            ->setMaxResults(1)
2109
                            ->execute()
2110
                            ->fetch(\PDO::FETCH_ASSOC);
2111
                    }
2112
            }
2113
        }
2114
        // If wsRec is set to an array, evaluate it:
2115
        if (is_array($wsRec)) {
2116
            if ($this->isAdmin()) {
2117
                return array_merge($wsRec, ['_ACCESS' => 'admin']);
2118
            }
2119
            switch ((string)$wsRec['uid']) {
2120
                    case '0':
2121
                        $retVal = $this->groupData['workspace_perms'] & Permission::PAGE_SHOW
2122
                            ? array_merge($wsRec, ['_ACCESS' => 'online'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
2123
                            : false;
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
2124
                        break;
2125
                    default:
2126
                        // Checking if the guy is admin:
2127
                        if (GeneralUtility::inList($wsRec['adminusers'], 'be_users_' . $this->user['uid'])) {
2128
                            return array_merge($wsRec, ['_ACCESS' => 'owner']);
2129
                        }
2130
                        // Checking if he is owner through a user group of his:
2131
                        foreach ($this->userGroupsUID as $groupUid) {
2132
                            if (GeneralUtility::inList($wsRec['adminusers'], 'be_groups_' . $groupUid)) {
2133
                                return array_merge($wsRec, ['_ACCESS' => 'owner']);
2134
                            }
2135
                        }
2136
                        // Checking if he is member as user:
2137
                        if (GeneralUtility::inList($wsRec['members'], 'be_users_' . $this->user['uid'])) {
2138
                            return array_merge($wsRec, ['_ACCESS' => 'member']);
2139
                        }
2140
                        // Checking if he is member through a user group of his:
2141
                        foreach ($this->userGroupsUID as $groupUid) {
2142
                            if (GeneralUtility::inList($wsRec['members'], 'be_groups_' . $groupUid)) {
2143
                                return array_merge($wsRec, ['_ACCESS' => 'member']);
2144
                            }
2145
                        }
2146
                }
0 ignored issues
show
Coding Style introduced by
Closing brace indented incorrectly; expected 12 spaces, found 16
Loading history...
2147
        }
2148
        return $retVal;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $retVal could also return false which is incompatible with the documented return type array. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
2149
    }
2150
2151
    /**
2152
     * Uses checkWorkspace() to check if current workspace is available for user.
2153
     * This function caches the result and so can be called many times with no performance loss.
2154
     *
2155
     * @return array See checkWorkspace()
2156
     * @see checkWorkspace()
2157
     */
2158
    public function checkWorkspaceCurrent()
2159
    {
2160
        if (!isset($this->checkWorkspaceCurrent_cache)) {
2161
            $this->checkWorkspaceCurrent_cache = $this->checkWorkspace($this->workspace);
2162
        }
2163
        return $this->checkWorkspaceCurrent_cache;
2164
    }
2165
2166
    /**
2167
     * Setting workspace ID
2168
     *
2169
     * @param int $workspaceId ID of workspace to set for backend user. If not valid the default workspace for BE user is found and set.
2170
     */
2171
    public function setWorkspace($workspaceId)
2172
    {
2173
        // Check workspace validity and if not found, revert to default workspace.
2174
        if (!$this->setTemporaryWorkspace($workspaceId)) {
2175
            $this->setDefaultWorkspace();
2176
        }
2177
        // Unset access cache:
2178
        $this->checkWorkspaceCurrent_cache = null;
2179
        // If ID is different from the stored one, change it:
2180
        if ((int)$this->workspace !== (int)$this->user['workspace_id']) {
2181
            $this->user['workspace_id'] = $this->workspace;
2182
            GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users')->update(
2183
                'be_users',
2184
                ['workspace_id' => $this->user['workspace_id']],
2185
                ['uid' => (int)$this->user['uid']]
2186
            );
2187
            $this->writelog(SystemLogType::EXTENSION, SystemLogGenericAction::UNDEFINED, SystemLogErrorClassification::MESSAGE, 0, 'User changed workspace to "' . $this->workspace . '"', []);
2188
        }
2189
    }
2190
2191
    /**
2192
     * Sets a temporary workspace in the context of the current backend user.
2193
     *
2194
     * @param int $workspaceId
2195
     * @return bool
2196
     */
2197
    public function setTemporaryWorkspace($workspaceId)
2198
    {
2199
        $result = false;
2200
        $workspaceRecord = $this->checkWorkspace($workspaceId);
2201
2202
        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...
2203
            $this->workspaceRec = $workspaceRecord;
2204
            $this->workspace = (int)$workspaceId;
2205
            $result = true;
2206
        }
2207
2208
        return $result;
2209
    }
2210
2211
    /**
2212
     * Sets the default workspace in the context of the current backend user.
2213
     */
2214
    public function setDefaultWorkspace()
2215
    {
2216
        $this->workspace = (int)$this->getDefaultWorkspace();
2217
        $this->workspaceRec = $this->checkWorkspace($this->workspace);
2218
    }
2219
2220
    /**
2221
     * Return default workspace ID for user,
2222
     * if EXT:workspaces is not installed the user will be pushed to the
2223
     * Live workspace, if he has access to. If no workspace is available for the user, the workspace ID is set to "-99"
2224
     *
2225
     * @return int Default workspace id.
2226
     */
2227
    public function getDefaultWorkspace()
2228
    {
2229
        if (!ExtensionManagementUtility::isLoaded('workspaces')) {
2230
            return 0;
2231
        }
2232
        // Online is default
2233
        if ($this->checkWorkspace(0)) {
2234
            return 0;
2235
        }
2236
        // Otherwise -99 is the fallback
2237
        $defaultWorkspace = -99;
2238
        // Traverse all workspaces
2239
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
2240
        $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
2241
        $result = $queryBuilder->select('*')
2242
            ->from('sys_workspace')
2243
            ->orderBy('title')
2244
            ->execute();
2245
        while ($workspaceRecord = $result->fetch()) {
2246
            if ($this->checkWorkspace($workspaceRecord)) {
2247
                $defaultWorkspace = (int)$workspaceRecord['uid'];
2248
                break;
2249
            }
2250
        }
2251
        return $defaultWorkspace;
2252
    }
2253
2254
    /**
2255
     * Writes an entry in the logfile/table
2256
     * Documentation in "TYPO3 Core API"
2257
     *
2258
     * @param int $type Denotes which module that has submitted the entry. See "TYPO3 Core API". Use "4" for extensions.
2259
     * @param int $action Denotes which specific operation that wrote the entry. Use "0" when no sub-categorizing applies
2260
     * @param int $error Flag. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin)
2261
     * @param int $details_nr The message number. Specific for each $type and $action. This will make it possible to translate errormessages to other languages
2262
     * @param string $details Default text that follows the message (in english!). Possibly translated by identification through type/action/details_nr
2263
     * @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
2264
     * @param string $tablename Table name. Special field used by tce_main.php.
2265
     * @param int|string $recuid Record UID. Special field used by tce_main.php.
2266
     * @param int|string $recpid Record PID. Special field used by tce_main.php. OBSOLETE
2267
     * @param int $event_pid The page_uid (pid) where the event occurred. Used to select log-content for specific pages.
2268
     * @param string $NEWid Special field used by tce_main.php. NEWid string of newly created records.
2269
     * @param int $userId Alternative Backend User ID (used for logging login actions where this is not yet known).
2270
     * @return int Log entry ID.
2271
     */
2272
    public function writelog($type, $action, $error, $details_nr, $details, $data, $tablename = '', $recuid = '', $recpid = '', $event_pid = -1, $NEWid = '', $userId = 0)
2273
    {
2274
        if (!$userId && !empty($this->user['uid'])) {
2275
            $userId = $this->user['uid'];
2276
        }
2277
2278
        if (!empty($this->user['ses_backuserid'])) {
2279
            if (empty($data)) {
2280
                $data = [];
2281
            }
2282
            $data['originalUser'] = $this->user['ses_backuserid'];
2283
        }
2284
2285
        $fields = [
2286
            'userid' => (int)$userId,
2287
            'type' => (int)$type,
2288
            'action' => (int)$action,
2289
            'error' => (int)$error,
2290
            'details_nr' => (int)$details_nr,
2291
            'details' => $details,
2292
            'log_data' => serialize($data),
2293
            'tablename' => $tablename,
2294
            'recuid' => (int)$recuid,
2295
            'IP' => (string)GeneralUtility::getIndpEnv('REMOTE_ADDR'),
2296
            'tstamp' => $GLOBALS['EXEC_TIME'] ?? time(),
2297
            'event_pid' => (int)$event_pid,
2298
            'NEWid' => $NEWid,
2299
            'workspace' => $this->workspace
2300
        ];
2301
2302
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_log');
2303
        $connection->insert(
2304
            'sys_log',
2305
            $fields,
2306
            [
2307
                \PDO::PARAM_INT,
2308
                \PDO::PARAM_INT,
2309
                \PDO::PARAM_INT,
2310
                \PDO::PARAM_INT,
2311
                \PDO::PARAM_INT,
2312
                \PDO::PARAM_STR,
2313
                \PDO::PARAM_STR,
2314
                \PDO::PARAM_STR,
2315
                \PDO::PARAM_INT,
2316
                \PDO::PARAM_STR,
2317
                \PDO::PARAM_INT,
2318
                \PDO::PARAM_INT,
2319
                \PDO::PARAM_STR,
2320
                \PDO::PARAM_STR,
2321
            ]
2322
        );
2323
2324
        return (int)$connection->lastInsertId('sys_log');
2325
    }
2326
2327
    /**
2328
     * Sends a warning to $email if there has been a certain amount of failed logins during a period.
2329
     * If a login fails, this function is called. It will look up the sys_log to see if there
2330
     * have been more than $max failed logins the last $secondsBack seconds (default 3600).
2331
     * If so, an email with a warning is sent to $email.
2332
     *
2333
     * @param string $email Email address
2334
     * @param int $secondsBack Number of sections back in time to check. This is a kind of limit for how many failures an hour for instance.
2335
     * @param int $max Max allowed failures before a warning mail is sent
2336
     * @internal
2337
     */
2338
    public function checkLogFailures($email, $secondsBack = 3600, $max = 3)
2339
    {
2340
        if (!GeneralUtility::validEmail($email)) {
2341
            return;
2342
        }
2343
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
2344
2345
        // Get last flag set in the log for sending
2346
        $theTimeBack = $GLOBALS['EXEC_TIME'] - $secondsBack;
2347
        $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
2348
        $queryBuilder->select('tstamp')
2349
            ->from('sys_log')
2350
            ->where(
2351
                $queryBuilder->expr()->eq(
2352
                    'type',
2353
                    $queryBuilder->createNamedParameter(SystemLogType::LOGIN, \PDO::PARAM_INT)
2354
                ),
2355
                $queryBuilder->expr()->eq(
2356
                    'action',
2357
                    $queryBuilder->createNamedParameter(SystemLogLoginAction::SEND_FAILURE_WARNING_EMAIL, \PDO::PARAM_INT)
2358
                ),
2359
                $queryBuilder->expr()->gt(
2360
                    'tstamp',
2361
                    $queryBuilder->createNamedParameter($theTimeBack, \PDO::PARAM_INT)
2362
                )
2363
            )
2364
            ->orderBy('tstamp', 'DESC')
2365
            ->setMaxResults(1);
2366
        if ($testRow = $queryBuilder->execute()->fetch(\PDO::FETCH_ASSOC)) {
2367
            $theTimeBack = $testRow['tstamp'];
2368
        }
2369
2370
        $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
2371
        $result = $queryBuilder->select('*')
2372
            ->from('sys_log')
2373
            ->where(
2374
                $queryBuilder->expr()->eq(
2375
                    'type',
2376
                    $queryBuilder->createNamedParameter(SystemLogType::LOGIN, \PDO::PARAM_INT)
2377
                ),
2378
                $queryBuilder->expr()->eq(
2379
                    'action',
2380
                    $queryBuilder->createNamedParameter(SystemLogLoginAction::ATTEMPT, \PDO::PARAM_INT)
2381
                ),
2382
                $queryBuilder->expr()->neq(
2383
                    'error',
2384
                    $queryBuilder->createNamedParameter(SystemLogErrorClassification::MESSAGE, \PDO::PARAM_INT)
2385
                ),
2386
                $queryBuilder->expr()->gt(
2387
                    'tstamp',
2388
                    $queryBuilder->createNamedParameter($theTimeBack, \PDO::PARAM_INT)
2389
                )
2390
            )
2391
            ->orderBy('tstamp')
2392
            ->execute();
2393
2394
        $rowCount = $queryBuilder
2395
            ->count('uid')
2396
            ->execute()
2397
            ->fetchColumn(0);
2398
        // Check for more than $max number of error failures with the last period.
2399
        if ($rowCount > $max) {
2400
            // OK, so there were more than the max allowed number of login failures - so we will send an email then.
2401
            $this->sendLoginAttemptEmail($result, $email);
2402
            // Login failure attempt written to log
2403
            $this->writelog(SystemLogType::LOGIN, SystemLogLoginAction::SEND_FAILURE_WARNING_EMAIL, SystemLogErrorClassification::MESSAGE, 3, 'Failure warning (%s failures within %s seconds) sent by email to %s', [$rowCount, $secondsBack, $email]);
2404
        }
2405
    }
2406
2407
    /**
2408
     * Sends out an email if the number of attempts have exceeded a limit.
2409
     *
2410
     * @param Statement $result
2411
     * @param string $emailAddress
2412
     */
2413
    protected function sendLoginAttemptEmail(Statement $result, string $emailAddress): void
2414
    {
2415
        $emailData = [];
2416
        while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
2417
            $theData = unserialize($row['log_data'], ['allowed_classes' => false]);
2418
            $text = @sprintf($row['details'], (string)$theData[0], (string)$theData[1], (string)$theData[2]);
2419
            if ((int)$row['type'] === SystemLogType::LOGIN) {
2420
                $text = str_replace('###IP###', $row['IP'], $text);
2421
            }
2422
            $emailData[] = [
2423
                'row' => $row,
2424
                'text' => $text
2425
            ];
2426
        }
2427
        $email = GeneralUtility::makeInstance(FluidEmail::class)
2428
            ->to($emailAddress)
2429
            ->setTemplate('Security/LoginAttemptFailedWarning')
2430
            ->assign('lines', $emailData);
2431
        if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
2432
            $email->setRequest($GLOBALS['TYPO3_REQUEST']);
2433
        }
2434
        GeneralUtility::makeInstance(Mailer::class)->send($email);
2435
    }
2436
2437
    /**
2438
     * Getter for the cookie name
2439
     *
2440
     * @static
2441
     * @return string returns the configured cookie name
2442
     */
2443
    public static function getCookieName()
2444
    {
2445
        $configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']);
2446
        if (empty($configuredCookieName)) {
2447
            $configuredCookieName = 'be_typo_user';
2448
        }
2449
        return $configuredCookieName;
2450
    }
2451
2452
    /**
2453
     * If TYPO3_CONF_VARS['BE']['enabledBeUserIPLock'] is enabled and
2454
     * an IP-list is found in the User TSconfig objString "options.lockToIP",
2455
     * then make an IP comparison with REMOTE_ADDR and check if the IP address matches
2456
     *
2457
     * @return bool TRUE, if IP address validates OK (or no check is done at all because no restriction is set)
2458
     */
2459
    public function checkLockToIP()
2460
    {
2461
        $isValid = true;
2462
        if ($GLOBALS['TYPO3_CONF_VARS']['BE']['enabledBeUserIPLock']) {
2463
            $IPList = trim($this->getTSConfig()['options.']['lockToIP'] ?? '');
2464
            if (!empty($IPList)) {
2465
                $isValid = GeneralUtility::cmpIP(GeneralUtility::getIndpEnv('REMOTE_ADDR'), $IPList);
2466
            }
2467
        }
2468
        return $isValid;
2469
    }
2470
2471
    /**
2472
     * Check if user is logged in and if so, call ->fetchGroupData() to load group information and
2473
     * access lists of all kind, further check IP, set the ->uc array.
2474
     * If no user is logged in the default behaviour is to exit with an error message.
2475
     * This function is called right after ->start() in fx. the TYPO3 Bootstrap.
2476
     *
2477
     * @param bool $proceedIfNoUserIsLoggedIn if this option is set, then there won't be a redirect to the login screen of the Backend - used for areas in the backend which do not need user rights like the login page.
2478
     * @throws \RuntimeException
2479
     */
2480
    public function backendCheckLogin($proceedIfNoUserIsLoggedIn = false)
2481
    {
2482
        if (empty($this->user['uid'])) {
2483
            if ($proceedIfNoUserIsLoggedIn === false) {
2484
                $url = GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir;
2485
                \TYPO3\CMS\Core\Utility\HttpUtility::redirect($url);
2486
            }
2487
        } else {
2488
            // ...and if that's the case, call these functions
2489
            $this->fetchGroupData();
2490
            // The groups are fetched and ready for permission checking in this initialization.
2491
            // Tables.php must be read before this because stuff like the modules has impact in this
2492
            if ($this->checkLockToIP()) {
2493
                if ($this->isUserAllowedToLogin()) {
2494
                    // Setting the UC array. It's needed with fetchGroupData first, due to default/overriding of values.
2495
                    $this->backendSetUC();
2496
                    if ($this->loginSessionStarted) {
2497
                        // Process hooks
2498
                        $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['backendUserLogin'];
2499
                        foreach ($hooks ?? [] as $_funcRef) {
2500
                            $_params = ['user' => $this->user];
2501
                            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2502
                        }
2503
                    }
2504
                } else {
2505
                    throw new \RuntimeException('Login Error: TYPO3 is in maintenance mode at the moment. Only administrators are allowed access.', 1294585860);
2506
                }
2507
            } else {
2508
                throw new \RuntimeException('Login Error: IP locking prevented you from being authorized. Can\'t proceed, sorry.', 1294585861);
2509
            }
2510
        }
2511
    }
2512
2513
    /**
2514
     * Initialize the internal ->uc array for the backend user
2515
     * Will make the overrides if necessary, and write the UC back to the be_users record if changes has happened
2516
     *
2517
     * @internal
2518
     */
2519
    public function backendSetUC()
2520
    {
2521
        // UC - user configuration is a serialized array inside the user object
2522
        // If there is a saved uc we implement that instead of the default one.
2523
        $this->unpack_uc();
2524
        // Setting defaults if uc is empty
2525
        $updated = false;
2526
        $originalUc = [];
2527
        if (is_array($this->uc) && isset($this->uc['ucSetByInstallTool'])) {
2528
            $originalUc = $this->uc;
2529
            unset($originalUc['ucSetByInstallTool'], $this->uc);
2530
        }
2531
        if (!is_array($this->uc)) {
2532
            $this->uc = array_merge(
2533
                $this->uc_default,
2534
                (array)$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUC'],
2535
                GeneralUtility::removeDotsFromTS((array)($this->getTSConfig()['setup.']['default.'] ?? [])),
2536
                $originalUc
2537
            );
2538
            $this->overrideUC();
2539
            $updated = true;
2540
        }
2541
        // If TSconfig is updated, update the defaultUC.
2542
        if ($this->userTSUpdated) {
2543
            $this->overrideUC();
2544
            $updated = true;
2545
        }
2546
        // Setting default lang from be_user record.
2547
        if (!isset($this->uc['lang'])) {
2548
            $this->uc['lang'] = $this->user['lang'];
2549
            $updated = true;
2550
        }
2551
        // Setting the time of the first login:
2552
        if (!isset($this->uc['firstLoginTimeStamp'])) {
2553
            $this->uc['firstLoginTimeStamp'] = $GLOBALS['EXEC_TIME'];
2554
            $updated = true;
2555
        }
2556
        // Saving if updated.
2557
        if ($updated) {
2558
            $this->writeUC();
2559
        }
2560
    }
2561
2562
    /**
2563
     * Override: Call this function every time the uc is updated.
2564
     * That is 1) by reverting to default values, 2) in the setup-module, 3) userTS changes (userauthgroup)
2565
     *
2566
     * @internal
2567
     */
2568
    public function overrideUC()
2569
    {
2570
        $this->uc = array_merge((array)$this->uc, (array)($this->getTSConfig()['setup.']['override.'] ?? []));
2571
    }
2572
2573
    /**
2574
     * Clears the user[uc] and ->uc to blank strings. Then calls ->backendSetUC() to fill it again with reset contents
2575
     *
2576
     * @internal
2577
     */
2578
    public function resetUC()
2579
    {
2580
        $this->user['uc'] = '';
2581
        $this->uc = '';
0 ignored issues
show
Documentation Bug introduced by
It seems like '' of type string is incompatible with the declared type array of property $uc.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2582
        $this->backendSetUC();
2583
    }
2584
2585
    /**
2586
     * Determines whether a backend user is allowed to access the backend.
2587
     *
2588
     * The conditions are:
2589
     * + backend user is a regular user and adminOnly is not defined
2590
     * + backend user is an admin user
2591
     * + backend user is used in CLI context and adminOnly is explicitly set to "2" (see CommandLineUserAuthentication)
2592
     * + backend user is being controlled by an admin user
2593
     *
2594
     * @return bool Whether a backend user is allowed to access the backend
2595
     */
2596
    protected function isUserAllowedToLogin()
2597
    {
2598
        $isUserAllowedToLogin = false;
2599
        $adminOnlyMode = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'];
2600
        // Backend user is allowed if adminOnly is not set or user is an admin:
2601
        if (!$adminOnlyMode || $this->isAdmin()) {
2602
            $isUserAllowedToLogin = true;
2603
        } elseif ($this->user['ses_backuserid']) {
2604
            $backendUserId = (int)$this->user['ses_backuserid'];
2605
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
2606
            $isUserAllowedToLogin = (bool)$queryBuilder->count('uid')
2607
                ->from('be_users')
2608
                ->where(
2609
                    $queryBuilder->expr()->eq(
2610
                        'uid',
2611
                        $queryBuilder->createNamedParameter($backendUserId, \PDO::PARAM_INT)
2612
                    ),
2613
                    $queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT))
2614
                )
2615
                ->execute()
2616
                ->fetchColumn(0);
2617
        }
2618
        return $isUserAllowedToLogin;
2619
    }
2620
2621
    /**
2622
     * Logs out the current user and clears the form protection tokens.
2623
     */
2624
    public function logoff()
2625
    {
2626
        if (isset($GLOBALS['BE_USER'])
2627
            && $GLOBALS['BE_USER'] instanceof self
2628
            && isset($GLOBALS['BE_USER']->user['uid'])
2629
        ) {
2630
            FormProtectionFactory::get()->clean();
2631
            // Release the locked records
2632
            $this->releaseLockedRecords((int)$GLOBALS['BE_USER']->user['uid']);
2633
2634
            if ($this->isSystemMaintainer()) {
2635
                // If user is system maintainer, destroy its possibly valid install tool session.
2636
                $session = new SessionService();
2637
                if ($session->hasSession()) {
2638
                    $session->destroySession();
2639
                }
2640
            }
2641
        }
2642
        parent::logoff();
2643
    }
2644
2645
    /**
2646
     * Remove any "locked records" added for editing for the given user (= current backend user)
2647
     * @param int $userId
2648
     */
2649
    protected function releaseLockedRecords(int $userId)
2650
    {
2651
        if ($userId > 0) {
2652
            GeneralUtility::makeInstance(ConnectionPool::class)
2653
                ->getConnectionForTable('sys_lockedrecords')
2654
                ->delete(
2655
                    'sys_lockedrecords',
2656
                    ['userid' => $userId]
2657
                );
2658
        }
2659
    }
2660
}
2661