BackendUserAuthentication.php 91.1 KB
Newer Older
1
2
3
<?php
namespace TYPO3\CMS\Core\Authentication;

4
5
/**
 * This file is part of the TYPO3 CMS project.
6
 *
7
8
9
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
10
 *
11
12
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
13
 *
14
15
 * The TYPO3 project - inspiring people to share!
 */
16
17
18
19

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Backend\Utility\BackendUtility;

20
21
22
23
24
25
26
27
28
29
30
31
32
/**
 * TYPO3 backend user authentication
 * Contains most of the functions used for checking permissions, authenticating users,
 * setting up the user, and API for user from outside.
 * This class contains the configuration of the database fields used plus some
 * functions for the authentication process of backend users.
 *
 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
 * @internal
 */
class BackendUserAuthentication extends \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication {

	/**
33
34
	 * Should be set to the usergroup-column (id-list) in the user-record
	 * @var string
35
36
37
38
	 */
	public $usergroup_column = 'usergroup';

	/**
39
40
	 * The name of the group-table
	 * @var string
41
42
43
44
	 */
	public $usergroup_table = 'be_groups';

	/**
45
46
47
	 * holds lists of eg. tables, fields and other values related to the permission-system. See fetchGroupData
	 * @var array
	 * @internal
48
49
50
51
52
53
	 */
	public $groupData = array(
		'filemounts' => array()
	);

	/**
54
55
	 * This array will hold the groups that the user is a member of
	 * @var array
56
57
58
59
	 */
	public $userGroups = array();

	/**
60
61
	 * This array holds the uid's of the groups in the listed order
	 * @var array
62
63
64
65
	 */
	public $userGroupsUID = array();

	/**
66
67
	 * This is $this->userGroupsUID imploded to a comma list... Will correspond to the 'usergroup_cached_list'
	 * @var string
68
69
70
71
	 */
	public $groupList = '';

	/**
72
73
74
75
76
77
	 * User workspace.
	 * -99 is ERROR (none available)
	 * -1 is offline
	 * 0 is online
	 * >0 is custom workspaces
	 * @var int
78
79
80
81
	 */
	public $workspace = -99;

	/**
82
83
	 * Custom workspace record if any
	 * @var array
84
85
86
87
	 */
	public $workspaceRec = array();

	/**
88
89
90
91
92
	 * Used to accumulate data for the user-group.
	 * DON NOT USE THIS EXTERNALLY!
	 * Use $this->groupData instead
	 * @var array
	 * @internal
93
94
95
96
	 */
	public $dataLists = array(
		'webmount_list' => '',
		'filemount_list' => '',
97
		'file_permissions' => '',
98
99
100
101
102
103
104
105
106
107
108
109
		'modList' => '',
		'tables_select' => '',
		'tables_modify' => '',
		'pagetypes_select' => '',
		'non_exclude_fields' => '',
		'explicit_allowdeny' => '',
		'allowed_languages' => '',
		'workspace_perms' => '',
		'custom_options' => ''
	);

	/**
110
111
	 * For debugging/display of order in which subgroups are included.
	 * @var array
112
113
114
115
	 */
	public $includeHierarchy = array();

	/**
116
117
	 * List of group_id's in the order they are processed.
	 * @var array
118
119
120
121
	 */
	public $includeGroupArray = array();

	/**
122
123
	 * Set to 'WIN', if windows
	 * @var string
124
125
126
127
	 */
	public $OS = '';

	/**
128
129
	 * Used to accumulate the TSconfig data of the user
	 * @var array
130
131
132
133
	 */
	public $TSdataArray = array();

	/**
134
135
	 * Contains the non-parsed user TSconfig
	 * @var string
136
137
138
139
	 */
	public $userTS_text = '';

	/**
140
141
	 * Contains the parsed user TSconfig
	 * @var array
142
143
144
145
	 */
	public $userTS = array();

	/**
146
147
	 * Set internally if the user TSconfig was parsed and needs to be cached.
	 * @var bool
148
	 */
149
	public $userTSUpdated = FALSE;
150
151

	/**
152
153
	 * Set this from outside if you want the user TSconfig to ALWAYS be parsed and not fetched from cache.
	 * @var bool
154
	 */
155
	public $userTS_dontGetCached = FALSE;
156
157

	/**
158
159
	 * RTE availability errors collected.
	 * @var array
160
161
162
163
	 */
	public $RTE_errors = array();

	/**
164
165
	 * Contains last error message
	 * @var string
166
167
168
169
	 */
	public $errorMsg = '';

	/**
170
171
	 * Cache for checkWorkspaceCurrent()
	 * @var array|NULL
172
173
174
175
	 */
	public $checkWorkspaceCurrent_cache = NULL;

	/**
176
	 * @var \TYPO3\CMS\Core\Resource\ResourceStorage[]
177
178
179
180
181
182
183
184
185
	 */
	protected $fileStorages;

	/**
	 * @var array
	 */
	protected $filePermissions;

	/**
186
187
	 * Table to use for session data
	 * @var string
188
189
190
191
	 */
	public $session_table = 'be_sessions';

	/**
192
193
	 * Table in database with user data
	 * @var string
194
195
196
197
	 */
	public $user_table = 'be_users';

	/**
198
199
	 * Column for login-name
	 * @var string
200
201
202
203
	 */
	public $username_column = 'username';

	/**
204
205
	 * Column for password
	 * @var string
206
207
208
209
	 */
	public $userident_column = 'password';

	/**
210
211
	 * Column for user-id
	 * @var string
212
213
214
215
	 */
	public $userid_column = 'uid';

	/**
216
	 * @var string
217
218
219
220
	 */
	public $lastLogin_column = 'lastlogin';

	/**
221
	 * @var array
222
223
224
225
226
227
228
229
230
231
	 */
	public $enablecolumns = array(
		'rootLevel' => 1,
		'deleted' => 'deleted',
		'disabled' => 'disable',
		'starttime' => 'starttime',
		'endtime' => 'endtime'
	);

	/**
232
233
	 * Form field with login-name
	 * @var string
234
235
236
237
	 */
	public $formfield_uname = 'username';

	/**
238
239
	 * Form field with password
	 * @var string
240
241
242
243
	 */
	public $formfield_uident = 'userident';

	/**
244
245
	 * Form field with a unique value which is used to encrypt the password and username
	 * @var string
246
247
248
249
	 */
	public $formfield_chalvalue = 'challenge';

	/**
250
251
	 * Form field with status: *'login', 'logout'
	 * @var string
252
253
254
255
	 */
	public $formfield_status = 'login_status';

	/**
256
257
	 * Decides if the writelog() function is called at login and logout
	 * @var bool
258
	 */
259
	public $writeStdLog = TRUE;
260
261

	/**
262
263
	 * If the writelog() functions is called if a login-attempt has be tried without success
	 * @var bool
264
	 */
265
	public $writeAttemptLog = TRUE;
266
267

	/**
268
269
270
271
	 * if > 0 : session-timeout in seconds.
	 * if FALSE/<0 : no timeout.
	 * if string: The string is field name from the user table where the timeout can be found.
	 * @var string|int
272
273
274
275
	 */
	public $auth_timeout_field = 6000;

	/**
276
	 * @var bool
277
	 */
278
	public $challengeStoredInCookie = TRUE;
279
280

	/**
281
	 * @var int
282
	 */
283
	public $firstMainGroup = 0;
284
285

	/**
286
287
	 * User Config
	 * @var array
288
289
290
291
	 */
	public $uc;

	/**
292
293
294
295
296
297
298
	 * User Config Default values:
	 * The array may contain other fields for configuration.
	 * For this, see "setup" extension and "TSConfig" document (User TSconfig, "setup.[xxx]....")
	 * Reserved keys for other storage of session data:
	 * moduleData
	 * moduleSessionID
	 * @var array
299
300
301
302
303
304
305
306
	 */
	public $uc_default = array(
		'interfaceSetup' => '',
		// serialized content that is used to store interface pane and menu positions. Set by the logout.php-script
		'moduleData' => array(),
		// user-data for the modules
		'thumbnailsByDefault' => 1,
		'emailMeAtLogin' => 0,
Benni Mack's avatar
Benni Mack committed
307
		'startModule' => 'help_AboutmodulesAboutmodules',
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
		'hideSubmoduleIcons' => 0,
		'helpText' => 1,
		'titleLen' => 50,
		'edit_showFieldHelp' => 'icon',
		'edit_RTE' => '1',
		'edit_docModuleUpload' => '1',
		'navFrameWidth' => '',
		// Default is 245 pixels
		'navFrameResizable' => 0,
		'resizeTextareas' => 1,
		'resizeTextareas_MaxHeight' => 500,
		'resizeTextareas_Flexible' => 0
	);

	/**
	 * Constructor
	 */
	public function __construct() {
326
		parent::__construct();
327
328
329
330
331
332
333
334
		$this->name = self::getCookieName();
		$this->loginType = 'BE';
	}

	/**
	 * Returns TRUE if user is admin
	 * Basically this function evaluates if the ->user[admin] field has bit 0 set. If so, user is admin.
	 *
335
	 * @return bool
336
337
	 */
	public function isAdmin() {
338
		return is_array($this->user) && ($this->user['admin'] & 1) == 1;
339
340
341
342
343
344
345
	}

	/**
	 * Returns TRUE if the current user is a member of group $groupId
	 * $groupId must be set. $this->groupList must contain groups
	 * Will return TRUE also if the user is a member of a group through subgroups.
	 *
346
	 * @param int $groupId Group ID to look for in $this->groupList
347
	 * @return bool
348
349
	 */
	public function isMemberOfGroup($groupId) {
350
		$groupId = (int)$groupId;
351
		if ($this->groupList && $groupId) {
352
			return GeneralUtility::inList($this->groupList, $groupId);
353
		}
354
		return FALSE;
355
356
357
358
359
360
361
362
363
364
365
366
367
368
	}

	/**
	 * Checks if the permissions is granted based on a page-record ($row) and $perms (binary and'ed)
	 *
	 * Bits for permissions, see $perms variable:
	 *
	 * 1 - Show:	See/Copy page and the pagecontent.
	 * 16- Edit pagecontent: Change/Add/Delete/Move pagecontent.
	 * 2- Edit page: Change/Move the page, eg. change title, startdate, hidden.
	 * 4- Delete page: Delete the page and pagecontent.
	 * 8- New pages: Create new pages under the page.
	 *
	 * @param array $row Is the pagerow for which the permissions is checked
369
	 * @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.
370
	 * @return bool
371
372
373
374
375
376
377
378
379
	 */
	public function doesUserHaveAccess($row, $perms) {
		$userPerms = $this->calcPerms($row);
		return ($userPerms & $perms) == $perms;
	}

	/**
	 * Checks if the page id, $id, is found within the webmounts set up for the user.
	 * This should ALWAYS be checked for any page id a user works with, whether it's about reading, writing or whatever.
380
381
382
383
384
	 * The point is that this will add the security that a user can NEVER touch parts outside his mounted
	 * pages in the page tree. This is otherwise possible if the raw page permissions allows for it.
	 * So this security check just makes it easier to make safe user configurations.
	 * If the user is admin OR if this feature is disabled
	 * (fx. by setting TYPO3_CONF_VARS['BE']['lockBeUserToDBmounts']=0) then it returns "1" right away
385
386
	 * Otherwise the function will return the uid of the webmount which was first found in the rootline of the input page $id
	 *
387
	 * @param int $id Page ID to check
388
	 * @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!)
389
390
391
	 * @param bool|int $exitOnError If set, then the function will exit with an error message.
	 * @throws \RuntimeException
	 * @return int|NULL The page UID of a page in the rootline that matched a mount point
392
393
394
395
396
	 */
	public function isInWebMount($id, $readPerms = '', $exitOnError = 0) {
		if (!$GLOBALS['TYPO3_CONF_VARS']['BE']['lockBeUserToDBmounts'] || $this->isAdmin()) {
			return 1;
		}
397
		$id = (int)$id;
398
		// Check if input id is an offline version page in which case we will map id to the online version:
399
		$checkRec = BackendUtility::getRecord('pages', $id, 'pid,t3ver_oid');
400
		if ($checkRec['pid'] == -1) {
401
			$id = (int)$checkRec['t3ver_oid'];
402
403
404
405
406
407
		}
		if (!$readPerms) {
			$readPerms = $this->getPagePermsClause(1);
		}
		if ($id > 0) {
			$wM = $this->returnWebmounts();
408
			$rL = BackendUtility::BEgetRootLine($id, ' AND ' . $readPerms);
409
410
411
412
413
414
415
416
417
			foreach ($rL as $v) {
				if ($v['uid'] && in_array($v['uid'], $wM)) {
					return $v['uid'];
				}
			}
		}
		if ($exitOnError) {
			throw new \RuntimeException('Access Error: This page is not within your DB-mounts', 1294586445);
		}
418
		return NULL;
419
420
421
422
423
424
	}

	/**
	 * Checks access to a backend module with the $MCONF passed as first argument
	 *
	 * @param array $conf $MCONF array of a backend module!
425
	 * @param bool $exitOnError If set, an array will issue an error message and exit.
426
	 * @throws \RuntimeException
427
	 * @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
428
429
	 */
	public function modAccess($conf, $exitOnError) {
430
		if (!BackendUtility::isModuleSetInTBE_MODULES($conf['name'])) {
431
			if ($exitOnError) {
432
				throw new \RuntimeException('Fatal Error: This module "' . $conf['name'] . '" is not enabled in TBE_MODULES', 1294586446);
433
434
435
436
			}
			return FALSE;
		}
		// Workspaces check:
437
438
439
440
441
442
443
444
445
		if (
			!empty($conf['workspaces'])
			&& \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('workspaces')
			&& ($this->workspace !== 0 || !GeneralUtility::inList($conf['workspaces'], 'online'))
			&& ($this->workspace !== -1 || !GeneralUtility::inList($conf['workspaces'], 'offline'))
			&& ($this->workspace <= 0 || !GeneralUtility::inList($conf['workspaces'], 'custom'))
		) {
			if ($exitOnError) {
				throw new \RuntimeException('Workspace Error: This module "' . $conf['name'] . '" is not available under the current workspace', 1294586447);
446
			}
447
			return FALSE;
448
449
450
451
452
453
		}
		// Returns TRUE if conf[access] is not set at all or if the user is admin
		if (!$conf['access'] || $this->isAdmin()) {
			return TRUE;
		}
		// If $conf['access'] is set but not with 'admin' then we return TRUE, if the module is found in the modList
454
		$acs = FALSE;
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
		if (!strstr($conf['access'], 'admin') && $conf['name']) {
			$acs = $this->check('modules', $conf['name']);
		}
		if (!$acs && $exitOnError) {
			throw new \RuntimeException('Access Error: You don\'t have access to this module.', 1294586448);
		} else {
			return $acs;
		}
	}

	/**
	 * Returns a WHERE-clause for the pages-table where user permissions according to input argument, $perms, is validated.
	 * $perms is the "mask" used to select. Fx. if $perms is 1 then you'll get all pages that a user can actually see!
	 * 2^0 = show (1)
	 * 2^1 = edit (2)
	 * 2^2 = delete (4)
	 * 2^3 = new (8)
	 * If the user is 'admin' " 1=1" is returned (no effect)
	 * 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)
474
475
	 * The 95% use of this function is "->getPagePermsClause(1)" which will
	 * return WHERE clauses for *selecting* pages in backend listings - in other words this will check read permissions.
476
	 *
477
	 * @param int $perms Permission mask to use, see function description
478
479
480
481
482
483
484
	 * @return string Part of where clause. Prefix " AND " to this.
	 */
	public function getPagePermsClause($perms) {
		if (is_array($this->user)) {
			if ($this->isAdmin()) {
				return ' 1=1';
			}
485
			$perms = (int)$perms;
486
			// Make sure it's integer.
487
488
			$str = ' (' . '(pages.perms_everybody & ' . $perms . ' = ' . $perms . ')' . ' OR (pages.perms_userid = '
				. $this->user['uid'] . ' AND pages.perms_user & ' . $perms . ' = ' . $perms . ')';
489
490
491
			// User
			if ($this->groupList) {
				// Group (if any is set)
492
493
				$str .= ' OR (pages.perms_groupid in (' . $this->groupList . ') AND pages.perms_group & '
					. $perms . ' = ' . $perms . ')';
494
495
496
497
498
499
500
501
			}
			$str .= ')';
			// ****************
			// getPagePermsClause-HOOK
			// ****************
			if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getPagePermsClause'])) {
				foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['getPagePermsClause'] as $_funcRef) {
					$_params = array('currentClause' => $str, 'perms' => $perms);
502
					$str = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
503
504
505
506
507
508
509
510
511
512
				}
			}
			return $str;
		} else {
			return ' 1=0';
		}
	}

	/**
	 * Returns a combined binary representation of the current users permissions for the page-record, $row.
513
514
	 * The perms for user, group and everybody is OR'ed together (provided that the page-owner is the user
	 * and for the groups that the user is a member of the group.
515
516
517
	 * If the user is admin, 31 is returned	(full permissions for all five flags)
	 *
	 * @param array $row Input page row with all perms_* fields available.
518
	 * @return int Bitwise representation of the users permissions in relation to input page row, $row
519
520
521
522
523
524
	 */
	public function calcPerms($row) {
		// Return 31 for admin users.
		if ($this->isAdmin()) {
			return 31;
		}
525
526
527
528
		// Return 0 if page is not within the allowed web mount
		if (!$this->isInWebMount($row['uid'])) {
			return 0;
		}
529
		$out = 0;
530
531
532
533
		if (
			isset($row['perms_userid']) && isset($row['perms_user']) && isset($row['perms_groupid'])
			&& isset($row['perms_group']) && isset($row['perms_everybody']) && isset($this->groupList)
		) {
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
			if ($this->user['uid'] == $row['perms_userid']) {
				$out |= $row['perms_user'];
			}
			if ($this->isMemberOfGroup($row['perms_groupid'])) {
				$out |= $row['perms_group'];
			}
			$out |= $row['perms_everybody'];
		}
		// ****************
		// CALCPERMS hook
		// ****************
		if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['calcPerms'])) {
			foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['calcPerms'] as $_funcRef) {
				$_params = array(
					'row' => $row,
					'outputPermissions' => $out
				);
551
				$out = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
552
553
554
555
556
557
558
			}
		}
		return $out;
	}

	/**
	 * Returns TRUE if the RTE (Rich Text Editor) can be enabled for the user
559
560
	 * Strictly this is not permissions being checked but rather a series of settings like
	 * a loaded extension, browser/client type and a configuration option in ->uc[edit_RTE]
561
562
	 * The reasons for a FALSE return can be found in $this->RTE_errors
	 *
563
	 * @return bool
564
565
566
567
568
569
570
571
572
573
574
	 */
	public function isRTE() {
		// Start:
		$this->RTE_errors = array();
		if (!$this->uc['edit_RTE']) {
			$this->RTE_errors[] = 'RTE is not enabled for user!';
		}
		if (!$GLOBALS['TYPO3_CONF_VARS']['BE']['RTEenabled']) {
			$this->RTE_errors[] = 'RTE is not enabled in $TYPO3_CONF_VARS["BE"]["RTEenabled"]';
		}
		// Acquire RTE object:
575
		$RTE = BackendUtility::RTEgetObj();
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
		if (!is_object($RTE)) {
			$this->RTE_errors = array_merge($this->RTE_errors, $RTE);
		}
		if (!count($this->RTE_errors)) {
			return TRUE;
		} else {
			return FALSE;
		}
	}

	/**
	 * Returns TRUE if the $value is found in the list in a $this->groupData[] index pointed to by $type (array key).
	 * Can thus be users to check for modules, exclude-fields, select/modify permissions for tables etc.
	 * If user is admin TRUE is also returned
	 * Please see the document Inside TYPO3 for examples.
	 *
	 * @param string $type The type value; "webmounts", "filemounts", "pagetypes_select", "tables_select", "tables_modify", "non_exclude_fields", "modules
	 * @param string $value String to search for in the groupData-list
594
	 * @return bool TRUE if permission is granted (that is, the value was found in the groupData list - or the BE_USER is "admin")
595
596
597
	 */
	public function check($type, $value) {
		if (isset($this->groupData[$type])) {
598
			if ($this->isAdmin() || GeneralUtility::inList($this->groupData[$type], $value)) {
599
600
601
602
603
604
605
606
607
608
609
610
611
				return TRUE;
			}
		}
		return FALSE;
	}

	/**
	 * Checking the authMode of a select field with authMode set
	 *
	 * @param string $table Table name
	 * @param string $field Field name (must be configured in TCA and of type "select" with authMode set!)
	 * @param string $value Value to evaluation (single value, must not contain any of the chars ":,|")
	 * @param string $authMode Auth mode keyword (explicitAllow, explicitDeny, individual)
612
	 * @return bool Whether access is granted or not
613
614
615
616
617
618
619
	 */
	public function checkAuthMode($table, $field, $value, $authMode) {
		// Admin users can do anything:
		if ($this->isAdmin()) {
			return TRUE;
		}
		// Allow all blank values:
620
		if ((string)$value === '') {
621
622
623
624
625
626
627
			return TRUE;
		}
		// Certain characters are not allowed in the value
		if (preg_match('/[:|,]/', $value)) {
			return FALSE;
		}
		// Initialize:
628
		$testValue = $table . ':' . $field . ':' . $value;
629
630
		$out = TRUE;
		// Checking value:
631
		switch ((string)$authMode) {
632
			case 'explicitAllow':
633
				if (!GeneralUtility::inList($this->groupData['explicit_allowdeny'], ($testValue . ':ALLOW'))) {
634
635
636
637
					$out = FALSE;
				}
				break;
			case 'explicitDeny':
638
				if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) {
639
640
641
642
643
644
645
646
					$out = FALSE;
				}
				break;
			case 'individual':
				if (is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'][$field])) {
					$items = $GLOBALS['TCA'][$table]['columns'][$field]['config']['items'];
					if (is_array($items)) {
						foreach ($items as $iCfg) {
647
648
							if ((string)$iCfg[1] === (string)$value && $iCfg[4]) {
								switch ((string)$iCfg[4]) {
649
									case 'EXPL_ALLOW':
650
										if (!GeneralUtility::inList($this->groupData['explicit_allowdeny'], ($testValue . ':ALLOW'))) {
651
652
653
654
											$out = FALSE;
										}
										break;
									case 'EXPL_DENY':
655
										if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) {
656
657
658
											$out = FALSE;
										}
										break;
659
660
661
662
663
664
								}
								break;
							}
						}
					}
				}
665
				break;
666
667
668
669
670
671
672
		}
		return $out;
	}

	/**
	 * Checking if a language value (-1, 0 and >0 for sys_language records) is allowed to be edited by the user.
	 *
673
	 * @param int $langValue Language value to evaluate
674
	 * @return bool Returns TRUE if the language value is allowed, otherwise FALSE.
675
676
677
	 */
	public function checkLanguageAccess($langValue) {
		// The users language list must be non-blank - otherwise all languages are allowed.
678
		if (trim($this->groupData['allowed_languages']) !== '') {
679
			$langValue = (int)$langValue;
680
681
682
683
684
685
686
687
688
689
690
691
692
			// Language must either be explicitly allowed OR the lang Value be "-1" (all languages)
			if ($langValue != -1 && !$this->check('allowed_languages', $langValue)) {
				return FALSE;
			}
		}
		return TRUE;
	}

	/**
	 * Check if user has access to all existing localizations for a certain record
	 *
	 * @param string $table The table
	 * @param array $record The current record
693
	 * @return bool
694
695
696
	 */
	public function checkFullLanguagesAccess($table, $record) {
		$recordLocalizationAccess = $this->checkLanguageAccess(0);
697
		if ($recordLocalizationAccess && (BackendUtility::isTableLocalizable($table) || isset($GLOBALS['TCA'][$table]['ctrl']['transForeignTable']))) {
698
699
700
701
702
703
704
705
706
			if (isset($GLOBALS['TCA'][$table]['ctrl']['transForeignTable'])) {
				$l10nTable = $GLOBALS['TCA'][$table]['ctrl']['transForeignTable'];
				$pointerField = $GLOBALS['TCA'][$l10nTable]['ctrl']['transOrigPointerField'];
				$pointerValue = $record['uid'];
			} else {
				$l10nTable = $table;
				$pointerField = $GLOBALS['TCA'][$l10nTable]['ctrl']['transOrigPointerField'];
				$pointerValue = $record[$pointerField] > 0 ? $record[$pointerField] : $record['uid'];
			}
707
			$recordLocalizations = BackendUtility::getRecordsByField($l10nTable, $pointerField, $pointerValue, '', '', '', '1');
708
709
			if (is_array($recordLocalizations)) {
				foreach ($recordLocalizations as $localization) {
710
711
					$recordLocalizationAccess = $recordLocalizationAccess
						&& $this->checkLanguageAccess($localization[$GLOBALS['TCA'][$l10nTable]['ctrl']['languageField']]);
712
713
714
715
716
717
718
719
720
721
722
					if (!$recordLocalizationAccess) {
						break;
					}
				}
			}
		}
		return $recordLocalizationAccess;
	}

	/**
	 * Checking if a user has editing access to a record from a $GLOBALS['TCA'] table.
723
724
	 * The checks does not take page permissions and other "environmental" things into account.
	 * It only deal with record internals; If any values in the record fields disallows it.
725
726
	 * For instance languages settings, authMode selector boxes are evaluated (and maybe more in the future).
	 * It will check for workspace dependent access.
727
	 * The function takes an ID (int) or row (array) as second argument.
728
729
730
	 *
	 * @param string $table Table name
	 * @param mixed $idOrRow If integer, then this is the ID of the record. If Array this just represents fields in the record.
731
732
733
	 * @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.
	 * @param bool $deletedRecord Set, if testing a deleted record array.
	 * @param bool $checkFullLanguageAccess Set, whenever access to all translations of the record is required
734
	 * @return bool TRUE if OK, otherwise FALSE
735
736
	 */
	public function recordEditAccessInternals($table, $idOrRow, $newRecord = FALSE, $deletedRecord = FALSE, $checkFullLanguageAccess = FALSE) {
737
738
739
740
741
742
743
744
745
746
747
748
749
		if (!isset($GLOBALS['TCA'][$table])) {
			return FALSE;
		}
		// Always return TRUE for Admin users.
		if ($this->isAdmin()) {
			return TRUE;
		}
		// Fetching the record if the $idOrRow variable was not an array on input:
		if (!is_array($idOrRow)) {
			if ($deletedRecord) {
				$idOrRow = BackendUtility::getRecord($table, $idOrRow, '*', '', FALSE);
			} else {
				$idOrRow = BackendUtility::getRecord($table, $idOrRow);
750
751
			}
			if (!is_array($idOrRow)) {
752
753
				$this->errorMsg = 'ERROR: Record could not be fetched.';
				return FALSE;
754
			}
755
756
757
758
759
760
761
762
763
764
765
766
767
		}
		// Checking languages:
		if ($GLOBALS['TCA'][$table]['ctrl']['languageField']) {
			// Language field must be found in input row - otherwise it does not make sense.
			if (isset($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
				if (!$this->checkLanguageAccess($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']])) {
					$this->errorMsg = 'ERROR: Language was not allowed.';
					return FALSE;
				} elseif (
					$checkFullLanguageAccess && $idOrRow[$GLOBALS['TCA'][$table]['ctrl']['languageField']] == 0
					&& !$this->checkFullLanguagesAccess($table, $idOrRow)
				) {
					$this->errorMsg = 'ERROR: Related/affected language was not allowed.';
768
769
					return FALSE;
				}
770
771
772
			} else {
				$this->errorMsg = 'ERROR: The "languageField" field named "'
					. $GLOBALS['TCA'][$table]['ctrl']['languageField'] . '" was not found in testing record!';
773
774
				return FALSE;
			}
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
		} elseif (
			isset($GLOBALS['TCA'][$table]['ctrl']['transForeignTable']) && $checkFullLanguageAccess &&
			!$this->checkFullLanguagesAccess($table, $idOrRow)
		) {
			return FALSE;
		}
		// Checking authMode fields:
		if (is_array($GLOBALS['TCA'][$table]['columns'])) {
			foreach ($GLOBALS['TCA'][$table]['columns'] as $fieldName => $fieldValue) {
				if (isset($idOrRow[$fieldName])) {
					if (
						$fieldValue['config']['type'] === 'select' && $fieldValue['config']['authMode']
						&& $fieldValue['config']['authMode_enforce'] === 'strict'
					) {
						if (!$this->checkAuthMode($table, $fieldName, $idOrRow[$fieldName], $fieldValue['config']['authMode'])) {
							$this->errorMsg = 'ERROR: authMode "' . $fieldValue['config']['authMode']
								. '" failed for field "' . $fieldName . '" with value "'
								. $idOrRow[$fieldName] . '" evaluated';
							return FALSE;
794
795
796
797
						}
					}
				}
			}
798
799
800
801
802
803
		}
		// Checking "editlock" feature (doesn't apply to new records)
		if (!$newRecord && $GLOBALS['TCA'][$table]['ctrl']['editlock']) {
			if (isset($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['editlock']])) {
				if ($idOrRow[$GLOBALS['TCA'][$table]['ctrl']['editlock']]) {
					$this->errorMsg = 'ERROR: Record was locked for editing. Only admin users can change this state.';
804
805
					return FALSE;
				}
806
807
808
809
			} else {
				$this->errorMsg = 'ERROR: The "editLock" field named "' . $GLOBALS['TCA'][$table]['ctrl']['editlock']
					. '" was not found in testing record!';
				return FALSE;
810
			}
811
812
813
814
815
816
817
818
819
820
821
822
823
		}
		// Checking record permissions
		// THIS is where we can include a check for "perms_" fields for other records than pages...
		// Process any hooks
		if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['recordEditAccessInternals'])) {
			foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_userauthgroup.php']['recordEditAccessInternals'] as $funcRef) {
				$params = array(
					'table' => $table,
					'idOrRow' => $idOrRow,
					'newRecord' => $newRecord
				);
				if (!GeneralUtility::callUserFunction($funcRef, $params, $this)) {
					return FALSE;
824
825
826
				}
			}
		}
827
828
		// Finally, return TRUE if all is well.
		return TRUE;
829
830
831
	}

	/**
832
833
	 * Checks a type of permission against the compiled permission integer,
	 * $compiledPermissions, and in relation to table, $tableName
834
	 *
835
	 * @param int $compiledPermissions Could typically be the "compiled permissions" integer returned by ->calcPerms
836
837
	 * @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)
	 * @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)
838
	 * @return bool
839
	 * @access public (used by ClickMenuController)
840
841
842
843
844
845
	 */
	public function isPSet($compiledPermissions, $tableName, $actionType = '') {
		if ($this->isAdmin()) {
			$result = TRUE;
		} elseif ($tableName == 'pages') {
			switch ($actionType) {
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
				case 'edit':
					$result = ($compiledPermissions & 2) !== 0;
					break;
				case 'new':
					// Create new page OR page content
					$result = ($compiledPermissions & 8 + 16) !== 0;
					break;
				case 'delete':
					$result = ($compiledPermissions & 4) !== 0;
					break;
				case 'editcontent':
					$result = ($compiledPermissions & 16) !== 0;
					break;
				default:
					$result = FALSE;
861
862
863
864
865
866
867
868
869
870
			}
		} else {
			$result = ($compiledPermissions & 16) !== 0;
		}
		return $result;
	}

	/**
	 * Returns TRUE if the BE_USER is allowed to *create* shortcuts in the backend modules
	 *
871
	 * @return bool
872
873
	 */
	public function mayMakeShortcut() {
874
875
		return $this->getTSConfigVal('options.enableBookmarks')
			&& !$this->getTSConfigVal('options.mayNotCreateEditBookmarks');
876
877
878
879
880
881
	}

	/**
	 * Checking if editing of an existing record is allowed in current workspace if that is offline.
	 * Rules for editing in offline mode:
	 * - record supports versioning and is an offline version from workspace and has the corrent stage
882
883
	 * - or record (any) is in a branch where there is a page which is a version from the workspace
	 *   and where the stage is not preventing records
884
885
886
887
888
889
890
891
892
	 *
	 * @param string $table Table of record
	 * @param array $recData Integer (record uid) or array where fields are at least: pid, t3ver_wsid, t3ver_stage (if versioningWS is set)
	 * @return string String error code, telling the failure state. FALSE=All ok
	 */
	public function workspaceCannotEditRecord($table, $recData) {
		// Only test offline spaces:
		if ($this->workspace !== 0) {
			if (!is_array($recData)) {
893
894
895
896
897
				$recData = BackendUtility::getRecord(
					$table,
					$recData,
					'pid' . ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] ? ',t3ver_wsid,t3ver_stage' : '')
				);
898
899
			}
			if (is_array($recData)) {
900
901
				// We are testing a "version" (identified by a pid of -1): it can be edited provided
				// that workspace matches and versioning is enabled for the table.
902
				if ((int)$recData['pid'] === -1) {
903
904
905
					// No versioning, basic error, inconsistency even! Such records should not have a pid of -1!
					if (!$GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
						return 'Versioning disabled for table';
906
					} elseif ((int)$recData['t3ver_wsid'] !== $this->workspace) {
907
908
909
910
						// So does workspace match?
						return 'Workspace ID of record didn\'t match current workspace';
					} else {
						// So is the user allowed to "use" the edit stage within the workspace?
911
912
913
						return $this->workspaceCheckStageForCurrent(0)
							? FALSE
							: 'User\'s access level did not allow for editing';
914
915
916
917
918
919
920
					}
				} else {
					// We are testing a "live" record:
					// For "Live" records, check that PID for table allows editing
					if ($res = $this->workspaceAllowLiveRecordsInPID($recData['pid'], $table)) {
						// Live records are OK in this branch, but what about the stage of branch point, if any:
						// OK
921
922
923
						return $res > 0
							? FALSE
							: 'Stage for versioning root point and users access level did not allow for editing';
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
					} else {
						// If not offline and not in versionized branch, output error:
						return 'Online record was not in versionized branch!';
					}
				}
			} else {
				return 'No record';
			}
		} else {
			// OK because workspace is 0
			return FALSE;
		}
	}

	/**
	 * Evaluates if a user is allowed to edit the offline version
	 *
	 * @param string $table Table of record
	 * @param array $recData Integer (record uid) or array where fields are at least: pid, t3ver_wsid, t3ver_stage (if versioningWS is set)
	 * @return string String error code, telling the failure state. FALSE=All ok
	 * @see workspaceCannotEditRecord()
	 */
	public function workspaceCannotEditOfflineVersion($table, $recData) {
		if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
			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
954
955
956
957
958
959
960
961
962
963
964
965
966
967
					return $this->workspaceCannotEditRecord($table, $recData);
				} else {
					return 'Not an offline version';
				}
			} else {
				return 'No record';
			}
		} else {
			return 'Table does not support versioning.';
		}
	}

	/**
	 * Check if "live" records from $table may be created or edited in this PID.
	 * If the answer is FALSE it means the only valid way to create or edit records in the PID is by versioning
968
969
970
	 * 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
	 * of versioning because the element was within a versionized branch
	 * but NOT ok in terms of the state the root point had!
971
	 *
972
	 * @param int $pid PID value to check for. OBSOLETE!
973
974
975
976
	 * @param string $table Table name
	 * @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.
	 */
	public function workspaceAllowLiveRecordsInPID($pid, $table) {
977
978
979
980
981
982
983
		// Always for Live workspace AND if live-edit is enabled
		// and tables are completely without versioning it is ok as well.
		if (
			$this->workspace === 0
			|| $this->workspaceRec['live_edit'] && !$GLOBALS['TCA'][$table]['ctrl']['versioningWS']
			|| $GLOBALS['TCA'][$table]['ctrl']['versioningWS_alwaysAllowLiveEdit']
		) {
984
985
986
987
988
989
990
991
992
993
994
			// OK to create for this table.
			return 2;
		} else {
			// If the answer is FALSE it means the only valid way to create or edit records in the PID is by versioning
			return FALSE;
		}
	}

	/**
	 * Evaluates if a record from $table can be created in $pid
	 *
995
	 * @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!
996
	 * @param string $table Table name
997
	 * @return bool TRUE if OK.
998
999
1000
	 */
	public function workspaceCreateNewRecord($pid, $table) {
		if ($res = $this->workspaceAllowLiveRecordsInPID($pid, $table)) {