Passed
Push — master ( ab6540...8eb464 )
by
unknown
22:11
created

BackendUserAuthentication::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

1184
            $this->setWebmounts($alternativeWebmountPoints, /** @scrutinizer ignore-type */ $appendAlternativeWebmountPoints);
Loading history...
1185
        }
1186
    }
1187
1188
    /**
1189
     * Returns TRUE or FALSE, depending if an alert popup (a javascript confirmation) should be shown
1190
     * call like $GLOBALS['BE_USER']->jsConfirmation($BITMASK).
1191
     *
1192
     * @param int $bitmask Bitmask, one of \TYPO3\CMS\Core\Type\Bitmask\JsConfirmation
1193
     * @return bool TRUE if the confirmation should be shown
1194
     * @see JsConfirmation
1195
     */
1196
    public function jsConfirmation($bitmask)
1197
    {
1198
        try {
1199
            $alertPopupsSetting = trim((string)($this->getTSConfig()['options.']['alertPopups'] ?? ''));
1200
            $alertPopup = JsConfirmation::cast($alertPopupsSetting === '' ? null : (int)$alertPopupsSetting);
1201
        } catch (InvalidEnumerationValueException $e) {
1202
            $alertPopup = new JsConfirmation();
1203
        }
1204
1205
        return JsConfirmation::cast($bitmask)->matches($alertPopup);
1206
    }
1207
1208
    /**
1209
     * Initializes a lot of stuff like the access-lists, database-mountpoints and filemountpoints
1210
     * This method is called by ->backendCheckLogin() (from extending BackendUserAuthentication)
1211
     * if the backend user login has verified OK.
1212
     * Generally this is required initialization of a backend user.
1213
     *
1214
     * @internal
1215
     * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
1216
     */
1217
    public function fetchGroupData()
1218
    {
1219
        if ($this->user['uid']) {
1220
            // Get lists for the be_user record and set them as default/primary values.
1221
            // Enabled Backend Modules
1222
            $this->groupData['modules'] = $this->user['userMods'];
1223
            // Add available widgets
1224
            $this->groupData['available_widgets'] = $this->user['available_widgets'] ?? '';
1225
            // Add allowed mfa providers
1226
            $this->groupData['mfa_providers'] = $this->user['mfa_providers'] ?? '';
1227
            // Add Allowed Languages
1228
            $this->groupData['allowed_languages'] = $this->user['allowed_languages'];
1229
            // Set user value for workspace permissions.
1230
            $this->groupData['workspace_perms'] = $this->user['workspace_perms'];
1231
            // Database mountpoints
1232
            $this->groupData['webmounts'] = $this->user['db_mountpoints'];
1233
            // File mountpoints
1234
            $this->groupData['filemounts'] = $this->user['file_mountpoints'];
1235
            // Fileoperation permissions
1236
            $this->groupData['file_permissions'] = $this->user['file_permissions'];
1237
1238
            // Get the groups and accumulate their permission settings
1239
            $mountOptions = new BackendGroupMountOption($this->user['options']);
1240
            $groupResolver = GeneralUtility::makeInstance(GroupResolver::class);
1241
            $resolvedGroups = $groupResolver->resolveGroupsForUser($this->user, $this->usergroup_table);
1242
            foreach ($resolvedGroups as $groupInfo) {
1243
                // Add the group uid to internal arrays.
1244
                $this->userGroupsUID[] = (int)$groupInfo['uid'];
1245
                $this->userGroups[(int)$groupInfo['uid']] = $groupInfo;
1246
                // Mount group database-mounts
1247
                if ($mountOptions->shouldUserIncludePageMountsFromAssociatedGroups()) {
1248
                    $this->groupData['webmounts'] .= ',' . $groupInfo['db_mountpoints'];
1249
                }
1250
                // Mount group file-mounts
1251
                if ($mountOptions->shouldUserIncludePageMountsFromAssociatedGroups()) {
1252
                    $this->groupData['filemounts'] .= ',' . $groupInfo['file_mountpoints'];
1253
                }
1254
                // Gather permission detail fields
1255
                $this->groupData['modules'] .= ',' . $groupInfo['groupMods'];
1256
                $this->groupData['available_widgets'] .= ',' . $groupInfo['availableWidgets'];
1257
                $this->groupData['mfa_providers'] .= ',' . $groupInfo['mfa_providers'];
1258
                $this->groupData['tables_select'] .= ',' . $groupInfo['tables_select'];
1259
                $this->groupData['tables_modify'] .= ',' . $groupInfo['tables_modify'];
1260
                $this->groupData['pagetypes_select'] .= ',' . $groupInfo['pagetypes_select'];
1261
                $this->groupData['non_exclude_fields'] .= ',' . $groupInfo['non_exclude_fields'];
1262
                $this->groupData['explicit_allowdeny'] .= ',' . $groupInfo['explicit_allowdeny'];
1263
                $this->groupData['allowed_languages'] .= ',' . $groupInfo['allowed_languages'];
1264
                $this->groupData['custom_options'] .= ',' . $groupInfo['custom_options'];
1265
                $this->groupData['file_permissions'] .= ',' . $groupInfo['file_permissions'];
1266
                // Setting workspace permissions:
1267
                $this->groupData['workspace_perms'] |= $groupInfo['workspace_perms'];
1268
                if (!$this->firstMainGroup) {
1269
                    $this->firstMainGroup = (int)$groupInfo['uid'];
1270
                }
1271
            }
1272
1273
            // Populating the $this->userGroupsUID -array with the groups in the order in which they were LAST included.!!
1274
            // Finally this is the list of group_uid's in the order they are parsed (including subgroups!)
1275
            // and without duplicates (duplicates are presented with their last entrance in the list,
1276
            // which thus reflects the order of the TypoScript in TSconfig)
1277
            $this->userGroupsUID = array_reverse(array_unique(array_reverse($this->userGroupsUID)));
1278
1279
            $this->prepareUserTsConfig();
1280
1281
            // Processing webmounts
1282
            // Admin's always have the root mounted
1283
            if ($this->isAdmin() && !($this->getTSConfig()['options.']['dontMountAdminMounts'] ?? false)) {
1284
                $this->groupData['webmounts'] = '0,' . $this->groupData['webmounts'];
1285
            }
1286
            // The lists are cleaned for duplicates
1287
            $this->groupData['webmounts'] = StringUtility::uniqueList($this->groupData['webmounts'] ?? '');
1288
            $this->groupData['pagetypes_select'] = StringUtility::uniqueList($this->groupData['pagetypes_select'] ?? '');
1289
            $this->groupData['tables_select'] = StringUtility::uniqueList(($this->groupData['tables_modify'] ?? '') . ',' . ($this->groupData['tables_select'] ?? ''));
1290
            $this->groupData['tables_modify'] = StringUtility::uniqueList($this->groupData['tables_modify'] ?? '');
1291
            $this->groupData['non_exclude_fields'] = StringUtility::uniqueList($this->groupData['non_exclude_fields'] ?? '');
1292
            $this->groupData['explicit_allowdeny'] = StringUtility::uniqueList($this->groupData['explicit_allowdeny'] ?? '');
1293
            $this->groupData['allowed_languages'] = StringUtility::uniqueList($this->groupData['allowed_languages'] ?? '');
1294
            $this->groupData['custom_options'] = StringUtility::uniqueList($this->groupData['custom_options'] ?? '');
1295
            $this->groupData['modules'] = StringUtility::uniqueList($this->groupData['modules'] ?? '');
1296
            $this->groupData['available_widgets'] = StringUtility::uniqueList($this->groupData['available_widgets'] ?? '');
1297
            $this->groupData['mfa_providers'] = StringUtility::uniqueList($this->groupData['mfa_providers'] ?? '');
1298
            $this->groupData['file_permissions'] = StringUtility::uniqueList($this->groupData['file_permissions'] ?? '');
1299
1300
            // Check if the user access to all web mounts set
1301
            if (!empty(trim($this->groupData['webmounts']))) {
1302
                $validWebMounts = $this->filterValidWebMounts($this->groupData['webmounts']);
1303
                $this->groupData['webmounts'] = implode(',', $validWebMounts);
1304
            }
1305
            // Setting up workspace situation (after webmounts are processed!):
1306
            $this->workspaceInit();
1307
        }
1308
    }
1309
1310
    /**
1311
     * Checking read access to web mounts, but keeps "0" or empty strings.
1312
     * In any case, checks if the list of pages is visible for the backend user but also
1313
     * if the page is not deleted.
1314
     *
1315
     * @param string $listOfWebMounts a comma-separated list of webmounts, could also be empty, or contain "0"
1316
     * @return array a list of all valid web mounts the user has access to
1317
     */
1318
    protected function filterValidWebMounts(string $listOfWebMounts): array
1319
    {
1320
        // Checking read access to web mounts if there are mounts points (not empty string, false or 0)
1321
        $allWebMounts = explode(',', $listOfWebMounts);
1322
        // Selecting all web mounts with permission clause for reading
1323
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1324
        $queryBuilder->getRestrictions()
1325
            ->removeAll()
1326
            ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1327
1328
        $readablePagesOfWebMounts = $queryBuilder->select('uid')
1329
            ->from('pages')
1330
            // @todo DOCTRINE: check how to make getPagePermsClause() portable
1331
            ->where(
1332
                $this->getPagePermsClause(Permission::PAGE_SHOW),
1333
                $queryBuilder->expr()->in(
1334
                    'uid',
1335
                    $queryBuilder->createNamedParameter(
1336
                        GeneralUtility::intExplode(',', $listOfWebMounts),
1337
                        Connection::PARAM_INT_ARRAY
1338
                    )
1339
                )
1340
            )
1341
            ->execute()
1342
            ->fetchAll();
1343
        $readablePagesOfWebMounts = array_column(($readablePagesOfWebMounts ?: []), 'uid', 'uid');
1344
        foreach ($allWebMounts as $key => $mountPointUid) {
1345
            // If the mount ID is NOT found among selected pages, unset it:
1346
            if ($mountPointUid > 0 && !isset($readablePagesOfWebMounts[$mountPointUid])) {
1347
                unset($allWebMounts[$key]);
1348
            }
1349
        }
1350
        return $allWebMounts;
1351
    }
1352
1353
    /**
1354
     * This method parses the UserTSconfig from the current user and all their groups.
1355
     * If the contents are the same, parsing is skipped. No matching is applied here currently.
1356
     */
1357
    protected function prepareUserTsConfig(): void
1358
    {
1359
        $collectedUserTSconfig = [
1360
            'default' => $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig']
1361
        ];
1362
        // Default TSconfig for admin-users
1363
        if ($this->isAdmin()) {
1364
            $collectedUserTSconfig[] = 'admPanel.enable.all = 1';
1365
        }
1366
        // Setting defaults for sys_note author / email
1367
        $collectedUserTSconfig[] = '
1368
TCAdefaults.sys_note.author = ' . $this->user['realName'] . '
1369
TCAdefaults.sys_note.email = ' . $this->user['email'];
1370
1371
        // Loop through all groups and add their 'TSconfig' fields
1372
        foreach ($this->userGroupsUID as $groupId) {
1373
            $collectedUserTSconfig['group_' . $groupId] = $this->userGroups[$groupId]['TSconfig'] ?? '';
1374
        }
1375
1376
        $collectedUserTSconfig[] = $this->user['TSconfig'];
1377
        // Check external files
1378
        $collectedUserTSconfig = TypoScriptParser::checkIncludeLines_array($collectedUserTSconfig);
1379
        // Imploding with "[global]" will make sure that non-ended confinements with braces are ignored.
1380
        $userTS_text = implode("\n[GLOBAL]\n", $collectedUserTSconfig);
1381
        // Parsing the user TSconfig (or getting from cache)
1382
        $hash = md5('userTS:' . $userTS_text);
1383
        $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
1384
        if (!($this->userTS = $cache->get($hash))) {
1385
            $parseObj = GeneralUtility::makeInstance(TypoScriptParser::class);
1386
            $conditionMatcher = GeneralUtility::makeInstance(ConditionMatcher::class);
1387
            $parseObj->parse($userTS_text, $conditionMatcher);
1388
            $this->userTS = $parseObj->setup;
1389
            $cache->set($hash, $this->userTS, ['UserTSconfig'], 0);
1390
            // Ensure to update UC later
1391
            $this->userTSUpdated = true;
1392
        }
1393
    }
1394
1395
    /**
1396
     * Sets up all file storages for a user.
1397
     * Needs to be called AFTER the groups have been loaded.
1398
     */
1399
    protected function initializeFileStorages()
1400
    {
1401
        $this->fileStorages = [];
1402
        /** @var \TYPO3\CMS\Core\Resource\StorageRepository $storageRepository */
1403
        $storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
1404
        // Admin users have all file storages visible, without any filters
1405
        if ($this->isAdmin()) {
1406
            $storageObjects = $storageRepository->findAll();
1407
            foreach ($storageObjects as $storageObject) {
1408
                $this->fileStorages[$storageObject->getUid()] = $storageObject;
1409
            }
1410
        } else {
1411
            // Regular users only have storages that are defined in their filemounts
1412
            // Permissions and file mounts for the storage are added in StoragePermissionAspect
1413
            foreach ($this->getFileMountRecords() as $row) {
1414
                if (!array_key_exists((int)$row['base'], $this->fileStorages)) {
1415
                    $storageObject = $storageRepository->findByUid($row['base']);
1416
                    if ($storageObject) {
1417
                        $this->fileStorages[$storageObject->getUid()] = $storageObject;
1418
                    }
1419
                }
1420
            }
1421
        }
1422
1423
        // This has to be called always in order to set certain filters
1424
        $this->evaluateUserSpecificFileFilterSettings();
1425
    }
1426
1427
    /**
1428
     * Returns an array of category mount points. The category permissions from BE Groups
1429
     * are also taken into consideration and are merged into User permissions.
1430
     *
1431
     * @return array
1432
     */
1433
    public function getCategoryMountPoints()
1434
    {
1435
        $categoryMountPoints = '';
1436
1437
        // Category mounts of the groups
1438
        if (is_array($this->userGroups)) {
0 ignored issues
show
introduced by
The condition is_array($this->userGroups) is always true.
Loading history...
1439
            foreach ($this->userGroups as $group) {
1440
                if ($group['category_perms']) {
1441
                    $categoryMountPoints .= ',' . $group['category_perms'];
1442
                }
1443
            }
1444
        }
1445
1446
        // Category mounts of the user record
1447
        if ($this->user['category_perms']) {
1448
            $categoryMountPoints .= ',' . $this->user['category_perms'];
1449
        }
1450
1451
        // Make the ids unique
1452
        $categoryMountPoints = GeneralUtility::trimExplode(',', $categoryMountPoints);
1453
        $categoryMountPoints = array_filter($categoryMountPoints); // remove empty value
1454
        $categoryMountPoints = array_unique($categoryMountPoints); // remove unique value
1455
1456
        return $categoryMountPoints;
1457
    }
1458
1459
    /**
1460
     * Returns an array of file mount records, taking workspaces and user home and group home directories into account
1461
     * Needs to be called AFTER the groups have been loaded.
1462
     *
1463
     * @return array
1464
     * @internal
1465
     */
1466
    public function getFileMountRecords()
1467
    {
1468
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
1469
        $fileMountRecordCache = $runtimeCache->get('backendUserAuthenticationFileMountRecords') ?: [];
1470
1471
        if (!empty($fileMountRecordCache)) {
1472
            return $fileMountRecordCache;
1473
        }
1474
1475
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
1476
1477
        // Processing file mounts (both from the user and the groups)
1478
        $fileMounts = array_unique(GeneralUtility::intExplode(',', $this->groupData['filemounts'], true));
1479
1480
        // Limit file mounts if set in workspace record
1481
        if ($this->workspace > 0 && !empty($this->workspaceRec['file_mountpoints'])) {
1482
            $workspaceFileMounts = GeneralUtility::intExplode(',', $this->workspaceRec['file_mountpoints'], true);
1483
            $fileMounts = array_intersect($fileMounts, $workspaceFileMounts);
1484
        }
1485
1486
        if (!empty($fileMounts)) {
1487
            $orderBy = $GLOBALS['TCA']['sys_filemounts']['ctrl']['default_sortby'] ?? 'sorting';
1488
1489
            $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_filemounts');
1490
            $queryBuilder->getRestrictions()
1491
                ->removeAll()
1492
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1493
                ->add(GeneralUtility::makeInstance(HiddenRestriction::class))
1494
                ->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
1495
1496
            $queryBuilder->select('*')
1497
                ->from('sys_filemounts')
1498
                ->where(
1499
                    $queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($fileMounts, Connection::PARAM_INT_ARRAY))
1500
                );
1501
1502
            foreach (QueryHelper::parseOrderBy($orderBy) as $fieldAndDirection) {
1503
                $queryBuilder->addOrderBy(...$fieldAndDirection);
1504
            }
1505
1506
            $fileMountRecords = $queryBuilder->execute()->fetchAll(\PDO::FETCH_ASSOC);
1507
            if ($fileMountRecords !== false) {
1508
                foreach ($fileMountRecords as $fileMount) {
1509
                    $fileMountRecordCache[$fileMount['base'] . $fileMount['path']] = $fileMount;
1510
                }
1511
            }
1512
        }
1513
1514
        // Read-only file mounts
1515
        $readOnlyMountPoints = \trim($this->getTSConfig()['options.']['folderTree.']['altElementBrowserMountPoints'] ?? '');
1516
        if ($readOnlyMountPoints) {
1517
            // We cannot use the API here but need to fetch the default storage record directly
1518
            // to not instantiate it (which directly applies mount points) before all mount points are resolved!
1519
            $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_file_storage');
1520
            $defaultStorageRow = $queryBuilder->select('uid')
1521
                ->from('sys_file_storage')
1522
                ->where(
1523
                    $queryBuilder->expr()->eq('is_default', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT))
1524
                )
1525
                ->setMaxResults(1)
1526
                ->execute()
1527
                ->fetch(\PDO::FETCH_ASSOC);
1528
1529
            $readOnlyMountPointArray = GeneralUtility::trimExplode(',', $readOnlyMountPoints);
1530
            foreach ($readOnlyMountPointArray as $readOnlyMountPoint) {
1531
                $readOnlyMountPointConfiguration = GeneralUtility::trimExplode(':', $readOnlyMountPoint);
1532
                if (count($readOnlyMountPointConfiguration) === 2) {
1533
                    // A storage is passed in the configuration
1534
                    $storageUid = (int)$readOnlyMountPointConfiguration[0];
1535
                    $path = $readOnlyMountPointConfiguration[1];
1536
                } else {
1537
                    if (empty($defaultStorageRow)) {
1538
                        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);
1539
                    }
1540
                    // Backwards compatibility: If no storage is passed, we use the default storage
1541
                    $storageUid = $defaultStorageRow['uid'];
1542
                    $path = $readOnlyMountPointConfiguration[0];
1543
                }
1544
                $fileMountRecordCache[$storageUid . $path] = [
1545
                    'base' => $storageUid,
1546
                    'title' => $path,
1547
                    'path' => $path,
1548
                    'read_only' => true
1549
                ];
1550
            }
1551
        }
1552
1553
        // Personal or Group filemounts are not accessible if file mount list is set in workspace record
1554
        if ($this->workspace <= 0 || empty($this->workspaceRec['file_mountpoints'])) {
1555
            // If userHomePath is set, we attempt to mount it
1556
            if ($GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath']) {
1557
                [$userHomeStorageUid, $userHomeFilter] = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath'], 2);
1558
                $userHomeStorageUid = (int)$userHomeStorageUid;
1559
                $userHomeFilter = '/' . ltrim($userHomeFilter, '/');
1560
                if ($userHomeStorageUid > 0) {
1561
                    // Try and mount with [uid]_[username]
1562
                    $path = $userHomeFilter . $this->user['uid'] . '_' . $this->user['username'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1563
                    $fileMountRecordCache[$userHomeStorageUid . $path] = [
1564
                        'base' => $userHomeStorageUid,
1565
                        'title' => $this->user['username'],
1566
                        'path' => $path,
1567
                        'read_only' => false,
1568
                        'user_mount' => true
1569
                    ];
1570
                    // Try and mount with only [uid]
1571
                    $path = $userHomeFilter . $this->user['uid'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1572
                    $fileMountRecordCache[$userHomeStorageUid . $path] = [
1573
                        'base' => $userHomeStorageUid,
1574
                        'title' => $this->user['username'],
1575
                        'path' => $path,
1576
                        'read_only' => false,
1577
                        'user_mount' => true
1578
                    ];
1579
                }
1580
            }
1581
1582
            // Mount group home-dirs
1583
            $mountOptions = new BackendGroupMountOption((int)$this->user['options']);
1584
            if ($GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'] !== '' && $mountOptions->shouldUserIncludeFileMountsFromAssociatedGroups()) {
1585
                // If groupHomePath is set, we attempt to mount it
1586
                [$groupHomeStorageUid, $groupHomeFilter] = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'], 2);
1587
                $groupHomeStorageUid = (int)$groupHomeStorageUid;
1588
                $groupHomeFilter = '/' . ltrim($groupHomeFilter, '/');
1589
                if ($groupHomeStorageUid > 0) {
1590
                    foreach ($this->userGroups as $groupData) {
1591
                        $path = $groupHomeFilter . $groupData['uid'];
1592
                        $fileMountRecordCache[$groupHomeStorageUid . $path] = [
1593
                            'base' => $groupHomeStorageUid,
1594
                            'title' => $groupData['title'],
1595
                            'path' => $path,
1596
                            'read_only' => false,
1597
                            'user_mount' => true
1598
                        ];
1599
                    }
1600
                }
1601
            }
1602
        }
1603
1604
        $runtimeCache->set('backendUserAuthenticationFileMountRecords', $fileMountRecordCache);
1605
        return $fileMountRecordCache;
1606
    }
1607
1608
    /**
1609
     * Returns an array with the filemounts for the user.
1610
     * Each filemount is represented with an array of a "name", "path" and "type".
1611
     * If no filemounts an empty array is returned.
1612
     *
1613
     * @return \TYPO3\CMS\Core\Resource\ResourceStorage[]
1614
     */
1615
    public function getFileStorages()
1616
    {
1617
        // Initializing file mounts after the groups are fetched
1618
        if ($this->fileStorages === null) {
1619
            $this->initializeFileStorages();
1620
        }
1621
        return $this->fileStorages;
1622
    }
1623
1624
    /**
1625
     * Adds filters based on what the user has set
1626
     * this should be done in this place, and called whenever needed,
1627
     * but only when needed
1628
     */
1629
    public function evaluateUserSpecificFileFilterSettings()
1630
    {
1631
        // Add the option for also displaying the non-hidden files
1632
        if ($this->uc['showHiddenFilesAndFolders']) {
1633
            FileNameFilter::setShowHiddenFilesAndFolders(true);
1634
        }
1635
    }
1636
1637
    /**
1638
     * Returns the information about file permissions.
1639
     * Previously, this was stored in the DB field fileoper_perms now it is file_permissions.
1640
     * Besides it can be handled via userTSconfig
1641
     *
1642
     * permissions.file.default {
1643
     * addFile = 1
1644
     * readFile = 1
1645
     * writeFile = 1
1646
     * copyFile = 1
1647
     * moveFile = 1
1648
     * renameFile = 1
1649
     * deleteFile = 1
1650
     *
1651
     * addFolder = 1
1652
     * readFolder = 1
1653
     * writeFolder = 1
1654
     * copyFolder = 1
1655
     * moveFolder = 1
1656
     * renameFolder = 1
1657
     * deleteFolder = 1
1658
     * recursivedeleteFolder = 1
1659
     * }
1660
     *
1661
     * # overwrite settings for a specific storageObject
1662
     * permissions.file.storage.StorageUid {
1663
     * readFile = 1
1664
     * recursivedeleteFolder = 0
1665
     * }
1666
     *
1667
     * Please note that these permissions only apply, if the storage has the
1668
     * capabilities (browseable, writable), and if the driver allows for writing etc
1669
     *
1670
     * @return array
1671
     */
1672
    public function getFilePermissions()
1673
    {
1674
        if (!isset($this->filePermissions)) {
1675
            $filePermissions = [
1676
                // File permissions
1677
                'addFile' => false,
1678
                'readFile' => false,
1679
                'writeFile' => false,
1680
                'copyFile' => false,
1681
                'moveFile' => false,
1682
                'renameFile' => false,
1683
                'deleteFile' => false,
1684
                // Folder permissions
1685
                'addFolder' => false,
1686
                'readFolder' => false,
1687
                'writeFolder' => false,
1688
                'copyFolder' => false,
1689
                'moveFolder' => false,
1690
                'renameFolder' => false,
1691
                'deleteFolder' => false,
1692
                'recursivedeleteFolder' => false
1693
            ];
1694
            if ($this->isAdmin()) {
1695
                $filePermissions = array_map('is_bool', $filePermissions);
1696
            } else {
1697
                $userGroupRecordPermissions = GeneralUtility::trimExplode(',', $this->groupData['file_permissions'] ?? '', true);
1698
                array_walk(
1699
                    $userGroupRecordPermissions,
1700
                    function ($permission) use (&$filePermissions) {
1701
                        $filePermissions[$permission] = true;
1702
                    }
1703
                );
1704
1705
                // Finally overlay any userTSconfig
1706
                $permissionsTsConfig = $this->getTSConfig()['permissions.']['file.']['default.'] ?? [];
1707
                if (!empty($permissionsTsConfig)) {
1708
                    array_walk(
1709
                        $permissionsTsConfig,
1710
                        function ($value, $permission) use (&$filePermissions) {
1711
                            $filePermissions[$permission] = (bool)$value;
1712
                        }
1713
                    );
1714
                }
1715
            }
1716
            $this->filePermissions = $filePermissions;
1717
        }
1718
        return $this->filePermissions;
1719
    }
1720
1721
    /**
1722
     * Gets the file permissions for a storage
1723
     * by merging any storage-specific permissions for a
1724
     * storage with the default settings.
1725
     * Admin users will always get the default settings.
1726
     *
1727
     * @param \TYPO3\CMS\Core\Resource\ResourceStorage $storageObject
1728
     * @return array
1729
     */
1730
    public function getFilePermissionsForStorage(ResourceStorage $storageObject)
1731
    {
1732
        $finalUserPermissions = $this->getFilePermissions();
1733
        if (!$this->isAdmin()) {
1734
            $storageFilePermissions = $this->getTSConfig()['permissions.']['file.']['storage.'][$storageObject->getUid() . '.'] ?? [];
1735
            if (!empty($storageFilePermissions)) {
1736
                array_walk(
1737
                    $storageFilePermissions,
1738
                    function ($value, $permission) use (&$finalUserPermissions) {
1739
                        $finalUserPermissions[$permission] = (bool)$value;
1740
                    }
1741
                );
1742
            }
1743
        }
1744
        return $finalUserPermissions;
1745
    }
1746
1747
    /**
1748
     * Returns a \TYPO3\CMS\Core\Resource\Folder object that is used for uploading
1749
     * files by default.
1750
     * This is used for RTE and its magic images, as well as uploads
1751
     * in the TCEforms fields.
1752
     *
1753
     * The default upload folder for a user is the defaultFolder on the first
1754
     * filestorage/filemount that the user can access and to which files are allowed to be added
1755
     * however, you can set the users' upload folder like this:
1756
     *
1757
     * options.defaultUploadFolder = 3:myfolder/yourfolder/
1758
     *
1759
     * @param int $pid PageUid
1760
     * @param string $table Table name
1761
     * @param string $field Field name
1762
     * @return \TYPO3\CMS\Core\Resource\Folder|bool The default upload folder for this user
1763
     */
1764
    public function getDefaultUploadFolder($pid = null, $table = null, $field = null)
1765
    {
1766
        $uploadFolder = $this->getTSConfig()['options.']['defaultUploadFolder'] ?? '';
1767
        if ($uploadFolder) {
1768
            try {
1769
                $uploadFolder = GeneralUtility::makeInstance(ResourceFactory::class)->getFolderObjectFromCombinedIdentifier($uploadFolder);
1770
            } catch (Exception\FolderDoesNotExistException $e) {
1771
                $uploadFolder = null;
1772
            }
1773
        }
1774
        if (empty($uploadFolder)) {
1775
            foreach ($this->getFileStorages() as $storage) {
1776
                if ($storage->isDefault() && $storage->isWritable()) {
1777
                    try {
1778
                        $uploadFolder = $storage->getDefaultFolder();
1779
                        if ($uploadFolder->checkActionPermission('write')) {
1780
                            break;
1781
                        }
1782
                        $uploadFolder = null;
1783
                    } catch (Exception $folderAccessException) {
1784
                        // If the folder is not accessible (no permissions / does not exist) we skip this one.
1785
                    }
1786
                    break;
1787
                }
1788
            }
1789
            if (!$uploadFolder instanceof Folder) {
1790
                /** @var ResourceStorage $storage */
1791
                foreach ($this->getFileStorages() as $storage) {
1792
                    if ($storage->isWritable()) {
1793
                        try {
1794
                            $uploadFolder = $storage->getDefaultFolder();
1795
                            if ($uploadFolder->checkActionPermission('write')) {
1796
                                break;
1797
                            }
1798
                            $uploadFolder = null;
1799
                        } catch (Exception $folderAccessException) {
1800
                            // If the folder is not accessible (no permissions / does not exist) try the next one.
1801
                        }
1802
                    }
1803
                }
1804
            }
1805
        }
1806
1807
        // HOOK: getDefaultUploadFolder
1808
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getDefaultUploadFolder'] ?? [] as $_funcRef) {
1809
            $_params = [
1810
                'uploadFolder' => $uploadFolder,
1811
                'pid' => $pid,
1812
                'table' => $table,
1813
                'field' => $field,
1814
            ];
1815
            $uploadFolder = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1816
        }
1817
1818
        if ($uploadFolder instanceof Folder) {
1819
            return $uploadFolder;
1820
        }
1821
        return false;
1822
    }
1823
1824
    /**
1825
     * Returns a \TYPO3\CMS\Core\Resource\Folder object that could be used for uploading
1826
     * temporary files in user context. The folder _temp_ below the default upload folder
1827
     * of the user is used.
1828
     *
1829
     * @return \TYPO3\CMS\Core\Resource\Folder|null
1830
     * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::getDefaultUploadFolder()
1831
     */
1832
    public function getDefaultUploadTemporaryFolder()
1833
    {
1834
        $defaultTemporaryFolder = null;
1835
        $defaultFolder = $this->getDefaultUploadFolder();
1836
1837
        if ($defaultFolder !== false) {
1838
            $tempFolderName = '_temp_';
1839
            $createFolder = !$defaultFolder->hasFolder($tempFolderName);
1840
            if ($createFolder === true) {
1841
                try {
1842
                    $defaultTemporaryFolder = $defaultFolder->createFolder($tempFolderName);
1843
                } catch (Exception $folderAccessException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1844
                }
1845
            } else {
1846
                $defaultTemporaryFolder = $defaultFolder->getSubfolder($tempFolderName);
1847
            }
1848
        }
1849
1850
        return $defaultTemporaryFolder;
1851
    }
1852
1853
    /**
1854
     * Initializing workspace.
1855
     * Called from within this function, see fetchGroupData()
1856
     *
1857
     * @see fetchGroupData()
1858
     * @internal should only be used from within TYPO3 Core
1859
     */
1860
    public function workspaceInit()
1861
    {
1862
        // Initializing workspace by evaluating and setting the workspace, possibly updating it in the user record!
1863
        $this->setWorkspace($this->user['workspace_id']);
1864
        // Limiting the DB mountpoints if there any selected in the workspace record
1865
        $this->initializeDbMountpointsInWorkspace();
1866
        $allowed_languages = (string)($this->getTSConfig()['options.']['workspaces.']['allowed_languages.'][$this->workspace] ?? '');
1867
        if ($allowed_languages !== '') {
1868
            $this->groupData['allowed_languages'] = StringUtility::uniqueList($allowed_languages);
1869
        }
1870
    }
1871
1872
    /**
1873
     * Limiting the DB mountpoints if there any selected in the workspace record
1874
     */
1875
    protected function initializeDbMountpointsInWorkspace()
1876
    {
1877
        $dbMountpoints = trim($this->workspaceRec['db_mountpoints'] ?? '');
1878
        if ($this->workspace > 0 && $dbMountpoints != '') {
1879
            $filteredDbMountpoints = [];
1880
            // Notice: We cannot call $this->getPagePermsClause(1);
1881
            // as usual because the group-list is not available at this point.
1882
            // But bypassing is fine because all we want here is check if the
1883
            // workspace mounts are inside the current webmounts rootline.
1884
            // The actual permission checking on page level is done elsewhere
1885
            // as usual anyway before the page tree is rendered.
1886
            $readPerms = '1=1';
1887
            // Traverse mount points of the workspace, add them,
1888
            // but make sure they match against the users' DB mounts
1889
1890
            $workspaceWebMounts = GeneralUtility::intExplode(',', $dbMountpoints);
1891
            $webMountsOfUser = GeneralUtility::intExplode(',', $this->groupData['webmounts']);
1892
            $webMountsOfUser = array_combine($webMountsOfUser, $webMountsOfUser);
1893
1894
            $entryPointRootLineUids = [];
1895
            foreach ($webMountsOfUser as $webMountPageId) {
1896
                $rootLine = BackendUtility::BEgetRootLine($webMountPageId, '', true);
1897
                $entryPointRootLineUids[$webMountPageId] = array_map('intval', array_column($rootLine, 'uid'));
1898
            }
1899
            foreach ($entryPointRootLineUids as $webMountOfUser => $uidsOfRootLine) {
1900
                // Remove the DB mounts of the user if the DB mount is not in the list of
1901
                // workspace mounts
1902
                foreach ($workspaceWebMounts as $webmountOfWorkspace) {
1903
                    // This workspace DB mount is somewhere in the rootline of the users' web mount,
1904
                    // so this is "OK" to be included
1905
                    if (in_array($webmountOfWorkspace, $uidsOfRootLine, true)) {
1906
                        continue;
1907
                    }
1908
                    // Remove the user's DB Mount (possible via array_combine, see above)
1909
                    unset($webMountsOfUser[$webMountOfUser]);
1910
                }
1911
            }
1912
            $dbMountpoints = array_merge($workspaceWebMounts, $webMountsOfUser);
1913
            $dbMountpoints = array_unique($dbMountpoints);
1914
            foreach ($dbMountpoints as $mpId) {
1915
                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...
1916
                    $filteredDbMountpoints[] = $mpId;
1917
                }
1918
            }
1919
            // Re-insert webmounts
1920
            $this->groupData['webmounts'] = implode(',', $filteredDbMountpoints);
1921
        }
1922
    }
1923
1924
    /**
1925
     * Checking if a workspace is allowed for backend user
1926
     *
1927
     * @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)
1928
     * @param string $fields List of fields to select. Default fields are all
1929
     * @return array Output will also show how access was granted. Admin users will have a true output regardless of input.
1930
     * @internal should only be used from within TYPO3 Core
1931
     */
1932
    public function checkWorkspace($wsRec, $fields = '*')
1933
    {
1934
        $retVal = false;
1935
        // If not array, look up workspace record:
1936
        if (!is_array($wsRec)) {
1937
            switch ((string)$wsRec) {
1938
                case '0':
1939
                    $wsRec = ['uid' => $wsRec];
1940
                    break;
1941
                default:
1942
                    if (ExtensionManagementUtility::isLoaded('workspaces')) {
1943
                        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
1944
                        $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
1945
                        $wsRec = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields))
1946
                            ->from('sys_workspace')
1947
                            ->where($queryBuilder->expr()->eq(
1948
                                'uid',
1949
                                $queryBuilder->createNamedParameter($wsRec, \PDO::PARAM_INT)
1950
                            ))
1951
                            ->orderBy('title')
1952
                            ->setMaxResults(1)
1953
                            ->execute()
1954
                            ->fetch(\PDO::FETCH_ASSOC);
1955
                    }
1956
            }
1957
        }
1958
        // If wsRec is set to an array, evaluate it:
1959
        if (is_array($wsRec)) {
1960
            if ($this->isAdmin()) {
1961
                return array_merge($wsRec, ['_ACCESS' => 'admin']);
1962
            }
1963
            switch ((string)$wsRec['uid']) {
1964
                    case '0':
1965
                        $retVal = (($this->groupData['workspace_perms'] ?? 0) & 1)
1966
                            ? array_merge($wsRec, ['_ACCESS' => 'online'])
1967
                            : false;
1968
                        break;
1969
                    default:
1970
                        // Checking if the guy is admin:
1971
                        if (GeneralUtility::inList($wsRec['adminusers'], 'be_users_' . $this->user['uid'])) {
1972
                            return array_merge($wsRec, ['_ACCESS' => 'owner']);
1973
                        }
1974
                        // Checking if he is owner through a user group of his:
1975
                        foreach ($this->userGroupsUID as $groupUid) {
1976
                            if (GeneralUtility::inList($wsRec['adminusers'], 'be_groups_' . $groupUid)) {
1977
                                return array_merge($wsRec, ['_ACCESS' => 'owner']);
1978
                            }
1979
                        }
1980
                        // Checking if he is member as user:
1981
                        if (GeneralUtility::inList($wsRec['members'], 'be_users_' . $this->user['uid'])) {
1982
                            return array_merge($wsRec, ['_ACCESS' => 'member']);
1983
                        }
1984
                        // Checking if he is member through a user group of his:
1985
                        foreach ($this->userGroupsUID as $groupUid) {
1986
                            if (GeneralUtility::inList($wsRec['members'], 'be_groups_' . $groupUid)) {
1987
                                return array_merge($wsRec, ['_ACCESS' => 'member']);
1988
                            }
1989
                        }
1990
                }
1991
        }
1992
        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...
1993
    }
1994
1995
    /**
1996
     * Uses checkWorkspace() to check if current workspace is available for user.
1997
     * This function caches the result and so can be called many times with no performance loss.
1998
     *
1999
     * @return array See checkWorkspace()
2000
     * @see checkWorkspace()
2001
     * @internal should only be used from within TYPO3 Core
2002
     */
2003
    public function checkWorkspaceCurrent()
2004
    {
2005
        if (!isset($this->checkWorkspaceCurrent_cache)) {
2006
            $this->checkWorkspaceCurrent_cache = $this->checkWorkspace($this->workspace);
2007
        }
2008
        return $this->checkWorkspaceCurrent_cache;
2009
    }
2010
2011
    /**
2012
     * Setting workspace ID
2013
     *
2014
     * @param int $workspaceId ID of workspace to set for backend user. If not valid the default workspace for BE user is found and set.
2015
     * @internal should only be used from within TYPO3 Core
2016
     */
2017
    public function setWorkspace($workspaceId)
2018
    {
2019
        // Check workspace validity and if not found, revert to default workspace.
2020
        if (!$this->setTemporaryWorkspace($workspaceId)) {
2021
            $this->setDefaultWorkspace();
2022
        }
2023
        // Unset access cache:
2024
        $this->checkWorkspaceCurrent_cache = null;
2025
        // If ID is different from the stored one, change it:
2026
        if ((int)$this->workspace !== (int)$this->user['workspace_id']) {
2027
            $this->user['workspace_id'] = $this->workspace;
2028
            GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users')->update(
2029
                'be_users',
2030
                ['workspace_id' => $this->user['workspace_id']],
2031
                ['uid' => (int)$this->user['uid']]
2032
            );
2033
            $this->writelog(SystemLogType::EXTENSION, SystemLogGenericAction::UNDEFINED, SystemLogErrorClassification::MESSAGE, 0, 'User changed workspace to "' . $this->workspace . '"', []);
2034
        }
2035
    }
2036
2037
    /**
2038
     * Sets a temporary workspace in the context of the current backend user.
2039
     *
2040
     * @param int $workspaceId
2041
     * @return bool
2042
     * @internal should only be used from within TYPO3 Core
2043
     */
2044
    public function setTemporaryWorkspace($workspaceId)
2045
    {
2046
        $result = false;
2047
        $workspaceRecord = $this->checkWorkspace($workspaceId);
2048
2049
        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...
2050
            $this->workspaceRec = $workspaceRecord;
2051
            $this->workspace = (int)$workspaceId;
2052
            $result = true;
2053
        }
2054
2055
        return $result;
2056
    }
2057
2058
    /**
2059
     * Sets the default workspace in the context of the current backend user.
2060
     * @internal should only be used from within TYPO3 Core
2061
     */
2062
    public function setDefaultWorkspace()
2063
    {
2064
        $this->workspace = (int)$this->getDefaultWorkspace();
2065
        $this->workspaceRec = $this->checkWorkspace($this->workspace);
2066
    }
2067
2068
    /**
2069
     * Return default workspace ID for user,
2070
     * if EXT:workspaces is not installed the user will be pushed to the
2071
     * Live workspace, if he has access to. If no workspace is available for the user, the workspace ID is set to "-99"
2072
     *
2073
     * @return int Default workspace id.
2074
     * @internal should only be used from within TYPO3 Core
2075
     */
2076
    public function getDefaultWorkspace()
2077
    {
2078
        if (!ExtensionManagementUtility::isLoaded('workspaces')) {
2079
            return 0;
2080
        }
2081
        // Online is default
2082
        if ($this->checkWorkspace(0)) {
2083
            return 0;
2084
        }
2085
        // Otherwise -99 is the fallback
2086
        $defaultWorkspace = -99;
2087
        // Traverse all workspaces
2088
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace');
2089
        $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
2090
        $result = $queryBuilder->select('*')
2091
            ->from('sys_workspace')
2092
            ->orderBy('title')
2093
            ->execute();
2094
        while ($workspaceRecord = $result->fetch()) {
2095
            if ($this->checkWorkspace($workspaceRecord)) {
2096
                $defaultWorkspace = (int)$workspaceRecord['uid'];
2097
                break;
2098
            }
2099
        }
2100
        return $defaultWorkspace;
2101
    }
2102
2103
    /**
2104
     * Writes an entry in the logfile/table
2105
     * Documentation in "TYPO3 Core API"
2106
     *
2107
     * @param int $type Denotes which module that has submitted the entry. See "TYPO3 Core API". Use "4" for extensions.
2108
     * @param int $action Denotes which specific operation that wrote the entry. Use "0" when no sub-categorizing applies
2109
     * @param int $error Flag. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin)
2110
     * @param int $details_nr The message number. Specific for each $type and $action. This will make it possible to translate errormessages to other languages
2111
     * @param string $details Default text that follows the message (in english!). Possibly translated by identification through type/action/details_nr
2112
     * @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
2113
     * @param string $tablename Table name. Special field used by tce_main.php.
2114
     * @param int|string $recuid Record UID. Special field used by tce_main.php.
2115
     * @param int|string $recpid Record PID. Special field used by tce_main.php. OBSOLETE
2116
     * @param int $event_pid The page_uid (pid) where the event occurred. Used to select log-content for specific pages.
2117
     * @param string $NEWid Special field used by tce_main.php. NEWid string of newly created records.
2118
     * @param int $userId Alternative Backend User ID (used for logging login actions where this is not yet known).
2119
     * @return int Log entry ID.
2120
     */
2121
    public function writelog($type, $action, $error, $details_nr, $details, $data, $tablename = '', $recuid = '', $recpid = '', $event_pid = -1, $NEWid = '', $userId = 0)
2122
    {
2123
        if (!$userId && !empty($this->user['uid'])) {
2124
            $userId = $this->user['uid'];
2125
        }
2126
2127
        if ($backuserid = $this->getOriginalUserIdWhenInSwitchUserMode()) {
2128
            if (empty($data)) {
2129
                $data = [];
2130
            }
2131
            $data['originalUser'] = $backuserid;
2132
        }
2133
2134
        $fields = [
2135
            'userid' => (int)$userId,
2136
            'type' => (int)$type,
2137
            'action' => (int)$action,
2138
            'error' => (int)$error,
2139
            'details_nr' => (int)$details_nr,
2140
            'details' => $details,
2141
            'log_data' => serialize($data),
2142
            'tablename' => $tablename,
2143
            'recuid' => (int)$recuid,
2144
            'IP' => (string)GeneralUtility::getIndpEnv('REMOTE_ADDR'),
2145
            'tstamp' => $GLOBALS['EXEC_TIME'] ?? time(),
2146
            'event_pid' => (int)$event_pid,
2147
            'NEWid' => $NEWid,
2148
            'workspace' => $this->workspace
2149
        ];
2150
2151
        $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_log');
2152
        $connection->insert(
2153
            'sys_log',
2154
            $fields,
2155
            [
2156
                \PDO::PARAM_INT,
2157
                \PDO::PARAM_INT,
2158
                \PDO::PARAM_INT,
2159
                \PDO::PARAM_INT,
2160
                \PDO::PARAM_INT,
2161
                \PDO::PARAM_STR,
2162
                \PDO::PARAM_STR,
2163
                \PDO::PARAM_STR,
2164
                \PDO::PARAM_INT,
2165
                \PDO::PARAM_STR,
2166
                \PDO::PARAM_INT,
2167
                \PDO::PARAM_INT,
2168
                \PDO::PARAM_STR,
2169
                \PDO::PARAM_STR,
2170
            ]
2171
        );
2172
2173
        return (int)$connection->lastInsertId('sys_log');
2174
    }
2175
2176
    /**
2177
     * Getter for the cookie name
2178
     *
2179
     * @static
2180
     * @return string returns the configured cookie name
2181
     */
2182
    public static function getCookieName()
2183
    {
2184
        $configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']);
2185
        if (empty($configuredCookieName)) {
2186
            $configuredCookieName = 'be_typo_user';
2187
        }
2188
        return $configuredCookieName;
2189
    }
2190
2191
    /**
2192
     * Check if user is logged in and if so, call ->fetchGroupData() to load group information and
2193
     * access lists of all kind, further check IP, set the ->uc array.
2194
     * If no user is logged in the default behaviour is to exit with an error message.
2195
     * This function is called right after ->start() in fx. the TYPO3 Bootstrap.
2196
     *
2197
     * @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.
2198
     * @throws \RuntimeException
2199
     * @todo deprecate
2200
     */
2201
    public function backendCheckLogin($proceedIfNoUserIsLoggedIn = false)
2202
    {
2203
        if (empty($this->user['uid'])) {
2204
            if ($proceedIfNoUserIsLoggedIn === false) {
2205
                $url = GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir;
2206
                throw new ImmediateResponseException(new RedirectResponse($url, 303), 1607271747);
2207
            }
2208
        } else {
2209
            if ($this->isUserAllowedToLogin()) {
2210
                $this->initializeBackendLogin();
2211
            } else {
2212
                throw new \RuntimeException('Login Error: TYPO3 is in maintenance mode at the moment. Only administrators are allowed access.', 1294585860);
2213
            }
2214
        }
2215
    }
2216
2217
    /**
2218
     * @internal
2219
     */
2220
    public function initializeBackendLogin(): void
2221
    {
2222
        // The groups are fetched and ready for permission checking in this initialization.
2223
        // Tables.php must be read before this because stuff like the modules has impact in this
2224
        $this->fetchGroupData();
2225
        // Setting the UC array. It's needed with fetchGroupData first, due to default/overriding of values.
2226
        $this->backendSetUC();
2227
        if ($this->loginSessionStarted) {
2228
            // Also, if there is a recovery link set, unset it now
2229
            // this will be moved into its own Event at a later stage.
2230
            // If a token was set previously, this is now unset, as it was now possible to log-in
2231
            if ($this->user['password_reset_token'] ?? '') {
2232
                GeneralUtility::makeInstance(ConnectionPool::class)
2233
                    ->getConnectionForTable($this->user_table)
2234
                    ->update($this->user_table, ['password_reset_token' => ''], ['uid' => $this->user['uid']]);
2235
            }
2236
            // Process hooks
2237
            $hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['backendUserLogin'];
2238
            foreach ($hooks ?? [] as $_funcRef) {
2239
                $_params = ['user' => $this->user];
2240
                GeneralUtility::callUserFunction($_funcRef, $_params, $this);
2241
            }
2242
        }
2243
    }
2244
2245
    /**
2246
     * Initialize the internal ->uc array for the backend user
2247
     * Will make the overrides if necessary, and write the UC back to the be_users record if changes has happened
2248
     *
2249
     * @internal
2250
     */
2251
    public function backendSetUC()
2252
    {
2253
        // UC - user configuration is a serialized array inside the user object
2254
        // If there is a saved uc we implement that instead of the default one.
2255
        $this->unpack_uc();
2256
        // Setting defaults if uc is empty
2257
        $updated = false;
2258
        $originalUc = [];
2259
        if (is_array($this->uc) && isset($this->uc['ucSetByInstallTool'])) {
2260
            $originalUc = $this->uc;
2261
            unset($originalUc['ucSetByInstallTool'], $this->uc);
2262
        }
2263
        if (!is_array($this->uc)) {
2264
            $this->uc = array_merge(
2265
                $this->uc_default,
2266
                (array)$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUC'],
2267
                GeneralUtility::removeDotsFromTS((array)($this->getTSConfig()['setup.']['default.'] ?? [])),
2268
                $originalUc
2269
            );
2270
            $this->overrideUC();
2271
            $updated = true;
2272
        }
2273
        // If TSconfig is updated, update the defaultUC.
2274
        if ($this->userTSUpdated) {
2275
            $this->overrideUC();
2276
            $updated = true;
2277
        }
2278
        // Setting default lang from be_user record.
2279
        if (!isset($this->uc['lang'])) {
2280
            $this->uc['lang'] = $this->user['lang'];
2281
            $updated = true;
2282
        }
2283
        // Setting the time of the first login:
2284
        if (!isset($this->uc['firstLoginTimeStamp'])) {
2285
            $this->uc['firstLoginTimeStamp'] = $GLOBALS['EXEC_TIME'];
2286
            $updated = true;
2287
        }
2288
        // Saving if updated.
2289
        if ($updated) {
2290
            $this->writeUC();
2291
        }
2292
    }
2293
2294
    /**
2295
     * Override: Call this function every time the uc is updated.
2296
     * That is 1) by reverting to default values, 2) in the setup-module, 3) userTS changes (userauthgroup)
2297
     *
2298
     * @internal
2299
     */
2300
    public function overrideUC()
2301
    {
2302
        $this->uc = array_merge((array)$this->uc, (array)($this->getTSConfig()['setup.']['override.'] ?? []));
2303
    }
2304
2305
    /**
2306
     * Clears the user[uc] and ->uc to blank strings. Then calls ->backendSetUC() to fill it again with reset contents
2307
     *
2308
     * @internal
2309
     */
2310
    public function resetUC()
2311
    {
2312
        $this->user['uc'] = '';
2313
        $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...
2314
        $this->backendSetUC();
2315
    }
2316
2317
    /**
2318
     * Determines whether a backend user is allowed to access the backend.
2319
     *
2320
     * The conditions are:
2321
     * + backend user is a regular user and adminOnly is not defined
2322
     * + backend user is an admin user
2323
     * + backend user is used in CLI context and adminOnly is explicitly set to "2" (see CommandLineUserAuthentication)
2324
     * + backend user is being controlled by an admin user
2325
     *
2326
     * @return bool Whether a backend user is allowed to access the backend
2327
     * @internal
2328
     */
2329
    public function isUserAllowedToLogin()
2330
    {
2331
        $isUserAllowedToLogin = false;
2332
        $adminOnlyMode = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'];
2333
        // Backend user is allowed if adminOnly is not set or user is an admin:
2334
        if (!$adminOnlyMode || $this->isAdmin()) {
2335
            $isUserAllowedToLogin = true;
2336
        } elseif ($backUserId = $this->getOriginalUserIdWhenInSwitchUserMode()) {
2337
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
2338
            $isUserAllowedToLogin = (bool)$queryBuilder->count('uid')
2339
                ->from('be_users')
2340
                ->where(
2341
                    $queryBuilder->expr()->eq(
2342
                        'uid',
2343
                        $queryBuilder->createNamedParameter($backUserId, \PDO::PARAM_INT)
2344
                    ),
2345
                    $queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT))
2346
                )
2347
                ->execute()
2348
                ->fetchColumn(0);
2349
        }
2350
        return $isUserAllowedToLogin;
2351
    }
2352
2353
    /**
2354
     * Logs out the current user and clears the form protection tokens.
2355
     */
2356
    public function logoff()
2357
    {
2358
        if (isset($GLOBALS['BE_USER'])
2359
            && $GLOBALS['BE_USER'] instanceof self
2360
            && isset($GLOBALS['BE_USER']->user['uid'])
2361
        ) {
2362
            FormProtectionFactory::get()->clean();
2363
            // Release the locked records
2364
            $this->releaseLockedRecords((int)$GLOBALS['BE_USER']->user['uid']);
2365
2366
            if ($this->isSystemMaintainer()) {
2367
                // If user is system maintainer, destroy its possibly valid install tool session.
2368
                $session = new SessionService();
2369
                $session->destroySession();
2370
            }
2371
        }
2372
        parent::logoff();
2373
    }
2374
2375
    /**
2376
     * Remove any "locked records" added for editing for the given user (= current backend user)
2377
     * @param int $userId
2378
     */
2379
    protected function releaseLockedRecords(int $userId)
2380
    {
2381
        if ($userId > 0) {
2382
            GeneralUtility::makeInstance(ConnectionPool::class)
2383
                ->getConnectionForTable('sys_lockedrecords')
2384
                ->delete(
2385
                    'sys_lockedrecords',
2386
                    ['userid' => $userId]
2387
                );
2388
        }
2389
    }
2390
2391
    /**
2392
     * Returns the uid of the backend user to return to.
2393
     * This is set when the current session is a "switch-user" session.
2394
     *
2395
     * @return int|null The user id
2396
     * @internal should only be used from within TYPO3 Core
2397
     */
2398
    public function getOriginalUserIdWhenInSwitchUserMode(): ?int
2399
    {
2400
        $originalUserId = $this->getSessionData('backuserid');
2401
        return $originalUserId ? (int)$originalUserId : null;
2402
    }
2403
2404
    /**
2405
     * @internal
2406
     */
2407
    protected function evaluateMfaRequirements(): void
2408
    {
2409
        // In case the current session is a "switch-user" session, MFA is not required
2410
        if ($this->getOriginalUserIdWhenInSwitchUserMode() !== null) {
2411
            $this->logger->debug('MFA is skipped in switch user mode', [
2412
                $this->userid_column => $this->user[$this->userid_column],
2413
                $this->username_column => $this->user[$this->username_column],
2414
            ]);
2415
            return;
2416
        }
2417
        parent::evaluateMfaRequirements();
2418
    }
2419
}
2420