Passed
Push — master ( f31c4e...b775ab )
by
unknown
13:38
created

workspaceAllowsLiveEditingInTable()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 17
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 7
nc 4
nop 1
dl 0
loc 17
rs 9.6111
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the TYPO3 CMS project.
5
 *
6
 * It is free software; you can redistribute it and/or modify it under
7
 * the terms of the GNU General Public License, either version 2
8
 * of the License, or any later version.
9
 *
10
 * For the full copyright and license information, please read the
11
 * LICENSE.txt file that was distributed with this source code.
12
 *
13
 * The TYPO3 project - inspiring people to share!
14
 */
15
16
namespace TYPO3\CMS\Core\Authentication;
17
18
use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher;
19
use TYPO3\CMS\Backend\Utility\BackendUtility;
20
use TYPO3\CMS\Core\Cache\CacheManager;
21
use TYPO3\CMS\Core\Core\Environment;
22
use TYPO3\CMS\Core\Database\Connection;
23
use TYPO3\CMS\Core\Database\ConnectionPool;
24
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
25
use TYPO3\CMS\Core\Database\Query\QueryHelper;
26
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
27
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
28
use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction;
29
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction;
30
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
31
use TYPO3\CMS\Core\Resource\Exception;
32
use TYPO3\CMS\Core\Resource\Filter\FileNameFilter;
33
use TYPO3\CMS\Core\Resource\Folder;
34
use TYPO3\CMS\Core\Resource\ResourceFactory;
35
use TYPO3\CMS\Core\Resource\ResourceStorage;
36
use TYPO3\CMS\Core\Resource\StorageRepository;
37
use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction;
38
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
39
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
40
use TYPO3\CMS\Core\Type\Bitmask\BackendGroupMountOption;
41
use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
42
use TYPO3\CMS\Core\Type\Bitmask\Permission;
43
use TYPO3\CMS\Core\Type\Exception\InvalidEnumerationValueException;
44
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
45
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
46
use TYPO3\CMS\Core\Utility\GeneralUtility;
47
use TYPO3\CMS\Core\Utility\HttpUtility;
48
use TYPO3\CMS\Core\Utility\StringUtility;
49
use TYPO3\CMS\Install\Service\SessionService;
50
51
/**
52
 * TYPO3 backend user authentication
53
 * Contains most of the functions used for checking permissions, authenticating users,
54
 * setting up the user, and API for user from outside.
55
 * This class contains the configuration of the database fields used plus some
56
 * functions for the authentication process of backend users.
57
 */
58
class BackendUserAuthentication extends AbstractUserAuthentication
59
{
60
    public const ROLE_SYSTEMMAINTAINER = 'systemMaintainer';
61
62
    /**
63
     * Should be set to the usergroup-column (id-list) in the user-record
64
     * @var string
65
     */
66
    public $usergroup_column = 'usergroup';
67
68
    /**
69
     * The name of the group-table
70
     * @var string
71
     */
72
    public $usergroup_table = 'be_groups';
73
74
    /**
75
     * holds lists of eg. tables, fields and other values related to the permission-system. See fetchGroupData
76
     * @var array
77
     * @internal
78
     */
79
    public $groupData = [
80
        'filemounts' => []
81
    ];
82
83
    /**
84
     * This array will hold the groups that the user is a member of
85
     * @var array
86
     */
87
    public $userGroups = [];
88
89
    /**
90
     * This array holds the uid's of the groups in the listed order
91
     * @var array
92
     */
93
    public $userGroupsUID = [];
94
95
    /**
96
     * This is $this->userGroupsUID imploded to a comma list... Will correspond to the 'usergroup_cached_list'
97
     * @var string
98
     */
99
    public $groupList = '';
100
101
    /**
102
     * User workspace.
103
     * -99 is ERROR (none available)
104
     * 0 is online
105
     * >0 is custom workspaces
106
     * @var int
107
     */
108
    public $workspace = -99;
109
110
    /**
111
     * Custom workspace record if any
112
     * @var array
113
     */
114
    public $workspaceRec = [];
115
116
    /**
117
     * Used to accumulate data for the user-group.
118
     * DON NOT USE THIS EXTERNALLY!
119
     * Use $this->groupData instead
120
     * @var array
121
     * @internal
122
     */
123
    public $dataLists = [
124
        'webmount_list' => '',
125
        'filemount_list' => '',
126
        'file_permissions' => '',
127
        'modList' => '',
128
        'tables_select' => '',
129
        'tables_modify' => '',
130
        'pagetypes_select' => '',
131
        'non_exclude_fields' => '',
132
        'explicit_allowdeny' => '',
133
        'allowed_languages' => '',
134
        'workspace_perms' => '',
135
        'available_widgets' => '',
136
        'custom_options' => ''
137
    ];
138
139
    /**
140
     * List of group_id's in the order they are processed.
141
     * @var array
142
     * @internal should only be used from within TYPO3 Core
143
     */
144
    public $includeGroupArray = [];
145
146
    /**
147
     * @var array Parsed user TSconfig
148
     */
149
    protected $userTS = [];
150
151
    /**
152
     * @var bool True if the user TSconfig was parsed and needs to be cached.
153
     */
154
    protected $userTSUpdated = false;
155
156
    /**
157
     * Contains last error message
158
     * @internal should only be used from within TYPO3 Core
159
     * @var string
160
     */
161
    public $errorMsg = '';
162
163
    /**
164
     * Cache for checkWorkspaceCurrent()
165
     * @var array|null
166
     */
167
    protected $checkWorkspaceCurrent_cache;
168
169
    /**
170
     * @var \TYPO3\CMS\Core\Resource\ResourceStorage[]
171
     */
172
    protected $fileStorages;
173
174
    /**
175
     * @var array
176
     */
177
    protected $filePermissions;
178
179
    /**
180
     * Table in database with user data
181
     * @var string
182
     */
183
    public $user_table = 'be_users';
184
185
    /**
186
     * Column for login-name
187
     * @var string
188
     */
189
    public $username_column = 'username';
190
191
    /**
192
     * Column for password
193
     * @var string
194
     */
195
    public $userident_column = 'password';
196
197
    /**
198
     * Column for user-id
199
     * @var string
200
     */
201
    public $userid_column = 'uid';
202
203
    /**
204
     * @var string
205
     */
206
    public $lastLogin_column = 'lastlogin';
207
208
    /**
209
     * @var array
210
     */
211
    public $enablecolumns = [
212
        'rootLevel' => 1,
213
        'deleted' => 'deleted',
214
        'disabled' => 'disable',
215
        'starttime' => 'starttime',
216
        'endtime' => 'endtime'
217
    ];
218
219
    /**
220
     * Form field with login-name
221
     * @var string
222
     */
223
    public $formfield_uname = 'username';
224
225
    /**
226
     * Form field with password
227
     * @var string
228
     */
229
    public $formfield_uident = 'userident';
230
231
    /**
232
     * Form field with status: *'login', 'logout'
233
     * @var string
234
     */
235
    public $formfield_status = 'login_status';
236
237
    /**
238
     * Decides if the writelog() function is called at login and logout
239
     * @var bool
240
     */
241
    public $writeStdLog = true;
242
243
    /**
244
     * If the writelog() functions is called if a login-attempt has be tried without success
245
     * @var bool
246
     */
247
    public $writeAttemptLog = true;
248
249
    /**
250
     * Session timeout (on the server), defaults to 8 hours for backend user
251
     *
252
     * If >0: session-timeout in seconds.
253
     * If <=0: Instant logout after login.
254
     * The value must be at least 180 to avoid side effects.
255
     *
256
     * @var int
257
     * @internal should only be used from within TYPO3 Core
258
     */
259
    public $sessionTimeout = 28800;
260
261
    /**
262
     * @var int
263
     * @internal should only be used from within TYPO3 Core
264
     */
265
    public $firstMainGroup = 0;
266
267
    /**
268
     * User Config
269
     * @var array
270
     */
271
    public $uc;
272
273
    /**
274
     * User Config Default values:
275
     * The array may contain other fields for configuration.
276
     * For this, see "setup" extension and "TSconfig" document (User TSconfig, "setup.[xxx]....")
277
     * Reserved keys for other storage of session data:
278
     * moduleData
279
     * moduleSessionID
280
     * @var array
281
     * @internal should only be used from within TYPO3 Core
282
     */
283
    public $uc_default = [
284
        'interfaceSetup' => '',
285
        // serialized content that is used to store interface pane and menu positions. Set by the logout.php-script
286
        'moduleData' => [],
287
        // user-data for the modules
288
        'emailMeAtLogin' => 0,
289
        'titleLen' => 50,
290
        'edit_RTE' => '1',
291
        'edit_docModuleUpload' => '1',
292
        'resizeTextareas_MaxHeight' => 500,
293
    ];
294
295
    /**
296
     * Login type, used for services.
297
     * @var string
298
     */
299
    public $loginType = 'BE';
300
301
    /**
302
     * Constructor
303
     */
304
    public function __construct()
305
    {
306
        $this->name = self::getCookieName();
307
        $this->sessionTimeout = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['sessionTimeout'];
308
        parent::__construct();
309
    }
310
311
    /**
312
     * Returns TRUE if user is admin
313
     * Basically this function evaluates if the ->user[admin] field has bit 0 set. If so, user is admin.
314
     *
315
     * @return bool
316
     */
317
    public function isAdmin()
318
    {
319
        return is_array($this->user) && ($this->user['admin'] & 1) == 1;
320
    }
321
322
    /**
323
     * Returns TRUE if the current user is a member of group $groupId
324
     * $groupId must be set. $this->groupList must contain groups
325
     * Will return TRUE also if the user is a member of a group through subgroups.
326
     *
327
     * @param int $groupId Group ID to look for in $this->groupList
328
     * @return bool
329
     * @internal should only be used from within TYPO3 Core, use Context API for quicker access
330
     */
331
    public function isMemberOfGroup($groupId)
332
    {
333
        $groupId = (int)$groupId;
334
        if ($this->groupList && $groupId) {
335
            return GeneralUtility::inList($this->groupList, (string)$groupId);
336
        }
337
        return false;
338
    }
339
340
    /**
341
     * Checks if the permissions is granted based on a page-record ($row) and $perms (binary and'ed)
342
     *
343
     * Bits for permissions, see $perms variable:
344
     *
345
     * 1  - Show:             See/Copy page and the pagecontent.
346
     * 2  - Edit page:        Change/Move the page, eg. change title, startdate, hidden.
347
     * 4  - Delete page:      Delete the page and pagecontent.
348
     * 8  - New pages:        Create new pages under the page.
349
     * 16 - Edit pagecontent: Change/Add/Delete/Move pagecontent.
350
     *
351
     * @param array $row Is the pagerow for which the permissions is checked
352
     * @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.
353
     * @return bool
354
     */
355
    public function doesUserHaveAccess($row, $perms)
356
    {
357
        $userPerms = $this->calcPerms($row);
358
        return ($userPerms & $perms) == $perms;
359
    }
360
361
    /**
362
     * Checks if the page id or page record ($idOrRow) is found within the webmounts set up for the user.
363
     * This should ALWAYS be checked for any page id a user works with, whether it's about reading, writing or whatever.
364
     * The point is that this will add the security that a user can NEVER touch parts outside his mounted
365
     * pages in the page tree. This is otherwise possible if the raw page permissions allows for it.
366
     * So this security check just makes it easier to make safe user configurations.
367
     * If the user is admin OR if this feature is disabled
368
     * (fx. by setting TYPO3_CONF_VARS['BE']['lockBeUserToDBmounts']=0) then it returns "1" right away
369
     * Otherwise the function will return the uid of the webmount which was first found in the rootline of the input page $id
370
     *
371
     * @param int|array $idOrRow Page ID or full page record to check
372
     * @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!)
373
     * @param bool|int $exitOnError If set, then the function will exit with an error message.
374
     * @throws \RuntimeException
375
     * @return int|null The page UID of a page in the rootline that matched a mount point
376
     */
377
    public function isInWebMount($idOrRow, $readPerms = '', $exitOnError = 0)
378
    {
379
        if (!$GLOBALS['TYPO3_CONF_VARS']['BE']['lockBeUserToDBmounts'] || $this->isAdmin()) {
380
            return 1;
381
        }
382
        $checkRec = [];
383
        $fetchPageFromDatabase = true;
384
        if (is_array($idOrRow)) {
385
            if (empty($idOrRow['uid'])) {
386
                throw new \RuntimeException('The given page record is invalid. Missing uid.', 1578950324);
387
            }
388
            $checkRec = $idOrRow;
389
            $id = (int)$idOrRow['uid'];
390
            // ensure the required fields are present on the record
391
            if (isset($checkRec['t3ver_oid'], $checkRec[$GLOBALS['TCA']['pages']['ctrl']['languageField']], $checkRec[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']])) {
392
                $fetchPageFromDatabase = false;
393
            }
394
        } else {
395
            $id = (int)$idOrRow;
396
        }
397
        if ($fetchPageFromDatabase) {
398
            // Check if input id is an offline version page in which case we will map id to the online version:
399
            $checkRec = BackendUtility::getRecord(
400
                'pages',
401
                $id,
402
                't3ver_oid,'
403
                . $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'] . ','
404
                . $GLOBALS['TCA']['pages']['ctrl']['languageField']
405
            );
406
        }
407
        if ($checkRec['t3ver_oid'] > 0) {
408
            $id = (int)$checkRec['t3ver_oid'];
409
        }
410
        // if current rec is a translation then get uid from l10n_parent instead
411
        // because web mounts point to pages in default language and rootline returns uids of default languages
412
        if ((int)$checkRec[$GLOBALS['TCA']['pages']['ctrl']['languageField']] !== 0 && (int)$checkRec[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] !== 0) {
413
            $id = (int)$checkRec[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']];
414
        }
415
        if (!$readPerms) {
416
            $readPerms = $this->getPagePermsClause(Permission::PAGE_SHOW);
417
        }
418
        if ($id > 0) {
419
            $wM = $this->returnWebmounts();
420
            $rL = BackendUtility::BEgetRootLine($id, ' AND ' . $readPerms, true);
421
            foreach ($rL as $v) {
422
                if ($v['uid'] && in_array($v['uid'], $wM)) {
423
                    return $v['uid'];
424
                }
425
            }
426
        }
427
        if ($exitOnError) {
428
            throw new \RuntimeException('Access Error: This page is not within your DB-mounts', 1294586445);
429
        }
430
        return null;
431
    }
432
433
    /**
434
     * Checks access to a backend module with the $MCONF passed as first argument
435
     *
436
     * @param array $conf $MCONF array of a backend module!
437
     * @throws \RuntimeException
438
     * @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
439
     */
440
    public function modAccess($conf)
441
    {
442
        if (!BackendUtility::isModuleSetInTBE_MODULES($conf['name'])) {
443
            throw new \RuntimeException('Fatal Error: This module "' . $conf['name'] . '" is not enabled in TBE_MODULES', 1294586446);
444
        }
445
        // Workspaces check:
446
        if (
447
            !empty($conf['workspaces'])
448
            && ExtensionManagementUtility::isLoaded('workspaces')
449
            && ($this->workspace !== 0 || !GeneralUtility::inList($conf['workspaces'], 'online'))
450
            && ($this->workspace <= 0 || !GeneralUtility::inList($conf['workspaces'], 'custom'))
451
        ) {
452
            throw new \RuntimeException('Workspace Error: This module "' . $conf['name'] . '" is not available under the current workspace', 1294586447);
453
        }
454
        // Returns false if conf[access] is set to system maintainers and the user is system maintainer
455
        if (strpos($conf['access'], self::ROLE_SYSTEMMAINTAINER) !== false && !$this->isSystemMaintainer()) {
456
            throw new \RuntimeException('This module "' . $conf['name'] . '" is only available as system maintainer', 1504804727);
457
        }
458
        // Returns TRUE if conf[access] is not set at all or if the user is admin
459
        if (!$conf['access'] || $this->isAdmin()) {
460
            return true;
461
        }
462
        // If $conf['access'] is set but not with 'admin' then we return TRUE, if the module is found in the modList
463
        $acs = false;
464
        if (strpos($conf['access'], 'admin') === false && $conf['name']) {
465
            $acs = $this->check('modules', $conf['name']);
466
        }
467
        if (!$acs) {
468
            throw new \RuntimeException('Access Error: You don\'t have access to this module.', 1294586448);
469
        }
470
        return $acs;
471
    }
472
473
    /**
474
     * Checks if the user is in the valid list of allowed system maintainers. if the list is not set,
475
     * then all admins are system maintainers. If the list is empty, no one is system maintainer (good for production
476
     * systems). If the currently logged in user is in "switch user" mode, this method will return false.
477
     *
478
     * @return bool
479
     */
480
    public function isSystemMaintainer(): bool
481
    {
482
        if (!$this->isAdmin()) {
483
            return false;
484
        }
485
486
        if ((int)$GLOBALS['BE_USER']->user['ses_backuserid'] !== 0) {
487
            return false;
488
        }
489
        if (Environment::getContext()->isDevelopment()) {
490
            return true;
491
        }
492
        $systemMaintainers = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? [];
493
        $systemMaintainers = array_map('intval', $systemMaintainers);
494
        if (!empty($systemMaintainers)) {
495
            return in_array((int)$this->user['uid'], $systemMaintainers, true);
496
        }
497
        // No system maintainers set up yet, so any admin is allowed to access the modules
498
        // but explicitly no system maintainers allowed (empty string in TYPO3_CONF_VARS).
499
        // @todo: this needs to be adjusted once system maintainers can log into the install tool with their credentials
500
        if (isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'])
501
            && empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'])) {
502
            return false;
503
        }
504
        return true;
505
    }
506
507
    /**
508
     * Returns a WHERE-clause for the pages-table where user permissions according to input argument, $perms, is validated.
509
     * $perms is the "mask" used to select. Fx. if $perms is 1 then you'll get all pages that a user can actually see!
510
     * 2^0 = show (1)
511
     * 2^1 = edit (2)
512
     * 2^2 = delete (4)
513
     * 2^3 = new (8)
514
     * If the user is 'admin' " 1=1" is returned (no effect)
515
     * 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)
516
     * The 95% use of this function is "->getPagePermsClause(1)" which will
517
     * return WHERE clauses for *selecting* pages in backend listings - in other words this will check read permissions.
518
     *
519
     * @param int $perms Permission mask to use, see function description
520
     * @return string Part of where clause. Prefix " AND " to this.
521
     * @internal should only be used from within TYPO3 Core, use PagePermissionDatabaseRestriction instead.
522
     */
523
    public function getPagePermsClause($perms)
524
    {
525
        if (is_array($this->user)) {
526
            if ($this->isAdmin()) {
527
                return ' 1=1';
528
            }
529
            // Make sure it's integer.
530
            $perms = (int)$perms;
531
            $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
532
                ->getQueryBuilderForTable('pages')
533
                ->expr();
534
535
            // User
536
            $constraint = $expressionBuilder->orX(
537
                $expressionBuilder->comparison(
538
                    $expressionBuilder->bitAnd('pages.perms_everybody', $perms),
539
                    ExpressionBuilder::EQ,
540
                    $perms
541
                ),
542
                $expressionBuilder->andX(
543
                    $expressionBuilder->eq('pages.perms_userid', (int)$this->user['uid']),
544
                    $expressionBuilder->comparison(
545
                        $expressionBuilder->bitAnd('pages.perms_user', $perms),
546
                        ExpressionBuilder::EQ,
547
                        $perms
548
                    )
549
                )
550
            );
551
552
            // Group (if any is set)
553
            if ($this->groupList) {
554
                $constraint->add(
555
                    $expressionBuilder->andX(
556
                        $expressionBuilder->in(
557
                            'pages.perms_groupid',
558
                            GeneralUtility::intExplode(',', $this->groupList)
559
                        ),
560
                        $expressionBuilder->comparison(
561
                            $expressionBuilder->bitAnd('pages.perms_group', $perms),
562
                            ExpressionBuilder::EQ,
563
                            $perms
564
                        )
565
                    )
566
                );
567
            }
568
569
            $constraint = ' (' . (string)$constraint . ')';
570
571
            // ****************
572
            // getPagePermsClause-HOOK
573
            // ****************
574
            foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getPagePermsClause'] ?? [] as $_funcRef) {
575
                $_params = ['currentClause' => $constraint, 'perms' => $perms];
576
                $constraint = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
577
            }
578
            return $constraint;
579
        }
580
        return ' 1=0';
581
    }
582
583
    /**
584
     * Returns a combined binary representation of the current users permissions for the page-record, $row.
585
     * The perms for user, group and everybody is OR'ed together (provided that the page-owner is the user
586
     * and for the groups that the user is a member of the group.
587
     * If the user is admin, 31 is returned	(full permissions for all five flags)
588
     *
589
     * @param array $row Input page row with all perms_* fields available.
590
     * @return int Bitwise representation of the users permissions in relation to input page row, $row
591
     */
592
    public function calcPerms($row)
593
    {
594
        // Return 31 for admin users.
595
        if ($this->isAdmin()) {
596
            return Permission::ALL;
597
        }
598
        // Return 0 if page is not within the allowed web mount
599
        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...
600
            return Permission::NOTHING;
601
        }
602
        $out = Permission::NOTHING;
603
        if (
604
            isset($row['perms_userid']) && isset($row['perms_user']) && isset($row['perms_groupid'])
605
            && isset($row['perms_group']) && isset($row['perms_everybody']) && isset($this->groupList)
606
        ) {
607
            if ($this->user['uid'] == $row['perms_userid']) {
608
                $out |= $row['perms_user'];
609
            }
610
            if ($this->isMemberOfGroup($row['perms_groupid'])) {
611
                $out |= $row['perms_group'];
612
            }
613
            $out |= $row['perms_everybody'];
614
        }
615
        // ****************
616
        // CALCPERMS hook
617
        // ****************
618
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['calcPerms'] ?? [] as $_funcRef) {
619
            $_params = [
620
                'row' => $row,
621
                'outputPermissions' => $out
622
            ];
623
            $out = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
624
        }
625
        return $out;
626
    }
627
628
    /**
629
     * Returns TRUE if the RTE (Rich Text Editor) is enabled for the user.
630
     *
631
     * @return bool
632
     * @internal should only be used from within TYPO3 Core
633
     */
634
    public function isRTE()
635
    {
636
        return (bool)$this->uc['edit_RTE'];
637
    }
638
639
    /**
640
     * Returns TRUE if the $value is found in the list in a $this->groupData[] index pointed to by $type (array key).
641
     * Can thus be users to check for modules, exclude-fields, select/modify permissions for tables etc.
642
     * If user is admin TRUE is also returned
643
     * Please see the document Inside TYPO3 for examples.
644
     *
645
     * @param string $type The type value; "webmounts", "filemounts", "pagetypes_select", "tables_select", "tables_modify", "non_exclude_fields", "modules", "available_widgets"
646
     * @param string $value String to search for in the groupData-list
647
     * @return bool TRUE if permission is granted (that is, the value was found in the groupData list - or the BE_USER is "admin")
648
     */
649
    public function check($type, $value)
650
    {
651
        return isset($this->groupData[$type])
652
            && ($this->isAdmin() || GeneralUtility::inList($this->groupData[$type], $value));
653
    }
654
655
    /**
656
     * Checking the authMode of a select field with authMode set
657
     *
658
     * @param string $table Table name
659
     * @param string $field Field name (must be configured in TCA and of type "select" with authMode set!)
660
     * @param string $value Value to evaluation (single value, must not contain any of the chars ":,|")
661
     * @param string $authMode Auth mode keyword (explicitAllow, explicitDeny, individual)
662
     * @return bool Whether access is granted or not
663
     */
664
    public function checkAuthMode($table, $field, $value, $authMode)
665
    {
666
        // Admin users can do anything:
667
        if ($this->isAdmin()) {
668
            return true;
669
        }
670
        // Allow all blank values:
671
        if ((string)$value === '') {
672
            return true;
673
        }
674
        // Allow dividers:
675
        if ($value === '--div--') {
676
            return true;
677
        }
678
        // Certain characters are not allowed in the value
679
        if (preg_match('/[:|,]/', $value)) {
680
            return false;
681
        }
682
        // Initialize:
683
        $testValue = $table . ':' . $field . ':' . $value;
684
        $out = true;
685
        // Checking value:
686
        switch ((string)$authMode) {
687
            case 'explicitAllow':
688
                if (!GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':ALLOW')) {
689
                    $out = false;
690
                }
691
                break;
692
            case 'explicitDeny':
693
                if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) {
694
                    $out = false;
695
                }
696
                break;
697
            case 'individual':
698
                if (is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
699
                    $items = $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'];
700
                    if (is_array($items)) {
701
                        foreach ($items as $iCfg) {
702
                            if ((string)$iCfg[1] === (string)$value && $iCfg[4]) {
703
                                switch ((string)$iCfg[4]) {
704
                                    case 'EXPL_ALLOW':
705
                                        if (!GeneralUtility::inList(
706
                                            $this->groupData['explicit_allowdeny'],
707
                                            $testValue . ':ALLOW'
708
                                        )) {
709
                                            $out = false;
710
                                        }
711
                                        break;
712
                                    case 'EXPL_DENY':
713
                                        if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) {
714
                                            $out = false;
715
                                        }
716
                                        break;
717
                                }
718
                                break;
719
                            }
720
                        }
721
                    }
722
                }
723
                break;
724
        }
725
        return $out;
726
    }
727
728
    /**
729
     * Checking if a language value (-1, 0 and >0 for sys_language records) is allowed to be edited by the user.
730
     *
731
     * @param int $langValue Language value to evaluate
732
     * @return bool Returns TRUE if the language value is allowed, otherwise FALSE.
733
     */
734
    public function checkLanguageAccess($langValue)
735
    {
736
        // The users language list must be non-blank - otherwise all languages are allowed.
737
        if (trim($this->groupData['allowed_languages']) !== '') {
738
            $langValue = (int)$langValue;
739
            // Language must either be explicitly allowed OR the lang Value be "-1" (all languages)
740
            if ($langValue != -1 && !$this->check('allowed_languages', (string)$langValue)) {
741
                return false;
742
            }
743
        }
744
        return true;
745
    }
746
747
    /**
748
     * Check if user has access to all existing localizations for a certain record
749
     *
750
     * @param string $table The table
751
     * @param array $record The current record
752
     * @return bool
753
     */
754
    public function checkFullLanguagesAccess($table, $record)
755
    {
756
        if (!$this->checkLanguageAccess(0)) {
757
            return false;
758
        }
759
760
        if (BackendUtility::isTableLocalizable($table)) {
761
            $pointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
762
            $pointerValue = $record[$pointerField] > 0 ? $record[$pointerField] : $record['uid'];
763
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
764
            $queryBuilder->getRestrictions()
765
                ->removeAll()
766
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
767
                ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->workspace));
768
            $recordLocalizations = $queryBuilder->select('*')
769
                ->from($table)
770
                ->where(
771
                    $queryBuilder->expr()->eq(
772
                        $pointerField,
773
                        $queryBuilder->createNamedParameter($pointerValue, \PDO::PARAM_INT)
774
                    )
775
                )
776
                ->execute()
777
                ->fetchAll();
778
779
            foreach ($recordLocalizations as $recordLocalization) {
780
                if (!$this->checkLanguageAccess($recordLocalization[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
781
                    return false;
782
                }
783
            }
784
        }
785
        return true;
786
    }
787
788
    /**
789
     * Checking if a user has editing access to a record from a $GLOBALS['TCA'] table.
790
     * The checks does not take page permissions and other "environmental" things into account.
791
     * It only deal with record internals; If any values in the record fields disallows it.
792
     * For instance languages settings, authMode selector boxes are evaluated (and maybe more in the future).
793
     * It will check for workspace dependent access.
794
     * The function takes an ID (int) or row (array) as second argument.
795
     *
796
     * @param string $table Table name
797
     * @param int|array $idOrRow If integer, then this is the ID of the record. If Array this just represents fields in the record.
798
     * @param bool $newRecord Set, if testing a new (non-existing) record array. Will disable certain checks that doesn't make much sense in that context.
799
     * @param bool $deletedRecord Set, if testing a deleted record array.
800
     * @param bool $checkFullLanguageAccess Set, whenever access to all translations of the record is required
801
     * @return bool TRUE if OK, otherwise FALSE
802
     * @internal should only be used from within TYPO3 Core
803
     */
804
    public function recordEditAccessInternals($table, $idOrRow, $newRecord = false, $deletedRecord = false, $checkFullLanguageAccess = false)
805
    {
806
        if (!isset($GLOBALS['TCA'][$table])) {
807
            return false;
808
        }
809
        // Always return TRUE for Admin users.
810
        if ($this->isAdmin()) {
811
            return true;
812
        }
813
        // Fetching the record if the $idOrRow variable was not an array on input:
814
        if (!is_array($idOrRow)) {
815
            if ($deletedRecord) {
816
                $idOrRow = BackendUtility::getRecord($table, $idOrRow, '*', '', false);
817
            } else {
818
                $idOrRow = BackendUtility::getRecord($table, $idOrRow);
819
            }
820
            if (!is_array($idOrRow)) {
821
                $this->errorMsg = 'ERROR: Record could not be fetched.';
822
                return false;
823
            }
824
        }
825
        // Checking languages:
826
        if ($table === 'pages' && $checkFullLanguageAccess && !$this->checkFullLanguagesAccess($table, $idOrRow)) {
827
            return false;
828
        }
829
        if ($GLOBALS['TCA'][$table]['ctrl']['languageField']) {
830
            // Language field must be found in input row - otherwise it does not make sense.
831
            if (isset($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
832
                if (!$this->checkLanguageAccess($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
833
                    $this->errorMsg = 'ERROR: Language was not allowed.';
834
                    return false;
835
                }
836
                if (
837
                    $checkFullLanguageAccess && $idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']] == 0
838
                    && !$this->checkFullLanguagesAccess($table, $idOrRow)
839
                ) {
840
                    $this->errorMsg = 'ERROR: Related/affected language was not allowed.';
841
                    return false;
842
                }
843
            } else {
844
                $this->errorMsg = 'ERROR: The "languageField" field named "'
845
                    . $GLOBALS['TCA'][$table]['ctrl']['languageField'] . '" was not found in testing record!';
846
                return false;
847
            }
848
        }
849
        // Checking authMode fields:
850
        if (is_array($GLOBALS['TCA'][$table]['columns'])) {
851
            foreach ($GLOBALS['TCA'][$table]['columns'] as $fieldName => $fieldValue) {
852
                if (isset($idOrRow[$fieldName])) {
853
                    if (
854
                        $fieldValue['config']['type'] === 'select' && $fieldValue['config']['authMode']
855
                        && $fieldValue['config']['authMode_enforce'] === 'strict'
856
                    ) {
857
                        if (!$this->checkAuthMode($table, $fieldName, $idOrRow[$fieldName], $fieldValue['config']['authMode'])) {
858
                            $this->errorMsg = 'ERROR: authMode "' . $fieldValue['config']['authMode']
859
                                . '" failed for field "' . $fieldName . '" with value "'
860
                                . $idOrRow[$fieldName] . '" evaluated';
861
                            return false;
862
                        }
863
                    }
864
                }
865
            }
866
        }
867
        // Checking "editlock" feature (doesn't apply to new records)
868
        if (!$newRecord && $GLOBALS['TCA'][$table]['ctrl']['editlock']) {
869
            if (isset($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['editlock']])) {
870
                if ($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['editlock']]) {
871
                    $this->errorMsg = 'ERROR: Record was locked for editing. Only admin users can change this state.';
872
                    return false;
873
                }
874
            } else {
875
                $this->errorMsg = 'ERROR: The "editLock" field named "' . $GLOBALS['TCA'][$table]['ctrl']['editlock']
876
                    . '" was not found in testing record!';
877
                return false;
878
            }
879
        }
880
        // Checking record permissions
881
        // THIS is where we can include a check for "perms_" fields for other records than pages...
882
        // Process any hooks
883
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['recordEditAccessInternals'] ?? [] as $funcRef) {
884
            $params = [
885
                'table' => $table,
886
                'idOrRow' => $idOrRow,
887
                'newRecord' => $newRecord
888
            ];
889
            if (!GeneralUtility::callUserFunction($funcRef, $params, $this)) {
890
                return false;
891
            }
892
        }
893
        // Finally, return TRUE if all is well.
894
        return true;
895
    }
896
897
    /**
898
     * Returns TRUE if the BE_USER is allowed to *create* shortcuts in the backend modules
899
     *
900
     * @return bool
901
     */
902
    public function mayMakeShortcut()
903
    {
904
        return ($this->getTSConfig()['options.']['enableBookmarks'] ?? false)
905
            && !($this->getTSConfig()['options.']['mayNotCreateEditBookmarks'] ?? false);
906
    }
907
908
    /**
909
     * Checking if editing of an existing record is allowed in current workspace if that is offline.
910
     * Rules for editing in offline mode:
911
     * - record supports versioning and is an offline version from workspace and has the current stage
912
     * - or record (any) is in a branch where there is a page which is a version from the workspace
913
     *   and where the stage is not preventing records
914
     *
915
     * @param string $table Table of record
916
     * @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)
917
     * @return string String error code, telling the failure state. FALSE=All ok
918
     * @internal should only be used from within TYPO3 Core
919
     */
920
    public function workspaceCannotEditRecord($table, $recData)
921
    {
922
        // Only test if the user is in a workspace
923
        if ($this->workspace === 0) {
924
            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...
925
        }
926
        $tableSupportsVersioning = BackendUtility::isTableWorkspaceEnabled($table);
927
        if (!is_array($recData)) {
928
            $recData = BackendUtility::getRecord(
929
                $table,
930
                $recData,
931
                'pid' . ($tableSupportsVersioning ? ',t3ver_oid,t3ver_wsid,t3ver_stage' : '')
932
            );
933
        }
934
        if (is_array($recData)) {
935
            // We are testing a "version" (identified by having a t3ver_oid): it can be edited provided
936
            // that workspace matches and versioning is enabled for the table.
937
            if ($tableSupportsVersioning && (int)($recData['t3ver_oid'] ?? 0) > 0) {
938
                if ((int)$recData['t3ver_wsid'] !== $this->workspace) {
939
                    // So does workspace match?
940
                    return 'Workspace ID of record didn\'t match current workspace';
941
                }
942
                // So is the user allowed to "use" the edit stage within the workspace?
943
                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...
944
                        ? false
945
                        : 'User\'s access level did not allow for editing';
946
            }
947
            // Check if we are testing a "live" record
948
            if ($this->workspaceAllowsLiveEditingInTable($table)) {
949
                // Live records are OK in the current workspace
950
                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...
951
            }
952
            // If not offline, output error
953
            return 'Online record was not in a workspace!';
954
        }
955
        return 'No record';
956
    }
957
958
    /**
959
     * Evaluates if a user is allowed to edit the offline version
960
     *
961
     * @param string $table Table of record
962
     * @param array|int $recData Integer (record uid) or array where fields are at least: pid, t3ver_wsid, t3ver_stage (if versioningWS is set)
963
     * @return string String error code, telling the failure state. FALSE=All ok
964
     * @see workspaceCannotEditRecord()
965
     * @internal this method will be moved to EXT:workspaces
966
     */
967
    public function workspaceCannotEditOfflineVersion($table, $recData)
968
    {
969
        if (!BackendUtility::isTableWorkspaceEnabled($table)) {
970
            return 'Table does not support versioning.';
971
        }
972
        if (!is_array($recData)) {
973
            $recData = BackendUtility::getRecord($table, $recData, 'uid,pid,t3ver_oid,t3ver_wsid,t3ver_stage');
974
        }
975
        if (is_array($recData)) {
976
            if ((int)$recData['t3ver_oid'] > 0) {
977
                return $this->workspaceCannotEditRecord($table, $recData);
978
            }
979
            return 'Not an offline version';
980
        }
981
        return 'No record';
982
    }
983
984
    /**
985
     * Checks if a record is allowed to be edited in the current workspace.
986
     * This is not bound to an actual record, but to the mere fact if the user is in a workspace
987
     * and depending on the table settings.
988
     *
989
     * @param string $table
990
     * @return bool
991
     * @internal should only be used from within TYPO3 Core
992
     */
993
    public function workspaceAllowsLiveEditingInTable(string $table): bool
994
    {
995
        // In live workspace the record can be added/modified
996
        if ($this->workspace === 0) {
997
            return true;
998
        }
999
        // Workspace setting allows to "live edit" records of tables without versioning
1000
        if ($this->workspaceRec['live_edit'] && !BackendUtility::isTableWorkspaceEnabled($table)) {
1001
            return true;
1002
        }
1003
        // Always for Live workspace AND if live-edit is enabled
1004
        // and tables are completely without versioning it is ok as well.
1005
        if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS_alwaysAllowLiveEdit']) {
1006
            return true;
1007
        }
1008
        // If the answer is FALSE it means the only valid way to create or edit records by creating records in the workspace
1009
        return false;
1010
    }
1011
1012
    /**
1013
     * Evaluates if a record from $table can be created. If the table is not set up for versioning,
1014
     * and the "live edit" flag of the page is set, return false. In live workspace this is always true,
1015
     * as all records can be created in live workspace
1016
     *
1017
     * @param string $table Table name
1018
     * @return bool
1019
     * @internal should only be used from within TYPO3 Core
1020
     */
1021
    public function workspaceCanCreateNewRecord(string $table): bool
1022
    {
1023
        // If LIVE records cannot be created due to workspace restrictions, prepare creation of placeholder-record
1024
        if (!$this->workspaceAllowsLiveEditingInTable($table) && !BackendUtility::isTableWorkspaceEnabled($table)) {
1025
            return false;
1026
        }
1027
        return true;
1028
    }
1029
1030
    /**
1031
     * Evaluates if auto creation of a version of a record is allowed.
1032
     * Auto-creation of version: In offline workspace, test if versioning is
1033
     * enabled and look for workspace version of input record.
1034
     * If there is no versionized record found we will create one and save to that.
1035
     *
1036
     * @param string $table Table of the record
1037
     * @param int $id UID of record
1038
     * @param int $recpid PID of record
1039
     * @return bool TRUE if ok.
1040
     * @internal should only be used from within TYPO3 Core
1041
     */
1042
    public function workspaceAllowAutoCreation($table, $id, $recpid)
1043
    {
1044
        // No version can be created in live workspace
1045
        if ($this->workspace === 0) {
1046
            return false;
1047
        }
1048
        // No versioning support for this table, so no version can be created
1049
        if (!BackendUtility::isTableWorkspaceEnabled($table)) {
1050
            return false;
1051
        }
1052
        if ($recpid < 0) {
1053
            return false;
1054
        }
1055
        // There must be no existing version of this record in workspace
1056
        if (BackendUtility::getWorkspaceVersionOfRecord($this->workspace, $table, $id, 'uid')) {
1057
            return false;
1058
        }
1059
        return true;
1060
    }
1061
1062
    /**
1063
     * Checks if an element stage allows access for the user in the current workspace
1064
     * In live workspace (= 0) access is always granted for any stage.
1065
     * Admins are always allowed.
1066
     * An option for custom workspaces allows members to also edit when the stage is "Review"
1067
     *
1068
     * @param int $stage Stage id from an element: -1,0 = editing, 1 = reviewer, >1 = owner
1069
     * @return bool TRUE if user is allowed access
1070
     * @internal should only be used from within TYPO3 Core
1071
     */
1072
    public function workspaceCheckStageForCurrent($stage)
1073
    {
1074
        // Always allow for admins
1075
        if ($this->isAdmin()) {
1076
            return true;
1077
        }
1078
        // Always OK for live workspace
1079
        if ($this->workspace === 0 || !ExtensionManagementUtility::isLoaded('workspaces')) {
1080
            return true;
1081
        }
1082
        $stage = (int)$stage;
1083
        $stat = $this->checkWorkspaceCurrent();
1084
        $accessType = $stat['_ACCESS'];
1085
        // Workspace owners are always allowed for stage change
1086
        if ($accessType === 'owner') {
1087
            return true;
1088
        }
1089
1090
        // Check if custom staging is activated
1091
        $workspaceRec = BackendUtility::getRecord('sys_workspace', $stat['uid']);
1092
        if ($workspaceRec['custom_stages'] > 0 && $stage !== 0 && $stage !== -10) {
1093
            // Get custom stage record
1094
            $workspaceStageRec = BackendUtility::getRecord('sys_workspace_stage', $stage);
1095
            // Check if the user is responsible for the current stage
1096
            if (
1097
                $accessType === 'member'
1098
                && GeneralUtility::inList($workspaceStageRec['responsible_persons'], 'be_users_' . $this->user['uid'])
1099
            ) {
1100
                return true;
1101
            }
1102
            // Check if the user is in a group which is responsible for the current stage
1103
            foreach ($this->userGroupsUID as $groupUid) {
1104
                if (
1105
                    $accessType === 'member'
1106
                    && GeneralUtility::inList($workspaceStageRec['responsible_persons'], 'be_groups_' . $groupUid)
1107
                ) {
1108
                    return true;
1109
                }
1110
            }
1111
        } elseif ($stage === -10 || $stage === -20) {
1112
            // Nobody is allowed to do that except the owner (which was checked above)
1113
            return false;
1114
        } elseif (
1115
            $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...
1116
            || $accessType === 'member' && $stage <= 0
1117
        ) {
1118
            return true;
1119
        }
1120
        return false;
1121
    }
1122
1123
    /**
1124
     * Returns TRUE if the user has access to publish content from the workspace ID given.
1125
     * Admin-users are always granted access to do this
1126
     * If the workspace ID is 0 (live) all users have access also
1127
     * For custom workspaces it depends on whether the user is owner OR like with
1128
     * draft workspace if the user has access to Live workspace.
1129
     *
1130
     * @param int $wsid Workspace UID; 0,1+
1131
     * @return bool Returns TRUE if the user has access to publish content from the workspace ID given.
1132
     * @internal this method will be moved to EXT:workspaces
1133
     */
1134
    public function workspacePublishAccess($wsid)
1135
    {
1136
        if ($this->isAdmin()) {
1137
            return true;
1138
        }
1139
        $wsAccess = $this->checkWorkspace($wsid);
1140
        // If no access to workspace, of course you cannot publish!
1141
        if ($wsAccess === false) {
0 ignored issues
show
introduced by
The condition $wsAccess === false is always false.
Loading history...
1142
            return false;
1143
        }
1144
        if ((int)$wsAccess['uid'] === 0) {
1145
            // If access to Live workspace, no problem.
1146
            return true;
1147
        }
1148
        // Custom workspaces
1149
        // 1. Owners can always publish
1150
        if ($wsAccess['_ACCESS'] === 'owner') {
1151
            return true;
1152
        }
1153
        // 2. User has access to online workspace which is OK as well as long as publishing
1154
        // access is not limited by workspace option.
1155
        return $this->checkWorkspace(0) && !($wsAccess['publish_access'] & 2);
1156
    }
1157
1158
    /**
1159
     * Returns full parsed user TSconfig array, merged with TSconfig from groups.
1160
     *
1161
     * Example:
1162
     * [
1163
     *     'options.' => [
1164
     *         'fooEnabled' => '0',
1165
     *         'fooEnabled.' => [
1166
     *             'tt_content' => 1,
1167
     *         ],
1168
     *     ],
1169
     * ]
1170
     *
1171
     * @return array Parsed and merged user TSconfig array
1172
     */
1173
    public function getTSConfig()
1174
    {
1175
        return $this->userTS;
1176
    }
1177
1178
    /**
1179
     * Returns an array with the webmounts.
1180
     * If no webmounts, and empty array is returned.
1181
     * Webmounts permissions are checked in fetchGroupData()
1182
     *
1183
     * @return array of web mounts uids (may include '0')
1184
     */
1185
    public function returnWebmounts()
1186
    {
1187
        return (string)$this->groupData['webmounts'] != '' ? explode(',', $this->groupData['webmounts']) : [];
1188
    }
1189
1190
    /**
1191
     * Initializes the given mount points for the current Backend user.
1192
     *
1193
     * @param array $mountPointUids Page UIDs that should be used as web mountpoints
1194
     * @param bool $append If TRUE the given mount point will be appended. Otherwise the current mount points will be replaced.
1195
     */
1196
    public function setWebmounts(array $mountPointUids, $append = false)
1197
    {
1198
        if (empty($mountPointUids)) {
1199
            return;
1200
        }
1201
        if ($append) {
1202
            $currentWebMounts = GeneralUtility::intExplode(',', $this->groupData['webmounts']);
1203
            $mountPointUids = array_merge($currentWebMounts, $mountPointUids);
1204
        }
1205
        $this->groupData['webmounts'] = implode(',', array_unique($mountPointUids));
1206
    }
1207
1208
    /**
1209
     * Checks for alternative web mount points for the element browser.
1210
     *
1211
     * If there is a temporary mount point active in the page tree it will be used.
1212
     *
1213
     * If the User TSconfig options.pageTree.altElementBrowserMountPoints is not empty the pages configured
1214
     * there are used as web mounts If options.pageTree.altElementBrowserMountPoints.append is enabled,
1215
     * they are appended to the existing webmounts.
1216
     *
1217
     * @internal - do not use in your own extension
1218
     */
1219
    public function initializeWebmountsForElementBrowser()
1220
    {
1221
        $alternativeWebmountPoint = (int)$this->getSessionData('pageTree_temporaryMountPoint');
1222
        if ($alternativeWebmountPoint) {
1223
            $alternativeWebmountPoint = GeneralUtility::intExplode(',', (string)$alternativeWebmountPoint);
1224
            $this->setWebmounts($alternativeWebmountPoint);
1225
            return;
1226
        }
1227
1228
        $alternativeWebmountPoints = trim($this->getTSConfig()['options.']['pageTree.']['altElementBrowserMountPoints'] ?? '');
1229
        $appendAlternativeWebmountPoints = $this->getTSConfig()['options.']['pageTree.']['altElementBrowserMountPoints.']['append'] ?? '';
1230
        if ($alternativeWebmountPoints) {
1231
            $alternativeWebmountPoints = GeneralUtility::intExplode(',', $alternativeWebmountPoints);
1232
            $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

1232
            $this->setWebmounts($alternativeWebmountPoints, /** @scrutinizer ignore-type */ $appendAlternativeWebmountPoints);
Loading history...
1233
        }
1234
    }
1235
1236
    /**
1237
     * Returns TRUE or FALSE, depending if an alert popup (a javascript confirmation) should be shown
1238
     * call like $GLOBALS['BE_USER']->jsConfirmation($BITMASK).
1239
     *
1240
     * @param int $bitmask Bitmask, one of \TYPO3\CMS\Core\Type\Bitmask\JsConfirmation
1241
     * @return bool TRUE if the confirmation should be shown
1242
     * @see JsConfirmation
1243
     */
1244
    public function jsConfirmation($bitmask)
1245
    {
1246
        try {
1247
            $alertPopupsSetting = trim((string)($this->getTSConfig()['options.']['alertPopups'] ?? ''));
1248
            $alertPopup = JsConfirmation::cast($alertPopupsSetting === '' ? null : (int)$alertPopupsSetting);
1249
        } catch (InvalidEnumerationValueException $e) {
1250
            $alertPopup = new JsConfirmation();
1251
        }
1252
1253
        return JsConfirmation::cast($bitmask)->matches($alertPopup);
1254
    }
1255
1256
    /**
1257
     * Initializes a lot of stuff like the access-lists, database-mountpoints and filemountpoints
1258
     * This method is called by ->backendCheckLogin() (from extending BackendUserAuthentication)
1259
     * if the backend user login has verified OK.
1260
     * Generally this is required initialization of a backend user.
1261
     *
1262
     * @internal
1263
     * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
1264
     */
1265
    public function fetchGroupData()
1266
    {
1267
        if ($this->user['uid']) {
1268
            // Get lists for the be_user record and set them as default/primary values.
1269
            // Enabled Backend Modules
1270
            $this->dataLists['modList'] = $this->user['userMods'];
1271
            // Add available widgets
1272
            $this->dataLists['available_widgets'] = $this->user['available_widgets'];
1273
            // Add Allowed Languages
1274
            $this->dataLists['allowed_languages'] = $this->user['allowed_languages'];
1275
            // Set user value for workspace permissions.
1276
            $this->dataLists['workspace_perms'] = $this->user['workspace_perms'];
1277
            // Database mountpoints
1278
            $this->dataLists['webmount_list'] = $this->user['db_mountpoints'];
1279
            // File mountpoints
1280
            $this->dataLists['filemount_list'] = $this->user['file_mountpoints'];
1281
            // Fileoperation permissions
1282
            $this->dataLists['file_permissions'] = $this->user['file_permissions'];
1283
1284
            // BE_GROUPS:
1285
            // Get the groups...
1286
            if (!empty($this->user[$this->usergroup_column])) {
1287
                // Fetch groups will add a lot of information to the internal arrays: modules, accesslists, TSconfig etc.
1288
                // Refer to fetchGroups() function.
1289
                $this->fetchGroups($this->user[$this->usergroup_column]);
1290
            }
1291
            // Populating the $this->userGroupsUID -array with the groups in the order in which they were LAST included.!!
1292
            $this->userGroupsUID = array_reverse(array_unique(array_reverse($this->includeGroupArray)));
1293
            // Finally this is the list of group_uid's in the order they are parsed (including subgroups!)
1294
            // and without duplicates (duplicates are presented with their last entrance in the list,
1295
            // which thus reflects the order of the TypoScript in TSconfig)
1296
            $this->groupList = implode(',', $this->userGroupsUID);
1297
            $this->setCachedList($this->groupList);
1298
1299
            $this->prepareUserTsConfig();
1300
1301
            // Processing webmounts
1302
            // Admin's always have the root mounted
1303
            if ($this->isAdmin() && !($this->getTSConfig()['options.']['dontMountAdminMounts'] ?? false)) {
1304
                $this->dataLists['webmount_list'] = '0,' . $this->dataLists['webmount_list'];
1305
            }
1306
            // The lists are cleaned for duplicates
1307
            $this->groupData['webmounts'] = StringUtility::uniqueList($this->dataLists['webmount_list'] ?? '');
1308
            $this->groupData['pagetypes_select'] = StringUtility::uniqueList($this->dataLists['pagetypes_select'] ?? '');
1309
            $this->groupData['tables_select'] = StringUtility::uniqueList(($this->dataLists['tables_modify'] ?? '') . ',' . ($this->dataLists['tables_select'] ?? ''));
1310
            $this->groupData['tables_modify'] = StringUtility::uniqueList($this->dataLists['tables_modify'] ?? '');
1311
            $this->groupData['non_exclude_fields'] = StringUtility::uniqueList($this->dataLists['non_exclude_fields'] ?? '');
1312
            $this->groupData['explicit_allowdeny'] = StringUtility::uniqueList($this->dataLists['explicit_allowdeny'] ?? '');
1313
            $this->groupData['allowed_languages'] = StringUtility::uniqueList($this->dataLists['allowed_languages'] ?? '');
1314
            $this->groupData['custom_options'] = StringUtility::uniqueList($this->dataLists['custom_options'] ?? '');
1315
            $this->groupData['modules'] = StringUtility::uniqueList($this->dataLists['modList'] ?? '');
1316
            $this->groupData['available_widgets'] = StringUtility::uniqueList($this->dataLists['available_widgets'] ?? '');
1317
            $this->groupData['file_permissions'] = StringUtility::uniqueList($this->dataLists['file_permissions'] ?? '');
1318
            $this->groupData['workspace_perms'] = $this->dataLists['workspace_perms'];
1319
1320
            if (!empty(trim($this->groupData['webmounts']))) {
1321
                // Checking read access to web mounts if there are mounts points (not empty string, false or 0)
1322
                $webmounts = explode(',', $this->groupData['webmounts']);
1323
                // Selecting all web mounts with permission clause for reading
1324
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1325
                $queryBuilder->getRestrictions()
1326
                    ->removeAll()
1327
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1328
1329
                $MProws = $queryBuilder->select('uid')
1330
                    ->from('pages')
1331
                    // @todo DOCTRINE: check how to make getPagePermsClause() portable
1332
                    ->where(
1333
                        $this->getPagePermsClause(Permission::PAGE_SHOW),
1334
                        $queryBuilder->expr()->in(
1335
                            'uid',
1336
                            $queryBuilder->createNamedParameter(
1337
                                GeneralUtility::intExplode(',', $this->groupData['webmounts']),
1338
                                Connection::PARAM_INT_ARRAY
1339
                            )
1340
                        )
1341
                    )
1342
                    ->execute()
1343
                    ->fetchAll();
1344
                $MProws = array_column(($MProws ?: []), 'uid', 'uid');
1345
                foreach ($webmounts as $idx => $mountPointUid) {
1346
                    // If the mount ID is NOT found among selected pages, unset it:
1347
                    if ($mountPointUid > 0 && !isset($MProws[$mountPointUid])) {
1348
                        unset($webmounts[$idx]);
1349
                    }
1350
                }
1351
                // Implode mounts in the end.
1352
                $this->groupData['webmounts'] = implode(',', $webmounts);
1353
            }
1354
            // Setting up workspace situation (after webmounts are processed!):
1355
            $this->workspaceInit();
1356
        }
1357
    }
1358
1359
    /**
1360
     * This method parses the UserTSconfig from the current user and all their groups.
1361
     * If the contents are the same, parsing is skipped. No matching is applied here currently.
1362
     */
1363
    protected function prepareUserTsConfig(): void
1364
    {
1365
        $collectedUserTSconfig = [
1366
            'default' => $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig']
1367
        ];
1368
        // Default TSconfig for admin-users
1369
        if ($this->isAdmin()) {
1370
            $collectedUserTSconfig[] = 'admPanel.enable.all = 1';
1371
        }
1372
        // Setting defaults for sys_note author / email
1373
        $collectedUserTSconfig[] = '
1374
TCAdefaults.sys_note.author = ' . $this->user['realName'] . '
1375
TCAdefaults.sys_note.email = ' . $this->user['email'];
1376
1377
        // Loop through all groups and add their 'TSconfig' fields
1378
        foreach ($this->includeGroupArray as $groupId) {
1379
            $collectedUserTSconfig['group_' . $groupId] = $this->userGroups[$groupId]['TSconfig'] ?? '';
1380
        }
1381
1382
        $collectedUserTSconfig[] = $this->user['TSconfig'];
1383
        // Check external files
1384
        $collectedUserTSconfig = TypoScriptParser::checkIncludeLines_array($collectedUserTSconfig);
1385
        // Imploding with "[global]" will make sure that non-ended confinements with braces are ignored.
1386
        $userTS_text = implode("\n[GLOBAL]\n", $collectedUserTSconfig);
1387
        // Parsing the user TSconfig (or getting from cache)
1388
        $hash = md5('userTS:' . $userTS_text);
1389
        $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
1390
        if (!($this->userTS = $cache->get($hash))) {
1391
            $parseObj = GeneralUtility::makeInstance(TypoScriptParser::class);
1392
            $conditionMatcher = GeneralUtility::makeInstance(ConditionMatcher::class);
1393
            $parseObj->parse($userTS_text, $conditionMatcher);
1394
            $this->userTS = $parseObj->setup;
1395
            $cache->set($hash, $this->userTS, ['UserTSconfig'], 0);
1396
            // Ensure to update UC later
1397
            $this->userTSUpdated = true;
1398
        }
1399
    }
1400
1401
    /**
1402
     * Fetches the group records, subgroups and fills internal arrays.
1403
     * Function is called recursively to fetch subgroups
1404
     *
1405
     * @param string $grList Commalist of be_groups uid numbers
1406
     * @param string $idList List of already processed be_groups-uids so the function will not fall into an eternal recursion.
1407
     * @internal
1408
     */
1409
    public function fetchGroups($grList, $idList = '')
1410
    {
1411
        // Fetching records of the groups in $grList:
1412
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->usergroup_table);
1413
        $expressionBuilder = $queryBuilder->expr();
1414
        $constraints = $expressionBuilder->andX(
1415
            $expressionBuilder->eq(
1416
                'pid',
1417
                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1418
            ),
1419
            $expressionBuilder->in(
1420
                'uid',
1421
                $queryBuilder->createNamedParameter(
1422
                    GeneralUtility::intExplode(',', $grList),
1423
                    Connection::PARAM_INT_ARRAY
1424
                )
1425
            )
1426
        );
1427
        // Hook for manipulation of the WHERE sql sentence which controls which BE-groups are included
1428
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroupQuery'] ?? [] as $className) {
1429
            $hookObj = GeneralUtility::makeInstance($className);
1430
            if (method_exists($hookObj, 'fetchGroupQuery_processQuery')) {
1431
                $constraints = $hookObj->fetchGroupQuery_processQuery($this, $grList, $idList, (string)$constraints);
1432
            }
1433
        }
1434
        $res = $queryBuilder->select('*')
1435
            ->from($this->usergroup_table)
1436
            ->where($constraints)
1437
            ->execute();
1438
        // The userGroups array is filled
1439
        while ($row = $res->fetch(\PDO::FETCH_ASSOC)) {
1440
            $this->userGroups[$row['uid']] = $row;
1441
        }
1442
1443
        $mountOptions = new BackendGroupMountOption((int)$this->user['options']);
1444
        // Traversing records in the correct order
1445
        foreach (explode(',', $grList) as $uid) {
1446
            // Get row:
1447
            $row = $this->userGroups[$uid];
1448
            // Must be an array and $uid should not be in the idList, because then it is somewhere previously in the grouplist
1449
            if (is_array($row) && !GeneralUtility::inList($idList, $uid)) {
1450
                // Include sub groups
1451
                if (trim($row['subgroup'])) {
1452
                    // Make integer list
1453
                    $theList = implode(',', GeneralUtility::intExplode(',', $row['subgroup']));
1454
                    // Call recursively, pass along list of already processed groups so they are not recursed again.
1455
                    $this->fetchGroups($theList, $idList . ',' . $uid);
1456
                }
1457
                // Add the group uid, current list to the internal arrays.
1458
                $this->includeGroupArray[] = $uid;
1459
                // Mount group database-mounts
1460
                if ($mountOptions->shouldUserIncludePageMountsFromAssociatedGroups()) {
1461
                    $this->dataLists['webmount_list'] .= ',' . $row['db_mountpoints'];
1462
                }
1463
                // Mount group file-mounts
1464
                if ($mountOptions->shouldUserIncludeFileMountsFromAssociatedGroups()) {
1465
                    $this->dataLists['filemount_list'] .= ',' . $row['file_mountpoints'];
1466
                }
1467
                // The lists are made: groupMods, tables_select, tables_modify, pagetypes_select, non_exclude_fields, explicit_allowdeny, allowed_languages, custom_options
1468
                $this->dataLists['modList'] .= ',' . $row['groupMods'];
1469
                $this->dataLists['available_widgets'] .= ',' . $row['availableWidgets'];
1470
                $this->dataLists['tables_select'] .= ',' . $row['tables_select'];
1471
                $this->dataLists['tables_modify'] .= ',' . $row['tables_modify'];
1472
                $this->dataLists['pagetypes_select'] .= ',' . $row['pagetypes_select'];
1473
                $this->dataLists['non_exclude_fields'] .= ',' . $row['non_exclude_fields'];
1474
                $this->dataLists['explicit_allowdeny'] .= ',' . $row['explicit_allowdeny'];
1475
                $this->dataLists['allowed_languages'] .= ',' . $row['allowed_languages'];
1476
                $this->dataLists['custom_options'] .= ',' . $row['custom_options'];
1477
                $this->dataLists['file_permissions'] .= ',' . $row['file_permissions'];
1478
                // Setting workspace permissions:
1479
                $this->dataLists['workspace_perms'] |= $row['workspace_perms'];
1480
                // If this function is processing the users OWN group-list (not subgroups) AND
1481
                // if the ->firstMainGroup is not set, then the ->firstMainGroup will be set.
1482
                if ($idList === '' && !$this->firstMainGroup) {
1483
                    $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...
1484
                }
1485
            }
1486
        }
1487
        // HOOK: fetchGroups_postProcessing
1488
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroups_postProcessing'] ?? [] as $_funcRef) {
1489
            $_params = [];
1490
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1491
        }
1492
    }
1493
1494
    /**
1495
     * Updates the field be_users.usergroup_cached_list if the groupList of the user
1496
     * has changed/is different from the current list.
1497
     * The field "usergroup_cached_list" contains the list of groups which the user is a member of.
1498
     * After authentication (where these functions are called...) one can depend on this list being
1499
     * a representation of the exact groups/subgroups which the BE_USER has membership with.
1500
     *
1501
     * @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.
1502
     * @internal
1503
     */
1504
    public function setCachedList($cList)
1505
    {
1506
        if ((string)$cList != (string)$this->user['usergroup_cached_list']) {
1507
            GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users')->update(
1508
                'be_users',
1509
                ['usergroup_cached_list' => $cList],
1510
                ['uid' => (int)$this->user['uid']]
1511
            );
1512
        }
1513
    }
1514
1515
    /**
1516
     * Sets up all file storages for a user.
1517
     * Needs to be called AFTER the groups have been loaded.
1518
     */
1519
    protected function initializeFileStorages()
1520
    {
1521
        $this->fileStorages = [];
1522
        /** @var \TYPO3\CMS\Core\Resource\StorageRepository $storageRepository */
1523
        $storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
1524
        // Admin users have all file storages visible, without any filters
1525
        if ($this->isAdmin()) {
1526
            $storageObjects = $storageRepository->findAll();
1527
            foreach ($storageObjects as $storageObject) {
1528
                $this->fileStorages[$storageObject->getUid()] = $storageObject;
1529
            }
1530
        } else {
1531
            // Regular users only have storages that are defined in their filemounts
1532
            // Permissions and file mounts for the storage are added in StoragePermissionAspect
1533
            foreach ($this->getFileMountRecords() as $row) {
1534
                if (!array_key_exists((int)$row['base'], $this->fileStorages)) {
1535
                    $storageObject = $storageRepository->findByUid($row['base']);
1536
                    if ($storageObject) {
1537
                        $this->fileStorages[$storageObject->getUid()] = $storageObject;
1538
                    }
1539
                }
1540
            }
1541
        }
1542
1543
        // This has to be called always in order to set certain filters
1544
        $this->evaluateUserSpecificFileFilterSettings();
1545
    }
1546
1547
    /**
1548
     * Returns an array of category mount points. The category permissions from BE Groups
1549
     * are also taken into consideration and are merged into User permissions.
1550
     *
1551
     * @return array
1552
     */
1553
    public function getCategoryMountPoints()
1554
    {
1555
        $categoryMountPoints = '';
1556
1557
        // Category mounts of the groups
1558
        if (is_array($this->userGroups)) {
0 ignored issues
show
introduced by
The condition is_array($this->userGroups) is always true.
Loading history...
1559
            foreach ($this->userGroups as $group) {
1560
                if ($group['category_perms']) {
1561
                    $categoryMountPoints .= ',' . $group['category_perms'];
1562
                }
1563
            }
1564
        }
1565
1566
        // Category mounts of the user record
1567
        if ($this->user['category_perms']) {
1568
            $categoryMountPoints .= ',' . $this->user['category_perms'];
1569
        }
1570
1571
        // Make the ids unique
1572
        $categoryMountPoints = GeneralUtility::trimExplode(',', $categoryMountPoints);
1573
        $categoryMountPoints = array_filter($categoryMountPoints); // remove empty value
1574
        $categoryMountPoints = array_unique($categoryMountPoints); // remove unique value
1575
1576
        return $categoryMountPoints;
1577
    }
1578
1579
    /**
1580
     * Returns an array of file mount records, taking workspaces and user home and group home directories into account
1581
     * Needs to be called AFTER the groups have been loaded.
1582
     *
1583
     * @return array
1584
     * @internal
1585
     */
1586
    public function getFileMountRecords()
1587
    {
1588
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
1589
        $fileMountRecordCache = $runtimeCache->get('backendUserAuthenticationFileMountRecords') ?: [];
1590
1591
        if (!empty($fileMountRecordCache)) {
1592
            return $fileMountRecordCache;
1593
        }
1594
1595
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
1596
1597
        // Processing file mounts (both from the user and the groups)
1598
        $fileMounts = array_unique(GeneralUtility::intExplode(',', $this->dataLists['filemount_list'], true));
1599
1600
        // Limit file mounts if set in workspace record
1601
        if ($this->workspace > 0 && !empty($this->workspaceRec['file_mountpoints'])) {
1602
            $workspaceFileMounts = GeneralUtility::intExplode(',', $this->workspaceRec['file_mountpoints'], true);
1603
            $fileMounts = array_intersect($fileMounts, $workspaceFileMounts);
1604
        }
1605
1606
        if (!empty($fileMounts)) {
1607
            $orderBy = $GLOBALS['TCA']['sys_filemounts']['ctrl']['default_sortby'] ?? 'sorting';
1608
1609
            $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_filemounts');
1610
            $queryBuilder->getRestrictions()
1611
                ->removeAll()
1612
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1613
                ->add(GeneralUtility::makeInstance(HiddenRestriction::class))
1614
                ->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
1615
1616
            $queryBuilder->select('*')
1617
                ->from('sys_filemounts')
1618
                ->where(
1619
                    $queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($fileMounts, Connection::PARAM_INT_ARRAY))
1620
                );
1621
1622
            foreach (QueryHelper::parseOrderBy($orderBy) as $fieldAndDirection) {
1623
                $queryBuilder->addOrderBy(...$fieldAndDirection);
1624
            }
1625
1626
            $fileMountRecords = $queryBuilder->execute()->fetchAll(\PDO::FETCH_ASSOC);
1627
            if ($fileMountRecords !== false) {
1628
                foreach ($fileMountRecords as $fileMount) {
1629
                    $fileMountRecordCache[$fileMount['base'] . $fileMount['path']] = $fileMount;
1630
                }
1631
            }
1632
        }
1633
1634
        // Read-only file mounts
1635
        $readOnlyMountPoints = \trim($this->getTSConfig()['options.']['folderTree.']['altElementBrowserMountPoints'] ?? '');
1636
        if ($readOnlyMountPoints) {
1637
            // We cannot use the API here but need to fetch the default storage record directly
1638
            // to not instantiate it (which directly applies mount points) before all mount points are resolved!
1639
            $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_file_storage');
1640
            $defaultStorageRow = $queryBuilder->select('uid')
1641
                ->from('sys_file_storage')
1642
                ->where(
1643
                    $queryBuilder->expr()->eq('is_default', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT))
1644
                )
1645
                ->setMaxResults(1)
1646
                ->execute()
1647
                ->fetch(\PDO::FETCH_ASSOC);
1648
1649
            $readOnlyMountPointArray = GeneralUtility::trimExplode(',', $readOnlyMountPoints);
1650
            foreach ($readOnlyMountPointArray as $readOnlyMountPoint) {
1651
                $readOnlyMountPointConfiguration = GeneralUtility::trimExplode(':', $readOnlyMountPoint);
1652
                if (count($readOnlyMountPointConfiguration) === 2) {
1653
                    // A storage is passed in the configuration
1654
                    $storageUid = (int)$readOnlyMountPointConfiguration[0];
1655
                    $path = $readOnlyMountPointConfiguration[1];
1656
                } else {
1657
                    if (empty($defaultStorageRow)) {
1658
                        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);
1659
                    }
1660
                    // Backwards compatibility: If no storage is passed, we use the default storage
1661
                    $storageUid = $defaultStorageRow['uid'];
1662
                    $path = $readOnlyMountPointConfiguration[0];
1663
                }
1664
                $fileMountRecordCache[$storageUid . $path] = [
1665
                    'base' => $storageUid,
1666
                    'title' => $path,
1667
                    'path' => $path,
1668
                    'read_only' => true
1669
                ];
1670
            }
1671
        }
1672
1673
        // Personal or Group filemounts are not accessible if file mount list is set in workspace record
1674
        if ($this->workspace <= 0 || empty($this->workspaceRec['file_mountpoints'])) {
1675
            // If userHomePath is set, we attempt to mount it
1676
            if ($GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath']) {
1677
                [$userHomeStorageUid, $userHomeFilter] = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath'], 2);
1678
                $userHomeStorageUid = (int)$userHomeStorageUid;
1679
                $userHomeFilter = '/' . ltrim($userHomeFilter, '/');
1680
                if ($userHomeStorageUid > 0) {
1681
                    // Try and mount with [uid]_[username]
1682
                    $path = $userHomeFilter . $this->user['uid'] . '_' . $this->user['username'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1683
                    $fileMountRecordCache[$userHomeStorageUid . $path] = [
1684
                        'base' => $userHomeStorageUid,
1685
                        'title' => $this->user['username'],
1686
                        'path' => $path,
1687
                        'read_only' => false,
1688
                        'user_mount' => true
1689
                    ];
1690
                    // Try and mount with only [uid]
1691
                    $path = $userHomeFilter . $this->user['uid'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1692
                    $fileMountRecordCache[$userHomeStorageUid . $path] = [
1693
                        'base' => $userHomeStorageUid,
1694
                        'title' => $this->user['username'],
1695
                        'path' => $path,
1696
                        'read_only' => false,
1697
                        'user_mount' => true
1698
                    ];
1699
                }
1700
            }
1701
1702
            // Mount group home-dirs
1703
            $mountOptions = new BackendGroupMountOption((int)$this->user['options']);
1704
            if ($GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'] !== '' && $mountOptions->shouldUserIncludeFileMountsFromAssociatedGroups()) {
1705
                // If groupHomePath is set, we attempt to mount it
1706
                [$groupHomeStorageUid, $groupHomeFilter] = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'], 2);
1707
                $groupHomeStorageUid = (int)$groupHomeStorageUid;
1708
                $groupHomeFilter = '/' . ltrim($groupHomeFilter, '/');
1709
                if ($groupHomeStorageUid > 0) {
1710
                    foreach ($this->userGroups as $groupData) {
1711
                        $path = $groupHomeFilter . $groupData['uid'];
1712
                        $fileMountRecordCache[$groupHomeStorageUid . $path] = [
1713
                            'base' => $groupHomeStorageUid,
1714
                            'title' => $groupData['title'],
1715
                            'path' => $path,
1716
                            'read_only' => false,
1717
                            'user_mount' => true
1718
                        ];
1719
                    }
1720
                }
1721
            }
1722
        }
1723
1724
        $runtimeCache->set('backendUserAuthenticationFileMountRecords', $fileMountRecordCache);
1725
        return $fileMountRecordCache;
1726
    }
1727
1728
    /**
1729
     * Returns an array with the filemounts for the user.
1730
     * Each filemount is represented with an array of a "name", "path" and "type".
1731
     * If no filemounts an empty array is returned.
1732
     *
1733
     * @return \TYPO3\CMS\Core\Resource\ResourceStorage[]
1734
     */
1735
    public function getFileStorages()
1736
    {
1737
        // Initializing file mounts after the groups are fetched
1738
        if ($this->fileStorages === null) {
1739
            $this->initializeFileStorages();
1740
        }
1741
        return $this->fileStorages;
1742
    }
1743
1744
    /**
1745
     * Adds filters based on what the user has set
1746
     * this should be done in this place, and called whenever needed,
1747
     * but only when needed
1748
     */
1749
    public function evaluateUserSpecificFileFilterSettings()
1750
    {
1751
        // Add the option for also displaying the non-hidden files
1752
        if ($this->uc['showHiddenFilesAndFolders']) {
1753
            FileNameFilter::setShowHiddenFilesAndFolders(true);
1754
        }
1755
    }
1756
1757
    /**
1758
     * Returns the information about file permissions.
1759
     * Previously, this was stored in the DB field fileoper_perms now it is file_permissions.
1760
     * Besides it can be handled via userTSconfig
1761
     *
1762
     * permissions.file.default {
1763
     * addFile = 1
1764
     * readFile = 1
1765
     * writeFile = 1
1766
     * copyFile = 1
1767
     * moveFile = 1
1768
     * renameFile = 1
1769
     * deleteFile = 1
1770
     *
1771
     * addFolder = 1
1772
     * readFolder = 1
1773
     * writeFolder = 1
1774
     * copyFolder = 1
1775
     * moveFolder = 1
1776
     * renameFolder = 1
1777
     * deleteFolder = 1
1778
     * recursivedeleteFolder = 1
1779
     * }
1780
     *
1781
     * # overwrite settings for a specific storageObject
1782
     * permissions.file.storage.StorageUid {
1783
     * readFile = 1
1784
     * recursivedeleteFolder = 0
1785
     * }
1786
     *
1787
     * Please note that these permissions only apply, if the storage has the
1788
     * capabilities (browseable, writable), and if the driver allows for writing etc
1789
     *
1790
     * @return array
1791
     */
1792
    public function getFilePermissions()
1793
    {
1794
        if (!isset($this->filePermissions)) {
1795
            $filePermissions = [
1796
                // File permissions
1797
                'addFile' => false,
1798
                'readFile' => false,
1799
                'writeFile' => false,
1800
                'copyFile' => false,
1801
                'moveFile' => false,
1802
                'renameFile' => false,
1803
                'deleteFile' => false,
1804
                // Folder permissions
1805
                'addFolder' => false,
1806
                'readFolder' => false,
1807
                'writeFolder' => false,
1808
                'copyFolder' => false,
1809
                'moveFolder' => false,
1810
                'renameFolder' => false,
1811
                'deleteFolder' => false,
1812
                'recursivedeleteFolder' => false
1813
            ];
1814
            if ($this->isAdmin()) {
1815
                $filePermissions = array_map('is_bool', $filePermissions);
1816
            } else {
1817
                $userGroupRecordPermissions = GeneralUtility::trimExplode(',', $this->groupData['file_permissions'] ?? '', true);
1818
                array_walk(
1819
                    $userGroupRecordPermissions,
1820
                    function ($permission) use (&$filePermissions) {
1821
                        $filePermissions[$permission] = true;
1822
                    }
1823
                );
1824
1825
                // Finally overlay any userTSconfig
1826
                $permissionsTsConfig = $this->getTSConfig()['permissions.']['file.']['default.'] ?? [];
1827
                if (!empty($permissionsTsConfig)) {
1828
                    array_walk(
1829
                        $permissionsTsConfig,
1830
                        function ($value, $permission) use (&$filePermissions) {
1831
                            $filePermissions[$permission] = (bool)$value;
1832
                        }
1833
                    );
1834
                }
1835
            }
1836
            $this->filePermissions = $filePermissions;
1837
        }
1838
        return $this->filePermissions;
1839
    }
1840
1841
    /**
1842
     * Gets the file permissions for a storage
1843
     * by merging any storage-specific permissions for a
1844
     * storage with the default settings.
1845
     * Admin users will always get the default settings.
1846
     *
1847
     * @param \TYPO3\CMS\Core\Resource\ResourceStorage $storageObject
1848
     * @return array
1849
     */
1850
    public function getFilePermissionsForStorage(ResourceStorage $storageObject)
1851
    {
1852
        $finalUserPermissions = $this->getFilePermissions();
1853
        if (!$this->isAdmin()) {
1854
            $storageFilePermissions = $this->getTSConfig()['permissions.']['file.']['storage.'][$storageObject->getUid() . '.'] ?? [];
1855
            if (!empty($storageFilePermissions)) {
1856
                array_walk(
1857
                    $storageFilePermissions,
1858
                    function ($value, $permission) use (&$finalUserPermissions) {
1859
                        $finalUserPermissions[$permission] = (bool)$value;
1860
                    }
1861
                );
1862
            }
1863
        }
1864
        return $finalUserPermissions;
1865
    }
1866
1867
    /**
1868
     * Returns a \TYPO3\CMS\Core\Resource\Folder object that is used for uploading
1869
     * files by default.
1870
     * This is used for RTE and its magic images, as well as uploads
1871
     * in the TCEforms fields.
1872
     *
1873
     * The default upload folder for a user is the defaultFolder on the first
1874
     * filestorage/filemount that the user can access and to which files are allowed to be added
1875
     * however, you can set the users' upload folder like this:
1876
     *
1877
     * options.defaultUploadFolder = 3:myfolder/yourfolder/
1878
     *
1879
     * @param int $pid PageUid
1880
     * @param string $table Table name
1881
     * @param string $field Field name
1882
     * @return \TYPO3\CMS\Core\Resource\Folder|bool The default upload folder for this user
1883
     */
1884
    public function getDefaultUploadFolder($pid = null, $table = null, $field = null)
1885
    {
1886
        $uploadFolder = $this->getTSConfig()['options.']['defaultUploadFolder'] ?? '';
1887
        if ($uploadFolder) {
1888
            try {
1889
                $uploadFolder = GeneralUtility::makeInstance(ResourceFactory::class)->getFolderObjectFromCombinedIdentifier($uploadFolder);
1890
            } catch (Exception\FolderDoesNotExistException $e) {
1891
                $uploadFolder = null;
1892
            }
1893
        }
1894
        if (empty($uploadFolder)) {
1895
            foreach ($this->getFileStorages() as $storage) {
1896
                if ($storage->isDefault() && $storage->isWritable()) {
1897
                    try {
1898
                        $uploadFolder = $storage->getDefaultFolder();
1899
                        if ($uploadFolder->checkActionPermission('write')) {
1900
                            break;
1901
                        }
1902
                        $uploadFolder = null;
1903
                    } catch (Exception $folderAccessException) {
1904
                        // If the folder is not accessible (no permissions / does not exist) we skip this one.
1905
                    }
1906
                    break;
1907
                }
1908
            }
1909
            if (!$uploadFolder instanceof Folder) {
1910
                /** @var ResourceStorage $storage */
1911
                foreach ($this->getFileStorages() as $storage) {
1912
                    if ($storage->isWritable()) {
1913
                        try {
1914
                            $uploadFolder = $storage->getDefaultFolder();
1915
                            if ($uploadFolder->checkActionPermission('write')) {
1916
                                break;
1917
                            }
1918
                            $uploadFolder = null;
1919
                        } catch (Exception $folderAccessException) {
1920
                            // If the folder is not accessible (no permissions / does not exist) try the next one.
1921
                        }
1922
                    }
1923
                }
1924
            }
1925
        }
1926
1927
        // HOOK: getDefaultUploadFolder
1928
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getDefaultUploadFolder'] ?? [] as $_funcRef) {
1929
            $_params = [
1930
                'uploadFolder' => $uploadFolder,
1931
                'pid' => $pid,
1932
                'table' => $table,
1933
                'field' => $field,
1934
            ];
1935
            $uploadFolder = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1936
        }
1937
1938
        if ($uploadFolder instanceof Folder) {
1939
            return $uploadFolder;
1940
        }
1941
        return false;
1942
    }
1943
1944
    /**
1945
     * Returns a \TYPO3\CMS\Core\Resource\Folder object that could be used for uploading
1946
     * temporary files in user context. The folder _temp_ below the default upload folder
1947
     * of the user is used.
1948
     *
1949
     * @return \TYPO3\CMS\Core\Resource\Folder|null
1950
     * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::getDefaultUploadFolder()
1951
     */
1952
    public function getDefaultUploadTemporaryFolder()
1953
    {
1954
        $defaultTemporaryFolder = null;
1955
        $defaultFolder = $this->getDefaultUploadFolder();
1956
1957
        if ($defaultFolder !== false) {
1958
            $tempFolderName = '_temp_';
1959
            $createFolder = !$defaultFolder->hasFolder($tempFolderName);
1960
            if ($createFolder === true) {
1961
                try {
1962
                    $defaultTemporaryFolder = $defaultFolder->createFolder($tempFolderName);
1963
                } catch (Exception $folderAccessException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1964
                }
1965
            } else {
1966
                $defaultTemporaryFolder = $defaultFolder->getSubfolder($tempFolderName);
1967
            }
1968
        }
1969
1970
        return $defaultTemporaryFolder;
1971
    }
1972
1973
    /**
1974
     * Initializing workspace.
1975
     * Called from within this function, see fetchGroupData()
1976
     *
1977
     * @see fetchGroupData()
1978
     * @internal should only be used from within TYPO3 Core
1979
     */
1980
    public function workspaceInit()
1981
    {
1982
        // Initializing workspace by evaluating and setting the workspace, possibly updating it in the user record!
1983
        $this->setWorkspace($this->user['workspace_id']);
1984
        // Limiting the DB mountpoints if there any selected in the workspace record
1985
        $this->initializeDbMountpointsInWorkspace();
1986
        $allowed_languages = (string)($this->getTSConfig()['options.']['workspaces.']['allowed_languages.'][$this->workspace] ?? '');
1987
        if ($allowed_languages !== '') {
1988
            $this->groupData['allowed_languages'] = StringUtility::uniqueList($allowed_languages);
1989
        }
1990
    }
1991
1992
    /**
1993
     * Limiting the DB mountpoints if there any selected in the workspace record
1994
     */
1995
    protected function initializeDbMountpointsInWorkspace()
1996
    {
1997
        $dbMountpoints = trim($this->workspaceRec['db_mountpoints'] ?? '');
1998
        if ($this->workspace > 0 && $dbMountpoints != '') {
1999
            $filteredDbMountpoints = [];
2000
            // Notice: We cannot call $this->getPagePermsClause(1);
2001
            // as usual because the group-list is not available at this point.
2002
            // But bypassing is fine because all we want here is check if the
2003
            // workspace mounts are inside the current webmounts rootline.
2004
            // The actual permission checking on page level is done elsewhere
2005
            // as usual anyway before the page tree is rendered.
2006
            $readPerms = '1=1';
2007
            // Traverse mount points of the workspace, add them,
2008
            // but make sure they match against the users' DB mounts
2009
2010
            $workspaceWebMounts = GeneralUtility::intExplode(',', $dbMountpoints);
2011
            $webMountsOfUser = GeneralUtility::intExplode(',', $this->dataLists['webmount_list']);
2012
            $webMountsOfUser = array_combine($webMountsOfUser, $webMountsOfUser);
2013
2014
            $entryPointRootLineUids = [];
2015
            foreach ($webMountsOfUser as $webMountPageId) {
2016
                $rootLine = BackendUtility::BEgetRootLine($webMountPageId, '', true);
2017
                $entryPointRootLineUids[$webMountPageId] = array_map('intval', array_column($rootLine, 'uid'));
2018
            }
2019
            foreach ($entryPointRootLineUids as $webMountOfUser => $uidsOfRootLine) {
2020
                // Remove the DB mounts of the user if the DB mount is not in the list of
2021
                // workspace mounts
2022
                foreach ($workspaceWebMounts as $webmountOfWorkspace) {
2023
                    // This workspace DB mount is somewhere in the rootline of the users' web mount,
2024
                    // so this is "OK" to be included
2025
                    if (in_array($webmountOfWorkspace, $uidsOfRootLine, true)) {
2026
                        continue;
2027
                    }
2028
                    // Remove the user's DB Mount (possible via array_combine, see above)
2029
                    unset($webMountsOfUser[$webMountOfUser]);
2030
                }
2031
            }
2032
            $dbMountpoints = array_merge($workspaceWebMounts, $webMountsOfUser);
0 ignored issues
show
Bug introduced by
It seems like $webMountsOfUser can also be of type false; however, parameter $array2 of array_merge() does only seem to accept array|null, 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

2032
            $dbMountpoints = array_merge($workspaceWebMounts, /** @scrutinizer ignore-type */ $webMountsOfUser);
Loading history...
2033
            $dbMountpoints = array_unique($dbMountpoints);
2034
            foreach ($dbMountpoints as $mpId) {
2035
                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...
2036
                    $filteredDbMountpoints[] = $mpId;
2037
                }
2038
            }
2039
            // Re-insert webmounts
2040
            $this->groupData['webmounts'] = implode(',', $filteredDbMountpoints);
2041
        }
2042
    }
2043
2044
    /**
2045
     * Checking if a workspace is allowed for backend user
2046
     *
2047
     * @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)
2048
     * @param string $fields List of fields to select. Default fields are all
2049
     * @return array Output will also show how access was granted. Admin users will have a true output regardless of input.
2050
     * @internal should only be used from within TYPO3 Core
2051
     */
2052
    public function checkWorkspace($wsRec, $fields = '*')
2053
    {
2054
        $retVal = false;
2055
        // If not array, look up workspace record:
2056
        if (!is_array($wsRec)) {
2057
            switch ((string)$wsRec) {
2058
                case '0':
2059
                    $wsRec = ['uid' => $wsRec];
2060
                    break;
2061
                default:
2062
                    if (ExtensionManagementUtility::isLoaded('workspaces')) {
2063
                        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
2064
                        $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
2065
                        $wsRec = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields))
2066
                            ->from('sys_workspace')
2067
                            ->where($queryBuilder->expr()->eq(
2068
                                'uid',
2069
                                $queryBuilder->createNamedParameter($wsRec, \PDO::PARAM_INT)
2070
                            ))
2071
                            ->orderBy('title')
2072
                            ->setMaxResults(1)
2073
                            ->execute()
2074
                            ->fetch(\PDO::FETCH_ASSOC);
2075
                    }
2076
            }
2077
        }
2078
        // If wsRec is set to an array, evaluate it:
2079
        if (is_array($wsRec)) {
2080
            if ($this->isAdmin()) {
2081
                return array_merge($wsRec, ['_ACCESS' => 'admin']);
2082
            }
2083
            switch ((string)$wsRec['uid']) {
2084
                    case '0':
2085
                        $retVal = (($this->groupData['workspace_perms'] ?? 0) & 1)
2086
                            ? array_merge($wsRec, ['_ACCESS' => 'online'])
2087
                            : false;
2088
                        break;
2089
                    default:
2090
                        // Checking if the guy is admin:
2091
                        if (GeneralUtility::inList($wsRec['adminusers'], 'be_users_' . $this->user['uid'])) {
2092
                            return array_merge($wsRec, ['_ACCESS' => 'owner']);
2093
                        }
2094
                        // Checking if he is owner through a user group of his:
2095
                        foreach ($this->userGroupsUID as $groupUid) {
2096
                            if (GeneralUtility::inList($wsRec['adminusers'], 'be_groups_' . $groupUid)) {
2097
                                return array_merge($wsRec, ['_ACCESS' => 'owner']);
2098
                            }
2099
                        }
2100
                        // Checking if he is member as user:
2101
                        if (GeneralUtility::inList($wsRec['members'], 'be_users_' . $this->user['uid'])) {
2102
                            return array_merge($wsRec, ['_ACCESS' => 'member']);
2103
                        }
2104
                        // Checking if he is member through a user group of his:
2105
                        foreach ($this->userGroupsUID as $groupUid) {
2106
                            if (GeneralUtility::inList($wsRec['members'], 'be_groups_' . $groupUid)) {
2107
                                return array_merge($wsRec, ['_ACCESS' => 'member']);
2108
                            }
2109
                        }
2110
                }
2111
        }
2112
        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...
2113
    }
2114
2115
    /**
2116
     * Uses checkWorkspace() to check if current workspace is available for user.
2117
     * This function caches the result and so can be called many times with no performance loss.
2118
     *
2119
     * @return array See checkWorkspace()
2120
     * @see checkWorkspace()
2121
     * @internal should only be used from within TYPO3 Core
2122
     */
2123
    public function checkWorkspaceCurrent()
2124
    {
2125
        if (!isset($this->checkWorkspaceCurrent_cache)) {
2126
            $this->checkWorkspaceCurrent_cache = $this->checkWorkspace($this->workspace);
2127
        }
2128
        return $this->checkWorkspaceCurrent_cache;
2129
    }
2130
2131
    /**
2132
     * Setting workspace ID
2133
     *
2134
     * @param int $workspaceId ID of workspace to set for backend user. If not valid the default workspace for BE user is found and set.
2135
     * @internal should only be used from within TYPO3 Core
2136
     */
2137
    public function setWorkspace($workspaceId)
2138
    {
2139
        // Check workspace validity and if not found, revert to default workspace.
2140
        if (!$this->setTemporaryWorkspace($workspaceId)) {
2141
            $this->setDefaultWorkspace();
2142
        }
2143
        // Unset access cache:
2144
        $this->checkWorkspaceCurrent_cache = null;
2145
        // If ID is different from the stored one, change it:
2146
        if ((int)$this->workspace !== (int)$this->user['workspace_id']) {
2147
            $this->user['workspace_id'] = $this->workspace;
2148
            GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users')->update(
2149
                'be_users',
2150
                ['workspace_id' => $this->user['workspace_id']],
2151
                ['uid' => (int)$this->user['uid']]
2152
            );
2153
            $this->writelog(SystemLogType::EXTENSION, SystemLogGenericAction::UNDEFINED, SystemLogErrorClassification::MESSAGE, 0, 'User changed workspace to "' . $this->workspace . '"', []);
2154
        }
2155
    }
2156
2157
    /**
2158
     * Sets a temporary workspace in the context of the current backend user.
2159
     *
2160
     * @param int $workspaceId
2161
     * @return bool
2162
     * @internal should only be used from within TYPO3 Core
2163
     */
2164
    public function setTemporaryWorkspace($workspaceId)
2165
    {
2166
        $result = false;
2167
        $workspaceRecord = $this->checkWorkspace($workspaceId);
2168
2169
        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...
2170
            $this->workspaceRec = $workspaceRecord;
2171
            $this->workspace = (int)$workspaceId;
2172
            $result = true;
2173
        }
2174
2175
        return $result;
2176
    }
2177
2178
    /**
2179
     * Sets the default workspace in the context of the current backend user.
2180
     * @internal should only be used from within TYPO3 Core
2181
     */
2182
    public function setDefaultWorkspace()
2183
    {
2184
        $this->workspace = (int)$this->getDefaultWorkspace();
2185
        $this->workspaceRec = $this->checkWorkspace($this->workspace);
2186
    }
2187
2188
    /**
2189
     * Return default workspace ID for user,
2190
     * if EXT:workspaces is not installed the user will be pushed to the
2191
     * Live workspace, if he has access to. If no workspace is available for the user, the workspace ID is set to "-99"
2192
     *
2193
     * @return int Default workspace id.
2194
     * @internal should only be used from within TYPO3 Core
2195
     */
2196
    public function getDefaultWorkspace()
2197
    {
2198
        if (!ExtensionManagementUtility::isLoaded('workspaces')) {
2199
            return 0;
2200
        }
2201
        // Online is default
2202
        if ($this->checkWorkspace(0)) {
2203
            return 0;
2204
        }
2205
        // Otherwise -99 is the fallback
2206
        $defaultWorkspace = -99;
2207
        // Traverse all workspaces
2208
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
2209
        $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
2210
        $result = $queryBuilder->select('*')
2211
            ->from('sys_workspace')
2212
            ->orderBy('title')
2213
            ->execute();
2214
        while ($workspaceRecord = $result->fetch()) {
2215
            if ($this->checkWorkspace($workspaceRecord)) {
2216
                $defaultWorkspace = (int)$workspaceRecord['uid'];
2217
                break;
2218
            }
2219
        }
2220
        return $defaultWorkspace;
2221
    }
2222
2223
    /**
2224
     * Writes an entry in the logfile/table
2225
     * Documentation in "TYPO3 Core API"
2226
     *
2227
     * @param int $type Denotes which module that has submitted the entry. See "TYPO3 Core API". Use "4" for extensions.
2228
     * @param int $action Denotes which specific operation that wrote the entry. Use "0" when no sub-categorizing applies
2229
     * @param int $error Flag. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin)
2230
     * @param int $details_nr The message number. Specific for each $type and $action. This will make it possible to translate errormessages to other languages
2231
     * @param string $details Default text that follows the message (in english!). Possibly translated by identification through type/action/details_nr
2232
     * @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
2233
     * @param string $tablename Table name. Special field used by tce_main.php.
2234
     * @param int|string $recuid Record UID. Special field used by tce_main.php.
2235
     * @param int|string $recpid Record PID. Special field used by tce_main.php. OBSOLETE
2236
     * @param int $event_pid The page_uid (pid) where the event occurred. Used to select log-content for specific pages.
2237
     * @param string $NEWid Special field used by tce_main.php. NEWid string of newly created records.
2238
     * @param int $userId Alternative Backend User ID (used for logging login actions where this is not yet known).
2239
     * @return int Log entry ID.
2240
     */
2241
    public function writelog($type, $action, $error, $details_nr, $details, $data, $tablename = '', $recuid = '', $recpid = '', $event_pid = -1, $NEWid = '', $userId = 0)
2242
    {
2243
        if (!$userId && !empty($this->user['uid'])) {
2244
            $userId = $this->user['uid'];
2245
        }
2246
2247
        if (!empty($this->user['ses_backuserid'])) {
2248
            if (empty($data)) {
2249
                $data = [];
2250
            }
2251
            $data['originalUser'] = $this->user['ses_backuserid'];
2252
        }
2253
2254
        $fields = [
2255
            'userid' => (int)$userId,
2256
            'type' => (int)$type,
2257
            'action' => (int)$action,
2258
            'error' => (int)$error,
2259
            'details_nr' => (int)$details_nr,
2260
            'details' => $details,
2261
            'log_data' => serialize($data),
2262
            'tablename' => $tablename,
2263
            'recuid' => (int)$recuid,
2264
            'IP' => (string)GeneralUtility::getIndpEnv('REMOTE_ADDR'),
2265
            'tstamp' => $GLOBALS['EXEC_TIME'] ?? time(),
2266
            'event_pid' => (int)$event_pid,
2267
            'NEWid' => $NEWid,
2268
            'workspace' => $this->workspace
2269
        ];
2270
2271
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_log');
2272
        $connection->insert(
2273
            'sys_log',
2274
            $fields,
2275
            [
2276
                \PDO::PARAM_INT,
2277
                \PDO::PARAM_INT,
2278
                \PDO::PARAM_INT,
2279
                \PDO::PARAM_INT,
2280
                \PDO::PARAM_INT,
2281
                \PDO::PARAM_STR,
2282
                \PDO::PARAM_STR,
2283
                \PDO::PARAM_STR,
2284
                \PDO::PARAM_INT,
2285
                \PDO::PARAM_STR,
2286
                \PDO::PARAM_INT,
2287
                \PDO::PARAM_INT,
2288
                \PDO::PARAM_STR,
2289
                \PDO::PARAM_STR,
2290
            ]
2291
        );
2292
2293
        return (int)$connection->lastInsertId('sys_log');
2294
    }
2295
2296
    /**
2297
     * Getter for the cookie name
2298
     *
2299
     * @static
2300
     * @return string returns the configured cookie name
2301
     */
2302
    public static function getCookieName()
2303
    {
2304
        $configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']);
2305
        if (empty($configuredCookieName)) {
2306
            $configuredCookieName = 'be_typo_user';
2307
        }
2308
        return $configuredCookieName;
2309
    }
2310
2311
    /**
2312
     * If TYPO3_CONF_VARS['BE']['enabledBeUserIPLock'] is enabled and
2313
     * an IP-list is found in the User TSconfig objString "options.lockToIP",
2314
     * then make an IP comparison with REMOTE_ADDR and check if the IP address matches
2315
     *
2316
     * @return bool TRUE, if IP address validates OK (or no check is done at all because no restriction is set)
2317
     * @internal should only be used from within TYPO3 Core
2318
     */
2319
    public function checkLockToIP()
2320
    {
2321
        $isValid = true;
2322
        if ($GLOBALS['TYPO3_CONF_VARS']['BE']['enabledBeUserIPLock']) {
2323
            $IPList = trim($this->getTSConfig()['options.']['lockToIP'] ?? '');
2324
            if (!empty($IPList)) {
2325
                $isValid = GeneralUtility::cmpIP(GeneralUtility::getIndpEnv('REMOTE_ADDR'), $IPList);
2326
            }
2327
        }
2328
        return $isValid;
2329
    }
2330
2331
    /**
2332
     * Check if user is logged in and if so, call ->fetchGroupData() to load group information and
2333
     * access lists of all kind, further check IP, set the ->uc array.
2334
     * If no user is logged in the default behaviour is to exit with an error message.
2335
     * This function is called right after ->start() in fx. the TYPO3 Bootstrap.
2336
     *
2337
     * @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.
2338
     * @throws \RuntimeException
2339
     */
2340
    public function backendCheckLogin($proceedIfNoUserIsLoggedIn = false)
2341
    {
2342
        if (empty($this->user['uid'])) {
2343
            if ($proceedIfNoUserIsLoggedIn === false) {
2344
                $url = GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir;
2345
                HttpUtility::redirect($url);
2346
            }
2347
        } else {
2348
            // ...and if that's the case, call these functions
2349
            $this->fetchGroupData();
2350
            // The groups are fetched and ready for permission checking in this initialization.
2351
            // Tables.php must be read before this because stuff like the modules has impact in this
2352
            if ($this->checkLockToIP()) {
2353
                if ($this->isUserAllowedToLogin()) {
2354
                    // Setting the UC array. It's needed with fetchGroupData first, due to default/overriding of values.
2355
                    $this->backendSetUC();
2356
                    if ($this->loginSessionStarted) {
2357
                        // Also, if there is a recovery link set, unset it now
2358
                        // this will be moved into its own Event at a later stage.
2359
                        // If a token was set previously, this is now unset, as it was now possible to log-in
2360
                        if ($this->user['password_reset_token'] ?? '') {
2361
                            GeneralUtility::makeInstance(ConnectionPool::class)
2362
                                ->getConnectionForTable($this->user_table)
2363
                                ->update($this->user_table, ['password_reset_token' => ''], ['uid' => $this->user['uid']]);
2364
                        }
2365
                        // Process hooks
2366
                        $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['backendUserLogin'];
2367
                        foreach ($hooks ?? [] as $_funcRef) {
2368
                            $_params = ['user' => $this->user];
2369
                            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2370
                        }
2371
                    }
2372
                } else {
2373
                    throw new \RuntimeException('Login Error: TYPO3 is in maintenance mode at the moment. Only administrators are allowed access.', 1294585860);
2374
                }
2375
            } else {
2376
                throw new \RuntimeException('Login Error: IP locking prevented you from being authorized. Can\'t proceed, sorry.', 1294585861);
2377
            }
2378
        }
2379
    }
2380
2381
    /**
2382
     * Initialize the internal ->uc array for the backend user
2383
     * Will make the overrides if necessary, and write the UC back to the be_users record if changes has happened
2384
     *
2385
     * @internal
2386
     */
2387
    public function backendSetUC()
2388
    {
2389
        // UC - user configuration is a serialized array inside the user object
2390
        // If there is a saved uc we implement that instead of the default one.
2391
        $this->unpack_uc();
2392
        // Setting defaults if uc is empty
2393
        $updated = false;
2394
        $originalUc = [];
2395
        if (is_array($this->uc) && isset($this->uc['ucSetByInstallTool'])) {
2396
            $originalUc = $this->uc;
2397
            unset($originalUc['ucSetByInstallTool'], $this->uc);
2398
        }
2399
        if (!is_array($this->uc)) {
2400
            $this->uc = array_merge(
2401
                $this->uc_default,
2402
                (array)$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUC'],
2403
                GeneralUtility::removeDotsFromTS((array)($this->getTSConfig()['setup.']['default.'] ?? [])),
2404
                $originalUc
2405
            );
2406
            $this->overrideUC();
2407
            $updated = true;
2408
        }
2409
        // If TSconfig is updated, update the defaultUC.
2410
        if ($this->userTSUpdated) {
2411
            $this->overrideUC();
2412
            $updated = true;
2413
        }
2414
        // Setting default lang from be_user record.
2415
        if (!isset($this->uc['lang'])) {
2416
            $this->uc['lang'] = $this->user['lang'];
2417
            $updated = true;
2418
        }
2419
        // Setting the time of the first login:
2420
        if (!isset($this->uc['firstLoginTimeStamp'])) {
2421
            $this->uc['firstLoginTimeStamp'] = $GLOBALS['EXEC_TIME'];
2422
            $updated = true;
2423
        }
2424
        // Saving if updated.
2425
        if ($updated) {
2426
            $this->writeUC();
2427
        }
2428
    }
2429
2430
    /**
2431
     * Override: Call this function every time the uc is updated.
2432
     * That is 1) by reverting to default values, 2) in the setup-module, 3) userTS changes (userauthgroup)
2433
     *
2434
     * @internal
2435
     */
2436
    public function overrideUC()
2437
    {
2438
        $this->uc = array_merge((array)$this->uc, (array)($this->getTSConfig()['setup.']['override.'] ?? []));
2439
    }
2440
2441
    /**
2442
     * Clears the user[uc] and ->uc to blank strings. Then calls ->backendSetUC() to fill it again with reset contents
2443
     *
2444
     * @internal
2445
     */
2446
    public function resetUC()
2447
    {
2448
        $this->user['uc'] = '';
2449
        $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...
2450
        $this->backendSetUC();
2451
    }
2452
2453
    /**
2454
     * Determines whether a backend user is allowed to access the backend.
2455
     *
2456
     * The conditions are:
2457
     * + backend user is a regular user and adminOnly is not defined
2458
     * + backend user is an admin user
2459
     * + backend user is used in CLI context and adminOnly is explicitly set to "2" (see CommandLineUserAuthentication)
2460
     * + backend user is being controlled by an admin user
2461
     *
2462
     * @return bool Whether a backend user is allowed to access the backend
2463
     */
2464
    protected function isUserAllowedToLogin()
2465
    {
2466
        $isUserAllowedToLogin = false;
2467
        $adminOnlyMode = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'];
2468
        // Backend user is allowed if adminOnly is not set or user is an admin:
2469
        if (!$adminOnlyMode || $this->isAdmin()) {
2470
            $isUserAllowedToLogin = true;
2471
        } elseif ($this->user['ses_backuserid']) {
2472
            $backendUserId = (int)$this->user['ses_backuserid'];
2473
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
2474
            $isUserAllowedToLogin = (bool)$queryBuilder->count('uid')
2475
                ->from('be_users')
2476
                ->where(
2477
                    $queryBuilder->expr()->eq(
2478
                        'uid',
2479
                        $queryBuilder->createNamedParameter($backendUserId, \PDO::PARAM_INT)
2480
                    ),
2481
                    $queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT))
2482
                )
2483
                ->execute()
2484
                ->fetchColumn(0);
2485
        }
2486
        return $isUserAllowedToLogin;
2487
    }
2488
2489
    /**
2490
     * Logs out the current user and clears the form protection tokens.
2491
     */
2492
    public function logoff()
2493
    {
2494
        if (isset($GLOBALS['BE_USER'])
2495
            && $GLOBALS['BE_USER'] instanceof self
2496
            && isset($GLOBALS['BE_USER']->user['uid'])
2497
        ) {
2498
            FormProtectionFactory::get()->clean();
2499
            // Release the locked records
2500
            $this->releaseLockedRecords((int)$GLOBALS['BE_USER']->user['uid']);
2501
2502
            if ($this->isSystemMaintainer()) {
2503
                // If user is system maintainer, destroy its possibly valid install tool session.
2504
                $session = new SessionService();
2505
                $session->destroySession();
2506
            }
2507
        }
2508
        parent::logoff();
2509
    }
2510
2511
    /**
2512
     * Remove any "locked records" added for editing for the given user (= current backend user)
2513
     * @param int $userId
2514
     */
2515
    protected function releaseLockedRecords(int $userId)
2516
    {
2517
        if ($userId > 0) {
2518
            GeneralUtility::makeInstance(ConnectionPool::class)
2519
                ->getConnectionForTable('sys_lockedrecords')
2520
                ->delete(
2521
                    'sys_lockedrecords',
2522
                    ['userid' => $userId]
2523
                );
2524
        }
2525
    }
2526
}
2527