getFilePermissionsForStorage()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

2437
            $this->logger->/** @scrutinizer ignore-call */ 
2438
                           debug('MFA is skipped in switch user mode', [

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
2438
                $this->userid_column => $this->user[$this->userid_column],
2439
                $this->username_column => $this->user[$this->username_column],
2440
            ]);
2441
            return;
2442
        }
2443
        parent::evaluateMfaRequirements();
2444
    }
2445
}
2446