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