1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the TYPO3 CMS project. |
5
|
|
|
* |
6
|
|
|
* It is free software; you can redistribute it and/or modify it under |
7
|
|
|
* the terms of the GNU General Public License, either version 2 |
8
|
|
|
* of the License, or any later version. |
9
|
|
|
* |
10
|
|
|
* For the full copyright and license information, please read the |
11
|
|
|
* LICENSE.txt file that was distributed with this source code. |
12
|
|
|
* |
13
|
|
|
* The TYPO3 project - inspiring people to share! |
14
|
|
|
*/ |
15
|
|
|
|
16
|
|
|
namespace TYPO3\CMS\Core\Authentication; |
17
|
|
|
|
18
|
|
|
use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher; |
19
|
|
|
use TYPO3\CMS\Backend\Utility\BackendUtility; |
20
|
|
|
use TYPO3\CMS\Core\Cache\CacheManager; |
21
|
|
|
use TYPO3\CMS\Core\Core\Environment; |
22
|
|
|
use TYPO3\CMS\Core\Database\Connection; |
23
|
|
|
use TYPO3\CMS\Core\Database\ConnectionPool; |
24
|
|
|
use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder; |
25
|
|
|
use TYPO3\CMS\Core\Database\Query\QueryHelper; |
26
|
|
|
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; |
27
|
|
|
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; |
28
|
|
|
use TYPO3\CMS\Core\Database\Query\Restriction\RootLevelRestriction; |
29
|
|
|
use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction; |
30
|
|
|
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory; |
31
|
|
|
use TYPO3\CMS\Core\Http\ImmediateResponseException; |
32
|
|
|
use TYPO3\CMS\Core\Http\RedirectResponse; |
33
|
|
|
use TYPO3\CMS\Core\Resource\Exception; |
34
|
|
|
use TYPO3\CMS\Core\Resource\Filter\FileNameFilter; |
35
|
|
|
use TYPO3\CMS\Core\Resource\Folder; |
36
|
|
|
use TYPO3\CMS\Core\Resource\ResourceFactory; |
37
|
|
|
use TYPO3\CMS\Core\Resource\ResourceStorage; |
38
|
|
|
use TYPO3\CMS\Core\Resource\StorageRepository; |
39
|
|
|
use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction; |
40
|
|
|
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification; |
41
|
|
|
use TYPO3\CMS\Core\SysLog\Type; |
42
|
|
|
use TYPO3\CMS\Core\SysLog\Type as SystemLogType; |
43
|
|
|
use TYPO3\CMS\Core\Type\Bitmask\BackendGroupMountOption; |
44
|
|
|
use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation; |
45
|
|
|
use TYPO3\CMS\Core\Type\Bitmask\Permission; |
46
|
|
|
use TYPO3\CMS\Core\Type\Exception\InvalidEnumerationValueException; |
47
|
|
|
use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser; |
48
|
|
|
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; |
49
|
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility; |
50
|
|
|
use TYPO3\CMS\Core\Utility\StringUtility; |
51
|
|
|
use TYPO3\CMS\Core\Versioning\VersionState; |
52
|
|
|
use TYPO3\CMS\Install\Service\SessionService; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* TYPO3 backend user authentication |
56
|
|
|
* Contains most of the functions used for checking permissions, authenticating users, |
57
|
|
|
* setting up the user, and API for user from outside. |
58
|
|
|
* This class contains the configuration of the database fields used plus some |
59
|
|
|
* functions for the authentication process of backend users. |
60
|
|
|
*/ |
61
|
|
|
class BackendUserAuthentication extends AbstractUserAuthentication |
62
|
|
|
{ |
63
|
|
|
public const ROLE_SYSTEMMAINTAINER = 'systemMaintainer'; |
64
|
|
|
|
65
|
|
|
/** |
66
|
|
|
* Should be set to the usergroup-column (id-list) in the user-record |
67
|
|
|
* @var string |
68
|
|
|
*/ |
69
|
|
|
public $usergroup_column = 'usergroup'; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* The name of the group-table |
73
|
|
|
* @var string |
74
|
|
|
*/ |
75
|
|
|
public $usergroup_table = 'be_groups'; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* holds lists of eg. tables, fields and other values related to the permission-system. See fetchGroupData |
79
|
|
|
* @var array |
80
|
|
|
* @internal |
81
|
|
|
*/ |
82
|
|
|
public $groupData = [ |
83
|
|
|
'allowed_languages' => '', |
84
|
|
|
'tables_select' => '', |
85
|
|
|
'tables_modify' => '', |
86
|
|
|
'pagetypes_select' => '', |
87
|
|
|
'non_exclude_fields' => '', |
88
|
|
|
'explicit_allowdeny' => '', |
89
|
|
|
'custom_options' => '', |
90
|
|
|
'file_permissions' => '', |
91
|
|
|
]; |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* This array holds the uid's of the groups in the listed order |
95
|
|
|
* @var array |
96
|
|
|
*/ |
97
|
|
|
public $userGroupsUID = []; |
98
|
|
|
|
99
|
|
|
/** |
100
|
|
|
* User workspace. |
101
|
|
|
* -99 is ERROR (none available) |
102
|
|
|
* 0 is online |
103
|
|
|
* >0 is custom workspaces |
104
|
|
|
* @var int |
105
|
|
|
*/ |
106
|
|
|
public $workspace = -99; |
107
|
|
|
|
108
|
|
|
/** |
109
|
|
|
* Custom workspace record if any |
110
|
|
|
* @var array |
111
|
|
|
*/ |
112
|
|
|
public $workspaceRec = []; |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* @var array Parsed user TSconfig |
116
|
|
|
*/ |
117
|
|
|
protected $userTS = []; |
118
|
|
|
|
119
|
|
|
/** |
120
|
|
|
* @var bool True if the user TSconfig was parsed and needs to be cached. |
121
|
|
|
*/ |
122
|
|
|
protected $userTSUpdated = false; |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Contains last error message |
126
|
|
|
* @internal should only be used from within TYPO3 Core |
127
|
|
|
* @var string |
128
|
|
|
*/ |
129
|
|
|
public $errorMsg = ''; |
130
|
|
|
|
131
|
|
|
/** |
132
|
|
|
* Cache for checkWorkspaceCurrent() |
133
|
|
|
* @var array|null |
134
|
|
|
*/ |
135
|
|
|
protected $checkWorkspaceCurrent_cache; |
136
|
|
|
|
137
|
|
|
/** |
138
|
|
|
* @var \TYPO3\CMS\Core\Resource\ResourceStorage[] |
139
|
|
|
*/ |
140
|
|
|
protected $fileStorages; |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* @var array |
144
|
|
|
*/ |
145
|
|
|
protected $filePermissions; |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* Table in database with user data |
149
|
|
|
* @var string |
150
|
|
|
*/ |
151
|
|
|
public $user_table = 'be_users'; |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* Column for login-name |
155
|
|
|
* @var string |
156
|
|
|
*/ |
157
|
|
|
public $username_column = 'username'; |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* Column for password |
161
|
|
|
* @var string |
162
|
|
|
*/ |
163
|
|
|
public $userident_column = 'password'; |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* Column for user-id |
167
|
|
|
* @var string |
168
|
|
|
*/ |
169
|
|
|
public $userid_column = 'uid'; |
170
|
|
|
|
171
|
|
|
/** |
172
|
|
|
* @var string |
173
|
|
|
*/ |
174
|
|
|
public $lastLogin_column = 'lastlogin'; |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* @var array |
178
|
|
|
*/ |
179
|
|
|
public $enablecolumns = [ |
180
|
|
|
'rootLevel' => 1, |
181
|
|
|
'deleted' => 'deleted', |
182
|
|
|
'disabled' => 'disable', |
183
|
|
|
'starttime' => 'starttime', |
184
|
|
|
'endtime' => 'endtime' |
185
|
|
|
]; |
186
|
|
|
|
187
|
|
|
/** |
188
|
|
|
* Form field with login-name |
189
|
|
|
* @var string |
190
|
|
|
*/ |
191
|
|
|
public $formfield_uname = 'username'; |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* Form field with password |
195
|
|
|
* @var string |
196
|
|
|
*/ |
197
|
|
|
public $formfield_uident = 'userident'; |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Form field with status: *'login', 'logout' |
201
|
|
|
* @var string |
202
|
|
|
*/ |
203
|
|
|
public $formfield_status = 'login_status'; |
204
|
|
|
|
205
|
|
|
/** |
206
|
|
|
* Decides if the writelog() function is called at login and logout |
207
|
|
|
* @var bool |
208
|
|
|
*/ |
209
|
|
|
public $writeStdLog = true; |
210
|
|
|
|
211
|
|
|
/** |
212
|
|
|
* If the writelog() functions is called if a login-attempt has be tried without success |
213
|
|
|
* @var bool |
214
|
|
|
*/ |
215
|
|
|
public $writeAttemptLog = true; |
216
|
|
|
|
217
|
|
|
/** |
218
|
|
|
* @var int |
219
|
|
|
* @internal should only be used from within TYPO3 Core |
220
|
|
|
*/ |
221
|
|
|
public $firstMainGroup = 0; |
222
|
|
|
|
223
|
|
|
/** |
224
|
|
|
* User Config |
225
|
|
|
* @var array|string |
226
|
|
|
*/ |
227
|
|
|
public $uc; |
228
|
|
|
|
229
|
|
|
/** |
230
|
|
|
* User Config Default values: |
231
|
|
|
* The array may contain other fields for configuration. |
232
|
|
|
* For this, see "setup" extension and "TSconfig" document (User TSconfig, "setup.[xxx]....") |
233
|
|
|
* Reserved keys for other storage of session data: |
234
|
|
|
* moduleData |
235
|
|
|
* moduleSessionID |
236
|
|
|
* @var array |
237
|
|
|
* @internal should only be used from within TYPO3 Core |
238
|
|
|
*/ |
239
|
|
|
public $uc_default = [ |
240
|
|
|
'interfaceSetup' => '', |
241
|
|
|
// serialized content that is used to store interface pane and menu positions. Set by the logout.php-script |
242
|
|
|
'moduleData' => [], |
243
|
|
|
// user-data for the modules |
244
|
|
|
'emailMeAtLogin' => 0, |
245
|
|
|
'titleLen' => 50, |
246
|
|
|
'edit_RTE' => '1', |
247
|
|
|
'edit_docModuleUpload' => '1', |
248
|
|
|
'resizeTextareas_MaxHeight' => 500, |
249
|
|
|
]; |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* Login type, used for services. |
253
|
|
|
* @var string |
254
|
|
|
*/ |
255
|
|
|
public $loginType = 'BE'; |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* Constructor |
259
|
|
|
*/ |
260
|
|
|
public function __construct() |
261
|
|
|
{ |
262
|
|
|
$this->name = self::getCookieName(); |
263
|
|
|
parent::__construct(); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* Returns TRUE if user is admin |
268
|
|
|
* Basically this function evaluates if the ->user[admin] field has bit 0 set. If so, user is admin. |
269
|
|
|
* |
270
|
|
|
* @return bool |
271
|
|
|
*/ |
272
|
|
|
public function isAdmin() |
273
|
|
|
{ |
274
|
|
|
return is_array($this->user) && ($this->user['admin'] & 1) == 1; |
275
|
|
|
} |
276
|
|
|
|
277
|
|
|
/** |
278
|
|
|
* Returns TRUE if the current user is a member of group $groupId |
279
|
|
|
* $groupId must be set. $this->userGroupsUID must contain groups |
280
|
|
|
* Will return TRUE also if the user is a member of a group through subgroups. |
281
|
|
|
* |
282
|
|
|
* @param int $groupId Group ID to look for in $this->userGroupsUID |
283
|
|
|
* @return bool |
284
|
|
|
* @internal should only be used from within TYPO3 Core, use Context API for quicker access |
285
|
|
|
*/ |
286
|
|
|
public function isMemberOfGroup($groupId) |
287
|
|
|
{ |
288
|
|
|
$groupId = (int)$groupId; |
289
|
|
|
if (!empty($this->userGroupsUID) && $groupId) { |
290
|
|
|
return in_array($groupId, $this->userGroupsUID, true); |
291
|
|
|
} |
292
|
|
|
return false; |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
/** |
296
|
|
|
* Checks if the permissions is granted based on a page-record ($row) and $perms (binary and'ed) |
297
|
|
|
* |
298
|
|
|
* Bits for permissions, see $perms variable: |
299
|
|
|
* |
300
|
|
|
* 1 - Show: See/Copy page and the pagecontent. |
301
|
|
|
* 2 - Edit page: Change/Move the page, eg. change title, startdate, hidden. |
302
|
|
|
* 4 - Delete page: Delete the page and pagecontent. |
303
|
|
|
* 8 - New pages: Create new pages under the page. |
304
|
|
|
* 16 - Edit pagecontent: Change/Add/Delete/Move pagecontent. |
305
|
|
|
* |
306
|
|
|
* @param array $row Is the pagerow for which the permissions is checked |
307
|
|
|
* @param int $perms Is the binary representation of the permission we are going to check. Every bit in this number represents a permission that must be set. See function explanation. |
308
|
|
|
* @return bool |
309
|
|
|
*/ |
310
|
|
|
public function doesUserHaveAccess($row, $perms) |
311
|
|
|
{ |
312
|
|
|
$userPerms = $this->calcPerms($row); |
313
|
|
|
return ($userPerms & $perms) == $perms; |
314
|
|
|
} |
315
|
|
|
|
316
|
|
|
/** |
317
|
|
|
* Checks if the page id or page record ($idOrRow) is found within the webmounts set up for the user. |
318
|
|
|
* This should ALWAYS be checked for any page id a user works with, whether it's about reading, writing or whatever. |
319
|
|
|
* The point is that this will add the security that a user can NEVER touch parts outside his mounted |
320
|
|
|
* pages in the page tree. This is otherwise possible if the raw page permissions allows for it. |
321
|
|
|
* So this security check just makes it easier to make safe user configurations. |
322
|
|
|
* If the user is admin then it returns "1" right away |
323
|
|
|
* Otherwise the function will return the uid of the webmount which was first found in the rootline of the input page $id |
324
|
|
|
* |
325
|
|
|
* @param int|array $idOrRow Page ID or full page record to check |
326
|
|
|
* @param string $readPerms Content of "->getPagePermsClause(1)" (read-permissions). If not set, they will be internally calculated (but if you have the correct value right away you can save that database lookup!) |
327
|
|
|
* @param bool|int $exitOnError If set, then the function will exit with an error message. |
328
|
|
|
* @throws \RuntimeException |
329
|
|
|
* @return int|null The page UID of a page in the rootline that matched a mount point |
330
|
|
|
*/ |
331
|
|
|
public function isInWebMount($idOrRow, $readPerms = '', $exitOnError = 0) |
332
|
|
|
{ |
333
|
|
|
if ($this->isAdmin()) { |
334
|
|
|
return 1; |
335
|
|
|
} |
336
|
|
|
$checkRec = []; |
337
|
|
|
$fetchPageFromDatabase = true; |
338
|
|
|
if (is_array($idOrRow)) { |
339
|
|
|
if (empty($idOrRow['uid'])) { |
340
|
|
|
throw new \RuntimeException('The given page record is invalid. Missing uid.', 1578950324); |
341
|
|
|
} |
342
|
|
|
$checkRec = $idOrRow; |
343
|
|
|
$id = (int)$idOrRow['uid']; |
344
|
|
|
// ensure the required fields are present on the record |
345
|
|
|
if (isset($checkRec['t3ver_oid'], $checkRec[$GLOBALS['TCA']['pages']['ctrl']['languageField']], $checkRec[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']])) { |
346
|
|
|
$fetchPageFromDatabase = false; |
347
|
|
|
} |
348
|
|
|
} else { |
349
|
|
|
$id = (int)$idOrRow; |
350
|
|
|
} |
351
|
|
|
if ($fetchPageFromDatabase) { |
352
|
|
|
// Check if input id is an offline version page in which case we will map id to the online version: |
353
|
|
|
$checkRec = BackendUtility::getRecord( |
354
|
|
|
'pages', |
355
|
|
|
$id, |
356
|
|
|
't3ver_oid,' |
357
|
|
|
. $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'] . ',' |
358
|
|
|
. $GLOBALS['TCA']['pages']['ctrl']['languageField'] |
359
|
|
|
); |
360
|
|
|
} |
361
|
|
|
if ($checkRec['t3ver_oid'] > 0) { |
362
|
|
|
$id = (int)$checkRec['t3ver_oid']; |
363
|
|
|
} |
364
|
|
|
// if current rec is a translation then get uid from l10n_parent instead |
365
|
|
|
// because web mounts point to pages in default language and rootline returns uids of default languages |
366
|
|
|
if ((int)$checkRec[$GLOBALS['TCA']['pages']['ctrl']['languageField']] !== 0 && (int)$checkRec[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']] !== 0) { |
367
|
|
|
$id = (int)$checkRec[$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField']]; |
368
|
|
|
} |
369
|
|
|
if (!$readPerms) { |
370
|
|
|
$readPerms = $this->getPagePermsClause(Permission::PAGE_SHOW); |
371
|
|
|
} |
372
|
|
|
if ($id > 0) { |
373
|
|
|
$wM = $this->returnWebmounts(); |
374
|
|
|
$rL = BackendUtility::BEgetRootLine($id, ' AND ' . $readPerms, true); |
375
|
|
|
foreach ($rL as $v) { |
376
|
|
|
if ($v['uid'] && in_array($v['uid'], $wM)) { |
377
|
|
|
return $v['uid']; |
378
|
|
|
} |
379
|
|
|
} |
380
|
|
|
} |
381
|
|
|
if ($exitOnError) { |
382
|
|
|
throw new \RuntimeException('Access Error: This page is not within your DB-mounts', 1294586445); |
383
|
|
|
} |
384
|
|
|
return null; |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
/** |
388
|
|
|
* Checks access to a backend module with the $MCONF passed as first argument |
389
|
|
|
* |
390
|
|
|
* @param array $conf $MCONF array of a backend module! |
391
|
|
|
* @throws \RuntimeException |
392
|
|
|
* @return bool Will return TRUE if $MCONF['access'] is not set at all, if the BE_USER is admin or if the module is enabled in the be_users/be_groups records of the user (specifically enabled). Will return FALSE if the module name is not even found in $TBE_MODULES |
393
|
|
|
*/ |
394
|
|
|
public function modAccess($conf) |
395
|
|
|
{ |
396
|
|
|
if (!BackendUtility::isModuleSetInTBE_MODULES($conf['name'])) { |
397
|
|
|
throw new \RuntimeException('Fatal Error: This module "' . $conf['name'] . '" is not enabled in TBE_MODULES', 1294586446); |
398
|
|
|
} |
399
|
|
|
// Workspaces check: |
400
|
|
|
if ( |
401
|
|
|
!empty($conf['workspaces']) |
402
|
|
|
&& ExtensionManagementUtility::isLoaded('workspaces') |
403
|
|
|
&& ($this->workspace !== 0 || !GeneralUtility::inList($conf['workspaces'], 'online')) |
404
|
|
|
&& ($this->workspace <= 0 || !GeneralUtility::inList($conf['workspaces'], 'custom')) |
405
|
|
|
) { |
406
|
|
|
throw new \RuntimeException('Workspace Error: This module "' . $conf['name'] . '" is not available under the current workspace', 1294586447); |
407
|
|
|
} |
408
|
|
|
// Returns false if conf[access] is set to system maintainers and the user is system maintainer |
409
|
|
|
if (strpos($conf['access'], self::ROLE_SYSTEMMAINTAINER) !== false && !$this->isSystemMaintainer()) { |
410
|
|
|
throw new \RuntimeException('This module "' . $conf['name'] . '" is only available as system maintainer', 1504804727); |
411
|
|
|
} |
412
|
|
|
// Returns TRUE if conf[access] is not set at all or if the user is admin |
413
|
|
|
if (!$conf['access'] || $this->isAdmin()) { |
414
|
|
|
return true; |
415
|
|
|
} |
416
|
|
|
// If $conf['access'] is set but not with 'admin' then we return TRUE, if the module is found in the modList |
417
|
|
|
$acs = false; |
418
|
|
|
if (strpos($conf['access'], 'admin') === false && $conf['name']) { |
419
|
|
|
$acs = $this->check('modules', $conf['name']); |
420
|
|
|
} |
421
|
|
|
if (!$acs) { |
422
|
|
|
throw new \RuntimeException('Access Error: You don\'t have access to this module.', 1294586448); |
423
|
|
|
} |
424
|
|
|
return $acs; |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
/** |
428
|
|
|
* Checks if the user is in the valid list of allowed system maintainers. if the list is not set, |
429
|
|
|
* then all admins are system maintainers. If the list is empty, no one is system maintainer (good for production |
430
|
|
|
* systems). If the currently logged in user is in "switch user" mode, this method will return false. |
431
|
|
|
* |
432
|
|
|
* @return bool |
433
|
|
|
*/ |
434
|
|
|
public function isSystemMaintainer(): bool |
435
|
|
|
{ |
436
|
|
|
if (!$this->isAdmin()) { |
437
|
|
|
return false; |
438
|
|
|
} |
439
|
|
|
|
440
|
|
|
if ($GLOBALS['BE_USER']->getOriginalUserIdWhenInSwitchUserMode()) { |
441
|
|
|
return false; |
442
|
|
|
} |
443
|
|
|
if (Environment::getContext()->isDevelopment()) { |
444
|
|
|
return true; |
445
|
|
|
} |
446
|
|
|
$systemMaintainers = $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? []; |
447
|
|
|
$systemMaintainers = array_map('intval', $systemMaintainers); |
448
|
|
|
if (!empty($systemMaintainers)) { |
449
|
|
|
return in_array((int)$this->user['uid'], $systemMaintainers, true); |
450
|
|
|
} |
451
|
|
|
// No system maintainers set up yet, so any admin is allowed to access the modules |
452
|
|
|
// but explicitly no system maintainers allowed (empty string in TYPO3_CONF_VARS). |
453
|
|
|
// @todo: this needs to be adjusted once system maintainers can log into the install tool with their credentials |
454
|
|
|
if (isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers']) |
455
|
|
|
&& empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'])) { |
456
|
|
|
return false; |
457
|
|
|
} |
458
|
|
|
return true; |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
/** |
462
|
|
|
* Returns a WHERE-clause for the pages-table where user permissions according to input argument, $perms, is validated. |
463
|
|
|
* $perms is the "mask" used to select. Fx. if $perms is 1 then you'll get all pages that a user can actually see! |
464
|
|
|
* 2^0 = show (1) |
465
|
|
|
* 2^1 = edit (2) |
466
|
|
|
* 2^2 = delete (4) |
467
|
|
|
* 2^3 = new (8) |
468
|
|
|
* If the user is 'admin' " 1=1" is returned (no effect) |
469
|
|
|
* If the user is not set at all (->user is not an array), then " 1=0" is returned (will cause no selection results at all) |
470
|
|
|
* The 95% use of this function is "->getPagePermsClause(1)" which will |
471
|
|
|
* return WHERE clauses for *selecting* pages in backend listings - in other words this will check read permissions. |
472
|
|
|
* |
473
|
|
|
* @param int $perms Permission mask to use, see function description |
474
|
|
|
* @return string Part of where clause. Prefix " AND " to this. |
475
|
|
|
* @internal should only be used from within TYPO3 Core, use PagePermissionDatabaseRestriction instead. |
476
|
|
|
*/ |
477
|
|
|
public function getPagePermsClause($perms) |
478
|
|
|
{ |
479
|
|
|
if (is_array($this->user)) { |
480
|
|
|
if ($this->isAdmin()) { |
481
|
|
|
return ' 1=1'; |
482
|
|
|
} |
483
|
|
|
// Make sure it's integer. |
484
|
|
|
$perms = (int)$perms; |
485
|
|
|
$expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class) |
486
|
|
|
->getQueryBuilderForTable('pages') |
487
|
|
|
->expr(); |
488
|
|
|
|
489
|
|
|
// User |
490
|
|
|
$constraint = $expressionBuilder->orX( |
491
|
|
|
$expressionBuilder->comparison( |
492
|
|
|
$expressionBuilder->bitAnd('pages.perms_everybody', $perms), |
493
|
|
|
ExpressionBuilder::EQ, |
494
|
|
|
$perms |
495
|
|
|
), |
496
|
|
|
$expressionBuilder->andX( |
497
|
|
|
$expressionBuilder->eq('pages.perms_userid', (int)$this->user['uid']), |
498
|
|
|
$expressionBuilder->comparison( |
499
|
|
|
$expressionBuilder->bitAnd('pages.perms_user', $perms), |
500
|
|
|
ExpressionBuilder::EQ, |
501
|
|
|
$perms |
502
|
|
|
) |
503
|
|
|
) |
504
|
|
|
); |
505
|
|
|
|
506
|
|
|
// Group (if any is set) |
507
|
|
|
if (!empty($this->userGroupsUID)) { |
508
|
|
|
$constraint->add( |
509
|
|
|
$expressionBuilder->andX( |
510
|
|
|
$expressionBuilder->in( |
511
|
|
|
'pages.perms_groupid', |
512
|
|
|
$this->userGroupsUID |
513
|
|
|
), |
514
|
|
|
$expressionBuilder->comparison( |
515
|
|
|
$expressionBuilder->bitAnd('pages.perms_group', $perms), |
516
|
|
|
ExpressionBuilder::EQ, |
517
|
|
|
$perms |
518
|
|
|
) |
519
|
|
|
) |
520
|
|
|
); |
521
|
|
|
} |
522
|
|
|
|
523
|
|
|
$constraint = ' (' . (string)$constraint . ')'; |
524
|
|
|
|
525
|
|
|
// **************** |
526
|
|
|
// getPagePermsClause-HOOK |
527
|
|
|
// **************** |
528
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getPagePermsClause'] ?? [] as $_funcRef) { |
529
|
|
|
$_params = ['currentClause' => $constraint, 'perms' => $perms]; |
530
|
|
|
$constraint = GeneralUtility::callUserFunction($_funcRef, $_params, $this); |
531
|
|
|
} |
532
|
|
|
return $constraint; |
533
|
|
|
} |
534
|
|
|
return ' 1=0'; |
535
|
|
|
} |
536
|
|
|
|
537
|
|
|
/** |
538
|
|
|
* Returns a combined binary representation of the current users permissions for the page-record, $row. |
539
|
|
|
* The perms for user, group and everybody is OR'ed together (provided that the page-owner is the user |
540
|
|
|
* and for the groups that the user is a member of the group. |
541
|
|
|
* If the user is admin, 31 is returned (full permissions for all five flags) |
542
|
|
|
* |
543
|
|
|
* @param array $row Input page row with all perms_* fields available. |
544
|
|
|
* @return int Bitwise representation of the users permissions in relation to input page row, $row |
545
|
|
|
*/ |
546
|
|
|
public function calcPerms($row) |
547
|
|
|
{ |
548
|
|
|
// Return 31 for admin users. |
549
|
|
|
if ($this->isAdmin()) { |
550
|
|
|
return Permission::ALL; |
551
|
|
|
} |
552
|
|
|
// Return 0 if page is not within the allowed web mount |
553
|
|
|
if (!$this->isInWebMount($row)) { |
|
|
|
|
554
|
|
|
return Permission::NOTHING; |
555
|
|
|
} |
556
|
|
|
$out = Permission::NOTHING; |
557
|
|
|
if ( |
558
|
|
|
isset($row['perms_userid']) && isset($row['perms_user']) && isset($row['perms_groupid']) |
559
|
|
|
&& isset($row['perms_group']) && isset($row['perms_everybody']) && !empty($this->userGroupsUID) |
560
|
|
|
) { |
561
|
|
|
if ($this->user['uid'] == $row['perms_userid']) { |
562
|
|
|
$out |= $row['perms_user']; |
563
|
|
|
} |
564
|
|
|
if ($this->isMemberOfGroup($row['perms_groupid'])) { |
565
|
|
|
$out |= $row['perms_group']; |
566
|
|
|
} |
567
|
|
|
$out |= $row['perms_everybody']; |
568
|
|
|
} |
569
|
|
|
// **************** |
570
|
|
|
// CALCPERMS hook |
571
|
|
|
// **************** |
572
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['calcPerms'] ?? [] as $_funcRef) { |
573
|
|
|
$_params = [ |
574
|
|
|
'row' => $row, |
575
|
|
|
'outputPermissions' => $out |
576
|
|
|
]; |
577
|
|
|
$out = GeneralUtility::callUserFunction($_funcRef, $_params, $this); |
578
|
|
|
} |
579
|
|
|
return $out; |
580
|
|
|
} |
581
|
|
|
|
582
|
|
|
/** |
583
|
|
|
* Returns TRUE if the RTE (Rich Text Editor) is enabled for the user. |
584
|
|
|
* |
585
|
|
|
* @return bool |
586
|
|
|
* @internal should only be used from within TYPO3 Core |
587
|
|
|
*/ |
588
|
|
|
public function isRTE() |
589
|
|
|
{ |
590
|
|
|
return (bool)$this->uc['edit_RTE']; |
591
|
|
|
} |
592
|
|
|
|
593
|
|
|
/** |
594
|
|
|
* Returns TRUE if the $value is found in the list in a $this->groupData[] index pointed to by $type (array key). |
595
|
|
|
* Can thus be users to check for modules, exclude-fields, select/modify permissions for tables etc. |
596
|
|
|
* If user is admin TRUE is also returned |
597
|
|
|
* Please see the document Inside TYPO3 for examples. |
598
|
|
|
* |
599
|
|
|
* @param string $type The type value; "webmounts", "filemounts", "pagetypes_select", "tables_select", "tables_modify", "non_exclude_fields", "modules", "available_widgets", "mfa_providers" |
600
|
|
|
* @param string $value String to search for in the groupData-list |
601
|
|
|
* @return bool TRUE if permission is granted (that is, the value was found in the groupData list - or the BE_USER is "admin") |
602
|
|
|
*/ |
603
|
|
|
public function check($type, $value) |
604
|
|
|
{ |
605
|
|
|
return isset($this->groupData[$type]) |
606
|
|
|
&& ($this->isAdmin() || GeneralUtility::inList($this->groupData[$type], $value)); |
607
|
|
|
} |
608
|
|
|
|
609
|
|
|
/** |
610
|
|
|
* Checking the authMode of a select field with authMode set |
611
|
|
|
* |
612
|
|
|
* @param string $table Table name |
613
|
|
|
* @param string $field Field name (must be configured in TCA and of type "select" with authMode set!) |
614
|
|
|
* @param string $value Value to evaluation (single value, must not contain any of the chars ":,|") |
615
|
|
|
* @param string $authMode Auth mode keyword (explicitAllow, explicitDeny, individual) |
616
|
|
|
* @return bool Whether access is granted or not |
617
|
|
|
*/ |
618
|
|
|
public function checkAuthMode($table, $field, $value, $authMode) |
619
|
|
|
{ |
620
|
|
|
// Admin users can do anything: |
621
|
|
|
if ($this->isAdmin()) { |
622
|
|
|
return true; |
623
|
|
|
} |
624
|
|
|
// Allow all blank values: |
625
|
|
|
if ((string)$value === '') { |
626
|
|
|
return true; |
627
|
|
|
} |
628
|
|
|
// Allow dividers: |
629
|
|
|
if ($value === '--div--') { |
630
|
|
|
return true; |
631
|
|
|
} |
632
|
|
|
// Certain characters are not allowed in the value |
633
|
|
|
if (preg_match('/[:|,]/', $value)) { |
634
|
|
|
return false; |
635
|
|
|
} |
636
|
|
|
// Initialize: |
637
|
|
|
$testValue = $table . ':' . $field . ':' . $value; |
638
|
|
|
$out = true; |
639
|
|
|
// Checking value: |
640
|
|
|
switch ((string)$authMode) { |
641
|
|
|
case 'explicitAllow': |
642
|
|
|
if (!GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':ALLOW')) { |
643
|
|
|
$out = false; |
644
|
|
|
} |
645
|
|
|
break; |
646
|
|
|
case 'explicitDeny': |
647
|
|
|
if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) { |
648
|
|
|
$out = false; |
649
|
|
|
} |
650
|
|
|
break; |
651
|
|
|
case 'individual': |
652
|
|
|
if (is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'][$field])) { |
653
|
|
|
$items = $GLOBALS['TCA'][$table]['columns'][$field]['config']['items']; |
654
|
|
|
if (is_array($items)) { |
655
|
|
|
foreach ($items as $iCfg) { |
656
|
|
|
if ((string)$iCfg[1] === (string)$value && $iCfg[4]) { |
657
|
|
|
switch ((string)$iCfg[4]) { |
658
|
|
|
case 'EXPL_ALLOW': |
659
|
|
|
if (!GeneralUtility::inList( |
660
|
|
|
$this->groupData['explicit_allowdeny'], |
661
|
|
|
$testValue . ':ALLOW' |
662
|
|
|
)) { |
663
|
|
|
$out = false; |
664
|
|
|
} |
665
|
|
|
break; |
666
|
|
|
case 'EXPL_DENY': |
667
|
|
|
if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) { |
668
|
|
|
$out = false; |
669
|
|
|
} |
670
|
|
|
break; |
671
|
|
|
} |
672
|
|
|
break; |
673
|
|
|
} |
674
|
|
|
} |
675
|
|
|
} |
676
|
|
|
} |
677
|
|
|
break; |
678
|
|
|
} |
679
|
|
|
return $out; |
680
|
|
|
} |
681
|
|
|
|
682
|
|
|
/** |
683
|
|
|
* Checking if a language value (-1, 0 and >0 for sys_language records) is allowed to be edited by the user. |
684
|
|
|
* |
685
|
|
|
* @param int $langValue Language value to evaluate |
686
|
|
|
* @return bool Returns TRUE if the language value is allowed, otherwise FALSE. |
687
|
|
|
*/ |
688
|
|
|
public function checkLanguageAccess($langValue) |
689
|
|
|
{ |
690
|
|
|
// The users language list must be non-blank - otherwise all languages are allowed. |
691
|
|
|
if (trim($this->groupData['allowed_languages']) !== '') { |
692
|
|
|
$langValue = (int)$langValue; |
693
|
|
|
// Language must either be explicitly allowed OR the lang Value be "-1" (all languages) |
694
|
|
|
if ($langValue != -1 && !$this->check('allowed_languages', (string)$langValue)) { |
695
|
|
|
return false; |
696
|
|
|
} |
697
|
|
|
} |
698
|
|
|
return true; |
699
|
|
|
} |
700
|
|
|
|
701
|
|
|
/** |
702
|
|
|
* Check if user has access to all existing localizations for a certain record |
703
|
|
|
* |
704
|
|
|
* @param string $table The table |
705
|
|
|
* @param array $record The current record |
706
|
|
|
* @return bool |
707
|
|
|
*/ |
708
|
|
|
public function checkFullLanguagesAccess($table, $record) |
709
|
|
|
{ |
710
|
|
|
if (!$this->checkLanguageAccess(0)) { |
711
|
|
|
return false; |
712
|
|
|
} |
713
|
|
|
|
714
|
|
|
if (BackendUtility::isTableLocalizable($table)) { |
715
|
|
|
$pointerField = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']; |
716
|
|
|
$pointerValue = $record[$pointerField] > 0 ? $record[$pointerField] : $record['uid']; |
717
|
|
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); |
718
|
|
|
$queryBuilder->getRestrictions() |
719
|
|
|
->removeAll() |
720
|
|
|
->add(GeneralUtility::makeInstance(DeletedRestriction::class)) |
721
|
|
|
->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->workspace)); |
722
|
|
|
$recordLocalizations = $queryBuilder->select('*') |
723
|
|
|
->from($table) |
724
|
|
|
->where( |
725
|
|
|
$queryBuilder->expr()->eq( |
726
|
|
|
$pointerField, |
727
|
|
|
$queryBuilder->createNamedParameter($pointerValue, \PDO::PARAM_INT) |
728
|
|
|
) |
729
|
|
|
) |
730
|
|
|
->execute() |
731
|
|
|
->fetchAll(); |
732
|
|
|
|
733
|
|
|
foreach ($recordLocalizations as $recordLocalization) { |
734
|
|
|
if (!$this->checkLanguageAccess($recordLocalization[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) { |
735
|
|
|
return false; |
736
|
|
|
} |
737
|
|
|
} |
738
|
|
|
} |
739
|
|
|
return true; |
740
|
|
|
} |
741
|
|
|
|
742
|
|
|
/** |
743
|
|
|
* Checking if a user has editing access to a record from a $GLOBALS['TCA'] table. |
744
|
|
|
* The checks does not take page permissions and other "environmental" things into account. |
745
|
|
|
* It only deal with record internals; If any values in the record fields disallows it. |
746
|
|
|
* For instance languages settings, authMode selector boxes are evaluated (and maybe more in the future). |
747
|
|
|
* It will check for workspace dependent access. |
748
|
|
|
* The function takes an ID (int) or row (array) as second argument. |
749
|
|
|
* |
750
|
|
|
* @param string $table Table name |
751
|
|
|
* @param int|array $idOrRow If integer, then this is the ID of the record. If Array this just represents fields in the record. |
752
|
|
|
* @param bool $newRecord Set, if testing a new (non-existing) record array. Will disable certain checks that doesn't make much sense in that context. |
753
|
|
|
* @param bool $deletedRecord Set, if testing a deleted record array. |
754
|
|
|
* @param bool $checkFullLanguageAccess Set, whenever access to all translations of the record is required |
755
|
|
|
* @return bool TRUE if OK, otherwise FALSE |
756
|
|
|
* @internal should only be used from within TYPO3 Core |
757
|
|
|
*/ |
758
|
|
|
public function recordEditAccessInternals($table, $idOrRow, $newRecord = false, $deletedRecord = false, $checkFullLanguageAccess = false): bool |
759
|
|
|
{ |
760
|
|
|
if (!isset($GLOBALS['TCA'][$table])) { |
761
|
|
|
return false; |
762
|
|
|
} |
763
|
|
|
// Always return TRUE for Admin users. |
764
|
|
|
if ($this->isAdmin()) { |
765
|
|
|
return true; |
766
|
|
|
} |
767
|
|
|
// Fetching the record if the $idOrRow variable was not an array on input: |
768
|
|
|
if (!is_array($idOrRow)) { |
769
|
|
|
if ($deletedRecord) { |
770
|
|
|
$idOrRow = BackendUtility::getRecord($table, $idOrRow, '*', '', false); |
771
|
|
|
} else { |
772
|
|
|
$idOrRow = BackendUtility::getRecord($table, $idOrRow); |
773
|
|
|
} |
774
|
|
|
if (!is_array($idOrRow)) { |
775
|
|
|
$this->errorMsg = 'ERROR: Record could not be fetched.'; |
776
|
|
|
return false; |
777
|
|
|
} |
778
|
|
|
} |
779
|
|
|
// Checking languages: |
780
|
|
|
if ($table === 'pages' && $checkFullLanguageAccess && !$this->checkFullLanguagesAccess($table, $idOrRow)) { |
781
|
|
|
return false; |
782
|
|
|
} |
783
|
|
|
if ($GLOBALS['TCA'][$table]['ctrl']['languageField'] ?? false) { |
784
|
|
|
// Language field must be found in input row - otherwise it does not make sense. |
785
|
|
|
if (isset($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) { |
786
|
|
|
if (!$this->checkLanguageAccess($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) { |
787
|
|
|
$this->errorMsg = 'ERROR: Language was not allowed.'; |
788
|
|
|
return false; |
789
|
|
|
} |
790
|
|
|
if ( |
791
|
|
|
$checkFullLanguageAccess && $idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']] == 0 |
792
|
|
|
&& !$this->checkFullLanguagesAccess($table, $idOrRow) |
793
|
|
|
) { |
794
|
|
|
$this->errorMsg = 'ERROR: Related/affected language was not allowed.'; |
795
|
|
|
return false; |
796
|
|
|
} |
797
|
|
|
} else { |
798
|
|
|
$this->errorMsg = 'ERROR: The "languageField" field named "' |
799
|
|
|
. $GLOBALS['TCA'][$table]['ctrl']['languageField'] . '" was not found in testing record!'; |
800
|
|
|
return false; |
801
|
|
|
} |
802
|
|
|
} |
803
|
|
|
// Checking authMode fields: |
804
|
|
|
if (is_array($GLOBALS['TCA'][$table]['columns'])) { |
805
|
|
|
foreach ($GLOBALS['TCA'][$table]['columns'] as $fieldName => $fieldValue) { |
806
|
|
|
if (isset($idOrRow[$fieldName]) |
807
|
|
|
&& ($fieldValue['config']['type'] ?? '') === 'select' |
808
|
|
|
&& ($fieldValue['config']['authMode'] ?? false) |
809
|
|
|
&& ($fieldValue['config']['authMode_enforce'] ?? '') === 'strict' |
810
|
|
|
&& !$this->checkAuthMode($table, $fieldName, $idOrRow[$fieldName], $fieldValue['config']['authMode'])) { |
811
|
|
|
$this->errorMsg = 'ERROR: authMode "' . $fieldValue['config']['authMode'] |
812
|
|
|
. '" failed for field "' . $fieldName . '" with value "' |
813
|
|
|
. $idOrRow[$fieldName] . '" evaluated'; |
814
|
|
|
return false; |
815
|
|
|
} |
816
|
|
|
} |
817
|
|
|
} |
818
|
|
|
// Checking "editlock" feature (doesn't apply to new records) |
819
|
|
|
if (!$newRecord && ($GLOBALS['TCA'][$table]['ctrl']['editlock'] ?? false)) { |
820
|
|
|
if (isset($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['editlock']])) { |
821
|
|
|
if ($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['editlock']]) { |
822
|
|
|
$this->errorMsg = 'ERROR: Record was locked for editing. Only admin users can change this state.'; |
823
|
|
|
return false; |
824
|
|
|
} |
825
|
|
|
} else { |
826
|
|
|
$this->errorMsg = 'ERROR: The "editLock" field named "' . $GLOBALS['TCA'][$table]['ctrl']['editlock'] |
827
|
|
|
. '" was not found in testing record!'; |
828
|
|
|
return false; |
829
|
|
|
} |
830
|
|
|
} |
831
|
|
|
// Checking record permissions |
832
|
|
|
// THIS is where we can include a check for "perms_" fields for other records than pages... |
833
|
|
|
// Process any hooks |
834
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['recordEditAccessInternals'] ?? [] as $funcRef) { |
835
|
|
|
$params = [ |
836
|
|
|
'table' => $table, |
837
|
|
|
'idOrRow' => $idOrRow, |
838
|
|
|
'newRecord' => $newRecord |
839
|
|
|
]; |
840
|
|
|
if (!GeneralUtility::callUserFunction($funcRef, $params, $this)) { |
841
|
|
|
return false; |
842
|
|
|
} |
843
|
|
|
} |
844
|
|
|
// Finally, return TRUE if all is well. |
845
|
|
|
return true; |
846
|
|
|
} |
847
|
|
|
|
848
|
|
|
/** |
849
|
|
|
* Returns TRUE if the BE_USER is allowed to *create* shortcuts in the backend modules |
850
|
|
|
* |
851
|
|
|
* @return bool |
852
|
|
|
*/ |
853
|
|
|
public function mayMakeShortcut() |
854
|
|
|
{ |
855
|
|
|
return ($this->getTSConfig()['options.']['enableBookmarks'] ?? false) |
856
|
|
|
&& !($this->getTSConfig()['options.']['mayNotCreateEditBookmarks'] ?? false); |
857
|
|
|
} |
858
|
|
|
|
859
|
|
|
/** |
860
|
|
|
* Checking if editing of an existing record is allowed in current workspace if that is offline. |
861
|
|
|
* Rules for editing in offline mode: |
862
|
|
|
* - record supports versioning and is an offline version from workspace and has the current stage |
863
|
|
|
* - or record (any) is in a branch where there is a page which is a version from the workspace |
864
|
|
|
* and where the stage is not preventing records |
865
|
|
|
* |
866
|
|
|
* @param string $table Table of record |
867
|
|
|
* @param array|int $recData Integer (record uid) or array where fields are at least: pid, t3ver_wsid, t3ver_oid, t3ver_stage (if versioningWS is set) |
868
|
|
|
* @return string String error code, telling the failure state. FALSE=All ok |
869
|
|
|
* @internal should only be used from within TYPO3 Core |
870
|
|
|
*/ |
871
|
|
|
public function workspaceCannotEditRecord($table, $recData) |
872
|
|
|
{ |
873
|
|
|
// Only test if the user is in a workspace |
874
|
|
|
if ($this->workspace === 0) { |
875
|
|
|
return false; |
|
|
|
|
876
|
|
|
} |
877
|
|
|
$tableSupportsVersioning = BackendUtility::isTableWorkspaceEnabled($table); |
878
|
|
|
if (!is_array($recData)) { |
879
|
|
|
$recData = BackendUtility::getRecord( |
880
|
|
|
$table, |
881
|
|
|
$recData, |
882
|
|
|
'pid' . ($tableSupportsVersioning ? ',t3ver_oid,t3ver_wsid,t3ver_state,t3ver_stage' : '') |
883
|
|
|
); |
884
|
|
|
} |
885
|
|
|
if (is_array($recData)) { |
886
|
|
|
// We are testing a "version" (identified by having a t3ver_oid): it can be edited provided |
887
|
|
|
// that workspace matches and versioning is enabled for the table. |
888
|
|
|
$versionState = new VersionState($recData['t3ver_state'] ?? 0); |
889
|
|
|
if ($tableSupportsVersioning |
890
|
|
|
&& ( |
891
|
|
|
$versionState->equals(VersionState::NEW_PLACEHOLDER) || (int)(($recData['t3ver_oid'] ?? 0) > 0) |
892
|
|
|
) |
893
|
|
|
) { |
894
|
|
|
if ((int)$recData['t3ver_wsid'] !== $this->workspace) { |
895
|
|
|
// So does workspace match? |
896
|
|
|
return 'Workspace ID of record didn\'t match current workspace'; |
897
|
|
|
} |
898
|
|
|
// So is the user allowed to "use" the edit stage within the workspace? |
899
|
|
|
return $this->workspaceCheckStageForCurrent(0) |
|
|
|
|
900
|
|
|
? false |
901
|
|
|
: 'User\'s access level did not allow for editing'; |
902
|
|
|
} |
903
|
|
|
// Check if we are testing a "live" record |
904
|
|
|
if ($this->workspaceAllowsLiveEditingInTable($table)) { |
905
|
|
|
// Live records are OK in the current workspace |
906
|
|
|
return false; |
|
|
|
|
907
|
|
|
} |
908
|
|
|
// If not offline, output error |
909
|
|
|
return 'Online record was not in a workspace!'; |
910
|
|
|
} |
911
|
|
|
return 'No record'; |
912
|
|
|
} |
913
|
|
|
|
914
|
|
|
/** |
915
|
|
|
* Evaluates if a user is allowed to edit the offline version |
916
|
|
|
* |
917
|
|
|
* @param string $table Table of record |
918
|
|
|
* @param array|int $recData Integer (record uid) or array where fields are at least: pid, t3ver_wsid, t3ver_stage (if versioningWS is set) |
919
|
|
|
* @return string String error code, telling the failure state. FALSE=All ok |
920
|
|
|
* @see workspaceCannotEditRecord() |
921
|
|
|
* @internal this method will be moved to EXT:workspaces |
922
|
|
|
*/ |
923
|
|
|
public function workspaceCannotEditOfflineVersion($table, $recData) |
924
|
|
|
{ |
925
|
|
|
if (!BackendUtility::isTableWorkspaceEnabled($table)) { |
926
|
|
|
return 'Table does not support versioning.'; |
927
|
|
|
} |
928
|
|
|
if (!is_array($recData)) { |
929
|
|
|
$recData = BackendUtility::getRecord($table, $recData, 'uid,pid,t3ver_oid,t3ver_wsid,t3ver_state,t3ver_stage'); |
930
|
|
|
} |
931
|
|
|
if (is_array($recData)) { |
932
|
|
|
$versionState = new VersionState($recData['t3ver_state']); |
933
|
|
|
if ($versionState->equals(VersionState::NEW_PLACEHOLDER) || (int)$recData['t3ver_oid'] > 0) { |
934
|
|
|
return $this->workspaceCannotEditRecord($table, $recData); |
935
|
|
|
} |
936
|
|
|
return 'Not an offline version'; |
937
|
|
|
} |
938
|
|
|
return 'No record'; |
939
|
|
|
} |
940
|
|
|
|
941
|
|
|
/** |
942
|
|
|
* Checks if a record is allowed to be edited in the current workspace. |
943
|
|
|
* This is not bound to an actual record, but to the mere fact if the user is in a workspace |
944
|
|
|
* and depending on the table settings. |
945
|
|
|
* |
946
|
|
|
* @param string $table |
947
|
|
|
* @return bool |
948
|
|
|
* @internal should only be used from within TYPO3 Core |
949
|
|
|
*/ |
950
|
|
|
public function workspaceAllowsLiveEditingInTable(string $table): bool |
951
|
|
|
{ |
952
|
|
|
// In live workspace the record can be added/modified |
953
|
|
|
if ($this->workspace === 0) { |
954
|
|
|
return true; |
955
|
|
|
} |
956
|
|
|
// Workspace setting allows to "live edit" records of tables without versioning |
957
|
|
|
if (($this->workspaceRec['live_edit'] ?? false) |
958
|
|
|
&& !BackendUtility::isTableWorkspaceEnabled($table) |
959
|
|
|
) { |
960
|
|
|
return true; |
961
|
|
|
} |
962
|
|
|
// Always for Live workspace AND if live-edit is enabled |
963
|
|
|
// and tables are completely without versioning it is ok as well. |
964
|
|
|
if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS_alwaysAllowLiveEdit'] ?? false) { |
965
|
|
|
return true; |
966
|
|
|
} |
967
|
|
|
// If the answer is FALSE it means the only valid way to create or edit records by creating records in the workspace |
968
|
|
|
return false; |
969
|
|
|
} |
970
|
|
|
|
971
|
|
|
/** |
972
|
|
|
* Evaluates if a record from $table can be created. If the table is not set up for versioning, |
973
|
|
|
* and the "live edit" flag of the page is set, return false. In live workspace this is always true, |
974
|
|
|
* as all records can be created in live workspace |
975
|
|
|
* |
976
|
|
|
* @param string $table Table name |
977
|
|
|
* @return bool |
978
|
|
|
* @internal should only be used from within TYPO3 Core |
979
|
|
|
*/ |
980
|
|
|
public function workspaceCanCreateNewRecord(string $table): bool |
981
|
|
|
{ |
982
|
|
|
// If LIVE records cannot be created due to workspace restrictions, prepare creation of placeholder-record |
983
|
|
|
if (!$this->workspaceAllowsLiveEditingInTable($table) && !BackendUtility::isTableWorkspaceEnabled($table)) { |
984
|
|
|
return false; |
985
|
|
|
} |
986
|
|
|
return true; |
987
|
|
|
} |
988
|
|
|
|
989
|
|
|
/** |
990
|
|
|
* Evaluates if auto creation of a version of a record is allowed. |
991
|
|
|
* Auto-creation of version: In offline workspace, test if versioning is |
992
|
|
|
* enabled and look for workspace version of input record. |
993
|
|
|
* If there is no versionized record found we will create one and save to that. |
994
|
|
|
* |
995
|
|
|
* @param string $table Table of the record |
996
|
|
|
* @param int $id UID of record |
997
|
|
|
* @param int $recpid PID of record |
998
|
|
|
* @return bool TRUE if ok. |
999
|
|
|
* @internal should only be used from within TYPO3 Core |
1000
|
|
|
*/ |
1001
|
|
|
public function workspaceAllowAutoCreation($table, $id, $recpid) |
1002
|
|
|
{ |
1003
|
|
|
// No version can be created in live workspace |
1004
|
|
|
if ($this->workspace === 0) { |
1005
|
|
|
return false; |
1006
|
|
|
} |
1007
|
|
|
// No versioning support for this table, so no version can be created |
1008
|
|
|
if (!BackendUtility::isTableWorkspaceEnabled($table)) { |
1009
|
|
|
return false; |
1010
|
|
|
} |
1011
|
|
|
if ($recpid < 0) { |
1012
|
|
|
return false; |
1013
|
|
|
} |
1014
|
|
|
// There must be no existing version of this record in workspace |
1015
|
|
|
if (BackendUtility::getWorkspaceVersionOfRecord($this->workspace, $table, $id, 'uid')) { |
1016
|
|
|
return false; |
1017
|
|
|
} |
1018
|
|
|
return true; |
1019
|
|
|
} |
1020
|
|
|
|
1021
|
|
|
/** |
1022
|
|
|
* Checks if an element stage allows access for the user in the current workspace |
1023
|
|
|
* In live workspace (= 0) access is always granted for any stage. |
1024
|
|
|
* Admins are always allowed. |
1025
|
|
|
* An option for custom workspaces allows members to also edit when the stage is "Review" |
1026
|
|
|
* |
1027
|
|
|
* @param int $stage Stage id from an element: -1,0 = editing, 1 = reviewer, >1 = owner |
1028
|
|
|
* @return bool TRUE if user is allowed access |
1029
|
|
|
* @internal should only be used from within TYPO3 Core |
1030
|
|
|
*/ |
1031
|
|
|
public function workspaceCheckStageForCurrent($stage) |
1032
|
|
|
{ |
1033
|
|
|
// Always allow for admins |
1034
|
|
|
if ($this->isAdmin()) { |
1035
|
|
|
return true; |
1036
|
|
|
} |
1037
|
|
|
// Always OK for live workspace |
1038
|
|
|
if ($this->workspace === 0 || !ExtensionManagementUtility::isLoaded('workspaces')) { |
1039
|
|
|
return true; |
1040
|
|
|
} |
1041
|
|
|
$stage = (int)$stage; |
1042
|
|
|
$stat = $this->checkWorkspaceCurrent(); |
1043
|
|
|
$accessType = $stat['_ACCESS']; |
1044
|
|
|
// Workspace owners are always allowed for stage change |
1045
|
|
|
if ($accessType === 'owner') { |
1046
|
|
|
return true; |
1047
|
|
|
} |
1048
|
|
|
|
1049
|
|
|
// Check if custom staging is activated |
1050
|
|
|
$workspaceRec = BackendUtility::getRecord('sys_workspace', $stat['uid']); |
1051
|
|
|
if ($workspaceRec['custom_stages'] > 0 && $stage !== 0 && $stage !== -10) { |
1052
|
|
|
// Get custom stage record |
1053
|
|
|
$workspaceStageRec = BackendUtility::getRecord('sys_workspace_stage', $stage); |
1054
|
|
|
// Check if the user is responsible for the current stage |
1055
|
|
|
if ( |
1056
|
|
|
$accessType === 'member' |
1057
|
|
|
&& GeneralUtility::inList($workspaceStageRec['responsible_persons'], 'be_users_' . $this->user['uid']) |
1058
|
|
|
) { |
1059
|
|
|
return true; |
1060
|
|
|
} |
1061
|
|
|
// Check if the user is in a group which is responsible for the current stage |
1062
|
|
|
foreach ($this->userGroupsUID as $groupUid) { |
1063
|
|
|
if ( |
1064
|
|
|
$accessType === 'member' |
1065
|
|
|
&& GeneralUtility::inList($workspaceStageRec['responsible_persons'], 'be_groups_' . $groupUid) |
1066
|
|
|
) { |
1067
|
|
|
return true; |
1068
|
|
|
} |
1069
|
|
|
} |
1070
|
|
|
} elseif ($stage === -10 || $stage === -20) { |
1071
|
|
|
// Nobody is allowed to do that except the owner (which was checked above) |
1072
|
|
|
return false; |
1073
|
|
|
} elseif ( |
1074
|
|
|
$accessType === 'reviewer' && $stage <= 1 |
|
|
|
|
1075
|
|
|
|| $accessType === 'member' && $stage <= 0 |
1076
|
|
|
) { |
1077
|
|
|
return true; |
1078
|
|
|
} |
1079
|
|
|
return false; |
1080
|
|
|
} |
1081
|
|
|
|
1082
|
|
|
/** |
1083
|
|
|
* Returns TRUE if the user has access to publish content from the workspace ID given. |
1084
|
|
|
* Admin-users are always granted access to do this |
1085
|
|
|
* If the workspace ID is 0 (live) all users have access also |
1086
|
|
|
* For custom workspaces it depends on whether the user is owner OR like with |
1087
|
|
|
* draft workspace if the user has access to Live workspace. |
1088
|
|
|
* |
1089
|
|
|
* @param int $wsid Workspace UID; 0,1+ |
1090
|
|
|
* @return bool Returns TRUE if the user has access to publish content from the workspace ID given. |
1091
|
|
|
* @internal this method will be moved to EXT:workspaces |
1092
|
|
|
*/ |
1093
|
|
|
public function workspacePublishAccess($wsid) |
1094
|
|
|
{ |
1095
|
|
|
if ($this->isAdmin()) { |
1096
|
|
|
return true; |
1097
|
|
|
} |
1098
|
|
|
$wsAccess = $this->checkWorkspace($wsid); |
1099
|
|
|
// If no access to workspace, of course you cannot publish! |
1100
|
|
|
if ($wsAccess === false) { |
|
|
|
|
1101
|
|
|
return false; |
1102
|
|
|
} |
1103
|
|
|
if ((int)$wsAccess['uid'] === 0) { |
1104
|
|
|
// If access to Live workspace, no problem. |
1105
|
|
|
return true; |
1106
|
|
|
} |
1107
|
|
|
// Custom workspaces |
1108
|
|
|
// 1. Owners can always publish |
1109
|
|
|
if ($wsAccess['_ACCESS'] === 'owner') { |
1110
|
|
|
return true; |
1111
|
|
|
} |
1112
|
|
|
// 2. User has access to online workspace which is OK as well as long as publishing |
1113
|
|
|
// access is not limited by workspace option. |
1114
|
|
|
return $this->checkWorkspace(0) && !($wsAccess['publish_access'] & 2); |
1115
|
|
|
} |
1116
|
|
|
|
1117
|
|
|
/** |
1118
|
|
|
* Returns full parsed user TSconfig array, merged with TSconfig from groups. |
1119
|
|
|
* |
1120
|
|
|
* Example: |
1121
|
|
|
* [ |
1122
|
|
|
* 'options.' => [ |
1123
|
|
|
* 'fooEnabled' => '0', |
1124
|
|
|
* 'fooEnabled.' => [ |
1125
|
|
|
* 'tt_content' => 1, |
1126
|
|
|
* ], |
1127
|
|
|
* ], |
1128
|
|
|
* ] |
1129
|
|
|
* |
1130
|
|
|
* @return array Parsed and merged user TSconfig array |
1131
|
|
|
*/ |
1132
|
|
|
public function getTSConfig() |
1133
|
|
|
{ |
1134
|
|
|
return $this->userTS; |
1135
|
|
|
} |
1136
|
|
|
|
1137
|
|
|
/** |
1138
|
|
|
* Returns an array with the webmounts. |
1139
|
|
|
* If no webmounts, and empty array is returned. |
1140
|
|
|
* Webmounts permissions are checked in fetchGroupData() |
1141
|
|
|
* |
1142
|
|
|
* @return array of web mounts uids (may include '0') |
1143
|
|
|
*/ |
1144
|
|
|
public function returnWebmounts() |
1145
|
|
|
{ |
1146
|
|
|
return (string)$this->groupData['webmounts'] != '' ? explode(',', $this->groupData['webmounts']) : []; |
1147
|
|
|
} |
1148
|
|
|
|
1149
|
|
|
/** |
1150
|
|
|
* Initializes the given mount points for the current Backend user. |
1151
|
|
|
* |
1152
|
|
|
* @param array $mountPointUids Page UIDs that should be used as web mountpoints |
1153
|
|
|
* @param bool $append If TRUE the given mount point will be appended. Otherwise the current mount points will be replaced. |
1154
|
|
|
*/ |
1155
|
|
|
public function setWebmounts(array $mountPointUids, $append = false) |
1156
|
|
|
{ |
1157
|
|
|
if (empty($mountPointUids)) { |
1158
|
|
|
return; |
1159
|
|
|
} |
1160
|
|
|
if ($append) { |
1161
|
|
|
$currentWebMounts = GeneralUtility::intExplode(',', $this->groupData['webmounts']); |
1162
|
|
|
$mountPointUids = array_merge($currentWebMounts, $mountPointUids); |
1163
|
|
|
} |
1164
|
|
|
$this->groupData['webmounts'] = implode(',', array_unique($mountPointUids)); |
1165
|
|
|
} |
1166
|
|
|
|
1167
|
|
|
/** |
1168
|
|
|
* Checks for alternative web mount points for the element browser. |
1169
|
|
|
* |
1170
|
|
|
* If there is a temporary mount point active in the page tree it will be used. |
1171
|
|
|
* |
1172
|
|
|
* If the User TSconfig options.pageTree.altElementBrowserMountPoints is not empty the pages configured |
1173
|
|
|
* there are used as web mounts If options.pageTree.altElementBrowserMountPoints.append is enabled, |
1174
|
|
|
* they are appended to the existing webmounts. |
1175
|
|
|
* |
1176
|
|
|
* @internal - do not use in your own extension |
1177
|
|
|
*/ |
1178
|
|
|
public function initializeWebmountsForElementBrowser() |
1179
|
|
|
{ |
1180
|
|
|
$alternativeWebmountPoint = (int)$this->getSessionData('pageTree_temporaryMountPoint'); |
1181
|
|
|
if ($alternativeWebmountPoint) { |
1182
|
|
|
$alternativeWebmountPoint = GeneralUtility::intExplode(',', (string)$alternativeWebmountPoint); |
1183
|
|
|
$this->setWebmounts($alternativeWebmountPoint); |
1184
|
|
|
return; |
1185
|
|
|
} |
1186
|
|
|
|
1187
|
|
|
$alternativeWebmountPoints = trim($this->getTSConfig()['options.']['pageTree.']['altElementBrowserMountPoints'] ?? ''); |
1188
|
|
|
$appendAlternativeWebmountPoints = $this->getTSConfig()['options.']['pageTree.']['altElementBrowserMountPoints.']['append'] ?? ''; |
1189
|
|
|
if ($alternativeWebmountPoints) { |
1190
|
|
|
$alternativeWebmountPoints = GeneralUtility::intExplode(',', $alternativeWebmountPoints); |
1191
|
|
|
$this->setWebmounts($alternativeWebmountPoints, $appendAlternativeWebmountPoints); |
|
|
|
|
1192
|
|
|
} |
1193
|
|
|
} |
1194
|
|
|
|
1195
|
|
|
/** |
1196
|
|
|
* Returns TRUE or FALSE, depending if an alert popup (a javascript confirmation) should be shown |
1197
|
|
|
* call like $GLOBALS['BE_USER']->jsConfirmation($BITMASK). |
1198
|
|
|
* |
1199
|
|
|
* @param int $bitmask Bitmask, one of \TYPO3\CMS\Core\Type\Bitmask\JsConfirmation |
1200
|
|
|
* @return bool TRUE if the confirmation should be shown |
1201
|
|
|
* @see JsConfirmation |
1202
|
|
|
*/ |
1203
|
|
|
public function jsConfirmation($bitmask) |
1204
|
|
|
{ |
1205
|
|
|
try { |
1206
|
|
|
$alertPopupsSetting = trim((string)($this->getTSConfig()['options.']['alertPopups'] ?? '')); |
1207
|
|
|
$alertPopup = JsConfirmation::cast($alertPopupsSetting === '' ? null : (int)$alertPopupsSetting); |
1208
|
|
|
} catch (InvalidEnumerationValueException $e) { |
1209
|
|
|
$alertPopup = new JsConfirmation(); |
1210
|
|
|
} |
1211
|
|
|
|
1212
|
|
|
return JsConfirmation::cast($bitmask)->matches($alertPopup); |
1213
|
|
|
} |
1214
|
|
|
|
1215
|
|
|
/** |
1216
|
|
|
* Initializes a lot of stuff like the access-lists, database-mountpoints and filemountpoints |
1217
|
|
|
* This method is called by ->backendCheckLogin() (from extending BackendUserAuthentication) |
1218
|
|
|
* if the backend user login has verified OK. |
1219
|
|
|
* Generally this is required initialization of a backend user. |
1220
|
|
|
* |
1221
|
|
|
* @internal |
1222
|
|
|
* @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser |
1223
|
|
|
*/ |
1224
|
|
|
public function fetchGroupData() |
1225
|
|
|
{ |
1226
|
|
|
if ($this->user['uid']) { |
1227
|
|
|
// Get lists for the be_user record and set them as default/primary values. |
1228
|
|
|
// Enabled Backend Modules |
1229
|
|
|
$this->groupData['modules'] = $this->user['userMods']; |
1230
|
|
|
// Add available widgets |
1231
|
|
|
$this->groupData['available_widgets'] = $this->user['available_widgets'] ?? ''; |
1232
|
|
|
// Add allowed mfa providers |
1233
|
|
|
$this->groupData['mfa_providers'] = $this->user['mfa_providers'] ?? ''; |
1234
|
|
|
// Add Allowed Languages |
1235
|
|
|
$this->groupData['allowed_languages'] = $this->user['allowed_languages'] ?? ''; |
1236
|
|
|
// Set user value for workspace permissions. |
1237
|
|
|
$this->groupData['workspace_perms'] = $this->user['workspace_perms']; |
1238
|
|
|
// Database mountpoints |
1239
|
|
|
$this->groupData['webmounts'] = $this->user['db_mountpoints']; |
1240
|
|
|
// File mountpoints |
1241
|
|
|
$this->groupData['filemounts'] = $this->user['file_mountpoints']; |
1242
|
|
|
// Fileoperation permissions |
1243
|
|
|
$this->groupData['file_permissions'] = $this->user['file_permissions']; |
1244
|
|
|
|
1245
|
|
|
// Get the groups and accumulate their permission settings |
1246
|
|
|
$mountOptions = new BackendGroupMountOption($this->user['options']); |
1247
|
|
|
$groupResolver = GeneralUtility::makeInstance(GroupResolver::class); |
1248
|
|
|
$resolvedGroups = $groupResolver->resolveGroupsForUser($this->user, $this->usergroup_table); |
1249
|
|
|
foreach ($resolvedGroups as $groupInfo) { |
1250
|
|
|
$groupInfo += [ |
1251
|
|
|
'uid' => 0, |
1252
|
|
|
'db_mountpoints' => '', |
1253
|
|
|
'file_mountpoints' => '', |
1254
|
|
|
'groupMods' => '', |
1255
|
|
|
'availableWidgets' => '', |
1256
|
|
|
'mfa_providers' => '', |
1257
|
|
|
'tables_select' => '', |
1258
|
|
|
'tables_modify' => '', |
1259
|
|
|
'pagetypes_select' => '', |
1260
|
|
|
'non_exclude_fields' => '', |
1261
|
|
|
'explicit_allowdeny' => '', |
1262
|
|
|
'allowed_languages' => '', |
1263
|
|
|
'custom_options' => '', |
1264
|
|
|
'file_permissions' => '', |
1265
|
|
|
'workspace_perms' => 0, // Bitflag. |
1266
|
|
|
]; |
1267
|
|
|
// Add the group uid to internal arrays. |
1268
|
|
|
$this->userGroupsUID[] = (int)$groupInfo['uid']; |
1269
|
|
|
$this->userGroups[(int)$groupInfo['uid']] = $groupInfo; |
1270
|
|
|
// Mount group database-mounts |
1271
|
|
|
if ($mountOptions->shouldUserIncludePageMountsFromAssociatedGroups()) { |
1272
|
|
|
$this->groupData['webmounts'] .= ',' . $groupInfo['db_mountpoints']; |
1273
|
|
|
} |
1274
|
|
|
// Mount group file-mounts |
1275
|
|
|
if ($mountOptions->shouldUserIncludePageMountsFromAssociatedGroups()) { |
1276
|
|
|
$this->groupData['filemounts'] .= ',' . $groupInfo['file_mountpoints']; |
1277
|
|
|
} |
1278
|
|
|
// Gather permission detail fields |
1279
|
|
|
$this->groupData['modules'] .= ',' . $groupInfo['groupMods']; |
1280
|
|
|
$this->groupData['available_widgets'] .= ',' . $groupInfo['availableWidgets']; |
1281
|
|
|
$this->groupData['mfa_providers'] .= ',' . $groupInfo['mfa_providers']; |
1282
|
|
|
$this->groupData['tables_select'] .= ',' . $groupInfo['tables_select']; |
1283
|
|
|
$this->groupData['tables_modify'] .= ',' . $groupInfo['tables_modify']; |
1284
|
|
|
$this->groupData['pagetypes_select'] .= ',' . $groupInfo['pagetypes_select']; |
1285
|
|
|
$this->groupData['non_exclude_fields'] .= ',' . $groupInfo['non_exclude_fields']; |
1286
|
|
|
$this->groupData['explicit_allowdeny'] .= ',' . $groupInfo['explicit_allowdeny']; |
1287
|
|
|
$this->groupData['allowed_languages'] .= ',' . $groupInfo['allowed_languages']; |
1288
|
|
|
$this->groupData['custom_options'] .= ',' . $groupInfo['custom_options']; |
1289
|
|
|
$this->groupData['file_permissions'] .= ',' . $groupInfo['file_permissions']; |
1290
|
|
|
// Setting workspace permissions: |
1291
|
|
|
$this->groupData['workspace_perms'] |= $groupInfo['workspace_perms']; |
1292
|
|
|
if (!$this->firstMainGroup) { |
1293
|
|
|
$this->firstMainGroup = (int)$groupInfo['uid']; |
1294
|
|
|
} |
1295
|
|
|
} |
1296
|
|
|
|
1297
|
|
|
// Populating the $this->userGroupsUID -array with the groups in the order in which they were LAST included.!! |
1298
|
|
|
// Finally this is the list of group_uid's in the order they are parsed (including subgroups!) |
1299
|
|
|
// and without duplicates (duplicates are presented with their last entrance in the list, |
1300
|
|
|
// which thus reflects the order of the TypoScript in TSconfig) |
1301
|
|
|
$this->userGroupsUID = array_reverse(array_unique(array_reverse($this->userGroupsUID))); |
1302
|
|
|
|
1303
|
|
|
$this->prepareUserTsConfig(); |
1304
|
|
|
|
1305
|
|
|
// Processing webmounts |
1306
|
|
|
// Admin's always have the root mounted |
1307
|
|
|
if ($this->isAdmin() && !($this->getTSConfig()['options.']['dontMountAdminMounts'] ?? false)) { |
1308
|
|
|
$this->groupData['webmounts'] = '0,' . $this->groupData['webmounts']; |
1309
|
|
|
} |
1310
|
|
|
// The lists are cleaned for duplicates |
1311
|
|
|
$this->groupData['webmounts'] = StringUtility::uniqueList($this->groupData['webmounts'] ?? ''); |
1312
|
|
|
$this->groupData['pagetypes_select'] = StringUtility::uniqueList($this->groupData['pagetypes_select'] ?? ''); |
1313
|
|
|
$this->groupData['tables_select'] = StringUtility::uniqueList(($this->groupData['tables_modify'] ?? '') . ',' . ($this->groupData['tables_select'] ?? '')); |
1314
|
|
|
$this->groupData['tables_modify'] = StringUtility::uniqueList($this->groupData['tables_modify'] ?? ''); |
1315
|
|
|
$this->groupData['non_exclude_fields'] = StringUtility::uniqueList($this->groupData['non_exclude_fields'] ?? ''); |
1316
|
|
|
$this->groupData['explicit_allowdeny'] = StringUtility::uniqueList($this->groupData['explicit_allowdeny'] ?? ''); |
1317
|
|
|
$this->groupData['allowed_languages'] = StringUtility::uniqueList($this->groupData['allowed_languages'] ?? ''); |
1318
|
|
|
$this->groupData['custom_options'] = StringUtility::uniqueList($this->groupData['custom_options'] ?? ''); |
1319
|
|
|
$this->groupData['modules'] = StringUtility::uniqueList($this->groupData['modules'] ?? ''); |
1320
|
|
|
$this->groupData['available_widgets'] = StringUtility::uniqueList($this->groupData['available_widgets'] ?? ''); |
1321
|
|
|
$this->groupData['mfa_providers'] = StringUtility::uniqueList($this->groupData['mfa_providers'] ?? ''); |
1322
|
|
|
$this->groupData['file_permissions'] = StringUtility::uniqueList($this->groupData['file_permissions'] ?? ''); |
1323
|
|
|
|
1324
|
|
|
// Check if the user access to all web mounts set |
1325
|
|
|
if (!empty(trim($this->groupData['webmounts']))) { |
1326
|
|
|
$validWebMounts = $this->filterValidWebMounts($this->groupData['webmounts']); |
1327
|
|
|
$this->groupData['webmounts'] = implode(',', $validWebMounts); |
1328
|
|
|
} |
1329
|
|
|
// Setting up workspace situation (after webmounts are processed!): |
1330
|
|
|
$this->workspaceInit(); |
1331
|
|
|
} |
1332
|
|
|
} |
1333
|
|
|
|
1334
|
|
|
/** |
1335
|
|
|
* Checking read access to web mounts, but keeps "0" or empty strings. |
1336
|
|
|
* In any case, checks if the list of pages is visible for the backend user but also |
1337
|
|
|
* if the page is not deleted. |
1338
|
|
|
* |
1339
|
|
|
* @param string $listOfWebMounts a comma-separated list of webmounts, could also be empty, or contain "0" |
1340
|
|
|
* @return array a list of all valid web mounts the user has access to |
1341
|
|
|
*/ |
1342
|
|
|
protected function filterValidWebMounts(string $listOfWebMounts): array |
1343
|
|
|
{ |
1344
|
|
|
// Checking read access to web mounts if there are mounts points (not empty string, false or 0) |
1345
|
|
|
$allWebMounts = explode(',', $listOfWebMounts); |
1346
|
|
|
// Selecting all web mounts with permission clause for reading |
1347
|
|
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); |
1348
|
|
|
$queryBuilder->getRestrictions() |
1349
|
|
|
->removeAll() |
1350
|
|
|
->add(GeneralUtility::makeInstance(DeletedRestriction::class)); |
1351
|
|
|
|
1352
|
|
|
$readablePagesOfWebMounts = $queryBuilder->select('uid') |
1353
|
|
|
->from('pages') |
1354
|
|
|
// @todo DOCTRINE: check how to make getPagePermsClause() portable |
1355
|
|
|
->where( |
1356
|
|
|
$this->getPagePermsClause(Permission::PAGE_SHOW), |
1357
|
|
|
$queryBuilder->expr()->in( |
1358
|
|
|
'uid', |
1359
|
|
|
$queryBuilder->createNamedParameter( |
1360
|
|
|
GeneralUtility::intExplode(',', $listOfWebMounts), |
1361
|
|
|
Connection::PARAM_INT_ARRAY |
1362
|
|
|
) |
1363
|
|
|
) |
1364
|
|
|
) |
1365
|
|
|
->execute() |
1366
|
|
|
->fetchAll(); |
1367
|
|
|
$readablePagesOfWebMounts = array_column(($readablePagesOfWebMounts ?: []), 'uid', 'uid'); |
1368
|
|
|
foreach ($allWebMounts as $key => $mountPointUid) { |
1369
|
|
|
// If the mount ID is NOT found among selected pages, unset it: |
1370
|
|
|
if ($mountPointUid > 0 && !isset($readablePagesOfWebMounts[$mountPointUid])) { |
1371
|
|
|
unset($allWebMounts[$key]); |
1372
|
|
|
} |
1373
|
|
|
} |
1374
|
|
|
return $allWebMounts; |
1375
|
|
|
} |
1376
|
|
|
|
1377
|
|
|
/** |
1378
|
|
|
* This method parses the UserTSconfig from the current user and all their groups. |
1379
|
|
|
* If the contents are the same, parsing is skipped. No matching is applied here currently. |
1380
|
|
|
*/ |
1381
|
|
|
protected function prepareUserTsConfig(): void |
1382
|
|
|
{ |
1383
|
|
|
$collectedUserTSconfig = [ |
1384
|
|
|
'default' => $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUserTSconfig'] |
1385
|
|
|
]; |
1386
|
|
|
// Default TSconfig for admin-users |
1387
|
|
|
if ($this->isAdmin()) { |
1388
|
|
|
$collectedUserTSconfig[] = 'admPanel.enable.all = 1'; |
1389
|
|
|
} |
1390
|
|
|
// Setting defaults for sys_note author / email |
1391
|
|
|
$collectedUserTSconfig[] = ' |
1392
|
|
|
TCAdefaults.sys_note.author = ' . $this->user['realName'] . ' |
1393
|
|
|
TCAdefaults.sys_note.email = ' . $this->user['email']; |
1394
|
|
|
|
1395
|
|
|
// Loop through all groups and add their 'TSconfig' fields |
1396
|
|
|
foreach ($this->userGroupsUID as $groupId) { |
1397
|
|
|
$collectedUserTSconfig['group_' . $groupId] = $this->userGroups[$groupId]['TSconfig'] ?? ''; |
1398
|
|
|
} |
1399
|
|
|
|
1400
|
|
|
$collectedUserTSconfig[] = $this->user['TSconfig']; |
1401
|
|
|
// Check external files |
1402
|
|
|
$collectedUserTSconfig = TypoScriptParser::checkIncludeLines_array($collectedUserTSconfig); |
1403
|
|
|
// Imploding with "[global]" will make sure that non-ended confinements with braces are ignored. |
1404
|
|
|
$userTS_text = implode("\n[GLOBAL]\n", $collectedUserTSconfig); |
1405
|
|
|
// Parsing the user TSconfig (or getting from cache) |
1406
|
|
|
$hash = md5('userTS:' . $userTS_text); |
1407
|
|
|
$cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('hash'); |
1408
|
|
|
if (!($this->userTS = $cache->get($hash))) { |
1409
|
|
|
$parseObj = GeneralUtility::makeInstance(TypoScriptParser::class); |
1410
|
|
|
$conditionMatcher = GeneralUtility::makeInstance(ConditionMatcher::class); |
1411
|
|
|
$parseObj->parse($userTS_text, $conditionMatcher); |
1412
|
|
|
$this->userTS = $parseObj->setup; |
1413
|
|
|
$cache->set($hash, $this->userTS, ['UserTSconfig'], 0); |
1414
|
|
|
// Ensure to update UC later |
1415
|
|
|
$this->userTSUpdated = true; |
1416
|
|
|
} |
1417
|
|
|
} |
1418
|
|
|
|
1419
|
|
|
/** |
1420
|
|
|
* Sets up all file storages for a user. |
1421
|
|
|
* Needs to be called AFTER the groups have been loaded. |
1422
|
|
|
*/ |
1423
|
|
|
protected function initializeFileStorages() |
1424
|
|
|
{ |
1425
|
|
|
$this->fileStorages = []; |
1426
|
|
|
/** @var \TYPO3\CMS\Core\Resource\StorageRepository $storageRepository */ |
1427
|
|
|
$storageRepository = GeneralUtility::makeInstance(StorageRepository::class); |
1428
|
|
|
// Admin users have all file storages visible, without any filters |
1429
|
|
|
if ($this->isAdmin()) { |
1430
|
|
|
$storageObjects = $storageRepository->findAll(); |
1431
|
|
|
foreach ($storageObjects as $storageObject) { |
1432
|
|
|
$this->fileStorages[$storageObject->getUid()] = $storageObject; |
1433
|
|
|
} |
1434
|
|
|
} else { |
1435
|
|
|
// Regular users only have storages that are defined in their filemounts |
1436
|
|
|
// Permissions and file mounts for the storage are added in StoragePermissionAspect |
1437
|
|
|
foreach ($this->getFileMountRecords() as $row) { |
1438
|
|
|
if (!array_key_exists((int)$row['base'], $this->fileStorages)) { |
1439
|
|
|
$storageObject = $storageRepository->findByUid($row['base']); |
1440
|
|
|
if ($storageObject) { |
1441
|
|
|
$this->fileStorages[$storageObject->getUid()] = $storageObject; |
1442
|
|
|
} |
1443
|
|
|
} |
1444
|
|
|
} |
1445
|
|
|
} |
1446
|
|
|
|
1447
|
|
|
// This has to be called always in order to set certain filters |
1448
|
|
|
$this->evaluateUserSpecificFileFilterSettings(); |
1449
|
|
|
} |
1450
|
|
|
|
1451
|
|
|
/** |
1452
|
|
|
* Returns an array of category mount points. The category permissions from BE Groups |
1453
|
|
|
* are also taken into consideration and are merged into User permissions. |
1454
|
|
|
* |
1455
|
|
|
* @return array |
1456
|
|
|
*/ |
1457
|
|
|
public function getCategoryMountPoints() |
1458
|
|
|
{ |
1459
|
|
|
$categoryMountPoints = ''; |
1460
|
|
|
|
1461
|
|
|
// Category mounts of the groups |
1462
|
|
|
if (is_array($this->userGroups)) { |
|
|
|
|
1463
|
|
|
foreach ($this->userGroups as $group) { |
1464
|
|
|
if ($group['category_perms']) { |
1465
|
|
|
$categoryMountPoints .= ',' . $group['category_perms']; |
1466
|
|
|
} |
1467
|
|
|
} |
1468
|
|
|
} |
1469
|
|
|
|
1470
|
|
|
// Category mounts of the user record |
1471
|
|
|
if ($this->user['category_perms']) { |
1472
|
|
|
$categoryMountPoints .= ',' . $this->user['category_perms']; |
1473
|
|
|
} |
1474
|
|
|
|
1475
|
|
|
// Make the ids unique |
1476
|
|
|
$categoryMountPoints = GeneralUtility::trimExplode(',', $categoryMountPoints); |
1477
|
|
|
$categoryMountPoints = array_filter($categoryMountPoints); // remove empty value |
1478
|
|
|
$categoryMountPoints = array_unique($categoryMountPoints); // remove unique value |
1479
|
|
|
|
1480
|
|
|
return $categoryMountPoints; |
1481
|
|
|
} |
1482
|
|
|
|
1483
|
|
|
/** |
1484
|
|
|
* Returns an array of file mount records, taking workspaces and user home and group home directories into account |
1485
|
|
|
* Needs to be called AFTER the groups have been loaded. |
1486
|
|
|
* |
1487
|
|
|
* @return array |
1488
|
|
|
* @internal |
1489
|
|
|
*/ |
1490
|
|
|
public function getFileMountRecords() |
1491
|
|
|
{ |
1492
|
|
|
$runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime'); |
1493
|
|
|
$fileMountRecordCache = $runtimeCache->get('backendUserAuthenticationFileMountRecords') ?: []; |
1494
|
|
|
|
1495
|
|
|
if (!empty($fileMountRecordCache)) { |
1496
|
|
|
return $fileMountRecordCache; |
1497
|
|
|
} |
1498
|
|
|
|
1499
|
|
|
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); |
1500
|
|
|
|
1501
|
|
|
// Processing file mounts (both from the user and the groups) |
1502
|
|
|
$fileMounts = array_unique(GeneralUtility::intExplode(',', $this->groupData['filemounts'], true)); |
1503
|
|
|
|
1504
|
|
|
// Limit file mounts if set in workspace record |
1505
|
|
|
if ($this->workspace > 0 && !empty($this->workspaceRec['file_mountpoints'])) { |
1506
|
|
|
$workspaceFileMounts = GeneralUtility::intExplode(',', $this->workspaceRec['file_mountpoints'], true); |
1507
|
|
|
$fileMounts = array_intersect($fileMounts, $workspaceFileMounts); |
1508
|
|
|
} |
1509
|
|
|
|
1510
|
|
|
if (!empty($fileMounts)) { |
1511
|
|
|
$orderBy = $GLOBALS['TCA']['sys_filemounts']['ctrl']['default_sortby'] ?? 'sorting'; |
1512
|
|
|
|
1513
|
|
|
$queryBuilder = $connectionPool->getQueryBuilderForTable('sys_filemounts'); |
1514
|
|
|
$queryBuilder->getRestrictions() |
1515
|
|
|
->removeAll() |
1516
|
|
|
->add(GeneralUtility::makeInstance(DeletedRestriction::class)) |
1517
|
|
|
->add(GeneralUtility::makeInstance(HiddenRestriction::class)) |
1518
|
|
|
->add(GeneralUtility::makeInstance(RootLevelRestriction::class)); |
1519
|
|
|
|
1520
|
|
|
$queryBuilder->select('*') |
1521
|
|
|
->from('sys_filemounts') |
1522
|
|
|
->where( |
1523
|
|
|
$queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($fileMounts, Connection::PARAM_INT_ARRAY)) |
1524
|
|
|
); |
1525
|
|
|
|
1526
|
|
|
foreach (QueryHelper::parseOrderBy($orderBy) as $fieldAndDirection) { |
1527
|
|
|
$queryBuilder->addOrderBy(...$fieldAndDirection); |
1528
|
|
|
} |
1529
|
|
|
|
1530
|
|
|
$fileMountRecords = $queryBuilder->execute()->fetchAll(\PDO::FETCH_ASSOC); |
1531
|
|
|
if ($fileMountRecords !== false) { |
1532
|
|
|
foreach ($fileMountRecords as $fileMount) { |
1533
|
|
|
$fileMountRecordCache[$fileMount['base'] . $fileMount['path']] = $fileMount; |
1534
|
|
|
} |
1535
|
|
|
} |
1536
|
|
|
} |
1537
|
|
|
|
1538
|
|
|
// Read-only file mounts |
1539
|
|
|
$readOnlyMountPoints = \trim($this->getTSConfig()['options.']['folderTree.']['altElementBrowserMountPoints'] ?? ''); |
1540
|
|
|
if ($readOnlyMountPoints) { |
1541
|
|
|
// We cannot use the API here but need to fetch the default storage record directly |
1542
|
|
|
// to not instantiate it (which directly applies mount points) before all mount points are resolved! |
1543
|
|
|
$queryBuilder = $connectionPool->getQueryBuilderForTable('sys_file_storage'); |
1544
|
|
|
$defaultStorageRow = $queryBuilder->select('uid') |
1545
|
|
|
->from('sys_file_storage') |
1546
|
|
|
->where( |
1547
|
|
|
$queryBuilder->expr()->eq('is_default', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)) |
1548
|
|
|
) |
1549
|
|
|
->setMaxResults(1) |
1550
|
|
|
->execute() |
1551
|
|
|
->fetch(\PDO::FETCH_ASSOC); |
1552
|
|
|
|
1553
|
|
|
$readOnlyMountPointArray = GeneralUtility::trimExplode(',', $readOnlyMountPoints); |
1554
|
|
|
foreach ($readOnlyMountPointArray as $readOnlyMountPoint) { |
1555
|
|
|
$readOnlyMountPointConfiguration = GeneralUtility::trimExplode(':', $readOnlyMountPoint); |
1556
|
|
|
if (count($readOnlyMountPointConfiguration) === 2) { |
1557
|
|
|
// A storage is passed in the configuration |
1558
|
|
|
$storageUid = (int)$readOnlyMountPointConfiguration[0]; |
1559
|
|
|
$path = $readOnlyMountPointConfiguration[1]; |
1560
|
|
|
} else { |
1561
|
|
|
if (empty($defaultStorageRow)) { |
1562
|
|
|
throw new \RuntimeException('Read only mount points have been defined in User TsConfig without specific storage, but a default storage could not be resolved.', 1404472382); |
1563
|
|
|
} |
1564
|
|
|
// Backwards compatibility: If no storage is passed, we use the default storage |
1565
|
|
|
$storageUid = $defaultStorageRow['uid']; |
1566
|
|
|
$path = $readOnlyMountPointConfiguration[0]; |
1567
|
|
|
} |
1568
|
|
|
$fileMountRecordCache[$storageUid . $path] = [ |
1569
|
|
|
'base' => $storageUid, |
1570
|
|
|
'title' => $path, |
1571
|
|
|
'path' => $path, |
1572
|
|
|
'read_only' => true |
1573
|
|
|
]; |
1574
|
|
|
} |
1575
|
|
|
} |
1576
|
|
|
|
1577
|
|
|
// Personal or Group filemounts are not accessible if file mount list is set in workspace record |
1578
|
|
|
if ($this->workspace <= 0 || empty($this->workspaceRec['file_mountpoints'])) { |
1579
|
|
|
// If userHomePath is set, we attempt to mount it |
1580
|
|
|
if ($GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath']) { |
1581
|
|
|
[$userHomeStorageUid, $userHomeFilter] = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['userHomePath'], 2); |
1582
|
|
|
$userHomeStorageUid = (int)$userHomeStorageUid; |
1583
|
|
|
$userHomeFilter = '/' . ltrim($userHomeFilter, '/'); |
1584
|
|
|
if ($userHomeStorageUid > 0) { |
1585
|
|
|
// Try and mount with [uid]_[username] |
1586
|
|
|
$path = $userHomeFilter . $this->user['uid'] . '_' . $this->user['username'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir']; |
1587
|
|
|
$fileMountRecordCache[$userHomeStorageUid . $path] = [ |
1588
|
|
|
'base' => $userHomeStorageUid, |
1589
|
|
|
'title' => $this->user['username'], |
1590
|
|
|
'path' => $path, |
1591
|
|
|
'read_only' => false, |
1592
|
|
|
'user_mount' => true |
1593
|
|
|
]; |
1594
|
|
|
// Try and mount with only [uid] |
1595
|
|
|
$path = $userHomeFilter . $this->user['uid'] . $GLOBALS['TYPO3_CONF_VARS']['BE']['userUploadDir']; |
1596
|
|
|
$fileMountRecordCache[$userHomeStorageUid . $path] = [ |
1597
|
|
|
'base' => $userHomeStorageUid, |
1598
|
|
|
'title' => $this->user['username'], |
1599
|
|
|
'path' => $path, |
1600
|
|
|
'read_only' => false, |
1601
|
|
|
'user_mount' => true |
1602
|
|
|
]; |
1603
|
|
|
} |
1604
|
|
|
} |
1605
|
|
|
|
1606
|
|
|
// Mount group home-dirs |
1607
|
|
|
$mountOptions = new BackendGroupMountOption((int)$this->user['options']); |
1608
|
|
|
if ($GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'] !== '' && $mountOptions->shouldUserIncludeFileMountsFromAssociatedGroups()) { |
1609
|
|
|
// If groupHomePath is set, we attempt to mount it |
1610
|
|
|
[$groupHomeStorageUid, $groupHomeFilter] = explode(':', $GLOBALS['TYPO3_CONF_VARS']['BE']['groupHomePath'], 2); |
1611
|
|
|
$groupHomeStorageUid = (int)$groupHomeStorageUid; |
1612
|
|
|
$groupHomeFilter = '/' . ltrim($groupHomeFilter, '/'); |
1613
|
|
|
if ($groupHomeStorageUid > 0) { |
1614
|
|
|
foreach ($this->userGroups as $groupData) { |
1615
|
|
|
$path = $groupHomeFilter . $groupData['uid']; |
1616
|
|
|
$fileMountRecordCache[$groupHomeStorageUid . $path] = [ |
1617
|
|
|
'base' => $groupHomeStorageUid, |
1618
|
|
|
'title' => $groupData['title'], |
1619
|
|
|
'path' => $path, |
1620
|
|
|
'read_only' => false, |
1621
|
|
|
'user_mount' => true |
1622
|
|
|
]; |
1623
|
|
|
} |
1624
|
|
|
} |
1625
|
|
|
} |
1626
|
|
|
} |
1627
|
|
|
|
1628
|
|
|
$runtimeCache->set('backendUserAuthenticationFileMountRecords', $fileMountRecordCache); |
1629
|
|
|
return $fileMountRecordCache; |
1630
|
|
|
} |
1631
|
|
|
|
1632
|
|
|
/** |
1633
|
|
|
* Returns an array with the filemounts for the user. |
1634
|
|
|
* Each filemount is represented with an array of a "name", "path" and "type". |
1635
|
|
|
* If no filemounts an empty array is returned. |
1636
|
|
|
* |
1637
|
|
|
* @return \TYPO3\CMS\Core\Resource\ResourceStorage[] |
1638
|
|
|
*/ |
1639
|
|
|
public function getFileStorages() |
1640
|
|
|
{ |
1641
|
|
|
// Initializing file mounts after the groups are fetched |
1642
|
|
|
if ($this->fileStorages === null) { |
1643
|
|
|
$this->initializeFileStorages(); |
1644
|
|
|
} |
1645
|
|
|
return $this->fileStorages; |
1646
|
|
|
} |
1647
|
|
|
|
1648
|
|
|
/** |
1649
|
|
|
* Adds filters based on what the user has set |
1650
|
|
|
* this should be done in this place, and called whenever needed, |
1651
|
|
|
* but only when needed |
1652
|
|
|
*/ |
1653
|
|
|
public function evaluateUserSpecificFileFilterSettings() |
1654
|
|
|
{ |
1655
|
|
|
// Add the option for also displaying the non-hidden files |
1656
|
|
|
if ($this->uc['showHiddenFilesAndFolders'] ?? false) { |
1657
|
|
|
FileNameFilter::setShowHiddenFilesAndFolders(true); |
1658
|
|
|
} |
1659
|
|
|
} |
1660
|
|
|
|
1661
|
|
|
/** |
1662
|
|
|
* Returns the information about file permissions. |
1663
|
|
|
* Previously, this was stored in the DB field fileoper_perms now it is file_permissions. |
1664
|
|
|
* Besides it can be handled via userTSconfig |
1665
|
|
|
* |
1666
|
|
|
* permissions.file.default { |
1667
|
|
|
* addFile = 1 |
1668
|
|
|
* readFile = 1 |
1669
|
|
|
* writeFile = 1 |
1670
|
|
|
* copyFile = 1 |
1671
|
|
|
* moveFile = 1 |
1672
|
|
|
* renameFile = 1 |
1673
|
|
|
* deleteFile = 1 |
1674
|
|
|
* |
1675
|
|
|
* addFolder = 1 |
1676
|
|
|
* readFolder = 1 |
1677
|
|
|
* writeFolder = 1 |
1678
|
|
|
* copyFolder = 1 |
1679
|
|
|
* moveFolder = 1 |
1680
|
|
|
* renameFolder = 1 |
1681
|
|
|
* deleteFolder = 1 |
1682
|
|
|
* recursivedeleteFolder = 1 |
1683
|
|
|
* } |
1684
|
|
|
* |
1685
|
|
|
* # overwrite settings for a specific storageObject |
1686
|
|
|
* permissions.file.storage.StorageUid { |
1687
|
|
|
* readFile = 1 |
1688
|
|
|
* recursivedeleteFolder = 0 |
1689
|
|
|
* } |
1690
|
|
|
* |
1691
|
|
|
* Please note that these permissions only apply, if the storage has the |
1692
|
|
|
* capabilities (browseable, writable), and if the driver allows for writing etc |
1693
|
|
|
* |
1694
|
|
|
* @return array |
1695
|
|
|
*/ |
1696
|
|
|
public function getFilePermissions() |
1697
|
|
|
{ |
1698
|
|
|
if (!isset($this->filePermissions)) { |
1699
|
|
|
$filePermissions = [ |
1700
|
|
|
// File permissions |
1701
|
|
|
'addFile' => false, |
1702
|
|
|
'readFile' => false, |
1703
|
|
|
'writeFile' => false, |
1704
|
|
|
'copyFile' => false, |
1705
|
|
|
'moveFile' => false, |
1706
|
|
|
'renameFile' => false, |
1707
|
|
|
'deleteFile' => false, |
1708
|
|
|
// Folder permissions |
1709
|
|
|
'addFolder' => false, |
1710
|
|
|
'readFolder' => false, |
1711
|
|
|
'writeFolder' => false, |
1712
|
|
|
'copyFolder' => false, |
1713
|
|
|
'moveFolder' => false, |
1714
|
|
|
'renameFolder' => false, |
1715
|
|
|
'deleteFolder' => false, |
1716
|
|
|
'recursivedeleteFolder' => false |
1717
|
|
|
]; |
1718
|
|
|
if ($this->isAdmin()) { |
1719
|
|
|
$filePermissions = array_map('is_bool', $filePermissions); |
1720
|
|
|
} else { |
1721
|
|
|
$userGroupRecordPermissions = GeneralUtility::trimExplode(',', $this->groupData['file_permissions'] ?? '', true); |
1722
|
|
|
array_walk( |
1723
|
|
|
$userGroupRecordPermissions, |
1724
|
|
|
function ($permission) use (&$filePermissions) { |
1725
|
|
|
$filePermissions[$permission] = true; |
1726
|
|
|
} |
1727
|
|
|
); |
1728
|
|
|
|
1729
|
|
|
// Finally overlay any userTSconfig |
1730
|
|
|
$permissionsTsConfig = $this->getTSConfig()['permissions.']['file.']['default.'] ?? []; |
1731
|
|
|
if (!empty($permissionsTsConfig)) { |
1732
|
|
|
array_walk( |
1733
|
|
|
$permissionsTsConfig, |
1734
|
|
|
function ($value, $permission) use (&$filePermissions) { |
1735
|
|
|
$filePermissions[$permission] = (bool)$value; |
1736
|
|
|
} |
1737
|
|
|
); |
1738
|
|
|
} |
1739
|
|
|
} |
1740
|
|
|
$this->filePermissions = $filePermissions; |
1741
|
|
|
} |
1742
|
|
|
return $this->filePermissions; |
1743
|
|
|
} |
1744
|
|
|
|
1745
|
|
|
/** |
1746
|
|
|
* Gets the file permissions for a storage |
1747
|
|
|
* by merging any storage-specific permissions for a |
1748
|
|
|
* storage with the default settings. |
1749
|
|
|
* Admin users will always get the default settings. |
1750
|
|
|
* |
1751
|
|
|
* @param \TYPO3\CMS\Core\Resource\ResourceStorage $storageObject |
1752
|
|
|
* @return array |
1753
|
|
|
*/ |
1754
|
|
|
public function getFilePermissionsForStorage(ResourceStorage $storageObject) |
1755
|
|
|
{ |
1756
|
|
|
$finalUserPermissions = $this->getFilePermissions(); |
1757
|
|
|
if (!$this->isAdmin()) { |
1758
|
|
|
$storageFilePermissions = $this->getTSConfig()['permissions.']['file.']['storage.'][$storageObject->getUid() . '.'] ?? []; |
1759
|
|
|
if (!empty($storageFilePermissions)) { |
1760
|
|
|
array_walk( |
1761
|
|
|
$storageFilePermissions, |
1762
|
|
|
function ($value, $permission) use (&$finalUserPermissions) { |
1763
|
|
|
$finalUserPermissions[$permission] = (bool)$value; |
1764
|
|
|
} |
1765
|
|
|
); |
1766
|
|
|
} |
1767
|
|
|
} |
1768
|
|
|
return $finalUserPermissions; |
1769
|
|
|
} |
1770
|
|
|
|
1771
|
|
|
/** |
1772
|
|
|
* Returns a \TYPO3\CMS\Core\Resource\Folder object that is used for uploading |
1773
|
|
|
* files by default. |
1774
|
|
|
* This is used for RTE and its magic images, as well as uploads |
1775
|
|
|
* in the TCEforms fields. |
1776
|
|
|
* |
1777
|
|
|
* The default upload folder for a user is the defaultFolder on the first |
1778
|
|
|
* filestorage/filemount that the user can access and to which files are allowed to be added |
1779
|
|
|
* however, you can set the users' upload folder like this: |
1780
|
|
|
* |
1781
|
|
|
* options.defaultUploadFolder = 3:myfolder/yourfolder/ |
1782
|
|
|
* |
1783
|
|
|
* @param int $pid PageUid |
1784
|
|
|
* @param string $table Table name |
1785
|
|
|
* @param string $field Field name |
1786
|
|
|
* @return \TYPO3\CMS\Core\Resource\Folder|bool The default upload folder for this user |
1787
|
|
|
*/ |
1788
|
|
|
public function getDefaultUploadFolder($pid = null, $table = null, $field = null) |
1789
|
|
|
{ |
1790
|
|
|
$uploadFolder = $this->getTSConfig()['options.']['defaultUploadFolder'] ?? ''; |
1791
|
|
|
if ($uploadFolder) { |
1792
|
|
|
try { |
1793
|
|
|
$uploadFolder = GeneralUtility::makeInstance(ResourceFactory::class)->getFolderObjectFromCombinedIdentifier($uploadFolder); |
1794
|
|
|
} catch (Exception\FolderDoesNotExistException $e) { |
1795
|
|
|
$uploadFolder = null; |
1796
|
|
|
} |
1797
|
|
|
} |
1798
|
|
|
if (empty($uploadFolder)) { |
1799
|
|
|
foreach ($this->getFileStorages() as $storage) { |
1800
|
|
|
if ($storage->isDefault() && $storage->isWritable()) { |
1801
|
|
|
try { |
1802
|
|
|
$uploadFolder = $storage->getDefaultFolder(); |
1803
|
|
|
if ($uploadFolder->checkActionPermission('write')) { |
1804
|
|
|
break; |
1805
|
|
|
} |
1806
|
|
|
$uploadFolder = null; |
1807
|
|
|
} catch (Exception $folderAccessException) { |
1808
|
|
|
// If the folder is not accessible (no permissions / does not exist) we skip this one. |
1809
|
|
|
} |
1810
|
|
|
break; |
1811
|
|
|
} |
1812
|
|
|
} |
1813
|
|
|
if (!$uploadFolder instanceof Folder) { |
1814
|
|
|
/** @var ResourceStorage $storage */ |
1815
|
|
|
foreach ($this->getFileStorages() as $storage) { |
1816
|
|
|
if ($storage->isWritable()) { |
1817
|
|
|
try { |
1818
|
|
|
$uploadFolder = $storage->getDefaultFolder(); |
1819
|
|
|
if ($uploadFolder->checkActionPermission('write')) { |
1820
|
|
|
break; |
1821
|
|
|
} |
1822
|
|
|
$uploadFolder = null; |
1823
|
|
|
} catch (Exception $folderAccessException) { |
1824
|
|
|
// If the folder is not accessible (no permissions / does not exist) try the next one. |
1825
|
|
|
} |
1826
|
|
|
} |
1827
|
|
|
} |
1828
|
|
|
} |
1829
|
|
|
} |
1830
|
|
|
|
1831
|
|
|
// HOOK: getDefaultUploadFolder |
1832
|
|
|
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getDefaultUploadFolder'] ?? [] as $_funcRef) { |
1833
|
|
|
$_params = [ |
1834
|
|
|
'uploadFolder' => $uploadFolder, |
1835
|
|
|
'pid' => $pid, |
1836
|
|
|
'table' => $table, |
1837
|
|
|
'field' => $field, |
1838
|
|
|
]; |
1839
|
|
|
$uploadFolder = GeneralUtility::callUserFunction($_funcRef, $_params, $this); |
1840
|
|
|
} |
1841
|
|
|
|
1842
|
|
|
if ($uploadFolder instanceof Folder) { |
1843
|
|
|
return $uploadFolder; |
1844
|
|
|
} |
1845
|
|
|
return false; |
1846
|
|
|
} |
1847
|
|
|
|
1848
|
|
|
/** |
1849
|
|
|
* Returns a \TYPO3\CMS\Core\Resource\Folder object that could be used for uploading |
1850
|
|
|
* temporary files in user context. The folder _temp_ below the default upload folder |
1851
|
|
|
* of the user is used. |
1852
|
|
|
* |
1853
|
|
|
* @return \TYPO3\CMS\Core\Resource\Folder|null |
1854
|
|
|
* @see \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::getDefaultUploadFolder() |
1855
|
|
|
*/ |
1856
|
|
|
public function getDefaultUploadTemporaryFolder() |
1857
|
|
|
{ |
1858
|
|
|
$defaultTemporaryFolder = null; |
1859
|
|
|
$defaultFolder = $this->getDefaultUploadFolder(); |
1860
|
|
|
|
1861
|
|
|
if ($defaultFolder !== false) { |
1862
|
|
|
$tempFolderName = '_temp_'; |
1863
|
|
|
$createFolder = !$defaultFolder->hasFolder($tempFolderName); |
1864
|
|
|
if ($createFolder === true) { |
1865
|
|
|
try { |
1866
|
|
|
$defaultTemporaryFolder = $defaultFolder->createFolder($tempFolderName); |
1867
|
|
|
} catch (Exception $folderAccessException) { |
|
|
|
|
1868
|
|
|
} |
1869
|
|
|
} else { |
1870
|
|
|
$defaultTemporaryFolder = $defaultFolder->getSubfolder($tempFolderName); |
1871
|
|
|
} |
1872
|
|
|
} |
1873
|
|
|
|
1874
|
|
|
return $defaultTemporaryFolder; |
1875
|
|
|
} |
1876
|
|
|
|
1877
|
|
|
/** |
1878
|
|
|
* Initializing workspace. |
1879
|
|
|
* Called from within this function, see fetchGroupData() |
1880
|
|
|
* |
1881
|
|
|
* @see fetchGroupData() |
1882
|
|
|
* @internal should only be used from within TYPO3 Core |
1883
|
|
|
*/ |
1884
|
|
|
public function workspaceInit() |
1885
|
|
|
{ |
1886
|
|
|
// Initializing workspace by evaluating and setting the workspace, possibly updating it in the user record! |
1887
|
|
|
$this->setWorkspace($this->user['workspace_id']); |
1888
|
|
|
// Limiting the DB mountpoints if there any selected in the workspace record |
1889
|
|
|
$this->initializeDbMountpointsInWorkspace(); |
1890
|
|
|
$allowed_languages = (string)($this->getTSConfig()['options.']['workspaces.']['allowed_languages.'][$this->workspace] ?? ''); |
1891
|
|
|
if ($allowed_languages !== '') { |
1892
|
|
|
$this->groupData['allowed_languages'] = StringUtility::uniqueList($allowed_languages); |
1893
|
|
|
} |
1894
|
|
|
} |
1895
|
|
|
|
1896
|
|
|
/** |
1897
|
|
|
* Limiting the DB mountpoints if there any selected in the workspace record |
1898
|
|
|
*/ |
1899
|
|
|
protected function initializeDbMountpointsInWorkspace() |
1900
|
|
|
{ |
1901
|
|
|
$dbMountpoints = trim($this->workspaceRec['db_mountpoints'] ?? ''); |
1902
|
|
|
if ($this->workspace > 0 && $dbMountpoints != '') { |
1903
|
|
|
$filteredDbMountpoints = []; |
1904
|
|
|
// Notice: We cannot call $this->getPagePermsClause(1); |
1905
|
|
|
// as usual because the group-list is not available at this point. |
1906
|
|
|
// But bypassing is fine because all we want here is check if the |
1907
|
|
|
// workspace mounts are inside the current webmounts rootline. |
1908
|
|
|
// The actual permission checking on page level is done elsewhere |
1909
|
|
|
// as usual anyway before the page tree is rendered. |
1910
|
|
|
$readPerms = '1=1'; |
1911
|
|
|
// Traverse mount points of the workspace, add them, |
1912
|
|
|
// but make sure they match against the users' DB mounts |
1913
|
|
|
|
1914
|
|
|
$workspaceWebMounts = GeneralUtility::intExplode(',', $dbMountpoints); |
1915
|
|
|
$webMountsOfUser = GeneralUtility::intExplode(',', $this->groupData['webmounts']); |
1916
|
|
|
$webMountsOfUser = array_combine($webMountsOfUser, $webMountsOfUser) ?: []; |
1917
|
|
|
|
1918
|
|
|
$entryPointRootLineUids = []; |
1919
|
|
|
foreach ($webMountsOfUser as $webMountPageId) { |
1920
|
|
|
$rootLine = BackendUtility::BEgetRootLine($webMountPageId, '', true); |
1921
|
|
|
$entryPointRootLineUids[$webMountPageId] = array_map('intval', array_column($rootLine, 'uid')); |
1922
|
|
|
} |
1923
|
|
|
foreach ($entryPointRootLineUids as $webMountOfUser => $uidsOfRootLine) { |
1924
|
|
|
// Remove the DB mounts of the user if the DB mount is not in the list of |
1925
|
|
|
// workspace mounts |
1926
|
|
|
foreach ($workspaceWebMounts as $webmountOfWorkspace) { |
1927
|
|
|
// This workspace DB mount is somewhere in the rootline of the users' web mount, |
1928
|
|
|
// so this is "OK" to be included |
1929
|
|
|
if (in_array($webmountOfWorkspace, $uidsOfRootLine, true)) { |
1930
|
|
|
continue; |
1931
|
|
|
} |
1932
|
|
|
// Remove the user's DB Mount (possible via array_combine, see above) |
1933
|
|
|
unset($webMountsOfUser[$webMountOfUser]); |
1934
|
|
|
} |
1935
|
|
|
} |
1936
|
|
|
$dbMountpoints = array_merge($workspaceWebMounts, $webMountsOfUser); |
1937
|
|
|
$dbMountpoints = array_unique($dbMountpoints); |
1938
|
|
|
foreach ($dbMountpoints as $mpId) { |
1939
|
|
|
if ($this->isInWebMount($mpId, $readPerms)) { |
|
|
|
|
1940
|
|
|
$filteredDbMountpoints[] = $mpId; |
1941
|
|
|
} |
1942
|
|
|
} |
1943
|
|
|
// Re-insert webmounts |
1944
|
|
|
$this->groupData['webmounts'] = implode(',', $filteredDbMountpoints); |
1945
|
|
|
} |
1946
|
|
|
} |
1947
|
|
|
|
1948
|
|
|
/** |
1949
|
|
|
* Checking if a workspace is allowed for backend user |
1950
|
|
|
* |
1951
|
|
|
* @param mixed $wsRec If integer, workspace record is looked up, if array it is seen as a Workspace record with at least uid, title, members and adminusers columns. Can be faked for workspaces uid 0 and -1 (online and offline) |
1952
|
|
|
* @param string $fields List of fields to select. Default fields are all |
1953
|
|
|
* @return array Output will also show how access was granted. Admin users will have a true output regardless of input. |
1954
|
|
|
* @internal should only be used from within TYPO3 Core |
1955
|
|
|
*/ |
1956
|
|
|
public function checkWorkspace($wsRec, $fields = '*') |
1957
|
|
|
{ |
1958
|
|
|
$retVal = false; |
1959
|
|
|
// If not array, look up workspace record: |
1960
|
|
|
if (!is_array($wsRec)) { |
1961
|
|
|
switch ((string)$wsRec) { |
1962
|
|
|
case '0': |
1963
|
|
|
$wsRec = ['uid' => $wsRec]; |
1964
|
|
|
break; |
1965
|
|
|
default: |
1966
|
|
|
if (ExtensionManagementUtility::isLoaded('workspaces')) { |
1967
|
|
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace'); |
1968
|
|
|
$queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class)); |
1969
|
|
|
$wsRec = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields)) |
1970
|
|
|
->from('sys_workspace') |
1971
|
|
|
->where($queryBuilder->expr()->eq( |
1972
|
|
|
'uid', |
1973
|
|
|
$queryBuilder->createNamedParameter($wsRec, \PDO::PARAM_INT) |
1974
|
|
|
)) |
1975
|
|
|
->orderBy('title') |
1976
|
|
|
->setMaxResults(1) |
1977
|
|
|
->execute() |
1978
|
|
|
->fetch(\PDO::FETCH_ASSOC); |
1979
|
|
|
} |
1980
|
|
|
} |
1981
|
|
|
} |
1982
|
|
|
// If wsRec is set to an array, evaluate it: |
1983
|
|
|
if (is_array($wsRec)) { |
1984
|
|
|
if ($this->isAdmin()) { |
1985
|
|
|
return array_merge($wsRec, ['_ACCESS' => 'admin']); |
1986
|
|
|
} |
1987
|
|
|
switch ((string)$wsRec['uid']) { |
1988
|
|
|
case '0': |
1989
|
|
|
$retVal = (($this->groupData['workspace_perms'] ?? 0) & 1) |
1990
|
|
|
? array_merge($wsRec, ['_ACCESS' => 'online']) |
1991
|
|
|
: false; |
1992
|
|
|
break; |
1993
|
|
|
default: |
1994
|
|
|
// Checking if the guy is admin: |
1995
|
|
|
if (GeneralUtility::inList($wsRec['adminusers'], 'be_users_' . $this->user['uid'])) { |
1996
|
|
|
return array_merge($wsRec, ['_ACCESS' => 'owner']); |
1997
|
|
|
} |
1998
|
|
|
// Checking if he is owner through a user group of his: |
1999
|
|
|
foreach ($this->userGroupsUID as $groupUid) { |
2000
|
|
|
if (GeneralUtility::inList($wsRec['adminusers'], 'be_groups_' . $groupUid)) { |
2001
|
|
|
return array_merge($wsRec, ['_ACCESS' => 'owner']); |
2002
|
|
|
} |
2003
|
|
|
} |
2004
|
|
|
// Checking if he is member as user: |
2005
|
|
|
if (GeneralUtility::inList($wsRec['members'], 'be_users_' . $this->user['uid'])) { |
2006
|
|
|
return array_merge($wsRec, ['_ACCESS' => 'member']); |
2007
|
|
|
} |
2008
|
|
|
// Checking if he is member through a user group of his: |
2009
|
|
|
foreach ($this->userGroupsUID as $groupUid) { |
2010
|
|
|
if (GeneralUtility::inList($wsRec['members'], 'be_groups_' . $groupUid)) { |
2011
|
|
|
return array_merge($wsRec, ['_ACCESS' => 'member']); |
2012
|
|
|
} |
2013
|
|
|
} |
2014
|
|
|
} |
2015
|
|
|
} |
2016
|
|
|
return $retVal; |
|
|
|
|
2017
|
|
|
} |
2018
|
|
|
|
2019
|
|
|
/** |
2020
|
|
|
* Uses checkWorkspace() to check if current workspace is available for user. |
2021
|
|
|
* This function caches the result and so can be called many times with no performance loss. |
2022
|
|
|
* |
2023
|
|
|
* @return array See checkWorkspace() |
2024
|
|
|
* @see checkWorkspace() |
2025
|
|
|
* @internal should only be used from within TYPO3 Core |
2026
|
|
|
*/ |
2027
|
|
|
public function checkWorkspaceCurrent() |
2028
|
|
|
{ |
2029
|
|
|
if (!isset($this->checkWorkspaceCurrent_cache)) { |
2030
|
|
|
$this->checkWorkspaceCurrent_cache = $this->checkWorkspace($this->workspace); |
2031
|
|
|
} |
2032
|
|
|
return $this->checkWorkspaceCurrent_cache; |
2033
|
|
|
} |
2034
|
|
|
|
2035
|
|
|
/** |
2036
|
|
|
* Setting workspace ID |
2037
|
|
|
* |
2038
|
|
|
* @param int $workspaceId ID of workspace to set for backend user. If not valid the default workspace for BE user is found and set. |
2039
|
|
|
* @internal should only be used from within TYPO3 Core |
2040
|
|
|
*/ |
2041
|
|
|
public function setWorkspace($workspaceId) |
2042
|
|
|
{ |
2043
|
|
|
// Check workspace validity and if not found, revert to default workspace. |
2044
|
|
|
if (!$this->setTemporaryWorkspace($workspaceId)) { |
2045
|
|
|
$this->setDefaultWorkspace(); |
2046
|
|
|
} |
2047
|
|
|
// Unset access cache: |
2048
|
|
|
$this->checkWorkspaceCurrent_cache = null; |
2049
|
|
|
// If ID is different from the stored one, change it: |
2050
|
|
|
if ((int)$this->workspace !== (int)$this->user['workspace_id']) { |
2051
|
|
|
$this->user['workspace_id'] = $this->workspace; |
2052
|
|
|
GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('be_users')->update( |
2053
|
|
|
'be_users', |
2054
|
|
|
['workspace_id' => $this->user['workspace_id']], |
2055
|
|
|
['uid' => (int)$this->user['uid']] |
2056
|
|
|
); |
2057
|
|
|
$this->writelog(SystemLogType::EXTENSION, SystemLogGenericAction::UNDEFINED, SystemLogErrorClassification::MESSAGE, 0, 'User changed workspace to "' . $this->workspace . '"', []); |
2058
|
|
|
} |
2059
|
|
|
} |
2060
|
|
|
|
2061
|
|
|
/** |
2062
|
|
|
* Sets a temporary workspace in the context of the current backend user. |
2063
|
|
|
* |
2064
|
|
|
* @param int $workspaceId |
2065
|
|
|
* @return bool |
2066
|
|
|
* @internal should only be used from within TYPO3 Core |
2067
|
|
|
*/ |
2068
|
|
|
public function setTemporaryWorkspace($workspaceId) |
2069
|
|
|
{ |
2070
|
|
|
$result = false; |
2071
|
|
|
$workspaceRecord = $this->checkWorkspace($workspaceId); |
2072
|
|
|
|
2073
|
|
|
if ($workspaceRecord) { |
|
|
|
|
2074
|
|
|
$this->workspaceRec = $workspaceRecord; |
2075
|
|
|
$this->workspace = (int)$workspaceId; |
2076
|
|
|
$result = true; |
2077
|
|
|
} |
2078
|
|
|
|
2079
|
|
|
return $result; |
2080
|
|
|
} |
2081
|
|
|
|
2082
|
|
|
/** |
2083
|
|
|
* Sets the default workspace in the context of the current backend user. |
2084
|
|
|
* @internal should only be used from within TYPO3 Core |
2085
|
|
|
*/ |
2086
|
|
|
public function setDefaultWorkspace() |
2087
|
|
|
{ |
2088
|
|
|
$this->workspace = (int)$this->getDefaultWorkspace(); |
2089
|
|
|
$this->workspaceRec = $this->checkWorkspace($this->workspace); |
2090
|
|
|
} |
2091
|
|
|
|
2092
|
|
|
/** |
2093
|
|
|
* Return default workspace ID for user, |
2094
|
|
|
* if EXT:workspaces is not installed the user will be pushed to the |
2095
|
|
|
* Live workspace, if he has access to. If no workspace is available for the user, the workspace ID is set to "-99" |
2096
|
|
|
* |
2097
|
|
|
* @return int Default workspace id. |
2098
|
|
|
* @internal should only be used from within TYPO3 Core |
2099
|
|
|
*/ |
2100
|
|
|
public function getDefaultWorkspace() |
2101
|
|
|
{ |
2102
|
|
|
if (!ExtensionManagementUtility::isLoaded('workspaces')) { |
2103
|
|
|
return 0; |
2104
|
|
|
} |
2105
|
|
|
// Online is default |
2106
|
|
|
if ($this->checkWorkspace(0)) { |
2107
|
|
|
return 0; |
2108
|
|
|
} |
2109
|
|
|
// Otherwise -99 is the fallback |
2110
|
|
|
$defaultWorkspace = -99; |
2111
|
|
|
// Traverse all workspaces |
2112
|
|
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_workspace'); |
2113
|
|
|
$queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(RootLevelRestriction::class)); |
2114
|
|
|
$result = $queryBuilder->select('*') |
2115
|
|
|
->from('sys_workspace') |
2116
|
|
|
->orderBy('title') |
2117
|
|
|
->execute(); |
2118
|
|
|
while ($workspaceRecord = $result->fetch()) { |
2119
|
|
|
if ($this->checkWorkspace($workspaceRecord)) { |
2120
|
|
|
$defaultWorkspace = (int)$workspaceRecord['uid']; |
2121
|
|
|
break; |
2122
|
|
|
} |
2123
|
|
|
} |
2124
|
|
|
return $defaultWorkspace; |
2125
|
|
|
} |
2126
|
|
|
|
2127
|
|
|
/** |
2128
|
|
|
* Writes an entry in the logfile/table |
2129
|
|
|
* Documentation in "TYPO3 Core API" |
2130
|
|
|
* |
2131
|
|
|
* @param int $type Denotes which module that has submitted the entry. See "TYPO3 Core API". Use "4" for extensions. |
2132
|
|
|
* @param int $action Denotes which specific operation that wrote the entry. Use "0" when no sub-categorizing applies |
2133
|
|
|
* @param int $error Flag. 0 = message, 1 = error (user problem), 2 = System Error (which should not happen), 3 = security notice (admin) |
2134
|
|
|
* @param int $details_nr The message number. Specific for each $type and $action. This will make it possible to translate errormessages to other languages |
2135
|
|
|
* @param string $details Default text that follows the message (in english!). Possibly translated by identification through type/action/details_nr |
2136
|
|
|
* @param array $data Data that follows the log. Might be used to carry special information. If an array the first 5 entries (0-4) will be sprintf'ed with the details-text |
2137
|
|
|
* @param string $tablename Table name. Special field used by tce_main.php. |
2138
|
|
|
* @param int|string $recuid Record UID. Special field used by tce_main.php. |
2139
|
|
|
* @param int|string $recpid Record PID. Special field used by tce_main.php. OBSOLETE |
2140
|
|
|
* @param int $event_pid The page_uid (pid) where the event occurred. Used to select log-content for specific pages. |
2141
|
|
|
* @param string $NEWid Special field used by tce_main.php. NEWid string of newly created records. |
2142
|
|
|
* @param int $userId Alternative Backend User ID (used for logging login actions where this is not yet known). |
2143
|
|
|
* @return int Log entry ID. |
2144
|
|
|
*/ |
2145
|
|
|
public function writelog($type, $action, $error, $details_nr, $details, $data, $tablename = '', $recuid = '', $recpid = '', $event_pid = -1, $NEWid = '', $userId = 0) |
2146
|
|
|
{ |
2147
|
|
|
if (!$userId && !empty($this->user['uid'])) { |
2148
|
|
|
$userId = $this->user['uid']; |
2149
|
|
|
} |
2150
|
|
|
|
2151
|
|
|
if ($backuserid = $this->getOriginalUserIdWhenInSwitchUserMode()) { |
2152
|
|
|
if (empty($data)) { |
2153
|
|
|
$data = []; |
2154
|
|
|
} |
2155
|
|
|
$data['originalUser'] = $backuserid; |
2156
|
|
|
} |
2157
|
|
|
|
2158
|
|
|
// @todo Remove this once this method is properly typed. |
2159
|
|
|
$type = (int)$type; |
2160
|
|
|
|
2161
|
|
|
$fields = [ |
2162
|
|
|
'userid' => (int)$userId, |
2163
|
|
|
'type' => $type, |
2164
|
|
|
'channel' => Type::toChannel($type), |
2165
|
|
|
'level' => Type::toLevel($type), |
2166
|
|
|
'action' => (int)$action, |
2167
|
|
|
'error' => (int)$error, |
2168
|
|
|
'details_nr' => (int)$details_nr, |
2169
|
|
|
'details' => $details, |
2170
|
|
|
'log_data' => serialize($data), |
2171
|
|
|
'tablename' => $tablename, |
2172
|
|
|
'recuid' => (int)$recuid, |
2173
|
|
|
'IP' => (string)GeneralUtility::getIndpEnv('REMOTE_ADDR'), |
2174
|
|
|
'tstamp' => $GLOBALS['EXEC_TIME'] ?? time(), |
2175
|
|
|
'event_pid' => (int)$event_pid, |
2176
|
|
|
'NEWid' => $NEWid, |
2177
|
|
|
'workspace' => $this->workspace |
2178
|
|
|
]; |
2179
|
|
|
|
2180
|
|
|
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_log'); |
2181
|
|
|
$connection->insert( |
2182
|
|
|
'sys_log', |
2183
|
|
|
$fields, |
2184
|
|
|
[ |
2185
|
|
|
\PDO::PARAM_INT, |
2186
|
|
|
\PDO::PARAM_INT, |
2187
|
|
|
\PDO::PARAM_STR, |
2188
|
|
|
\PDO::PARAM_STR, |
2189
|
|
|
\PDO::PARAM_INT, |
2190
|
|
|
\PDO::PARAM_INT, |
2191
|
|
|
\PDO::PARAM_INT, |
2192
|
|
|
\PDO::PARAM_STR, |
2193
|
|
|
\PDO::PARAM_STR, |
2194
|
|
|
\PDO::PARAM_STR, |
2195
|
|
|
\PDO::PARAM_INT, |
2196
|
|
|
\PDO::PARAM_STR, |
2197
|
|
|
\PDO::PARAM_INT, |
2198
|
|
|
\PDO::PARAM_INT, |
2199
|
|
|
\PDO::PARAM_STR, |
2200
|
|
|
\PDO::PARAM_STR, |
2201
|
|
|
] |
2202
|
|
|
); |
2203
|
|
|
|
2204
|
|
|
return (int)$connection->lastInsertId('sys_log'); |
2205
|
|
|
} |
2206
|
|
|
|
2207
|
|
|
/** |
2208
|
|
|
* Getter for the cookie name |
2209
|
|
|
* |
2210
|
|
|
* @static |
2211
|
|
|
* @return string returns the configured cookie name |
2212
|
|
|
*/ |
2213
|
|
|
public static function getCookieName() |
2214
|
|
|
{ |
2215
|
|
|
$configuredCookieName = trim($GLOBALS['TYPO3_CONF_VARS']['BE']['cookieName']); |
2216
|
|
|
if (empty($configuredCookieName)) { |
2217
|
|
|
$configuredCookieName = 'be_typo_user'; |
2218
|
|
|
} |
2219
|
|
|
return $configuredCookieName; |
2220
|
|
|
} |
2221
|
|
|
|
2222
|
|
|
/** |
2223
|
|
|
* Check if user is logged in and if so, call ->fetchGroupData() to load group information and |
2224
|
|
|
* access lists of all kind, further check IP, set the ->uc array. |
2225
|
|
|
* If no user is logged in the default behaviour is to exit with an error message. |
2226
|
|
|
* This function is called right after ->start() in fx. the TYPO3 Bootstrap. |
2227
|
|
|
* |
2228
|
|
|
* @param bool $proceedIfNoUserIsLoggedIn if this option is set, then there won't be a redirect to the login screen of the Backend - used for areas in the backend which do not need user rights like the login page. |
2229
|
|
|
* @throws \RuntimeException |
2230
|
|
|
* @todo deprecate |
2231
|
|
|
*/ |
2232
|
|
|
public function backendCheckLogin($proceedIfNoUserIsLoggedIn = false) |
2233
|
|
|
{ |
2234
|
|
|
if (empty($this->user['uid'])) { |
2235
|
|
|
if ($proceedIfNoUserIsLoggedIn === false) { |
2236
|
|
|
$url = GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir; |
2237
|
|
|
throw new ImmediateResponseException(new RedirectResponse($url, 303), 1607271747); |
2238
|
|
|
} |
2239
|
|
|
} else { |
2240
|
|
|
if ($this->isUserAllowedToLogin()) { |
2241
|
|
|
$this->initializeBackendLogin(); |
2242
|
|
|
} else { |
2243
|
|
|
throw new \RuntimeException('Login Error: TYPO3 is in maintenance mode at the moment. Only administrators are allowed access.', 1294585860); |
2244
|
|
|
} |
2245
|
|
|
} |
2246
|
|
|
} |
2247
|
|
|
|
2248
|
|
|
/** |
2249
|
|
|
* @internal |
2250
|
|
|
*/ |
2251
|
|
|
public function initializeBackendLogin(): void |
2252
|
|
|
{ |
2253
|
|
|
// The groups are fetched and ready for permission checking in this initialization. |
2254
|
|
|
// Tables.php must be read before this because stuff like the modules has impact in this |
2255
|
|
|
$this->fetchGroupData(); |
2256
|
|
|
// Setting the UC array. It's needed with fetchGroupData first, due to default/overriding of values. |
2257
|
|
|
$this->backendSetUC(); |
2258
|
|
|
if ($this->loginSessionStarted) { |
2259
|
|
|
// Also, if there is a recovery link set, unset it now |
2260
|
|
|
// this will be moved into its own Event at a later stage. |
2261
|
|
|
// If a token was set previously, this is now unset, as it was now possible to log-in |
2262
|
|
|
if ($this->user['password_reset_token'] ?? '') { |
2263
|
|
|
GeneralUtility::makeInstance(ConnectionPool::class) |
2264
|
|
|
->getConnectionForTable($this->user_table) |
2265
|
|
|
->update($this->user_table, ['password_reset_token' => ''], ['uid' => $this->user['uid']]); |
2266
|
|
|
} |
2267
|
|
|
// Process hooks |
2268
|
|
|
$hooks = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['backendUserLogin']; |
2269
|
|
|
foreach ($hooks ?? [] as $_funcRef) { |
2270
|
|
|
$_params = ['user' => $this->user]; |
2271
|
|
|
GeneralUtility::callUserFunction($_funcRef, $_params, $this); |
2272
|
|
|
} |
2273
|
|
|
} |
2274
|
|
|
} |
2275
|
|
|
|
2276
|
|
|
/** |
2277
|
|
|
* Initialize the internal ->uc array for the backend user |
2278
|
|
|
* Will make the overrides if necessary, and write the UC back to the be_users record if changes has happened |
2279
|
|
|
* |
2280
|
|
|
* @internal |
2281
|
|
|
*/ |
2282
|
|
|
public function backendSetUC() |
2283
|
|
|
{ |
2284
|
|
|
// UC - user configuration is a serialized array inside the user object |
2285
|
|
|
// If there is a saved uc we implement that instead of the default one. |
2286
|
|
|
$this->unpack_uc(); |
2287
|
|
|
// Setting defaults if uc is empty |
2288
|
|
|
$updated = false; |
2289
|
|
|
if (!is_array($this->uc)) { |
2290
|
|
|
$this->uc = array_merge( |
2291
|
|
|
$this->uc_default, |
2292
|
|
|
(array)$GLOBALS['TYPO3_CONF_VARS']['BE']['defaultUC'], |
2293
|
|
|
GeneralUtility::removeDotsFromTS((array)($this->getTSConfig()['setup.']['default.'] ?? [])) |
2294
|
|
|
); |
2295
|
|
|
$this->overrideUC(); |
2296
|
|
|
$updated = true; |
2297
|
|
|
} |
2298
|
|
|
// If TSconfig is updated, update the defaultUC. |
2299
|
|
|
if ($this->userTSUpdated) { |
2300
|
|
|
$this->overrideUC(); |
2301
|
|
|
$updated = true; |
2302
|
|
|
} |
2303
|
|
|
// Setting default lang from be_user record, also update for backwards-compatibility |
2304
|
|
|
// @deprecated This will be removed in TYPO3 v12 |
2305
|
|
|
if (!isset($this->uc['lang']) || $this->uc['lang'] !== $this->user['lang']) { |
2306
|
|
|
$this->uc['lang'] = $this->user['lang']; |
2307
|
|
|
$updated = true; |
2308
|
|
|
} |
2309
|
|
|
// Setting the time of the first login: |
2310
|
|
|
if (!isset($this->uc['firstLoginTimeStamp'])) { |
2311
|
|
|
$this->uc['firstLoginTimeStamp'] = $GLOBALS['EXEC_TIME']; |
2312
|
|
|
$updated = true; |
2313
|
|
|
} |
2314
|
|
|
// Saving if updated. |
2315
|
|
|
if ($updated) { |
2316
|
|
|
$this->writeUC(); |
2317
|
|
|
} |
2318
|
|
|
} |
2319
|
|
|
|
2320
|
|
|
/** |
2321
|
|
|
* Override: Call this function every time the uc is updated. |
2322
|
|
|
* That is 1) by reverting to default values, 2) in the setup-module, 3) userTS changes (userauthgroup) |
2323
|
|
|
* |
2324
|
|
|
* @internal |
2325
|
|
|
*/ |
2326
|
|
|
public function overrideUC() |
2327
|
|
|
{ |
2328
|
|
|
$this->uc = array_merge((array)$this->uc, (array)($this->getTSConfig()['setup.']['override.'] ?? [])); |
2329
|
|
|
} |
2330
|
|
|
|
2331
|
|
|
/** |
2332
|
|
|
* Clears the user[uc] and ->uc to blank strings. Then calls ->backendSetUC() to fill it again with reset contents |
2333
|
|
|
* |
2334
|
|
|
* @internal |
2335
|
|
|
*/ |
2336
|
|
|
public function resetUC() |
2337
|
|
|
{ |
2338
|
|
|
$this->user['uc'] = ''; |
2339
|
|
|
$this->uc = ''; |
2340
|
|
|
$this->backendSetUC(); |
2341
|
|
|
} |
2342
|
|
|
|
2343
|
|
|
/** |
2344
|
|
|
* Determines whether a backend user is allowed to access the backend. |
2345
|
|
|
* |
2346
|
|
|
* The conditions are: |
2347
|
|
|
* + backend user is a regular user and adminOnly is not defined |
2348
|
|
|
* + backend user is an admin user |
2349
|
|
|
* + backend user is used in CLI context and adminOnly is explicitly set to "2" (see CommandLineUserAuthentication) |
2350
|
|
|
* + backend user is being controlled by an admin user |
2351
|
|
|
* |
2352
|
|
|
* @return bool Whether a backend user is allowed to access the backend |
2353
|
|
|
* @internal |
2354
|
|
|
*/ |
2355
|
|
|
public function isUserAllowedToLogin() |
2356
|
|
|
{ |
2357
|
|
|
$isUserAllowedToLogin = false; |
2358
|
|
|
$adminOnlyMode = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['adminOnly']; |
2359
|
|
|
// Backend user is allowed if adminOnly is not set or user is an admin: |
2360
|
|
|
if (!$adminOnlyMode || $this->isAdmin()) { |
2361
|
|
|
$isUserAllowedToLogin = true; |
2362
|
|
|
} elseif ($backUserId = $this->getOriginalUserIdWhenInSwitchUserMode()) { |
2363
|
|
|
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users'); |
2364
|
|
|
$isUserAllowedToLogin = (bool)$queryBuilder->count('uid') |
2365
|
|
|
->from('be_users') |
2366
|
|
|
->where( |
2367
|
|
|
$queryBuilder->expr()->eq( |
2368
|
|
|
'uid', |
2369
|
|
|
$queryBuilder->createNamedParameter($backUserId, \PDO::PARAM_INT) |
2370
|
|
|
), |
2371
|
|
|
$queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(1, \PDO::PARAM_INT)) |
2372
|
|
|
) |
2373
|
|
|
->execute() |
2374
|
|
|
->fetchOne(); |
2375
|
|
|
} |
2376
|
|
|
return $isUserAllowedToLogin; |
2377
|
|
|
} |
2378
|
|
|
|
2379
|
|
|
/** |
2380
|
|
|
* Logs out the current user and clears the form protection tokens. |
2381
|
|
|
*/ |
2382
|
|
|
public function logoff() |
2383
|
|
|
{ |
2384
|
|
|
if (isset($GLOBALS['BE_USER']) |
2385
|
|
|
&& $GLOBALS['BE_USER'] instanceof self |
2386
|
|
|
&& isset($GLOBALS['BE_USER']->user['uid']) |
2387
|
|
|
) { |
2388
|
|
|
FormProtectionFactory::get()->clean(); |
2389
|
|
|
// Release the locked records |
2390
|
|
|
$this->releaseLockedRecords((int)$GLOBALS['BE_USER']->user['uid']); |
2391
|
|
|
|
2392
|
|
|
if ($this->isSystemMaintainer()) { |
2393
|
|
|
// If user is system maintainer, destroy its possibly valid install tool session. |
2394
|
|
|
$session = new SessionService(); |
2395
|
|
|
$session->destroySession(); |
2396
|
|
|
} |
2397
|
|
|
} |
2398
|
|
|
parent::logoff(); |
2399
|
|
|
} |
2400
|
|
|
|
2401
|
|
|
/** |
2402
|
|
|
* Remove any "locked records" added for editing for the given user (= current backend user) |
2403
|
|
|
* @param int $userId |
2404
|
|
|
*/ |
2405
|
|
|
protected function releaseLockedRecords(int $userId) |
2406
|
|
|
{ |
2407
|
|
|
if ($userId > 0) { |
2408
|
|
|
GeneralUtility::makeInstance(ConnectionPool::class) |
2409
|
|
|
->getConnectionForTable('sys_lockedrecords') |
2410
|
|
|
->delete( |
2411
|
|
|
'sys_lockedrecords', |
2412
|
|
|
['userid' => $userId] |
2413
|
|
|
); |
2414
|
|
|
} |
2415
|
|
|
} |
2416
|
|
|
|
2417
|
|
|
/** |
2418
|
|
|
* Returns the uid of the backend user to return to. |
2419
|
|
|
* This is set when the current session is a "switch-user" session. |
2420
|
|
|
* |
2421
|
|
|
* @return int|null The user id |
2422
|
|
|
* @internal should only be used from within TYPO3 Core |
2423
|
|
|
*/ |
2424
|
|
|
public function getOriginalUserIdWhenInSwitchUserMode(): ?int |
2425
|
|
|
{ |
2426
|
|
|
$originalUserId = $this->getSessionData('backuserid'); |
2427
|
|
|
return $originalUserId ? (int)$originalUserId : null; |
2428
|
|
|
} |
2429
|
|
|
|
2430
|
|
|
/** |
2431
|
|
|
* @internal |
2432
|
|
|
*/ |
2433
|
|
|
protected function evaluateMfaRequirements(): void |
2434
|
|
|
{ |
2435
|
|
|
// In case the current session is a "switch-user" session, MFA is not required |
2436
|
|
|
if ($this->getOriginalUserIdWhenInSwitchUserMode() !== null) { |
2437
|
|
|
$this->logger->debug('MFA is skipped in switch user mode', [ |
|
|
|
|
2438
|
|
|
$this->userid_column => $this->user[$this->userid_column], |
2439
|
|
|
$this->username_column => $this->user[$this->username_column], |
2440
|
|
|
]); |
2441
|
|
|
return; |
2442
|
|
|
} |
2443
|
|
|
parent::evaluateMfaRequirements(); |
2444
|
|
|
} |
2445
|
|
|
} |
2446
|
|
|
|
In PHP, under loose comparison (like
==
, or!=
, orswitch
conditions), values of different types might be equal.For
integer
values, zero is a special case, in particular the following results might be unexpected: