Passed
Push — master ( 1b99f4...51c8f2 )
by
unknown
20:27
created

initializeDbMountpointsInWorkspace()   B

Complexity

Conditions 9
Paths 25

Size

Total Lines 46
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

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

1238
            $this->setWebmounts($alternativeWebmountPoints, /** @scrutinizer ignore-type */ $appendAlternativeWebmountPoints);
Loading history...
1239
        }
1240
    }
1241
1242
    /**
1243
     * Returns TRUE or FALSE, depending if an alert popup (a javascript confirmation) should be shown
1244
     * call like $GLOBALS['BE_USER']->jsConfirmation($BITMASK).
1245
     *
1246
     * @param int $bitmask Bitmask, one of \TYPO3\CMS\Core\Type\Bitmask\JsConfirmation
1247
     * @return bool TRUE if the confirmation should be shown
1248
     * @see JsConfirmation
1249
     */
1250
    public function jsConfirmation($bitmask)
1251
    {
1252
        try {
1253
            $alertPopupsSetting = trim((string)($this->getTSConfig()['options.']['alertPopups'] ?? ''));
1254
            $alertPopup = JsConfirmation::cast($alertPopupsSetting === '' ? null : (int)$alertPopupsSetting);
1255
        } catch (InvalidEnumerationValueException $e) {
1256
            $alertPopup = new JsConfirmation();
1257
        }
1258
1259
        return JsConfirmation::cast($bitmask)->matches($alertPopup);
1260
    }
1261
1262
    /**
1263
     * Initializes a lot of stuff like the access-lists, database-mountpoints and filemountpoints
1264
     * This method is called by ->backendCheckLogin() (from extending BackendUserAuthentication)
1265
     * if the backend user login has verified OK.
1266
     * Generally this is required initialization of a backend user.
1267
     *
1268
     * @internal
1269
     * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
1270
     */
1271
    public function fetchGroupData()
1272
    {
1273
        if ($this->user['uid']) {
1274
            // Get lists for the be_user record and set them as default/primary values.
1275
            // Enabled Backend Modules
1276
            $this->dataLists['modList'] = $this->user['userMods'];
1277
            // Add available widgets
1278
            $this->dataLists['available_widgets'] = $this->user['available_widgets'];
1279
            // Add Allowed Languages
1280
            $this->dataLists['allowed_languages'] = $this->user['allowed_languages'];
1281
            // Set user value for workspace permissions.
1282
            $this->dataLists['workspace_perms'] = $this->user['workspace_perms'];
1283
            // Database mountpoints
1284
            $this->dataLists['webmount_list'] = $this->user['db_mountpoints'];
1285
            // File mountpoints
1286
            $this->dataLists['filemount_list'] = $this->user['file_mountpoints'];
1287
            // Fileoperation permissions
1288
            $this->dataLists['file_permissions'] = $this->user['file_permissions'];
1289
1290
            // BE_GROUPS:
1291
            // Get the groups...
1292
            if (!empty($this->user[$this->usergroup_column])) {
1293
                // Fetch groups will add a lot of information to the internal arrays: modules, accesslists, TSconfig etc.
1294
                // Refer to fetchGroups() function.
1295
                $this->fetchGroups($this->user[$this->usergroup_column]);
1296
            }
1297
            // Populating the $this->userGroupsUID -array with the groups in the order in which they were LAST included.!!
1298
            $this->userGroupsUID = array_reverse(array_unique(array_reverse($this->includeGroupArray)));
1299
            // Finally this is the list of group_uid's in the order they are parsed (including subgroups!)
1300
            // and without duplicates (duplicates are presented with their last entrance in the list,
1301
            // which thus reflects the order of the TypoScript in TSconfig)
1302
            $this->groupList = implode(',', $this->userGroupsUID);
1303
            $this->setCachedList($this->groupList);
1304
1305
            $this->prepareUserTsConfig();
1306
1307
            // Processing webmounts
1308
            // Admin's always have the root mounted
1309
            if ($this->isAdmin() && !($this->getTSConfig()['options.']['dontMountAdminMounts'] ?? false)) {
1310
                $this->dataLists['webmount_list'] = '0,' . $this->dataLists['webmount_list'];
1311
            }
1312
            // The lists are cleaned for duplicates
1313
            $this->groupData['webmounts'] = StringUtility::uniqueList($this->dataLists['webmount_list'] ?? '');
1314
            $this->groupData['pagetypes_select'] = StringUtility::uniqueList($this->dataLists['pagetypes_select'] ?? '');
1315
            $this->groupData['tables_select'] = StringUtility::uniqueList(($this->dataLists['tables_modify'] ?? '') . ',' . ($this->dataLists['tables_select'] ?? ''));
1316
            $this->groupData['tables_modify'] = StringUtility::uniqueList($this->dataLists['tables_modify'] ?? '');
1317
            $this->groupData['non_exclude_fields'] = StringUtility::uniqueList($this->dataLists['non_exclude_fields'] ?? '');
1318
            $this->groupData['explicit_allowdeny'] = StringUtility::uniqueList($this->dataLists['explicit_allowdeny'] ?? '');
1319
            $this->groupData['allowed_languages'] = StringUtility::uniqueList($this->dataLists['allowed_languages'] ?? '');
1320
            $this->groupData['custom_options'] = StringUtility::uniqueList($this->dataLists['custom_options'] ?? '');
1321
            $this->groupData['modules'] = StringUtility::uniqueList($this->dataLists['modList'] ?? '');
1322
            $this->groupData['available_widgets'] = StringUtility::uniqueList($this->dataLists['available_widgets'] ?? '');
1323
            $this->groupData['file_permissions'] = StringUtility::uniqueList($this->dataLists['file_permissions'] ?? '');
1324
            $this->groupData['workspace_perms'] = $this->dataLists['workspace_perms'];
1325
1326
            if (!empty(trim($this->groupData['webmounts']))) {
1327
                // Checking read access to web mounts if there are mounts points (not empty string, false or 0)
1328
                $webmounts = explode(',', $this->groupData['webmounts']);
1329
                // Selecting all web mounts with permission clause for reading
1330
                $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1331
                $queryBuilder->getRestrictions()
1332
                    ->removeAll()
1333
                    ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1334
1335
                $MProws = $queryBuilder->select('uid')
1336
                    ->from('pages')
1337
                    // @todo DOCTRINE: check how to make getPagePermsClause() portable
1338
                    ->where(
1339
                        $this->getPagePermsClause(Permission::PAGE_SHOW),
1340
                        $queryBuilder->expr()->in(
1341
                            'uid',
1342
                            $queryBuilder->createNamedParameter(
1343
                                GeneralUtility::intExplode(',', $this->groupData['webmounts']),
1344
                                Connection::PARAM_INT_ARRAY
1345
                            )
1346
                        )
1347
                    )
1348
                    ->execute()
1349
                    ->fetchAll();
1350
                $MProws = array_column(($MProws ?: []), 'uid', 'uid');
1351
                foreach ($webmounts as $idx => $mountPointUid) {
1352
                    // If the mount ID is NOT found among selected pages, unset it:
1353
                    if ($mountPointUid > 0 && !isset($MProws[$mountPointUid])) {
1354
                        unset($webmounts[$idx]);
1355
                    }
1356
                }
1357
                // Implode mounts in the end.
1358
                $this->groupData['webmounts'] = implode(',', $webmounts);
1359
            }
1360
            // Setting up workspace situation (after webmounts are processed!):
1361
            $this->workspaceInit();
1362
        }
1363
    }
1364
1365
    /**
1366
     * This method parses the UserTSconfig from the current user and all their groups.
1367
     * If the contents are the same, parsing is skipped. No matching is applied here currently.
1368
     */
1369
    protected function prepareUserTsConfig(): void
1370
    {
1371
        $collectedUserTSconfig = [
1372
            'default' => $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig']
1373
        ];
1374
        // Default TSconfig for admin-users
1375
        if ($this->isAdmin()) {
1376
            $collectedUserTSconfig[] = 'admPanel.enable.all = 1';
1377
        }
1378
        // Setting defaults for sys_note author / email
1379
        $collectedUserTSconfig[] = '
1380
TCAdefaults.sys_note.author = ' . $this->user['realName'] . '
1381
TCAdefaults.sys_note.email = ' . $this->user['email'];
1382
1383
        // Loop through all groups and add their 'TSconfig' fields
1384
        foreach ($this->includeGroupArray as $groupId) {
1385
            $collectedUserTSconfig['group_' . $groupId] = $this->userGroups[$groupId]['TSconfig'] ?? '';
1386
        }
1387
1388
        $collectedUserTSconfig[] = $this->user['TSconfig'];
1389
        // Check external files
1390
        $collectedUserTSconfig = TypoScriptParser::checkIncludeLines_array($collectedUserTSconfig);
1391
        // Imploding with "[global]" will make sure that non-ended confinements with braces are ignored.
1392
        $userTS_text = implode("\n[GLOBAL]\n", $collectedUserTSconfig);
1393
        // Parsing the user TSconfig (or getting from cache)
1394
        $hash = md5('userTS:' . $userTS_text);
1395
        $cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash');
1396
        if (!($this->userTS = $cache->get($hash))) {
1397
            $parseObj = GeneralUtility::makeInstance(TypoScriptParser::class);
1398
            $conditionMatcher = GeneralUtility::makeInstance(ConditionMatcher::class);
1399
            $parseObj->parse($userTS_text, $conditionMatcher);
1400
            $this->userTS = $parseObj->setup;
1401
            $cache->set($hash, $this->userTS, ['UserTSconfig'], 0);
1402
            // Ensure to update UC later
1403
            $this->userTSUpdated = true;
1404
        }
1405
    }
1406
1407
    /**
1408
     * Fetches the group records, subgroups and fills internal arrays.
1409
     * Function is called recursively to fetch subgroups
1410
     *
1411
     * @param string $grList Commalist of be_groups uid numbers
1412
     * @param string $idList List of already processed be_groups-uids so the function will not fall into an eternal recursion.
1413
     * @internal
1414
     */
1415
    public function fetchGroups($grList, $idList = '')
1416
    {
1417
        // Fetching records of the groups in $grList:
1418
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->usergroup_table);
1419
        $expressionBuilder = $queryBuilder->expr();
1420
        $constraints = $expressionBuilder->andX(
1421
            $expressionBuilder->eq(
1422
                'pid',
1423
                $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
1424
            ),
1425
            $expressionBuilder->in(
1426
                'uid',
1427
                $queryBuilder->createNamedParameter(
1428
                    GeneralUtility::intExplode(',', $grList),
1429
                    Connection::PARAM_INT_ARRAY
1430
                )
1431
            )
1432
        );
1433
        // Hook for manipulation of the WHERE sql sentence which controls which BE-groups are included
1434
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroupQuery'] ?? [] as $className) {
1435
            $hookObj = GeneralUtility::makeInstance($className);
1436
            if (method_exists($hookObj, 'fetchGroupQuery_processQuery')) {
1437
                $constraints = $hookObj->fetchGroupQuery_processQuery($this, $grList, $idList, (string)$constraints);
1438
            }
1439
        }
1440
        $res = $queryBuilder->select('*')
1441
            ->from($this->usergroup_table)
1442
            ->where($constraints)
1443
            ->execute();
1444
        // The userGroups array is filled
1445
        while ($row = $res->fetch(\PDO::FETCH_ASSOC)) {
1446
            $this->userGroups[$row['uid']] = $row;
1447
        }
1448
1449
        $mountOptions = new BackendGroupMountOption((int)$this->user['options']);
1450
        // Traversing records in the correct order
1451
        foreach (explode(',', $grList) as $uid) {
1452
            // Get row:
1453
            $row = $this->userGroups[$uid];
1454
            // Must be an array and $uid should not be in the idList, because then it is somewhere previously in the grouplist
1455
            if (is_array($row) && !GeneralUtility::inList($idList, $uid)) {
1456
                // Include sub groups
1457
                if (trim($row['subgroup'])) {
1458
                    // Make integer list
1459
                    $theList = implode(',', GeneralUtility::intExplode(',', $row['subgroup']));
1460
                    // Call recursively, pass along list of already processed groups so they are not recursed again.
1461
                    $this->fetchGroups($theList, $idList . ',' . $uid);
1462
                }
1463
                // Add the group uid, current list to the internal arrays.
1464
                $this->includeGroupArray[] = $uid;
1465
                // Mount group database-mounts
1466
                if ($mountOptions->shouldUserIncludePageMountsFromAssociatedGroups()) {
1467
                    $this->dataLists['webmount_list'] .= ',' . $row['db_mountpoints'];
1468
                }
1469
                // Mount group file-mounts
1470
                if ($mountOptions->shouldUserIncludeFileMountsFromAssociatedGroups()) {
1471
                    $this->dataLists['filemount_list'] .= ',' . $row['file_mountpoints'];
1472
                }
1473
                // The lists are made: groupMods, tables_select, tables_modify, pagetypes_select, non_exclude_fields, explicit_allowdeny, allowed_languages, custom_options
1474
                $this->dataLists['modList'] .= ',' . $row['groupMods'];
1475
                $this->dataLists['available_widgets'] .= ',' . $row['availableWidgets'];
1476
                $this->dataLists['tables_select'] .= ',' . $row['tables_select'];
1477
                $this->dataLists['tables_modify'] .= ',' . $row['tables_modify'];
1478
                $this->dataLists['pagetypes_select'] .= ',' . $row['pagetypes_select'];
1479
                $this->dataLists['non_exclude_fields'] .= ',' . $row['non_exclude_fields'];
1480
                $this->dataLists['explicit_allowdeny'] .= ',' . $row['explicit_allowdeny'];
1481
                $this->dataLists['allowed_languages'] .= ',' . $row['allowed_languages'];
1482
                $this->dataLists['custom_options'] .= ',' . $row['custom_options'];
1483
                $this->dataLists['file_permissions'] .= ',' . $row['file_permissions'];
1484
                // Setting workspace permissions:
1485
                $this->dataLists['workspace_perms'] |= $row['workspace_perms'];
1486
                // If this function is processing the users OWN group-list (not subgroups) AND
1487
                // if the ->firstMainGroup is not set, then the ->firstMainGroup will be set.
1488
                if ($idList === '' && !$this->firstMainGroup) {
1489
                    $this->firstMainGroup = $uid;
0 ignored issues
show
Documentation Bug introduced by
The property $firstMainGroup was declared of type integer, but $uid is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
1490
                }
1491
            }
1492
        }
1493
        // HOOK: fetchGroups_postProcessing
1494
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['fetchGroups_postProcessing'] ?? [] as $_funcRef) {
1495
            $_params = [];
1496
            GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1497
        }
1498
    }
1499
1500
    /**
1501
     * Updates the field be_users.usergroup_cached_list if the groupList of the user
1502
     * has changed/is different from the current list.
1503
     * The field "usergroup_cached_list" contains the list of groups which the user is a member of.
1504
     * After authentication (where these functions are called...) one can depend on this list being
1505
     * a representation of the exact groups/subgroups which the BE_USER has membership with.
1506
     *
1507
     * @param string $cList The newly compiled group-list which must be compared with the current list in the user record and possibly stored if a difference is detected.
1508
     * @internal
1509
     */
1510
    public function setCachedList($cList)
1511
    {
1512
        if ((string)$cList != (string)$this->user['usergroup_cached_list']) {
1513
            GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users')->update(
1514
                'be_users',
1515
                ['usergroup_cached_list' => $cList],
1516
                ['uid' => (int)$this->user['uid']]
1517
            );
1518
        }
1519
    }
1520
1521
    /**
1522
     * Sets up all file storages for a user.
1523
     * Needs to be called AFTER the groups have been loaded.
1524
     */
1525
    protected function initializeFileStorages()
1526
    {
1527
        $this->fileStorages = [];
1528
        /** @var \TYPO3\CMS\Core\Resource\StorageRepository $storageRepository */
1529
        $storageRepository = GeneralUtility::makeInstance(StorageRepository::class);
1530
        // Admin users have all file storages visible, without any filters
1531
        if ($this->isAdmin()) {
1532
            $storageObjects = $storageRepository->findAll();
1533
            foreach ($storageObjects as $storageObject) {
1534
                $this->fileStorages[$storageObject->getUid()] = $storageObject;
1535
            }
1536
        } else {
1537
            // Regular users only have storages that are defined in their filemounts
1538
            // Permissions and file mounts for the storage are added in StoragePermissionAspect
1539
            foreach ($this->getFileMountRecords() as $row) {
1540
                if (!array_key_exists((int)$row['base'], $this->fileStorages)) {
1541
                    $storageObject = $storageRepository->findByUid($row['base']);
1542
                    if ($storageObject) {
1543
                        $this->fileStorages[$storageObject->getUid()] = $storageObject;
1544
                    }
1545
                }
1546
            }
1547
        }
1548
1549
        // This has to be called always in order to set certain filters
1550
        $this->evaluateUserSpecificFileFilterSettings();
1551
    }
1552
1553
    /**
1554
     * Returns an array of category mount points. The category permissions from BE Groups
1555
     * are also taken into consideration and are merged into User permissions.
1556
     *
1557
     * @return array
1558
     */
1559
    public function getCategoryMountPoints()
1560
    {
1561
        $categoryMountPoints = '';
1562
1563
        // Category mounts of the groups
1564
        if (is_array($this->userGroups)) {
0 ignored issues
show
introduced by
The condition is_array($this->userGroups) is always true.
Loading history...
1565
            foreach ($this->userGroups as $group) {
1566
                if ($group['category_perms']) {
1567
                    $categoryMountPoints .= ',' . $group['category_perms'];
1568
                }
1569
            }
1570
        }
1571
1572
        // Category mounts of the user record
1573
        if ($this->user['category_perms']) {
1574
            $categoryMountPoints .= ',' . $this->user['category_perms'];
1575
        }
1576
1577
        // Make the ids unique
1578
        $categoryMountPoints = GeneralUtility::trimExplode(',', $categoryMountPoints);
1579
        $categoryMountPoints = array_filter($categoryMountPoints); // remove empty value
1580
        $categoryMountPoints = array_unique($categoryMountPoints); // remove unique value
1581
1582
        return $categoryMountPoints;
1583
    }
1584
1585
    /**
1586
     * Returns an array of file mount records, taking workspaces and user home and group home directories into account
1587
     * Needs to be called AFTER the groups have been loaded.
1588
     *
1589
     * @return array
1590
     * @internal
1591
     */
1592
    public function getFileMountRecords()
1593
    {
1594
        $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
1595
        $fileMountRecordCache = $runtimeCache->get('backendUserAuthenticationFileMountRecords') ?: [];
1596
1597
        if (!empty($fileMountRecordCache)) {
1598
            return $fileMountRecordCache;
1599
        }
1600
1601
        $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
1602
1603
        // Processing file mounts (both from the user and the groups)
1604
        $fileMounts = array_unique(GeneralUtility::intExplode(',', $this->dataLists['filemount_list'], true));
1605
1606
        // Limit file mounts if set in workspace record
1607
        if ($this->workspace > 0 && !empty($this->workspaceRec['file_mountpoints'])) {
1608
            $workspaceFileMounts = GeneralUtility::intExplode(',', $this->workspaceRec['file_mountpoints'], true);
1609
            $fileMounts = array_intersect($fileMounts, $workspaceFileMounts);
1610
        }
1611
1612
        if (!empty($fileMounts)) {
1613
            $orderBy = $GLOBALS['TCA']['sys_filemounts']['ctrl']['default_sortby'] ?? 'sorting';
1614
1615
            $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_filemounts');
1616
            $queryBuilder->getRestrictions()
1617
                ->removeAll()
1618
                ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1619
                ->add(GeneralUtility::makeInstance(HiddenRestriction::class))
1620
                ->add(GeneralUtility::makeInstance(RootLevelRestriction::class));
1621
1622
            $queryBuilder->select('*')
1623
                ->from('sys_filemounts')
1624
                ->where(
1625
                    $queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($fileMounts, Connection::PARAM_INT_ARRAY))
1626
                );
1627
1628
            foreach (QueryHelper::parseOrderBy($orderBy) as $fieldAndDirection) {
1629
                $queryBuilder->addOrderBy(...$fieldAndDirection);
1630
            }
1631
1632
            $fileMountRecords = $queryBuilder->execute()->fetchAll(\PDO::FETCH_ASSOC);
1633
            if ($fileMountRecords !== false) {
1634
                foreach ($fileMountRecords as $fileMount) {
1635
                    $fileMountRecordCache[$fileMount['base'] . $fileMount['path']] = $fileMount;
1636
                }
1637
            }
1638
        }
1639
1640
        // Read-only file mounts
1641
        $readOnlyMountPoints = \trim($this->getTSConfig()['options.']['folderTree.']['altElementBrowserMountPoints'] ?? '');
1642
        if ($readOnlyMountPoints) {
1643
            // We cannot use the API here but need to fetch the default storage record directly
1644
            // to not instantiate it (which directly applies mount points) before all mount points are resolved!
1645
            $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_file_storage');
1646
            $defaultStorageRow = $queryBuilder->select('uid')
1647
                ->from('sys_file_storage')
1648
                ->where(
1649
                    $queryBuilder->expr()->eq('is_default', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT))
1650
                )
1651
                ->setMaxResults(1)
1652
                ->execute()
1653
                ->fetch(\PDO::FETCH_ASSOC);
1654
1655
            $readOnlyMountPointArray = GeneralUtility::trimExplode(',', $readOnlyMountPoints);
1656
            foreach ($readOnlyMountPointArray as $readOnlyMountPoint) {
1657
                $readOnlyMountPointConfiguration = GeneralUtility::trimExplode(':', $readOnlyMountPoint);
1658
                if (count($readOnlyMountPointConfiguration) === 2) {
1659
                    // A storage is passed in the configuration
1660
                    $storageUid = (int)$readOnlyMountPointConfiguration[0];
1661
                    $path = $readOnlyMountPointConfiguration[1];
1662
                } else {
1663
                    if (empty($defaultStorageRow)) {
1664
                        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);
1665
                    }
1666
                    // Backwards compatibility: If no storage is passed, we use the default storage
1667
                    $storageUid = $defaultStorageRow['uid'];
1668
                    $path = $readOnlyMountPointConfiguration[0];
1669
                }
1670
                $fileMountRecordCache[$storageUid . $path] = [
1671
                    'base' => $storageUid,
1672
                    'title' => $path,
1673
                    'path' => $path,
1674
                    'read_only' => true
1675
                ];
1676
            }
1677
        }
1678
1679
        // Personal or Group filemounts are not accessible if file mount list is set in workspace record
1680
        if ($this->workspace <= 0 || empty($this->workspaceRec['file_mountpoints'])) {
1681
            // If userHomePath is set, we attempt to mount it
1682
            if ($GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath']) {
1683
                [$userHomeStorageUid, $userHomeFilter] = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath'], 2);
1684
                $userHomeStorageUid = (int)$userHomeStorageUid;
1685
                $userHomeFilter = '/' . ltrim($userHomeFilter, '/');
1686
                if ($userHomeStorageUid > 0) {
1687
                    // Try and mount with [uid]_[username]
1688
                    $path = $userHomeFilter . $this->user['uid'] . '_' . $this->user['username'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1689
                    $fileMountRecordCache[$userHomeStorageUid . $path] = [
1690
                        'base' => $userHomeStorageUid,
1691
                        'title' => $this->user['username'],
1692
                        'path' => $path,
1693
                        'read_only' => false,
1694
                        'user_mount' => true
1695
                    ];
1696
                    // Try and mount with only [uid]
1697
                    $path = $userHomeFilter . $this->user['uid'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir'];
1698
                    $fileMountRecordCache[$userHomeStorageUid . $path] = [
1699
                        'base' => $userHomeStorageUid,
1700
                        'title' => $this->user['username'],
1701
                        'path' => $path,
1702
                        'read_only' => false,
1703
                        'user_mount' => true
1704
                    ];
1705
                }
1706
            }
1707
1708
            // Mount group home-dirs
1709
            $mountOptions = new BackendGroupMountOption((int)$this->user['options']);
1710
            if ($GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'] !== '' && $mountOptions->shouldUserIncludeFileMountsFromAssociatedGroups()) {
1711
                // If groupHomePath is set, we attempt to mount it
1712
                [$groupHomeStorageUid, $groupHomeFilter] = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'], 2);
1713
                $groupHomeStorageUid = (int)$groupHomeStorageUid;
1714
                $groupHomeFilter = '/' . ltrim($groupHomeFilter, '/');
1715
                if ($groupHomeStorageUid > 0) {
1716
                    foreach ($this->userGroups as $groupData) {
1717
                        $path = $groupHomeFilter . $groupData['uid'];
1718
                        $fileMountRecordCache[$groupHomeStorageUid . $path] = [
1719
                            'base' => $groupHomeStorageUid,
1720
                            'title' => $groupData['title'],
1721
                            'path' => $path,
1722
                            'read_only' => false,
1723
                            'user_mount' => true
1724
                        ];
1725
                    }
1726
                }
1727
            }
1728
        }
1729
1730
        $runtimeCache->set('backendUserAuthenticationFileMountRecords', $fileMountRecordCache);
1731
        return $fileMountRecordCache;
1732
    }
1733
1734
    /**
1735
     * Returns an array with the filemounts for the user.
1736
     * Each filemount is represented with an array of a "name", "path" and "type".
1737
     * If no filemounts an empty array is returned.
1738
     *
1739
     * @return \TYPO3\CMS\Core\Resource\ResourceStorage[]
1740
     */
1741
    public function getFileStorages()
1742
    {
1743
        // Initializing file mounts after the groups are fetched
1744
        if ($this->fileStorages === null) {
1745
            $this->initializeFileStorages();
1746
        }
1747
        return $this->fileStorages;
1748
    }
1749
1750
    /**
1751
     * Adds filters based on what the user has set
1752
     * this should be done in this place, and called whenever needed,
1753
     * but only when needed
1754
     */
1755
    public function evaluateUserSpecificFileFilterSettings()
1756
    {
1757
        // Add the option for also displaying the non-hidden files
1758
        if ($this->uc['showHiddenFilesAndFolders']) {
1759
            FileNameFilter::setShowHiddenFilesAndFolders(true);
1760
        }
1761
    }
1762
1763
    /**
1764
     * Returns the information about file permissions.
1765
     * Previously, this was stored in the DB field fileoper_perms now it is file_permissions.
1766
     * Besides it can be handled via userTSconfig
1767
     *
1768
     * permissions.file.default {
1769
     * addFile = 1
1770
     * readFile = 1
1771
     * writeFile = 1
1772
     * copyFile = 1
1773
     * moveFile = 1
1774
     * renameFile = 1
1775
     * deleteFile = 1
1776
     *
1777
     * addFolder = 1
1778
     * readFolder = 1
1779
     * writeFolder = 1
1780
     * copyFolder = 1
1781
     * moveFolder = 1
1782
     * renameFolder = 1
1783
     * deleteFolder = 1
1784
     * recursivedeleteFolder = 1
1785
     * }
1786
     *
1787
     * # overwrite settings for a specific storageObject
1788
     * permissions.file.storage.StorageUid {
1789
     * readFile = 1
1790
     * recursivedeleteFolder = 0
1791
     * }
1792
     *
1793
     * Please note that these permissions only apply, if the storage has the
1794
     * capabilities (browseable, writable), and if the driver allows for writing etc
1795
     *
1796
     * @return array
1797
     */
1798
    public function getFilePermissions()
1799
    {
1800
        if (!isset($this->filePermissions)) {
1801
            $filePermissions = [
1802
                // File permissions
1803
                'addFile' => false,
1804
                'readFile' => false,
1805
                'writeFile' => false,
1806
                'copyFile' => false,
1807
                'moveFile' => false,
1808
                'renameFile' => false,
1809
                'deleteFile' => false,
1810
                // Folder permissions
1811
                'addFolder' => false,
1812
                'readFolder' => false,
1813
                'writeFolder' => false,
1814
                'copyFolder' => false,
1815
                'moveFolder' => false,
1816
                'renameFolder' => false,
1817
                'deleteFolder' => false,
1818
                'recursivedeleteFolder' => false
1819
            ];
1820
            if ($this->isAdmin()) {
1821
                $filePermissions = array_map('is_bool', $filePermissions);
1822
            } else {
1823
                $userGroupRecordPermissions = GeneralUtility::trimExplode(',', $this->groupData['file_permissions'] ?? '', true);
1824
                array_walk(
1825
                    $userGroupRecordPermissions,
1826
                    function ($permission) use (&$filePermissions) {
1827
                        $filePermissions[$permission] = true;
1828
                    }
1829
                );
1830
1831
                // Finally overlay any userTSconfig
1832
                $permissionsTsConfig = $this->getTSConfig()['permissions.']['file.']['default.'] ?? [];
1833
                if (!empty($permissionsTsConfig)) {
1834
                    array_walk(
1835
                        $permissionsTsConfig,
1836
                        function ($value, $permission) use (&$filePermissions) {
1837
                            $filePermissions[$permission] = (bool)$value;
1838
                        }
1839
                    );
1840
                }
1841
            }
1842
            $this->filePermissions = $filePermissions;
1843
        }
1844
        return $this->filePermissions;
1845
    }
1846
1847
    /**
1848
     * Gets the file permissions for a storage
1849
     * by merging any storage-specific permissions for a
1850
     * storage with the default settings.
1851
     * Admin users will always get the default settings.
1852
     *
1853
     * @param \TYPO3\CMS\Core\Resource\ResourceStorage $storageObject
1854
     * @return array
1855
     */
1856
    public function getFilePermissionsForStorage(ResourceStorage $storageObject)
1857
    {
1858
        $finalUserPermissions = $this->getFilePermissions();
1859
        if (!$this->isAdmin()) {
1860
            $storageFilePermissions = $this->getTSConfig()['permissions.']['file.']['storage.'][$storageObject->getUid() . '.'] ?? [];
1861
            if (!empty($storageFilePermissions)) {
1862
                array_walk(
1863
                    $storageFilePermissions,
1864
                    function ($value, $permission) use (&$finalUserPermissions) {
1865
                        $finalUserPermissions[$permission] = (bool)$value;
1866
                    }
1867
                );
1868
            }
1869
        }
1870
        return $finalUserPermissions;
1871
    }
1872
1873
    /**
1874
     * Returns a \TYPO3\CMS\Core\Resource\Folder object that is used for uploading
1875
     * files by default.
1876
     * This is used for RTE and its magic images, as well as uploads
1877
     * in the TCEforms fields.
1878
     *
1879
     * The default upload folder for a user is the defaultFolder on the first
1880
     * filestorage/filemount that the user can access and to which files are allowed to be added
1881
     * however, you can set the users' upload folder like this:
1882
     *
1883
     * options.defaultUploadFolder = 3:myfolder/yourfolder/
1884
     *
1885
     * @param int $pid PageUid
1886
     * @param string $table Table name
1887
     * @param string $field Field name
1888
     * @return \TYPO3\CMS\Core\Resource\Folder|bool The default upload folder for this user
1889
     */
1890
    public function getDefaultUploadFolder($pid = null, $table = null, $field = null)
1891
    {
1892
        $uploadFolder = $this->getTSConfig()['options.']['defaultUploadFolder'] ?? '';
1893
        if ($uploadFolder) {
1894
            try {
1895
                $uploadFolder = GeneralUtility::makeInstance(ResourceFactory::class)->getFolderObjectFromCombinedIdentifier($uploadFolder);
1896
            } catch (Exception\FolderDoesNotExistException $e) {
1897
                $uploadFolder = null;
1898
            }
1899
        }
1900
        if (empty($uploadFolder)) {
1901
            foreach ($this->getFileStorages() as $storage) {
1902
                if ($storage->isDefault() && $storage->isWritable()) {
1903
                    try {
1904
                        $uploadFolder = $storage->getDefaultFolder();
1905
                        if ($uploadFolder->checkActionPermission('write')) {
1906
                            break;
1907
                        }
1908
                        $uploadFolder = null;
1909
                    } catch (Exception $folderAccessException) {
1910
                        // If the folder is not accessible (no permissions / does not exist) we skip this one.
1911
                    }
1912
                    break;
1913
                }
1914
            }
1915
            if (!$uploadFolder instanceof Folder) {
1916
                /** @var ResourceStorage $storage */
1917
                foreach ($this->getFileStorages() as $storage) {
1918
                    if ($storage->isWritable()) {
1919
                        try {
1920
                            $uploadFolder = $storage->getDefaultFolder();
1921
                            if ($uploadFolder->checkActionPermission('write')) {
1922
                                break;
1923
                            }
1924
                            $uploadFolder = null;
1925
                        } catch (Exception $folderAccessException) {
1926
                            // If the folder is not accessible (no permissions / does not exist) try the next one.
1927
                        }
1928
                    }
1929
                }
1930
            }
1931
        }
1932
1933
        // HOOK: getDefaultUploadFolder
1934
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getDefaultUploadFolder'] ?? [] as $_funcRef) {
1935
            $_params = [
1936
                'uploadFolder' => $uploadFolder,
1937
                'pid' => $pid,
1938
                'table' => $table,
1939
                'field' => $field,
1940
            ];
1941
            $uploadFolder = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
1942
        }
1943
1944
        if ($uploadFolder instanceof Folder) {
1945
            return $uploadFolder;
1946
        }
1947
        return false;
1948
    }
1949
1950
    /**
1951
     * Returns a \TYPO3\CMS\Core\Resource\Folder object that could be used for uploading
1952
     * temporary files in user context. The folder _temp_ below the default upload folder
1953
     * of the user is used.
1954
     *
1955
     * @return \TYPO3\CMS\Core\Resource\Folder|null
1956
     * @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::getDefaultUploadFolder()
1957
     */
1958
    public function getDefaultUploadTemporaryFolder()
1959
    {
1960
        $defaultTemporaryFolder = null;
1961
        $defaultFolder = $this->getDefaultUploadFolder();
1962
1963
        if ($defaultFolder !== false) {
1964
            $tempFolderName = '_temp_';
1965
            $createFolder = !$defaultFolder->hasFolder($tempFolderName);
1966
            if ($createFolder === true) {
1967
                try {
1968
                    $defaultTemporaryFolder = $defaultFolder->createFolder($tempFolderName);
1969
                } catch (Exception $folderAccessException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1970
                }
1971
            } else {
1972
                $defaultTemporaryFolder = $defaultFolder->getSubfolder($tempFolderName);
1973
            }
1974
        }
1975
1976
        return $defaultTemporaryFolder;
1977
    }
1978
1979
    /**
1980
     * Initializing workspace.
1981
     * Called from within this function, see fetchGroupData()
1982
     *
1983
     * @see fetchGroupData()
1984
     * @internal should only be used from within TYPO3 Core
1985
     */
1986
    public function workspaceInit()
1987
    {
1988
        // Initializing workspace by evaluating and setting the workspace, possibly updating it in the user record!
1989
        $this->setWorkspace($this->user['workspace_id']);
1990
        // Limiting the DB mountpoints if there any selected in the workspace record
1991
        $this->initializeDbMountpointsInWorkspace();
1992
        $allowed_languages = (string)($this->getTSConfig()['options.']['workspaces.']['allowed_languages.'][$this->workspace] ?? '');
1993
        if ($allowed_languages !== '') {
1994
            $this->groupData['allowed_languages'] = StringUtility::uniqueList($allowed_languages);
1995
        }
1996
    }
1997
1998
    /**
1999
     * Limiting the DB mountpoints if there any selected in the workspace record
2000
     */
2001
    protected function initializeDbMountpointsInWorkspace()
2002
    {
2003
        $dbMountpoints = trim($this->workspaceRec['db_mountpoints'] ?? '');
2004
        if ($this->workspace > 0 && $dbMountpoints != '') {
2005
            $filteredDbMountpoints = [];
2006
            // Notice: We cannot call $this->getPagePermsClause(1);
2007
            // as usual because the group-list is not available at this point.
2008
            // But bypassing is fine because all we want here is check if the
2009
            // workspace mounts are inside the current webmounts rootline.
2010
            // The actual permission checking on page level is done elsewhere
2011
            // as usual anyway before the page tree is rendered.
2012
            $readPerms = '1=1';
2013
            // Traverse mount points of the workspace, add them,
2014
            // but make sure they match against the users' DB mounts
2015
2016
            $workspaceWebMounts = GeneralUtility::intExplode(',', $dbMountpoints);
2017
            $webMountsOfUser = GeneralUtility::intExplode(',', $this->dataLists['webmount_list']);
2018
            $webMountsOfUser = array_combine($webMountsOfUser, $webMountsOfUser);
2019
2020
            $entryPointRootLineUids = [];
2021
            foreach ($webMountsOfUser as $webMountPageId) {
2022
                $rootLine = BackendUtility::BEgetRootLine($webMountPageId, '', true);
2023
                $entryPointRootLineUids[$webMountPageId] = array_map('intval', array_column($rootLine, 'uid'));
2024
            }
2025
            foreach ($entryPointRootLineUids as $webMountOfUser => $uidsOfRootLine) {
2026
                // Remove the DB mounts of the user if the DB mount is not in the list of
2027
                // workspace mounts
2028
                foreach ($workspaceWebMounts as $webmountOfWorkspace) {
2029
                    // This workspace DB mount is somewhere in the rootline of the users' web mount,
2030
                    // so this is "OK" to be included
2031
                    if (in_array($webmountOfWorkspace, $uidsOfRootLine, true)) {
2032
                        continue;
2033
                    }
2034
                    // Remove the user's DB Mount (possible via array_combine, see above)
2035
                    unset($webMountsOfUser[$webMountOfUser]);
2036
                }
2037
            }
2038
            $dbMountpoints = array_merge($workspaceWebMounts, $webMountsOfUser);
0 ignored issues
show
Bug introduced by
It seems like $webMountsOfUser can also be of type false; however, parameter $array2 of array_merge() does only seem to accept array|null, maybe add an additional type check? ( Ignorable by Annotation )

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

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

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2567
        $this->backendSetUC();
2568
    }
2569
2570
    /**
2571
     * Determines whether a backend user is allowed to access the backend.
2572
     *
2573
     * The conditions are:
2574
     * + backend user is a regular user and adminOnly is not defined
2575
     * + backend user is an admin user
2576
     * + backend user is used in CLI context and adminOnly is explicitly set to "2" (see CommandLineUserAuthentication)
2577
     * + backend user is being controlled by an admin user
2578
     *
2579
     * @return bool Whether a backend user is allowed to access the backend
2580
     */
2581
    protected function isUserAllowedToLogin()
2582
    {
2583
        $isUserAllowedToLogin = false;
2584
        $adminOnlyMode = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly'];
2585
        // Backend user is allowed if adminOnly is not set or user is an admin:
2586
        if (!$adminOnlyMode || $this->isAdmin()) {
2587
            $isUserAllowedToLogin = true;
2588
        } elseif ($this->user['ses_backuserid']) {
2589
            $backendUserId = (int)$this->user['ses_backuserid'];
2590
            $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
2591
            $isUserAllowedToLogin = (bool)$queryBuilder->count('uid')
2592
                ->from('be_users')
2593
                ->where(
2594
                    $queryBuilder->expr()->eq(
2595
                        'uid',
2596
                        $queryBuilder->createNamedParameter($backendUserId, \PDO::PARAM_INT)
2597
                    ),
2598
                    $queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT))
2599
                )
2600
                ->execute()
2601
                ->fetchColumn(0);
2602
        }
2603
        return $isUserAllowedToLogin;
2604
    }
2605
2606
    /**
2607
     * Logs out the current user and clears the form protection tokens.
2608
     */
2609
    public function logoff()
2610
    {
2611
        if (isset($GLOBALS['BE_USER'])
2612
            && $GLOBALS['BE_USER'] instanceof self
2613
            && isset($GLOBALS['BE_USER']->user['uid'])
2614
        ) {
2615
            FormProtectionFactory::get()->clean();
2616
            // Release the locked records
2617
            $this->releaseLockedRecords((int)$GLOBALS['BE_USER']->user['uid']);
2618
2619
            if ($this->isSystemMaintainer()) {
2620
                // If user is system maintainer, destroy its possibly valid install tool session.
2621
                $session = new SessionService();
2622
                $session->destroySession();
2623
            }
2624
        }
2625
        parent::logoff();
2626
    }
2627
2628
    /**
2629
     * Remove any "locked records" added for editing for the given user (= current backend user)
2630
     * @param int $userId
2631
     */
2632
    protected function releaseLockedRecords(int $userId)
2633
    {
2634
        if ($userId > 0) {
2635
            GeneralUtility::makeInstance(ConnectionPool::class)
2636
                ->getConnectionForTable('sys_lockedrecords')
2637
                ->delete(
2638
                    'sys_lockedrecords',
2639
                    ['userid' => $userId]
2640
                );
2641
        }
2642
    }
2643
}
2644