Completed
Push — master ( d53f54...2e1e23 )
by
unknown
17:40
created

BackendUserAuthentication   F

Complexity

Total Complexity 374

Size/Duplication

Total Lines 2606
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 1059
dl 0
loc 2606
rs 1.364
c 2
b 0
f 0
wmc 374

63 Methods

Rating   Name   Duplication   Size   Complexity  
A initializeWebmountsForElementBrowser() 0 14 3
A setWebmounts() 0 10 3
A jsConfirmation() 0 10 3
A returnWebmounts() 0 3 2
A getTSConfig() 0 3 1
A initializeDbMountpointsInWorkspace() 0 22 5
A workspaceInit() 0 9 2
A setTemporaryWorkspace() 0 12 2
A setDefaultWorkspace() 0 4 1
C checkWorkspace() 0 61 14
A getDefaultWorkspace() 0 25 5
B writelog() 0 53 5
A releaseLockedRecords() 0 8 2
C calcPerms() 0 34 12
A mayMakeShortcut() 0 4 2
A workspaceCanCreateNewRecord() 0 7 3
A check() 0 4 3
A __construct() 0 6 1
B checkFullLanguagesAccess() 0 32 6
A getPagePermsClause() 0 58 5
A workspaceAllowsLiveEditingInTable() 0 17 5
A workspaceCreateNewRecord() 0 8 3
A isRTE() 0 3 1
B workspaceCannotEditRecord() 0 36 10
A workspacePublishAccess() 0 22 6
A isMemberOfGroup() 0 7 3
A checkLanguageAccess() 0 11 4
D isInWebMount() 0 54 16
C modAccess() 0 32 17
A workspaceCannotEditOfflineVersion() 0 15 5
A isAdmin() 0 3 2
D recordEditAccessInternals() 0 91 28
D workspaceCheckStageForCurrent() 0 49 19
A workspaceAllowAutoCreation() 0 18 5
A workspaceAllowLiveRecordsInPID() 0 14 5
A doesUserHaveAccess() 0 4 1
B isSystemMaintainer() 0 25 7
D checkAuthMode() 0 62 20
A getFileStorages() 0 7 2
A setWorkspace() 0 17 3
A overrideUC() 0 3 1
A setCachedList() 0 7 2
A evaluateUserSpecificFileFilterSettings() 0 5 2
A logoff() 0 17 5
A sendLoginAttemptEmail() 0 22 4
A initializeFileStorages() 0 26 6
A prepareUserTsConfig() 0 35 4
A getFilePermissionsForStorage() 0 15 3
C fetchGroups() 0 83 13
A getFilePermissions() 0 47 4
A checkLockToIP() 0 10 3
B fetchGroupData() 0 91 10
B backendCheckLogin() 0 37 8
A getCategoryMountPoints() 0 24 5
F getFileMountRecords() 0 139 21
F getDefaultUploadFolder() 0 58 16
A isUserAllowedToLogin() 0 23 4
B backendSetUC() 0 40 8
A checkLogFailures() 0 67 4
A checkWorkspaceCurrent() 0 6 2
A getCookieName() 0 7 2
A resetUC() 0 5 1
A getDefaultUploadTemporaryFolder() 0 19 4

How to fix   Complexity   

Complex Class

Complex classes like BackendUserAuthentication often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BackendUserAuthentication, and based on these observations, apply Extract Interface, too.

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

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

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

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

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
607
            return Permission::NOTHING;
608
        }
609
        $out = Permission::NOTHING;
610
        if (
611
            isset($row['perms_userid']) && isset($row['perms_user']) && isset($row['perms_groupid'])
612
            && isset($row['perms_group']) && isset($row['perms_everybody']) && isset($this->groupList)
613
        ) {
614
            if ($this->user['uid'] == $row['perms_userid']) {
615
                $out |= $row['perms_user'];
616
            }
617
            if ($this->isMemberOfGroup($row['perms_groupid'])) {
618
                $out |= $row['perms_group'];
619
            }
620
            $out |= $row['perms_everybody'];
621
        }
622
        // ****************
623
        // CALCPERMS hook
624
        // ****************
625
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['calcPerms'] ?? [] as $_funcRef) {
626
            $_params = [
627
                'row' => $row,
628
                'outputPermissions' => $out
629
            ];
630
            $out = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
631
        }
632
        return $out;
633
    }
634
635
    /**
636
     * Returns TRUE if the RTE (Rich Text Editor) is enabled for the user.
637
     *
638
     * @return bool
639
     * @internal should only be used from within TYPO3 Core
640
     */
641
    public function isRTE()
642
    {
643
        return (bool)$this->uc['edit_RTE'];
644
    }
645
646
    /**
647
     * Returns TRUE if the $value is found in the list in a $this->groupData[] index pointed to by $type (array key).
648
     * Can thus be users to check for modules, exclude-fields, select/modify permissions for tables etc.
649
     * If user is admin TRUE is also returned
650
     * Please see the document Inside TYPO3 for examples.
651
     *
652
     * @param string $type The type value; "webmounts", "filemounts", "pagetypes_select", "tables_select", "tables_modify", "non_exclude_fields", "modules", "available_widgets"
653
     * @param string $value String to search for in the groupData-list
654
     * @return bool TRUE if permission is granted (that is, the value was found in the groupData list - or the BE_USER is "admin")
655
     */
656
    public function check($type, $value)
657
    {
658
        return isset($this->groupData[$type])
659
            && ($this->isAdmin() || GeneralUtility::inList($this->groupData[$type], $value));
660
    }
661
662
    /**
663
     * Checking the authMode of a select field with authMode set
664
     *
665
     * @param string $table Table name
666
     * @param string $field Field name (must be configured in TCA and of type "select" with authMode set!)
667
     * @param string $value Value to evaluation (single value, must not contain any of the chars ":,|")
668
     * @param string $authMode Auth mode keyword (explicitAllow, explicitDeny, individual)
669
     * @return bool Whether access is granted or not
670
     */
671
    public function checkAuthMode($table, $field, $value, $authMode)
672
    {
673
        // Admin users can do anything:
674
        if ($this->isAdmin()) {
675
            return true;
676
        }
677
        // Allow all blank values:
678
        if ((string)$value === '') {
679
            return true;
680
        }
681
        // Allow dividers:
682
        if ($value === '--div--') {
683
            return true;
684
        }
685
        // Certain characters are not allowed in the value
686
        if (preg_match('/[:|,]/', $value)) {
687
            return false;
688
        }
689
        // Initialize:
690
        $testValue = $table . ':' . $field . ':' . $value;
691
        $out = true;
692
        // Checking value:
693
        switch ((string)$authMode) {
694
            case 'explicitAllow':
695
                if (!GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':ALLOW')) {
696
                    $out = false;
697
                }
698
                break;
699
            case 'explicitDeny':
700
                if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) {
701
                    $out = false;
702
                }
703
                break;
704
            case 'individual':
705
                if (is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
706
                    $items = $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'];
707
                    if (is_array($items)) {
708
                        foreach ($items as $iCfg) {
709
                            if ((string)$iCfg[1] === (string)$value && $iCfg[4]) {
710
                                switch ((string)$iCfg[4]) {
711
                                    case 'EXPL_ALLOW':
712
                                        if (!GeneralUtility::inList(
713
                                            $this->groupData['explicit_allowdeny'],
714
                                            $testValue . ':ALLOW'
715
                                        )) {
716
                                            $out = false;
717
                                        }
718
                                        break;
719
                                    case 'EXPL_DENY':
720
                                        if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) {
721
                                            $out = false;
722
                                        }
723
                                        break;
724
                                }
725
                                break;
726
                            }
727
                        }
728
                    }
729
                }
730
                break;
731
        }
732
        return $out;
733
    }
734
735
    /**
736
     * Checking if a language value (-1, 0 and >0 for sys_language records) is allowed to be edited by the user.
737
     *
738
     * @param int $langValue Language value to evaluate
739
     * @return bool Returns TRUE if the language value is allowed, otherwise FALSE.
740
     */
741
    public function checkLanguageAccess($langValue)
742
    {
743
        // The users language list must be non-blank - otherwise all languages are allowed.
744
        if (trim($this->groupData['allowed_languages']) !== '') {
745
            $langValue = (int)$langValue;
746
            // Language must either be explicitly allowed OR the lang Value be "-1" (all languages)
747
            if ($langValue != -1 && !$this->check('allowed_languages', $langValue)) {
748
                return false;
749
            }
750
        }
751
        return true;
752
    }
753
754
    /**
755
     * Check if user has access to all existing localizations for a certain record
756
     *
757
     * @param string $table The table
758
     * @param array $record The current record
759
     * @return bool
760
     */
761
    public function checkFullLanguagesAccess($table, $record)
762
    {
763
        if (!$this->checkLanguageAccess(0)) {
764
            return false;
765
        }
766
767
        if (BackendUtility::isTableLocalizable($table)) {
768
            $pointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
769
            $pointerValue = $record[$pointerField] > 0 ? $record[$pointerField] : $record['uid'];
770
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
771
            $queryBuilder->getRestrictions()
772
                ->removeAll()
773
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
774
                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->workspace));
775
            $recordLocalizations = $queryBuilder->select('*')
776
                ->from($table)
777
                ->where(
778
                    $queryBuilder->expr()->eq(
779
                        $pointerField,
780
                        $queryBuilder->createNamedParameter($pointerValue, \PDO::PARAM_INT)
781
                    )
782
                )
783
                ->execute()
784
                ->fetchAll();
785
786
            foreach ($recordLocalizations as $recordLocalization) {
787
                if (!$this->checkLanguageAccess($recordLocalization[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
788
                    return false;
789
                }
790
            }
791
        }
792
        return true;
793
    }
794
795
    /**
796
     * Checking if a user has editing access to a record from a $GLOBALS['TCA'] table.
797
     * The checks does not take page permissions and other "environmental" things into account.
798
     * It only deal with record internals; If any values in the record fields disallows it.
799
     * For instance languages settings, authMode selector boxes are evaluated (and maybe more in the future).
800
     * It will check for workspace dependent access.
801
     * The function takes an ID (int) or row (array) as second argument.
802
     *
803
     * @param string $table Table name
804
     * @param mixed $idOrRow If integer, then this is the ID of the record. If Array this just represents fields in the record.
805
     * @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.
806
     * @param bool $deletedRecord Set, if testing a deleted record array.
807
     * @param bool $checkFullLanguageAccess Set, whenever access to all translations of the record is required
808
     * @return bool TRUE if OK, otherwise FALSE
809
     * @internal should only be used from within TYPO3 Core
810
     */
811
    public function recordEditAccessInternals($table, $idOrRow, $newRecord = false, $deletedRecord = false, $checkFullLanguageAccess = false)
812
    {
813
        if (!isset($GLOBALS['TCA'][$table])) {
814
            return false;
815
        }
816
        // Always return TRUE for Admin users.
817
        if ($this->isAdmin()) {
818
            return true;
819
        }
820
        // Fetching the record if the $idOrRow variable was not an array on input:
821
        if (!is_array($idOrRow)) {
822
            if ($deletedRecord) {
823
                $idOrRow = BackendUtility::getRecord($table, $idOrRow, '*', '', false);
824
            } else {
825
                $idOrRow = BackendUtility::getRecord($table, $idOrRow);
826
            }
827
            if (!is_array($idOrRow)) {
828
                $this->errorMsg = 'ERROR: Record could not be fetched.';
829
                return false;
830
            }
831
        }
832
        // Checking languages:
833
        if ($table === 'pages' && $checkFullLanguageAccess && !$this->checkFullLanguagesAccess($table, $idOrRow)) {
834
            return false;
835
        }
836
        if ($GLOBALS['TCA'][$table]['ctrl']['languageField']) {
837
            // Language field must be found in input row - otherwise it does not make sense.
838
            if (isset($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
839
                if (!$this->checkLanguageAccess($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
840
                    $this->errorMsg = 'ERROR: Language was not allowed.';
841
                    return false;
842
                }
843
                if (
844
                    $checkFullLanguageAccess && $idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']] == 0
845
                    && !$this->checkFullLanguagesAccess($table, $idOrRow)
846
                ) {
847
                    $this->errorMsg = 'ERROR: Related/affected language was not allowed.';
848
                    return false;
849
                }
850
            } else {
851
                $this->errorMsg = 'ERROR: The "languageField" field named "'
852
                    . $GLOBALS['TCA'][$table]['ctrl']['languageField'] . '" was not found in testing record!';
853
                return false;
854
            }
855
        }
856
        // Checking authMode fields:
857
        if (is_array($GLOBALS['TCA'][$table]['columns'])) {
858
            foreach ($GLOBALS['TCA'][$table]['columns'] as $fieldName => $fieldValue) {
859
                if (isset($idOrRow[$fieldName])) {
860
                    if (
861
                        $fieldValue['config']['type'] === 'select' && $fieldValue['config']['authMode']
862
                        && $fieldValue['config']['authMode_enforce'] === 'strict'
863
                    ) {
864
                        if (!$this->checkAuthMode($table, $fieldName, $idOrRow[$fieldName], $fieldValue['config']['authMode'])) {
865
                            $this->errorMsg = 'ERROR: authMode "' . $fieldValue['config']['authMode']
866
                                . '" failed for field "' . $fieldName . '" with value "'
867
                                . $idOrRow[$fieldName] . '" evaluated';
868
                            return false;
869
                        }
870
                    }
871
                }
872
            }
873
        }
874
        // Checking "editlock" feature (doesn't apply to new records)
875
        if (!$newRecord && $GLOBALS['TCA'][$table]['ctrl']['editlock']) {
876
            if (isset($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['editlock']])) {
877
                if ($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['editlock']]) {
878
                    $this->errorMsg = 'ERROR: Record was locked for editing. Only admin users can change this state.';
879
                    return false;
880
                }
881
            } else {
882
                $this->errorMsg = 'ERROR: The "editLock" field named "' . $GLOBALS['TCA'][$table]['ctrl']['editlock']
883
                    . '" was not found in testing record!';
884
                return false;
885
            }
886
        }
887
        // Checking record permissions
888
        // THIS is where we can include a check for "perms_" fields for other records than pages...
889
        // Process any hooks
890
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['recordEditAccessInternals'] ?? [] as $funcRef) {
891
            $params = [
892
                'table' => $table,
893
                'idOrRow' => $idOrRow,
894
                'newRecord' => $newRecord
895
            ];
896
            if (!GeneralUtility::callUserFunction($funcRef, $params, $this)) {
897
                return false;
898
            }
899
        }
900
        // Finally, return TRUE if all is well.
901
        return true;
902
    }
903
904
    /**
905
     * Returns TRUE if the BE_USER is allowed to *create* shortcuts in the backend modules
906
     *
907
     * @return bool
908
     */
909
    public function mayMakeShortcut()
910
    {
911
        return ($this->getTSConfig()['options.']['enableBookmarks'] ?? false)
912
            && !($this->getTSConfig()['options.']['mayNotCreateEditBookmarks'] ?? false);
913
    }
914
915
    /**
916
     * Checking if editing of an existing record is allowed in current workspace if that is offline.
917
     * Rules for editing in offline mode:
918
     * - record supports versioning and is an offline version from workspace and has the current stage
919
     * - or record (any) is in a branch where there is a page which is a version from the workspace
920
     *   and where the stage is not preventing records
921
     *
922
     * @param string $table Table of record
923
     * @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)
924
     * @return string String error code, telling the failure state. FALSE=All ok
925
     * @internal should only be used from within TYPO3 Core
926
     */
927
    public function workspaceCannotEditRecord($table, $recData)
928
    {
929
        // Only test if the user is in a workspace
930
        if ($this->workspace === 0) {
931
            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...
932
        }
933
        $tableSupportsVersioning = BackendUtility::isTableWorkspaceEnabled($table);
934
        if (!is_array($recData)) {
935
            $recData = BackendUtility::getRecord(
936
                $table,
937
                $recData,
938
                'pid' . ($tableSupportsVersioning ? ',t3ver_oid,t3ver_wsid,t3ver_stage' : '')
939
            );
940
        }
941
        if (is_array($recData)) {
942
            // We are testing a "version" (identified by having a t3ver_oid): it can be edited provided
943
            // that workspace matches and versioning is enabled for the table.
944
            if ($tableSupportsVersioning && (int)($recData['t3ver_oid'] ?? 0) > 0) {
945
                if ((int)$recData['t3ver_wsid'] !== $this->workspace) {
946
                    // So does workspace match?
947
                    return 'Workspace ID of record didn\'t match current workspace';
948
                }
949
                // So is the user allowed to "use" the edit stage within the workspace?
950
                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...
951
                        ? false
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
952
                        : '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...
953
            }
954
            // Check if we are testing a "live" record
955
            if ($this->workspaceAllowsLiveEditingInTable($table)) {
956
                // Live records are OK in the current workspace
957
                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...
958
            }
959
            // If not offline, output error
960
            return 'Online record was not in a workspace!';
961
        }
962
        return 'No record';
963
    }
964
965
    /**
966
     * Evaluates if a user is allowed to edit the offline version
967
     *
968
     * @param string $table Table of record
969
     * @param array|int $recData Integer (record uid) or array where fields are at least: pid, t3ver_wsid, t3ver_stage (if versioningWS is set)
970
     * @return string String error code, telling the failure state. FALSE=All ok
971
     * @see workspaceCannotEditRecord()
972
     * @internal this method will be moved to EXT:workspaces
973
     */
974
    public function workspaceCannotEditOfflineVersion($table, $recData)
975
    {
976
        if (!BackendUtility::isTableWorkspaceEnabled($table)) {
977
            return 'Table does not support versioning.';
978
        }
979
        if (!is_array($recData)) {
980
            $recData = BackendUtility::getRecord($table, $recData, 'uid,pid,t3ver_oid,t3ver_wsid,t3ver_stage');
981
        }
982
        if (is_array($recData)) {
983
            if ((int)$recData['t3ver_oid'] > 0) {
984
                return $this->workspaceCannotEditRecord($table, $recData);
985
            }
986
            return 'Not an offline version';
987
        }
988
        return 'No record';
989
    }
990
991
    /**
992
     * Check if "live" records from $table may be created or edited in this PID.
993
     * If the answer is FALSE it means the only valid way to create or edit records in the PID is by versioning
994
     * 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
995
     * of versioning because the element was within a versionized branch
996
     * but NOT ok in terms of the state the root point had!
997
     *
998
     * Note: this method is not in use anymore and will likely be deprecated in future TYPO3 versions.
999
     *
1000
     * @param int $pid PID value to check for. OBSOLETE!
1001
     * @param string $table Table name
1002
     * @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.
1003
     * @internal should only be used from within TYPO3 Core
1004
     */
1005
    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

1005
    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...
1006
    {
1007
        // Always for Live workspace AND if live-edit is enabled
1008
        // and tables are completely without versioning it is ok as well.
1009
        if (
1010
            $this->workspace === 0
1011
            || $this->workspaceRec['live_edit'] && !BackendUtility::isTableWorkspaceEnabled($table)
1012
            || $GLOBALS['TCA'][$table]['ctrl']['versioningWS_alwaysAllowLiveEdit']
1013
        ) {
1014
            // OK to create for this table.
1015
            return 2;
1016
        }
1017
        // If the answer is FALSE it means the only valid way to create or edit records in the PID is by versioning
1018
        return false;
1019
    }
1020
1021
    /**
1022
     * Checks if a record is allowed to be edited in the current workspace.
1023
     * This is not bound to an actual record, but to the mere fact if the user is in a workspace
1024
     * and depending on the table settings.
1025
     *
1026
     * @param string $table
1027
     * @return bool
1028
     * @internal should only be used from within TYPO3 Core
1029
     */
1030
    public function workspaceAllowsLiveEditingInTable(string $table): bool
1031
    {
1032
        // In live workspace the record can be added/modified
1033
        if ($this->workspace === 0) {
1034
            return true;
1035
        }
1036
        // Workspace setting allows to "live edit" records of tables without versioning
1037
        if ($this->workspaceRec['live_edit'] && !BackendUtility::isTableWorkspaceEnabled($table)) {
1038
            return true;
1039
        }
1040
        // Always for Live workspace AND if live-edit is enabled
1041
        // and tables are completely without versioning it is ok as well.
1042
        if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS_alwaysAllowLiveEdit']) {
1043
            return true;
1044
        }
1045
        // If the answer is FALSE it means the only valid way to create or edit records by creating records in the workspace
1046
        return false;
1047
    }
1048
1049
    /**
1050
     * Evaluates if a record from $table can be created in $pid
1051
     *
1052
     * Note: this method is not in use anymore and will likely be deprecated in future TYPO3 versions.
1053
     *
1054
     * @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!
1055
     * @param string $table Table name
1056
     * @return bool TRUE if OK.
1057
     * @internal should only be used from within TYPO3 Core
1058
     */
1059
    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

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

1289
            $this->setWebmounts($alternativeWebmountPoints, /** @scrutinizer ignore-type */ $appendAlternativeWebmountPoints);
Loading history...
1290
        }
1291
    }
1292
1293
    /**
1294
     * Returns TRUE or FALSE, depending if an alert popup (a javascript confirmation) should be shown
1295
     * call like $GLOBALS['BE_USER']->jsConfirmation($BITMASK).
1296
     *
1297
     * @param int $bitmask Bitmask, one of \TYPO3\CMS\Core\Type\Bitmask\JsConfirmation
1298
     * @return bool TRUE if the confirmation should be shown
1299
     * @see JsConfirmation
1300
     */
1301
    public function jsConfirmation($bitmask)
1302
    {
1303
        try {
1304
            $alertPopupsSetting = trim((string)($this->getTSConfig()['options.']['alertPopups'] ?? ''));
1305
            $alertPopup = JsConfirmation::cast($alertPopupsSetting === '' ? null : (int)$alertPopupsSetting);
1306
        } catch (InvalidEnumerationValueException $e) {
1307
            $alertPopup = new JsConfirmation();
1308
        }
1309
1310
        return JsConfirmation::cast($bitmask)->matches($alertPopup);
1311
    }
1312
1313
    /**
1314
     * Initializes a lot of stuff like the access-lists, database-mountpoints and filemountpoints
1315
     * This method is called by ->backendCheckLogin() (from extending BackendUserAuthentication)
1316
     * if the backend user login has verified OK.
1317
     * Generally this is required initialization of a backend user.
1318
     *
1319
     * @internal
1320
     * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
1321
     */
1322
    public function fetchGroupData()
1323
    {
1324
        if ($this->user['uid']) {
1325
            // Get lists for the be_user record and set them as default/primary values.
1326
            // Enabled Backend Modules
1327
            $this->dataLists['modList'] = $this->user['userMods'];
1328
            // Add available widgets
1329
            $this->dataLists['available_widgets'] = $this->user['available_widgets'];
1330
            // Add Allowed Languages
1331
            $this->dataLists['allowed_languages'] = $this->user['allowed_languages'];
1332
            // Set user value for workspace permissions.
1333
            $this->dataLists['workspace_perms'] = $this->user['workspace_perms'];
1334
            // Database mountpoints
1335
            $this->dataLists['webmount_list'] = $this->user['db_mountpoints'];
1336
            // File mountpoints
1337
            $this->dataLists['filemount_list'] = $this->user['file_mountpoints'];
1338
            // Fileoperation permissions
1339
            $this->dataLists['file_permissions'] = $this->user['file_permissions'];
1340
1341
            // BE_GROUPS:
1342
            // Get the groups...
1343
            if (!empty($this->user[$this->usergroup_column])) {
1344
                // Fetch groups will add a lot of information to the internal arrays: modules, accesslists, TSconfig etc.
1345
                // Refer to fetchGroups() function.
1346
                $this->fetchGroups($this->user[$this->usergroup_column]);
1347
            }
1348
            // Populating the $this->userGroupsUID -array with the groups in the order in which they were LAST included.!!
1349
            $this->userGroupsUID = array_reverse(array_unique(array_reverse($this->includeGroupArray)));
1350
            // Finally this is the list of group_uid's in the order they are parsed (including subgroups!)
1351
            // and without duplicates (duplicates are presented with their last entrance in the list,
1352
            // which thus reflects the order of the TypoScript in TSconfig)
1353
            $this->groupList = implode(',', $this->userGroupsUID);
1354
            $this->setCachedList($this->groupList);
1355
1356
            $this->prepareUserTsConfig();
1357
1358
            // Processing webmounts
1359
            // Admin's always have the root mounted
1360
            if ($this->isAdmin() && !($this->getTSConfig()['options.']['dontMountAdminMounts'] ?? false)) {
1361
                $this->dataLists['webmount_list'] = '0,' . $this->dataLists['webmount_list'];
1362
            }
1363
            // The lists are cleaned for duplicates
1364
            $this->groupData['webmounts'] = GeneralUtility::uniqueList($this->dataLists['webmount_list']);
1365
            $this->groupData['pagetypes_select'] = GeneralUtility::uniqueList($this->dataLists['pagetypes_select']);
1366
            $this->groupData['tables_select'] = GeneralUtility::uniqueList($this->dataLists['tables_modify'] . ',' . $this->dataLists['tables_select']);
1367
            $this->groupData['tables_modify'] = GeneralUtility::uniqueList($this->dataLists['tables_modify']);
1368
            $this->groupData['non_exclude_fields'] = GeneralUtility::uniqueList($this->dataLists['non_exclude_fields']);
1369
            $this->groupData['explicit_allowdeny'] = GeneralUtility::uniqueList($this->dataLists['explicit_allowdeny']);
1370
            $this->groupData['allowed_languages'] = GeneralUtility::uniqueList($this->dataLists['allowed_languages']);
1371
            $this->groupData['custom_options'] = GeneralUtility::uniqueList($this->dataLists['custom_options']);
1372
            $this->groupData['modules'] = GeneralUtility::uniqueList($this->dataLists['modList']);
1373
            $this->groupData['available_widgets'] = GeneralUtility::uniqueList($this->dataLists['available_widgets']);
1374
            $this->groupData['file_permissions'] = GeneralUtility::uniqueList($this->dataLists['file_permissions']);
1375
            $this->groupData['workspace_perms'] = $this->dataLists['workspace_perms'];
1376
1377
            if (!empty(trim($this->groupData['webmounts']))) {
1378
                // Checking read access to web mounts if there are mounts points (not empty string, false or 0)
1379
                $webmounts = explode(',', $this->groupData['webmounts']);
1380
                // Selecting all web mounts with permission clause for reading
1381
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1382
                $queryBuilder->getRestrictions()
1383
                    ->removeAll()
1384
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1385
1386
                $MProws = $queryBuilder->select('uid')
1387
                    ->from('pages')
1388
                    // @todo DOCTRINE: check how to make getPagePermsClause() portable
1389
                    ->where(
1390
                        $this->getPagePermsClause(Permission::PAGE_SHOW),
1391
                        $queryBuilder->expr()->in(
1392
                            'uid',
1393
                            $queryBuilder->createNamedParameter(
1394
                                GeneralUtility::intExplode(',', $this->groupData['webmounts']),
1395
                                Connection::PARAM_INT_ARRAY
1396
                            )
1397
                        )
1398
                    )
1399
                    ->execute()
1400
                    ->fetchAll();
1401
                $MProws = array_column(($MProws ?: []), 'uid', 'uid');
1402
                foreach ($webmounts as $idx => $mountPointUid) {
1403
                    // If the mount ID is NOT found among selected pages, unset it:
1404
                    if ($mountPointUid > 0 && !isset($MProws[$mountPointUid])) {
1405
                        unset($webmounts[$idx]);
1406
                    }
1407
                }
1408
                // Implode mounts in the end.
1409
                $this->groupData['webmounts'] = implode(',', $webmounts);
1410
            }
1411
            // Setting up workspace situation (after webmounts are processed!):
1412
            $this->workspaceInit();
1413
        }
1414
    }
1415
1416
    /**
1417
     * This method parses the UserTSconfig from the current user and all their groups.
1418
     * If the contents are the same, parsing is skipped. No matching is applied here currently.
1419
     */
1420
    protected function prepareUserTsConfig(): void
1421
    {
1422
        $collectedUserTSconfig = [
1423
            'default' => $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig']
1424
        ];
1425
        // Default TSconfig for admin-users
1426
        if ($this->isAdmin()) {
1427
            $collectedUserTSconfig[] = 'admPanel.enable.all = 1';
1428
        }
1429
        // Setting defaults for sys_note author / email
1430
        $collectedUserTSconfig[] = '
1431
TCAdefaults.sys_note.author = ' . $this->user['realName'] . '
1432
TCAdefaults.sys_note.email = ' . $this->user['email'];
1433
1434
        // Loop through all groups and add their 'TSconfig' fields
1435
        foreach ($this->includeGroupArray as $groupId) {
1436
            $collectedUserTSconfig['group_' . $groupId] = $this->userGroups[$groupId]['TSconfig'] ?? '';
1437
        }
1438
1439
        $collectedUserTSconfig[] = $this->user['TSconfig'];
1440
        // Check external files
1441
        $collectedUserTSconfig = TypoScriptParser::checkIncludeLines_array($collectedUserTSconfig);
1442
        // Imploding with "[global]" will make sure that non-ended confinements with braces are ignored.
1443
        $userTS_text = implode("\n[GLOBAL]\n", $collectedUserTSconfig);
1444
        // Parsing the user TSconfig (or getting from cache)
1445
        $hash = md5('userTS:' . $userTS_text);
1446
        $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
1447
        if (!($this->userTS = $cache->get($hash))) {
1448
            $parseObj = GeneralUtility::makeInstance(TypoScriptParser::class);
1449
            $conditionMatcher = GeneralUtility::makeInstance(ConditionMatcher::class);
1450
            $parseObj->parse($userTS_text, $conditionMatcher);
1451
            $this->userTS = $parseObj->setup;
1452
            $cache->set($hash, $this->userTS, ['UserTSconfig'], 0);
1453
            // Ensure to update UC later
1454
            $this->userTSUpdated = true;
1455
        }
1456
    }
1457
1458
    /**
1459
     * Fetches the group records, subgroups and fills internal arrays.
1460
     * Function is called recursively to fetch subgroups
1461
     *
1462
     * @param string $grList Commalist of be_groups uid numbers
1463
     * @param string $idList List of already processed be_groups-uids so the function will not fall into an eternal recursion.
1464
     * @internal
1465
     */
1466
    public function fetchGroups($grList, $idList = '')
1467
    {
1468
        // Fetching records of the groups in $grList:
1469
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->usergroup_table);
1470
        $expressionBuilder = $queryBuilder->expr();
1471
        $constraints = $expressionBuilder->andX(
1472
            $expressionBuilder->eq(
1473
                'pid',
1474
                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1475
            ),
1476
            $expressionBuilder->in(
1477
                'uid',
1478
                $queryBuilder->createNamedParameter(
1479
                    GeneralUtility::intExplode(',', $grList),
1480
                    Connection::PARAM_INT_ARRAY
1481
                )
1482
            )
1483
        );
1484
        // Hook for manipulation of the WHERE sql sentence which controls which BE-groups are included
1485
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroupQuery'] ?? [] as $className) {
1486
            $hookObj = GeneralUtility::makeInstance($className);
1487
            if (method_exists($hookObj, 'fetchGroupQuery_processQuery')) {
1488
                $constraints = $hookObj->fetchGroupQuery_processQuery($this, $grList, $idList, (string)$constraints);
1489
            }
1490
        }
1491
        $res = $queryBuilder->select('*')
1492
            ->from($this->usergroup_table)
1493
            ->where($constraints)
1494
            ->execute();
1495
        // The userGroups array is filled
1496
        while ($row = $res->fetch(\PDO::FETCH_ASSOC)) {
1497
            $this->userGroups[$row['uid']] = $row;
1498
        }
1499
1500
        $permission = new Permission($this->user['options']);
1501
1502
        // Traversing records in the correct order
1503
        foreach (explode(',', $grList) as $uid) {
1504
            // Get row:
1505
            $row = $this->userGroups[$uid];
1506
            // Must be an array and $uid should not be in the idList, because then it is somewhere previously in the grouplist
1507
            if (is_array($row) && !GeneralUtility::inList($idList, $uid)) {
1508
                // Include sub groups
1509
                if (trim($row['subgroup'])) {
1510
                    // Make integer list
1511
                    $theList = implode(',', GeneralUtility::intExplode(',', $row['subgroup']));
1512
                    // Call recursively, pass along list of already processed groups so they are not recursed again.
1513
                    $this->fetchGroups($theList, $idList . ',' . $uid);
1514
                }
1515
                // Add the group uid, current list to the internal arrays.
1516
                $this->includeGroupArray[] = $uid;
1517
                // Mount group database-mounts
1518
                if ($permission->showPagePermissionIsGranted()) {
1519
                    $this->dataLists['webmount_list'] .= ',' . $row['db_mountpoints'];
1520
                }
1521
                // Mount group file-mounts
1522
                if ($permission->editPagePermissionIsGranted()) {
1523
                    $this->dataLists['filemount_list'] .= ',' . $row['file_mountpoints'];
1524
                }
1525
                // The lists are made: groupMods, tables_select, tables_modify, pagetypes_select, non_exclude_fields, explicit_allowdeny, allowed_languages, custom_options
1526
                $this->dataLists['modList'] .= ',' . $row['groupMods'];
1527
                $this->dataLists['available_widgets'] .= ',' . $row['availableWidgets'];
1528
                $this->dataLists['tables_select'] .= ',' . $row['tables_select'];
1529
                $this->dataLists['tables_modify'] .= ',' . $row['tables_modify'];
1530
                $this->dataLists['pagetypes_select'] .= ',' . $row['pagetypes_select'];
1531
                $this->dataLists['non_exclude_fields'] .= ',' . $row['non_exclude_fields'];
1532
                $this->dataLists['explicit_allowdeny'] .= ',' . $row['explicit_allowdeny'];
1533
                $this->dataLists['allowed_languages'] .= ',' . $row['allowed_languages'];
1534
                $this->dataLists['custom_options'] .= ',' . $row['custom_options'];
1535
                $this->dataLists['file_permissions'] .= ',' . $row['file_permissions'];
1536
                // Setting workspace permissions:
1537
                $this->dataLists['workspace_perms'] |= $row['workspace_perms'];
1538
                // If this function is processing the users OWN group-list (not subgroups) AND
1539
                // if the ->firstMainGroup is not set, then the ->firstMainGroup will be set.
1540
                if ($idList === '' && !$this->firstMainGroup) {
1541
                    $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...
1542
                }
1543
            }
1544
        }
1545
        // HOOK: fetchGroups_postProcessing
1546
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroups_postProcessing'] ?? [] as $_funcRef) {
1547
            $_params = [];
1548
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1549
        }
1550
    }
1551
1552
    /**
1553
     * Updates the field be_users.usergroup_cached_list if the groupList of the user
1554
     * has changed/is different from the current list.
1555
     * The field "usergroup_cached_list" contains the list of groups which the user is a member of.
1556
     * After authentication (where these functions are called...) one can depend on this list being
1557
     * a representation of the exact groups/subgroups which the BE_USER has membership with.
1558
     *
1559
     * @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.
1560
     * @internal
1561
     */
1562
    public function setCachedList($cList)
1563
    {
1564
        if ((string)$cList != (string)$this->user['usergroup_cached_list']) {
1565
            GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users')->update(
1566
                'be_users',
1567
                ['usergroup_cached_list' => $cList],
1568
                ['uid' => (int)$this->user['uid']]
1569
            );
1570
        }
1571
    }
1572
1573
    /**
1574
     * Sets up all file storages for a user.
1575
     * Needs to be called AFTER the groups have been loaded.
1576
     */
1577
    protected function initializeFileStorages()
1578
    {
1579
        $this->fileStorages = [];
1580
        /** @var \TYPO3\CMS\Core\Resource\StorageRepository $storageRepository */
1581
        $storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
1582
        // Admin users have all file storages visible, without any filters
1583
        if ($this->isAdmin()) {
1584
            $storageObjects = $storageRepository->findAll();
1585
            foreach ($storageObjects as $storageObject) {
1586
                $this->fileStorages[$storageObject->getUid()] = $storageObject;
1587
            }
1588
        } else {
1589
            // Regular users only have storages that are defined in their filemounts
1590
            // Permissions and file mounts for the storage are added in StoragePermissionAspect
1591
            foreach ($this->getFileMountRecords() as $row) {
1592
                if (!array_key_exists((int)$row['base'], $this->fileStorages)) {
1593
                    $storageObject = $storageRepository->findByUid($row['base']);
1594
                    if ($storageObject) {
1595
                        $this->fileStorages[$storageObject->getUid()] = $storageObject;
1596
                    }
1597
                }
1598
            }
1599
        }
1600
1601
        // This has to be called always in order to set certain filters
1602
        $this->evaluateUserSpecificFileFilterSettings();
1603
    }
1604
1605
    /**
1606
     * Returns an array of category mount points. The category permissions from BE Groups
1607
     * are also taken into consideration and are merged into User permissions.
1608
     *
1609
     * @return array
1610
     */
1611
    public function getCategoryMountPoints()
1612
    {
1613
        $categoryMountPoints = '';
1614
1615
        // Category mounts of the groups
1616
        if (is_array($this->userGroups)) {
0 ignored issues
show
introduced by
The condition is_array($this->userGroups) is always true.
Loading history...
1617
            foreach ($this->userGroups as $group) {
1618
                if ($group['category_perms']) {
1619
                    $categoryMountPoints .= ',' . $group['category_perms'];
1620
                }
1621
            }
1622
        }
1623
1624
        // Category mounts of the user record
1625
        if ($this->user['category_perms']) {
1626
            $categoryMountPoints .= ',' . $this->user['category_perms'];
1627
        }
1628
1629
        // Make the ids unique
1630
        $categoryMountPoints = GeneralUtility::trimExplode(',', $categoryMountPoints);
1631
        $categoryMountPoints = array_filter($categoryMountPoints); // remove empty value
1632
        $categoryMountPoints = array_unique($categoryMountPoints); // remove unique value
1633
1634
        return $categoryMountPoints;
1635
    }
1636
1637
    /**
1638
     * Returns an array of file mount records, taking workspaces and user home and group home directories into account
1639
     * Needs to be called AFTER the groups have been loaded.
1640
     *
1641
     * @return array
1642
     * @internal
1643
     */
1644
    public function getFileMountRecords()
1645
    {
1646
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
1647
        $fileMountRecordCache = $runtimeCache->get('backendUserAuthenticationFileMountRecords') ?: [];
1648
1649
        if (!empty($fileMountRecordCache)) {
1650
            return $fileMountRecordCache;
1651
        }
1652
1653
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
1654
1655
        // Processing file mounts (both from the user and the groups)
1656
        $fileMounts = array_unique(GeneralUtility::intExplode(',', $this->dataLists['filemount_list'], true));
1657
1658
        // Limit file mounts if set in workspace record
1659
        if ($this->workspace > 0 && !empty($this->workspaceRec['file_mountpoints'])) {
1660
            $workspaceFileMounts = GeneralUtility::intExplode(',', $this->workspaceRec['file_mountpoints'], true);
1661
            $fileMounts = array_intersect($fileMounts, $workspaceFileMounts);
1662
        }
1663
1664
        if (!empty($fileMounts)) {
1665
            $orderBy = $GLOBALS['TCA']['sys_filemounts']['ctrl']['default_sortby'] ?? 'sorting';
1666
1667
            $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_filemounts');
1668
            $queryBuilder->getRestrictions()
1669
                ->removeAll()
1670
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1671
                ->add(GeneralUtility::makeInstance(HiddenRestriction::class))
1672
                ->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
1673
1674
            $queryBuilder->select('*')
1675
                ->from('sys_filemounts')
1676
                ->where(
1677
                    $queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($fileMounts, Connection::PARAM_INT_ARRAY))
1678
                );
1679
1680
            foreach (QueryHelper::parseOrderBy($orderBy) as $fieldAndDirection) {
1681
                $queryBuilder->addOrderBy(...$fieldAndDirection);
1682
            }
1683
1684
            $fileMountRecords = $queryBuilder->execute()->fetchAll(\PDO::FETCH_ASSOC);
1685
            if ($fileMountRecords !== false) {
1686
                foreach ($fileMountRecords as $fileMount) {
1687
                    $fileMountRecordCache[$fileMount['base'] . $fileMount['path']] = $fileMount;
1688
                }
1689
            }
1690
        }
1691
1692
        // Read-only file mounts
1693
        $readOnlyMountPoints = \trim($this->getTSConfig()['options.']['folderTree.']['altElementBrowserMountPoints'] ?? '');
1694
        if ($readOnlyMountPoints) {
1695
            // We cannot use the API here but need to fetch the default storage record directly
1696
            // to not instantiate it (which directly applies mount points) before all mount points are resolved!
1697
            $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_file_storage');
1698
            $defaultStorageRow = $queryBuilder->select('uid')
1699
                ->from('sys_file_storage')
1700
                ->where(
1701
                    $queryBuilder->expr()->eq('is_default', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT))
1702
                )
1703
                ->setMaxResults(1)
1704
                ->execute()
1705
                ->fetch(\PDO::FETCH_ASSOC);
1706
1707
            $readOnlyMountPointArray = GeneralUtility::trimExplode(',', $readOnlyMountPoints);
1708
            foreach ($readOnlyMountPointArray as $readOnlyMountPoint) {
1709
                $readOnlyMountPointConfiguration = GeneralUtility::trimExplode(':', $readOnlyMountPoint);
1710
                if (count($readOnlyMountPointConfiguration) === 2) {
1711
                    // A storage is passed in the configuration
1712
                    $storageUid = (int)$readOnlyMountPointConfiguration[0];
1713
                    $path = $readOnlyMountPointConfiguration[1];
1714
                } else {
1715
                    if (empty($defaultStorageRow)) {
1716
                        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);
1717
                    }
1718
                    // Backwards compatibility: If no storage is passed, we use the default storage
1719
                    $storageUid = $defaultStorageRow['uid'];
1720
                    $path = $readOnlyMountPointConfiguration[0];
1721
                }
1722
                $fileMountRecordCache[$storageUid . $path] = [
1723
                    'base' => $storageUid,
1724
                    'title' => $path,
1725
                    'path' => $path,
1726
                    'read_only' => true
1727
                ];
1728
            }
1729
        }
1730
1731
        // Personal or Group filemounts are not accessible if file mount list is set in workspace record
1732
        if ($this->workspace <= 0 || empty($this->workspaceRec['file_mountpoints'])) {
1733
            // If userHomePath is set, we attempt to mount it
1734
            if ($GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath']) {
1735
                [$userHomeStorageUid, $userHomeFilter] = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath'], 2);
1736
                $userHomeStorageUid = (int)$userHomeStorageUid;
1737
                $userHomeFilter = '/' . ltrim($userHomeFilter, '/');
1738
                if ($userHomeStorageUid > 0) {
1739
                    // Try and mount with [uid]_[username]
1740
                    $path = $userHomeFilter . $this->user['uid'] . '_' . $this->user['username'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1741
                    $fileMountRecordCache[$userHomeStorageUid . $path] = [
1742
                        'base' => $userHomeStorageUid,
1743
                        'title' => $this->user['username'],
1744
                        'path' => $path,
1745
                        'read_only' => false,
1746
                        'user_mount' => true
1747
                    ];
1748
                    // Try and mount with only [uid]
1749
                    $path = $userHomeFilter . $this->user['uid'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1750
                    $fileMountRecordCache[$userHomeStorageUid . $path] = [
1751
                        'base' => $userHomeStorageUid,
1752
                        'title' => $this->user['username'],
1753
                        'path' => $path,
1754
                        'read_only' => false,
1755
                        'user_mount' => true
1756
                    ];
1757
                }
1758
            }
1759
1760
            // Mount group home-dirs
1761
            if ((new Permission($this->user['options'] ?? Permission::NOTHING))->editPagePermissionIsGranted() && $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'] != '') {
1762
                // If groupHomePath is set, we attempt to mount it
1763
                [$groupHomeStorageUid, $groupHomeFilter] = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'], 2);
1764
                $groupHomeStorageUid = (int)$groupHomeStorageUid;
1765
                $groupHomeFilter = '/' . ltrim($groupHomeFilter, '/');
1766
                if ($groupHomeStorageUid > 0) {
1767
                    foreach ($this->userGroups as $groupData) {
1768
                        $path = $groupHomeFilter . $groupData['uid'];
1769
                        $fileMountRecordCache[$groupHomeStorageUid . $path] = [
1770
                            'base' => $groupHomeStorageUid,
1771
                            'title' => $groupData['title'],
1772
                            'path' => $path,
1773
                            'read_only' => false,
1774
                            'user_mount' => true
1775
                        ];
1776
                    }
1777
                }
1778
            }
1779
        }
1780
1781
        $runtimeCache->set('backendUserAuthenticationFileMountRecords', $fileMountRecordCache);
1782
        return $fileMountRecordCache;
1783
    }
1784
1785
    /**
1786
     * Returns an array with the filemounts for the user.
1787
     * Each filemount is represented with an array of a "name", "path" and "type".
1788
     * If no filemounts an empty array is returned.
1789
     *
1790
     * @return \TYPO3\CMS\Core\Resource\ResourceStorage[]
1791
     */
1792
    public function getFileStorages()
1793
    {
1794
        // Initializing file mounts after the groups are fetched
1795
        if ($this->fileStorages === null) {
1796
            $this->initializeFileStorages();
1797
        }
1798
        return $this->fileStorages;
1799
    }
1800
1801
    /**
1802
     * Adds filters based on what the user has set
1803
     * this should be done in this place, and called whenever needed,
1804
     * but only when needed
1805
     */
1806
    public function evaluateUserSpecificFileFilterSettings()
1807
    {
1808
        // Add the option for also displaying the non-hidden files
1809
        if ($this->uc['showHiddenFilesAndFolders']) {
1810
            FileNameFilter::setShowHiddenFilesAndFolders(true);
1811
        }
1812
    }
1813
1814
    /**
1815
     * Returns the information about file permissions.
1816
     * Previously, this was stored in the DB field fileoper_perms now it is file_permissions.
1817
     * Besides it can be handled via userTSconfig
1818
     *
1819
     * permissions.file.default {
1820
     * addFile = 1
1821
     * readFile = 1
1822
     * writeFile = 1
1823
     * copyFile = 1
1824
     * moveFile = 1
1825
     * renameFile = 1
1826
     * deleteFile = 1
1827
     *
1828
     * addFolder = 1
1829
     * readFolder = 1
1830
     * writeFolder = 1
1831
     * copyFolder = 1
1832
     * moveFolder = 1
1833
     * renameFolder = 1
1834
     * deleteFolder = 1
1835
     * recursivedeleteFolder = 1
1836
     * }
1837
     *
1838
     * # overwrite settings for a specific storageObject
1839
     * permissions.file.storage.StorageUid {
1840
     * readFile = 1
1841
     * recursivedeleteFolder = 0
1842
     * }
1843
     *
1844
     * Please note that these permissions only apply, if the storage has the
1845
     * capabilities (browseable, writable), and if the driver allows for writing etc
1846
     *
1847
     * @return array
1848
     */
1849
    public function getFilePermissions()
1850
    {
1851
        if (!isset($this->filePermissions)) {
1852
            $filePermissions = [
1853
                // File permissions
1854
                'addFile' => false,
1855
                'readFile' => false,
1856
                'writeFile' => false,
1857
                'copyFile' => false,
1858
                'moveFile' => false,
1859
                'renameFile' => false,
1860
                'deleteFile' => false,
1861
                // Folder permissions
1862
                'addFolder' => false,
1863
                'readFolder' => false,
1864
                'writeFolder' => false,
1865
                'copyFolder' => false,
1866
                'moveFolder' => false,
1867
                'renameFolder' => false,
1868
                'deleteFolder' => false,
1869
                'recursivedeleteFolder' => false
1870
            ];
1871
            if ($this->isAdmin()) {
1872
                $filePermissions = array_map('is_bool', $filePermissions);
1873
            } else {
1874
                $userGroupRecordPermissions = GeneralUtility::trimExplode(',', $this->groupData['file_permissions'] ?? '', true);
1875
                array_walk(
1876
                    $userGroupRecordPermissions,
1877
                    function ($permission) use (&$filePermissions) {
1878
                        $filePermissions[$permission] = true;
1879
                    }
1880
                );
1881
1882
                // Finally overlay any userTSconfig
1883
                $permissionsTsConfig = $this->getTSConfig()['permissions.']['file.']['default.'] ?? [];
1884
                if (!empty($permissionsTsConfig)) {
1885
                    array_walk(
1886
                        $permissionsTsConfig,
1887
                        function ($value, $permission) use (&$filePermissions) {
1888
                            $filePermissions[$permission] = (bool)$value;
1889
                        }
1890
                    );
1891
                }
1892
            }
1893
            $this->filePermissions = $filePermissions;
1894
        }
1895
        return $this->filePermissions;
1896
    }
1897
1898
    /**
1899
     * Gets the file permissions for a storage
1900
     * by merging any storage-specific permissions for a
1901
     * storage with the default settings.
1902
     * Admin users will always get the default settings.
1903
     *
1904
     * @param \TYPO3\CMS\Core\Resource\ResourceStorage $storageObject
1905
     * @return array
1906
     */
1907
    public function getFilePermissionsForStorage(ResourceStorage $storageObject)
1908
    {
1909
        $finalUserPermissions = $this->getFilePermissions();
1910
        if (!$this->isAdmin()) {
1911
            $storageFilePermissions = $this->getTSConfig()['permissions.']['file.']['storage.'][$storageObject->getUid() . '.'] ?? [];
1912
            if (!empty($storageFilePermissions)) {
1913
                array_walk(
1914
                    $storageFilePermissions,
1915
                    function ($value, $permission) use (&$finalUserPermissions) {
1916
                        $finalUserPermissions[$permission] = (bool)$value;
1917
                    }
1918
                );
1919
            }
1920
        }
1921
        return $finalUserPermissions;
1922
    }
1923
1924
    /**
1925
     * Returns a \TYPO3\CMS\Core\Resource\Folder object that is used for uploading
1926
     * files by default.
1927
     * This is used for RTE and its magic images, as well as uploads
1928
     * in the TCEforms fields.
1929
     *
1930
     * The default upload folder for a user is the defaultFolder on the first
1931
     * filestorage/filemount that the user can access and to which files are allowed to be added
1932
     * however, you can set the users' upload folder like this:
1933
     *
1934
     * options.defaultUploadFolder = 3:myfolder/yourfolder/
1935
     *
1936
     * @param int $pid PageUid
1937
     * @param string $table Table name
1938
     * @param string $field Field name
1939
     * @return \TYPO3\CMS\Core\Resource\Folder|bool The default upload folder for this user
1940
     */
1941
    public function getDefaultUploadFolder($pid = null, $table = null, $field = null)
1942
    {
1943
        $uploadFolder = $this->getTSConfig()['options.']['defaultUploadFolder'] ?? '';
1944
        if ($uploadFolder) {
1945
            try {
1946
                $uploadFolder = GeneralUtility::makeInstance(ResourceFactory::class)->getFolderObjectFromCombinedIdentifier($uploadFolder);
1947
            } catch (Exception\FolderDoesNotExistException $e) {
1948
                $uploadFolder = null;
1949
            }
1950
        }
1951
        if (empty($uploadFolder)) {
1952
            foreach ($this->getFileStorages() as $storage) {
1953
                if ($storage->isDefault() && $storage->isWritable()) {
1954
                    try {
1955
                        $uploadFolder = $storage->getDefaultFolder();
1956
                        if ($uploadFolder->checkActionPermission('write')) {
1957
                            break;
1958
                        }
1959
                        $uploadFolder = null;
1960
                    } catch (Exception $folderAccessException) {
1961
                        // If the folder is not accessible (no permissions / does not exist) we skip this one.
1962
                    }
1963
                    break;
1964
                }
1965
            }
1966
            if (!$uploadFolder instanceof Folder) {
1967
                /** @var ResourceStorage $storage */
1968
                foreach ($this->getFileStorages() as $storage) {
1969
                    if ($storage->isWritable()) {
1970
                        try {
1971
                            $uploadFolder = $storage->getDefaultFolder();
1972
                            if ($uploadFolder->checkActionPermission('write')) {
1973
                                break;
1974
                            }
1975
                            $uploadFolder = null;
1976
                        } catch (Exception $folderAccessException) {
1977
                            // If the folder is not accessible (no permissions / does not exist) try the next one.
1978
                        }
1979
                    }
1980
                }
1981
            }
1982
        }
1983
1984
        // HOOK: getDefaultUploadFolder
1985
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getDefaultUploadFolder'] ?? [] as $_funcRef) {
1986
            $_params = [
1987
                'uploadFolder' => $uploadFolder,
1988
                'pid' => $pid,
1989
                'table' => $table,
1990
                'field' => $field,
1991
            ];
1992
            $uploadFolder = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1993
        }
1994
1995
        if ($uploadFolder instanceof Folder) {
1996
            return $uploadFolder;
1997
        }
1998
        return false;
1999
    }
2000
2001
    /**
2002
     * Returns a \TYPO3\CMS\Core\Resource\Folder object that could be used for uploading
2003
     * temporary files in user context. The folder _temp_ below the default upload folder
2004
     * of the user is used.
2005
     *
2006
     * @return \TYPO3\CMS\Core\Resource\Folder|null
2007
     * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::getDefaultUploadFolder()
2008
     */
2009
    public function getDefaultUploadTemporaryFolder()
2010
    {
2011
        $defaultTemporaryFolder = null;
2012
        $defaultFolder = $this->getDefaultUploadFolder();
2013
2014
        if ($defaultFolder !== false) {
2015
            $tempFolderName = '_temp_';
2016
            $createFolder = !$defaultFolder->hasFolder($tempFolderName);
2017
            if ($createFolder === true) {
2018
                try {
2019
                    $defaultTemporaryFolder = $defaultFolder->createFolder($tempFolderName);
2020
                } catch (Exception $folderAccessException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
2021
                }
2022
            } else {
2023
                $defaultTemporaryFolder = $defaultFolder->getSubfolder($tempFolderName);
2024
            }
2025
        }
2026
2027
        return $defaultTemporaryFolder;
2028
    }
2029
2030
    /**
2031
     * Initializing workspace.
2032
     * Called from within this function, see fetchGroupData()
2033
     *
2034
     * @see fetchGroupData()
2035
     * @internal should only be used from within TYPO3 Core
2036
     */
2037
    public function workspaceInit()
2038
    {
2039
        // Initializing workspace by evaluating and setting the workspace, possibly updating it in the user record!
2040
        $this->setWorkspace($this->user['workspace_id']);
2041
        // Limiting the DB mountpoints if there any selected in the workspace record
2042
        $this->initializeDbMountpointsInWorkspace();
2043
        $allowed_languages = $this->getTSConfig()['options.']['workspaces.']['allowed_languages.'][$this->workspace] ?? '';
2044
        if (!empty($allowed_languages)) {
2045
            $this->groupData['allowed_languages'] = GeneralUtility::uniqueList($allowed_languages);
2046
        }
2047
    }
2048
2049
    /**
2050
     * Limiting the DB mountpoints if there any selected in the workspace record
2051
     */
2052
    protected function initializeDbMountpointsInWorkspace()
2053
    {
2054
        $dbMountpoints = trim($this->workspaceRec['db_mountpoints'] ?? '');
2055
        if ($this->workspace > 0 && $dbMountpoints != '') {
2056
            $filteredDbMountpoints = [];
2057
            // Notice: We cannot call $this->getPagePermsClause(1);
2058
            // as usual because the group-list is not available at this point.
2059
            // But bypassing is fine because all we want here is check if the
2060
            // workspace mounts are inside the current webmounts rootline.
2061
            // The actual permission checking on page level is done elsewhere
2062
            // as usual anyway before the page tree is rendered.
2063
            $readPerms = '1=1';
2064
            // Traverse mount points of the
2065
            $dbMountpoints = GeneralUtility::intExplode(',', $dbMountpoints);
2066
            foreach ($dbMountpoints as $mpId) {
2067
                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...
2068
                    $filteredDbMountpoints[] = $mpId;
2069
                }
2070
            }
2071
            // Re-insert webmounts:
2072
            $filteredDbMountpoints = array_unique($filteredDbMountpoints);
2073
            $this->groupData['webmounts'] = implode(',', $filteredDbMountpoints);
2074
        }
2075
    }
2076
2077
    /**
2078
     * Checking if a workspace is allowed for backend user
2079
     *
2080
     * @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)
2081
     * @param string $fields List of fields to select. Default fields are all
2082
     * @return array Output will also show how access was granted. Admin users will have a true output regardless of input.
2083
     * @internal should only be used from within TYPO3 Core
2084
     */
2085
    public function checkWorkspace($wsRec, $fields = '*')
2086
    {
2087
        $retVal = false;
2088
        // If not array, look up workspace record:
2089
        if (!is_array($wsRec)) {
2090
            switch ((string)$wsRec) {
2091
                case '0':
2092
                    $wsRec = ['uid' => $wsRec];
2093
                    break;
2094
                default:
2095
                    if (ExtensionManagementUtility::isLoaded('workspaces')) {
2096
                        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
2097
                        $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
2098
                        $wsRec = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields))
2099
                            ->from('sys_workspace')
2100
                            ->where($queryBuilder->expr()->eq(
2101
                                'uid',
2102
                                $queryBuilder->createNamedParameter($wsRec, \PDO::PARAM_INT)
2103
                            ))
2104
                            ->orderBy('title')
2105
                            ->setMaxResults(1)
2106
                            ->execute()
2107
                            ->fetch(\PDO::FETCH_ASSOC);
2108
                    }
2109
            }
2110
        }
2111
        // If wsRec is set to an array, evaluate it:
2112
        if (is_array($wsRec)) {
2113
            if ($this->isAdmin()) {
2114
                return array_merge($wsRec, ['_ACCESS' => 'admin']);
2115
            }
2116
            switch ((string)$wsRec['uid']) {
2117
                    case '0':
2118
                        $retVal = (new Permission($this->groupData['workspace_perms'] ?? Permission::NOTHING))->showPagePermissionIsGranted()
2119
                            ? array_merge($wsRec, ['_ACCESS' => 'online'])
0 ignored issues
show
Coding Style introduced by
Expected 1 space before "?"; newline found
Loading history...
2120
                            : false;
0 ignored issues
show
Coding Style introduced by
Expected 1 space before ":"; newline found
Loading history...
2121
                        break;
2122
                    default:
2123
                        // Checking if the guy is admin:
2124
                        if (GeneralUtility::inList($wsRec['adminusers'], 'be_users_' . $this->user['uid'])) {
2125
                            return array_merge($wsRec, ['_ACCESS' => 'owner']);
2126
                        }
2127
                        // Checking if he is owner through a user group of his:
2128
                        foreach ($this->userGroupsUID as $groupUid) {
2129
                            if (GeneralUtility::inList($wsRec['adminusers'], 'be_groups_' . $groupUid)) {
2130
                                return array_merge($wsRec, ['_ACCESS' => 'owner']);
2131
                            }
2132
                        }
2133
                        // Checking if he is member as user:
2134
                        if (GeneralUtility::inList($wsRec['members'], 'be_users_' . $this->user['uid'])) {
2135
                            return array_merge($wsRec, ['_ACCESS' => 'member']);
2136
                        }
2137
                        // Checking if he is member through a user group of his:
2138
                        foreach ($this->userGroupsUID as $groupUid) {
2139
                            if (GeneralUtility::inList($wsRec['members'], 'be_groups_' . $groupUid)) {
2140
                                return array_merge($wsRec, ['_ACCESS' => 'member']);
2141
                            }
2142
                        }
2143
                }
0 ignored issues
show
Coding Style introduced by
Closing brace indented incorrectly; expected 12 spaces, found 16
Loading history...
2144
        }
2145
        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...
2146
    }
2147
2148
    /**
2149
     * Uses checkWorkspace() to check if current workspace is available for user.
2150
     * This function caches the result and so can be called many times with no performance loss.
2151
     *
2152
     * @return array See checkWorkspace()
2153
     * @see checkWorkspace()
2154
     * @internal should only be used from within TYPO3 Core
2155
     */
2156
    public function checkWorkspaceCurrent()
2157
    {
2158
        if (!isset($this->checkWorkspaceCurrent_cache)) {
2159
            $this->checkWorkspaceCurrent_cache = $this->checkWorkspace($this->workspace);
2160
        }
2161
        return $this->checkWorkspaceCurrent_cache;
2162
    }
2163
2164
    /**
2165
     * Setting workspace ID
2166
     *
2167
     * @param int $workspaceId ID of workspace to set for backend user. If not valid the default workspace for BE user is found and set.
2168
     * @internal should only be used from within TYPO3 Core
2169
     */
2170
    public function setWorkspace($workspaceId)
2171
    {
2172
        // Check workspace validity and if not found, revert to default workspace.
2173
        if (!$this->setTemporaryWorkspace($workspaceId)) {
2174
            $this->setDefaultWorkspace();
2175
        }
2176
        // Unset access cache:
2177
        $this->checkWorkspaceCurrent_cache = null;
2178
        // If ID is different from the stored one, change it:
2179
        if ((int)$this->workspace !== (int)$this->user['workspace_id']) {
2180
            $this->user['workspace_id'] = $this->workspace;
2181
            GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users')->update(
2182
                'be_users',
2183
                ['workspace_id' => $this->user['workspace_id']],
2184
                ['uid' => (int)$this->user['uid']]
2185
            );
2186
            $this->writelog(SystemLogType::EXTENSION, SystemLogGenericAction::UNDEFINED, SystemLogErrorClassification::MESSAGE, 0, 'User changed workspace to "' . $this->workspace . '"', []);
2187
        }
2188
    }
2189
2190
    /**
2191
     * Sets a temporary workspace in the context of the current backend user.
2192
     *
2193
     * @param int $workspaceId
2194
     * @return bool
2195
     * @internal should only be used from within TYPO3 Core
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
     * @internal should only be used from within TYPO3 Core
2214
     */
2215
    public function setDefaultWorkspace()
2216
    {
2217
        $this->workspace = (int)$this->getDefaultWorkspace();
2218
        $this->workspaceRec = $this->checkWorkspace($this->workspace);
2219
    }
2220
2221
    /**
2222
     * Return default workspace ID for user,
2223
     * if EXT:workspaces is not installed the user will be pushed to the
2224
     * Live workspace, if he has access to. If no workspace is available for the user, the workspace ID is set to "-99"
2225
     *
2226
     * @return int Default workspace id.
2227
     * @internal should only be used from within TYPO3 Core
2228
     */
2229
    public function getDefaultWorkspace()
2230
    {
2231
        if (!ExtensionManagementUtility::isLoaded('workspaces')) {
2232
            return 0;
2233
        }
2234
        // Online is default
2235
        if ($this->checkWorkspace(0)) {
2236
            return 0;
2237
        }
2238
        // Otherwise -99 is the fallback
2239
        $defaultWorkspace = -99;
2240
        // Traverse all workspaces
2241
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
2242
        $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
2243
        $result = $queryBuilder->select('*')
2244
            ->from('sys_workspace')
2245
            ->orderBy('title')
2246
            ->execute();
2247
        while ($workspaceRecord = $result->fetch()) {
2248
            if ($this->checkWorkspace($workspaceRecord)) {
2249
                $defaultWorkspace = (int)$workspaceRecord['uid'];
2250
                break;
2251
            }
2252
        }
2253
        return $defaultWorkspace;
2254
    }
2255
2256
    /**
2257
     * Writes an entry in the logfile/table
2258
     * Documentation in "TYPO3 Core API"
2259
     *
2260
     * @param int $type Denotes which module that has submitted the entry. See "TYPO3 Core API". Use "4" for extensions.
2261
     * @param int $action Denotes which specific operation that wrote the entry. Use "0" when no sub-categorizing applies
2262
     * @param int $error Flag. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin)
2263
     * @param int $details_nr The message number. Specific for each $type and $action. This will make it possible to translate errormessages to other languages
2264
     * @param string $details Default text that follows the message (in english!). Possibly translated by identification through type/action/details_nr
2265
     * @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
2266
     * @param string $tablename Table name. Special field used by tce_main.php.
2267
     * @param int|string $recuid Record UID. Special field used by tce_main.php.
2268
     * @param int|string $recpid Record PID. Special field used by tce_main.php. OBSOLETE
2269
     * @param int $event_pid The page_uid (pid) where the event occurred. Used to select log-content for specific pages.
2270
     * @param string $NEWid Special field used by tce_main.php. NEWid string of newly created records.
2271
     * @param int $userId Alternative Backend User ID (used for logging login actions where this is not yet known).
2272
     * @return int Log entry ID.
2273
     */
2274
    public function writelog($type, $action, $error, $details_nr, $details, $data, $tablename = '', $recuid = '', $recpid = '', $event_pid = -1, $NEWid = '', $userId = 0)
2275
    {
2276
        if (!$userId && !empty($this->user['uid'])) {
2277
            $userId = $this->user['uid'];
2278
        }
2279
2280
        if (!empty($this->user['ses_backuserid'])) {
2281
            if (empty($data)) {
2282
                $data = [];
2283
            }
2284
            $data['originalUser'] = $this->user['ses_backuserid'];
2285
        }
2286
2287
        $fields = [
2288
            'userid' => (int)$userId,
2289
            'type' => (int)$type,
2290
            'action' => (int)$action,
2291
            'error' => (int)$error,
2292
            'details_nr' => (int)$details_nr,
2293
            'details' => $details,
2294
            'log_data' => serialize($data),
2295
            'tablename' => $tablename,
2296
            'recuid' => (int)$recuid,
2297
            'IP' => (string)GeneralUtility::getIndpEnv('REMOTE_ADDR'),
2298
            'tstamp' => $GLOBALS['EXEC_TIME'] ?? time(),
2299
            'event_pid' => (int)$event_pid,
2300
            'NEWid' => $NEWid,
2301
            'workspace' => $this->workspace
2302
        ];
2303
2304
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_log');
2305
        $connection->insert(
2306
            'sys_log',
2307
            $fields,
2308
            [
2309
                \PDO::PARAM_INT,
2310
                \PDO::PARAM_INT,
2311
                \PDO::PARAM_INT,
2312
                \PDO::PARAM_INT,
2313
                \PDO::PARAM_INT,
2314
                \PDO::PARAM_STR,
2315
                \PDO::PARAM_STR,
2316
                \PDO::PARAM_STR,
2317
                \PDO::PARAM_INT,
2318
                \PDO::PARAM_STR,
2319
                \PDO::PARAM_INT,
2320
                \PDO::PARAM_INT,
2321
                \PDO::PARAM_STR,
2322
                \PDO::PARAM_STR,
2323
            ]
2324
        );
2325
2326
        return (int)$connection->lastInsertId('sys_log');
2327
    }
2328
2329
    /**
2330
     * Sends a warning to $email if there has been a certain amount of failed logins during a period.
2331
     * If a login fails, this function is called. It will look up the sys_log to see if there
2332
     * have been more than $max failed logins the last $secondsBack seconds (default 3600).
2333
     * If so, an email with a warning is sent to $email.
2334
     *
2335
     * @param string $email Email address
2336
     * @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.
2337
     * @param int $max Max allowed failures before a warning mail is sent
2338
     * @internal
2339
     */
2340
    public function checkLogFailures($email, $secondsBack = 3600, $max = 3)
2341
    {
2342
        if (!GeneralUtility::validEmail($email)) {
2343
            return;
2344
        }
2345
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
2346
2347
        // Get last flag set in the log for sending
2348
        $theTimeBack = $GLOBALS['EXEC_TIME'] - $secondsBack;
2349
        $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
2350
        $queryBuilder->select('tstamp')
2351
            ->from('sys_log')
2352
            ->where(
2353
                $queryBuilder->expr()->eq(
2354
                    'type',
2355
                    $queryBuilder->createNamedParameter(SystemLogType::LOGIN, \PDO::PARAM_INT)
2356
                ),
2357
                $queryBuilder->expr()->eq(
2358
                    'action',
2359
                    $queryBuilder->createNamedParameter(SystemLogLoginAction::SEND_FAILURE_WARNING_EMAIL, \PDO::PARAM_INT)
2360
                ),
2361
                $queryBuilder->expr()->gt(
2362
                    'tstamp',
2363
                    $queryBuilder->createNamedParameter($theTimeBack, \PDO::PARAM_INT)
2364
                )
2365
            )
2366
            ->orderBy('tstamp', 'DESC')
2367
            ->setMaxResults(1);
2368
        if ($testRow = $queryBuilder->execute()->fetch(\PDO::FETCH_ASSOC)) {
2369
            $theTimeBack = $testRow['tstamp'];
2370
        }
2371
2372
        $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_log');
2373
        $rowCount = $queryBuilder->count('uid')
2374
            ->from('sys_log')
2375
            ->where(
2376
                $queryBuilder->expr()->eq(
2377
                    'type',
2378
                    $queryBuilder->createNamedParameter(SystemLogType::LOGIN, \PDO::PARAM_INT)
2379
                ),
2380
                $queryBuilder->expr()->eq(
2381
                    'action',
2382
                    $queryBuilder->createNamedParameter(SystemLogLoginAction::ATTEMPT, \PDO::PARAM_INT)
2383
                ),
2384
                $queryBuilder->expr()->neq(
2385
                    'error',
2386
                    $queryBuilder->createNamedParameter(SystemLogErrorClassification::MESSAGE, \PDO::PARAM_INT)
2387
                ),
2388
                $queryBuilder->expr()->gt(
2389
                    'tstamp',
2390
                    $queryBuilder->createNamedParameter($theTimeBack, \PDO::PARAM_INT)
2391
                )
2392
            )
2393
            ->execute()
2394
            ->fetchColumn(0);
2395
2396
        // Check for more than $max number of error failures with the last period.
2397
        if ($rowCount > $max) {
2398
            $result = $queryBuilder
2399
                ->select('*')
2400
                ->orderBy('tstamp')
2401
                ->execute();
2402
2403
            // OK, so there were more than the max allowed number of login failures - so we will send an email then.
2404
            $this->sendLoginAttemptEmail($result, $email);
2405
            // Login failure attempt written to log
2406
            $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]);
2407
        }
2408
    }
2409
2410
    /**
2411
     * Sends out an email if the number of attempts have exceeded a limit.
2412
     *
2413
     * @param Statement $result
2414
     * @param string $emailAddress
2415
     */
2416
    protected function sendLoginAttemptEmail(Statement $result, string $emailAddress): void
2417
    {
2418
        $emailData = [];
2419
        while ($row = $result->fetch(\PDO::FETCH_ASSOC)) {
2420
            $theData = unserialize($row['log_data'], ['allowed_classes' => false]);
2421
            $text = @sprintf($row['details'], (string)$theData[0], (string)$theData[1], (string)$theData[2]);
2422
            if ((int)$row['type'] === SystemLogType::LOGIN) {
2423
                $text = str_replace('###IP###', $row['IP'], $text);
2424
            }
2425
            $emailData[] = [
2426
                'row' => $row,
2427
                'text' => $text
2428
            ];
2429
        }
2430
        $email = GeneralUtility::makeInstance(FluidEmail::class)
2431
            ->to($emailAddress)
2432
            ->setTemplate('Security/LoginAttemptFailedWarning')
2433
            ->assign('lines', $emailData);
2434
        if ($GLOBALS['TYPO3_REQUEST'] instanceof ServerRequestInterface) {
2435
            $email->setRequest($GLOBALS['TYPO3_REQUEST']);
2436
        }
2437
        GeneralUtility::makeInstance(Mailer::class)->send($email);
2438
    }
2439
2440
    /**
2441
     * Getter for the cookie name
2442
     *
2443
     * @static
2444
     * @return string returns the configured cookie name
2445
     */
2446
    public static function getCookieName()
2447
    {
2448
        $configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']);
2449
        if (empty($configuredCookieName)) {
2450
            $configuredCookieName = 'be_typo_user';
2451
        }
2452
        return $configuredCookieName;
2453
    }
2454
2455
    /**
2456
     * If TYPO3_CONF_VARS['BE']['enabledBeUserIPLock'] is enabled and
2457
     * an IP-list is found in the User TSconfig objString "options.lockToIP",
2458
     * then make an IP comparison with REMOTE_ADDR and check if the IP address matches
2459
     *
2460
     * @return bool TRUE, if IP address validates OK (or no check is done at all because no restriction is set)
2461
     * @internal should only be used from within TYPO3 Core
2462
     */
2463
    public function checkLockToIP()
2464
    {
2465
        $isValid = true;
2466
        if ($GLOBALS['TYPO3_CONF_VARS']['BE']['enabledBeUserIPLock']) {
2467
            $IPList = trim($this->getTSConfig()['options.']['lockToIP'] ?? '');
2468
            if (!empty($IPList)) {
2469
                $isValid = GeneralUtility::cmpIP(GeneralUtility::getIndpEnv('REMOTE_ADDR'), $IPList);
2470
            }
2471
        }
2472
        return $isValid;
2473
    }
2474
2475
    /**
2476
     * Check if user is logged in and if so, call ->fetchGroupData() to load group information and
2477
     * access lists of all kind, further check IP, set the ->uc array.
2478
     * If no user is logged in the default behaviour is to exit with an error message.
2479
     * This function is called right after ->start() in fx. the TYPO3 Bootstrap.
2480
     *
2481
     * @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.
2482
     * @throws \RuntimeException
2483
     */
2484
    public function backendCheckLogin($proceedIfNoUserIsLoggedIn = false)
2485
    {
2486
        if (empty($this->user['uid'])) {
2487
            if ($proceedIfNoUserIsLoggedIn === false) {
2488
                $url = GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir;
2489
                HttpUtility::redirect($url);
2490
            }
2491
        } else {
2492
            // ...and if that's the case, call these functions
2493
            $this->fetchGroupData();
2494
            // The groups are fetched and ready for permission checking in this initialization.
2495
            // Tables.php must be read before this because stuff like the modules has impact in this
2496
            if ($this->checkLockToIP()) {
2497
                if ($this->isUserAllowedToLogin()) {
2498
                    // Setting the UC array. It's needed with fetchGroupData first, due to default/overriding of values.
2499
                    $this->backendSetUC();
2500
                    if ($this->loginSessionStarted) {
2501
                        // Also, if there is a recovery link set, unset it now
2502
                        // this will be moved into its own Event at a later stage.
2503
                        // If a token was set previously, this is now unset, as it was now possible to log-in
2504
                        if ($this->user['password_reset_token'] ?? '') {
2505
                            GeneralUtility::makeInstance(ConnectionPool::class)
2506
                                ->getConnectionForTable($this->user_table)
2507
                                ->update($this->user_table, ['password_reset_token' => ''], ['uid' => $this->user['uid']]);
2508
                        }
2509
                        // Process hooks
2510
                        $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['backendUserLogin'];
2511
                        foreach ($hooks ?? [] as $_funcRef) {
2512
                            $_params = ['user' => $this->user];
2513
                            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2514
                        }
2515
                    }
2516
                } else {
2517
                    throw new \RuntimeException('Login Error: TYPO3 is in maintenance mode at the moment. Only administrators are allowed access.', 1294585860);
2518
                }
2519
            } else {
2520
                throw new \RuntimeException('Login Error: IP locking prevented you from being authorized. Can\'t proceed, sorry.', 1294585861);
2521
            }
2522
        }
2523
    }
2524
2525
    /**
2526
     * Initialize the internal ->uc array for the backend user
2527
     * Will make the overrides if necessary, and write the UC back to the be_users record if changes has happened
2528
     *
2529
     * @internal
2530
     */
2531
    public function backendSetUC()
2532
    {
2533
        // UC - user configuration is a serialized array inside the user object
2534
        // If there is a saved uc we implement that instead of the default one.
2535
        $this->unpack_uc();
2536
        // Setting defaults if uc is empty
2537
        $updated = false;
2538
        $originalUc = [];
2539
        if (is_array($this->uc) && isset($this->uc['ucSetByInstallTool'])) {
2540
            $originalUc = $this->uc;
2541
            unset($originalUc['ucSetByInstallTool'], $this->uc);
2542
        }
2543
        if (!is_array($this->uc)) {
2544
            $this->uc = array_merge(
2545
                $this->uc_default,
2546
                (array)$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUC'],
2547
                GeneralUtility::removeDotsFromTS((array)($this->getTSConfig()['setup.']['default.'] ?? [])),
2548
                $originalUc
2549
            );
2550
            $this->overrideUC();
2551
            $updated = true;
2552
        }
2553
        // If TSconfig is updated, update the defaultUC.
2554
        if ($this->userTSUpdated) {
2555
            $this->overrideUC();
2556
            $updated = true;
2557
        }
2558
        // Setting default lang from be_user record.
2559
        if (!isset($this->uc['lang'])) {
2560
            $this->uc['lang'] = $this->user['lang'];
2561
            $updated = true;
2562
        }
2563
        // Setting the time of the first login:
2564
        if (!isset($this->uc['firstLoginTimeStamp'])) {
2565
            $this->uc['firstLoginTimeStamp'] = $GLOBALS['EXEC_TIME'];
2566
            $updated = true;
2567
        }
2568
        // Saving if updated.
2569
        if ($updated) {
2570
            $this->writeUC();
2571
        }
2572
    }
2573
2574
    /**
2575
     * Override: Call this function every time the uc is updated.
2576
     * That is 1) by reverting to default values, 2) in the setup-module, 3) userTS changes (userauthgroup)
2577
     *
2578
     * @internal
2579
     */
2580
    public function overrideUC()
2581
    {
2582
        $this->uc = array_merge((array)$this->uc, (array)($this->getTSConfig()['setup.']['override.'] ?? []));
2583
    }
2584
2585
    /**
2586
     * Clears the user[uc] and ->uc to blank strings. Then calls ->backendSetUC() to fill it again with reset contents
2587
     *
2588
     * @internal
2589
     */
2590
    public function resetUC()
2591
    {
2592
        $this->user['uc'] = '';
2593
        $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...
2594
        $this->backendSetUC();
2595
    }
2596
2597
    /**
2598
     * Determines whether a backend user is allowed to access the backend.
2599
     *
2600
     * The conditions are:
2601
     * + backend user is a regular user and adminOnly is not defined
2602
     * + backend user is an admin user
2603
     * + backend user is used in CLI context and adminOnly is explicitly set to "2" (see CommandLineUserAuthentication)
2604
     * + backend user is being controlled by an admin user
2605
     *
2606
     * @return bool Whether a backend user is allowed to access the backend
2607
     */
2608
    protected function isUserAllowedToLogin()
2609
    {
2610
        $isUserAllowedToLogin = false;
2611
        $adminOnlyMode = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'];
2612
        // Backend user is allowed if adminOnly is not set or user is an admin:
2613
        if (!$adminOnlyMode || $this->isAdmin()) {
2614
            $isUserAllowedToLogin = true;
2615
        } elseif ($this->user['ses_backuserid']) {
2616
            $backendUserId = (int)$this->user['ses_backuserid'];
2617
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
2618
            $isUserAllowedToLogin = (bool)$queryBuilder->count('uid')
2619
                ->from('be_users')
2620
                ->where(
2621
                    $queryBuilder->expr()->eq(
2622
                        'uid',
2623
                        $queryBuilder->createNamedParameter($backendUserId, \PDO::PARAM_INT)
2624
                    ),
2625
                    $queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT))
2626
                )
2627
                ->execute()
2628
                ->fetchColumn(0);
2629
        }
2630
        return $isUserAllowedToLogin;
2631
    }
2632
2633
    /**
2634
     * Logs out the current user and clears the form protection tokens.
2635
     */
2636
    public function logoff()
2637
    {
2638
        if (isset($GLOBALS['BE_USER'])
2639
            && $GLOBALS['BE_USER'] instanceof self
2640
            && isset($GLOBALS['BE_USER']->user['uid'])
2641
        ) {
2642
            FormProtectionFactory::get()->clean();
2643
            // Release the locked records
2644
            $this->releaseLockedRecords((int)$GLOBALS['BE_USER']->user['uid']);
2645
2646
            if ($this->isSystemMaintainer()) {
2647
                // If user is system maintainer, destroy its possibly valid install tool session.
2648
                $session = new SessionService();
2649
                $session->destroySession();
2650
            }
2651
        }
2652
        parent::logoff();
2653
    }
2654
2655
    /**
2656
     * Remove any "locked records" added for editing for the given user (= current backend user)
2657
     * @param int $userId
2658
     */
2659
    protected function releaseLockedRecords(int $userId)
2660
    {
2661
        if ($userId > 0) {
2662
            GeneralUtility::makeInstance(ConnectionPool::class)
2663
                ->getConnectionForTable('sys_lockedrecords')
2664
                ->delete(
2665
                    'sys_lockedrecords',
2666
                    ['userid' => $userId]
2667
                );
2668
        }
2669
    }
2670
}
2671