Passed
Push — master ( f16b47...733353 )
by
unknown
13:48
created

workspaceAllowAutoCreation()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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