BackendUserAuthentication.php 92.5 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
use TYPO3\CMS\Core\Resource\ResourceStorage;
18
use TYPO3\CMS\Core\Type\Bitmask\Permission;
19
20
21
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Backend\Utility\BackendUtility;

22
23
24
25
26
27
28
29
30
31
/**
 * 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.
 */
class BackendUserAuthentication extends \TYPO3\CMS\Core\Authentication\AbstractUserAuthentication {

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

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

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

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

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

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

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

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

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

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

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

	/**
121
122
	 * Set to 'WIN', if windows
	 * @var string
123
	 * @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8, use the constant TYPO3_OS directly
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
	 * Contains last error message
	 * @var string
160
161
162
163
	 */
	public $errorMsg = '';

	/**
164
165
	 * Cache for checkWorkspaceCurrent()
	 * @var array|NULL
166
167
168
169
	 */
	public $checkWorkspaceCurrent_cache = NULL;

	/**
170
	 * @var \TYPO3\CMS\Core\Resource\ResourceStorage[]
171
172
173
174
175
176
177
178
179
	 */
	protected $fileStorages;

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

	/**
180
181
	 * Table to use for session data
	 * @var string
182
183
184
185
	 */
	public $session_table = 'be_sessions';

	/**
186
187
	 * Table in database with user data
	 * @var string
188
189
190
191
	 */
	public $user_table = 'be_users';

	/**
192
193
	 * Column for login-name
	 * @var string
194
195
196
197
	 */
	public $username_column = 'username';

	/**
198
199
	 * Column for password
	 * @var string
200
201
202
203
	 */
	public $userident_column = 'password';

	/**
204
205
	 * Column for user-id
	 * @var string
206
207
208
209
	 */
	public $userid_column = 'uid';

	/**
210
	 * @var string
211
212
213
214
	 */
	public $lastLogin_column = 'lastlogin';

	/**
215
	 * @var array
216
217
218
219
220
221
222
223
224
225
	 */
	public $enablecolumns = array(
		'rootLevel' => 1,
		'deleted' => 'deleted',
		'disabled' => 'disable',
		'starttime' => 'starttime',
		'endtime' => 'endtime'
	);

	/**
226
227
	 * Form field with login-name
	 * @var string
228
229
230
231
	 */
	public $formfield_uname = 'username';

	/**
232
233
	 * Form field with password
	 * @var string
234
235
236
237
	 */
	public $formfield_uident = 'userident';

	/**
238
239
	 * Form field with status: *'login', 'logout'
	 * @var string
240
241
242
243
	 */
	public $formfield_status = 'login_status';

	/**
244
245
	 * Decides if the writelog() function is called at login and logout
	 * @var bool
246
	 */
247
	public $writeStdLog = TRUE;
248
249

	/**
250
251
	 * If the writelog() functions is called if a login-attempt has be tried without success
	 * @var bool
252
	 */
253
	public $writeAttemptLog = TRUE;
254
255

	/**
256
257
258
259
	 * 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
260
261
262
263
	 */
	public $auth_timeout_field = 6000;

	/**
264
	 * @var int
265
	 */
266
	public $firstMainGroup = 0;
267
268

	/**
269
270
	 * User Config
	 * @var array
271
272
273
274
	 */
	public $uc;

	/**
275
276
277
278
279
280
281
	 * 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
282
283
284
285
286
287
288
289
	 */
	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
290
		'startModule' => 'help_AboutmodulesAboutmodules',
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
		'hideSubmoduleIcons' => 0,
		'titleLen' => 50,
		'edit_RTE' => '1',
		'edit_docModuleUpload' => '1',
		// Default is 245 pixels
		'navFrameResizable' => 0,
		'resizeTextareas' => 1,
		'resizeTextareas_MaxHeight' => 500,
		'resizeTextareas_Flexible' => 0
	);

	/**
	 * Constructor
	 */
	public function __construct() {
306
		parent::__construct();
307
308
		$this->name = self::getCookieName();
		$this->loginType = 'BE';
309
		$this->OS = TYPO3_OS;
310
311
312
313
314
315
	}

	/**
	 * Returns TRUE if user is admin
	 * Basically this function evaluates if the ->user[admin] field has bit 0 set. If so, user is admin.
	 *
316
	 * @return bool
317
318
	 */
	public function isAdmin() {
319
		return is_array($this->user) && ($this->user['admin'] & 1) == 1;
320
321
322
323
324
325
326
	}

	/**
	 * 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.
	 *
327
	 * @param int $groupId Group ID to look for in $this->groupList
328
	 * @return bool
329
330
	 */
	public function isMemberOfGroup($groupId) {
331
		$groupId = (int)$groupId;
332
		if ($this->groupList && $groupId) {
333
			return GeneralUtility::inList($this->groupList, $groupId);
334
		}
335
		return FALSE;
336
337
338
339
340
341
342
343
344
345
346
347
348
349
	}

	/**
	 * 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
350
	 * @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.
351
	 * @return bool
352
353
354
355
356
357
358
359
360
	 */
	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.
361
362
363
364
365
	 * 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
366
367
	 * Otherwise the function will return the uid of the webmount which was first found in the rootline of the input page $id
	 *
368
	 * @param int $id Page ID to check
369
	 * @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!)
370
371
372
	 * @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
373
374
375
376
377
	 */
	public function isInWebMount($id, $readPerms = '', $exitOnError = 0) {
		if (!$GLOBALS['TYPO3_CONF_VARS']['BE']['lockBeUserToDBmounts'] || $this->isAdmin()) {
			return 1;
		}
378
		$id = (int)$id;
379
		// Check if input id is an offline version page in which case we will map id to the online version:
380
		$checkRec = BackendUtility::getRecord('pages', $id, 'pid,t3ver_oid');
381
		if ($checkRec['pid'] == -1) {
382
			$id = (int)$checkRec['t3ver_oid'];
383
384
385
386
387
388
		}
		if (!$readPerms) {
			$readPerms = $this->getPagePermsClause(1);
		}
		if ($id > 0) {
			$wM = $this->returnWebmounts();
389
			$rL = BackendUtility::BEgetRootLine($id, ' AND ' . $readPerms);
390
391
392
393
394
395
396
397
398
			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);
		}
399
		return NULL;
400
401
402
403
404
405
	}

	/**
	 * Checks access to a backend module with the $MCONF passed as first argument
	 *
	 * @param array $conf $MCONF array of a backend module!
406
	 * @param bool $exitOnError If set, an array will issue an error message and exit.
407
	 * @throws \RuntimeException
408
	 * @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
409
410
	 */
	public function modAccess($conf, $exitOnError) {
411
		if (!BackendUtility::isModuleSetInTBE_MODULES($conf['name'])) {
412
			if ($exitOnError) {
413
				throw new \RuntimeException('Fatal Error: This module "' . $conf['name'] . '" is not enabled in TBE_MODULES', 1294586446);
414
415
416
417
			}
			return FALSE;
		}
		// Workspaces check:
418
419
420
421
422
423
424
425
426
		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);
427
			}
428
			return FALSE;
429
430
431
432
433
434
		}
		// 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
435
		$acs = FALSE;
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
		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)
455
456
	 * 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.
457
	 *
458
	 * @param int $perms Permission mask to use, see function description
459
460
461
462
463
464
465
	 * @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';
			}
466
			$perms = (int)$perms;
467
			// Make sure it's integer.
468
469
			$str = ' (' . '(pages.perms_everybody & ' . $perms . ' = ' . $perms . ')' . ' OR (pages.perms_userid = '
				. $this->user['uid'] . ' AND pages.perms_user & ' . $perms . ' = ' . $perms . ')';
470
471
472
			// User
			if ($this->groupList) {
				// Group (if any is set)
473
474
				$str .= ' OR (pages.perms_groupid in (' . $this->groupList . ') AND pages.perms_group & '
					. $perms . ' = ' . $perms . ')';
475
476
477
478
479
480
481
482
			}
			$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);
483
					$str = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
484
485
486
487
488
489
490
491
492
493
				}
			}
			return $str;
		} else {
			return ' 1=0';
		}
	}

	/**
	 * Returns a combined binary representation of the current users permissions for the page-record, $row.
494
495
	 * 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.
496
497
498
	 * 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.
499
	 * @return int Bitwise representation of the users permissions in relation to input page row, $row
500
501
502
503
	 */
	public function calcPerms($row) {
		// Return 31 for admin users.
		if ($this->isAdmin()) {
504
			return Permission::ALL;
505
		}
506
507
		// Return 0 if page is not within the allowed web mount
		if (!$this->isInWebMount($row['uid'])) {
508
			return Permission::NOTHING;
509
		}
510
		$out = Permission::NOTHING;
511
512
513
514
		if (
			isset($row['perms_userid']) && isset($row['perms_user']) && isset($row['perms_groupid'])
			&& isset($row['perms_group']) && isset($row['perms_everybody']) && isset($this->groupList)
		) {
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
			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
				);
532
				$out = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
533
534
535
536
537
538
			}
		}
		return $out;
	}

	/**
Christian Kuhn's avatar
Christian Kuhn committed
539
	 * Returns TRUE if the RTE (Rich Text Editor) is enabled for the user.
540
	 *
541
	 * @return bool
542
543
	 */
	public function isRTE() {
Christian Kuhn's avatar
Christian Kuhn committed
544
		return (bool)$this->uc['edit_RTE'];
545
546
547
548
549
550
551
552
553
554
	}

	/**
	 * 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
555
	 * @return bool TRUE if permission is granted (that is, the value was found in the groupData list - or the BE_USER is "admin")
556
557
558
	 */
	public function check($type, $value) {
		if (isset($this->groupData[$type])) {
559
			if ($this->isAdmin() || GeneralUtility::inList($this->groupData[$type], $value)) {
560
561
562
563
564
565
566
567
568
569
570
571
572
				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)
573
	 * @return bool Whether access is granted or not
574
575
576
577
578
579
580
	 */
	public function checkAuthMode($table, $field, $value, $authMode) {
		// Admin users can do anything:
		if ($this->isAdmin()) {
			return TRUE;
		}
		// Allow all blank values:
581
		if ((string)$value === '') {
582
583
584
585
586
587
588
			return TRUE;
		}
		// Certain characters are not allowed in the value
		if (preg_match('/[:|,]/', $value)) {
			return FALSE;
		}
		// Initialize:
589
		$testValue = $table . ':' . $field . ':' . $value;
590
591
		$out = TRUE;
		// Checking value:
592
		switch ((string)$authMode) {
593
			case 'explicitAllow':
594
				if (!GeneralUtility::inList($this->groupData['explicit_allowdeny'], ($testValue . ':ALLOW'))) {
595
596
597
598
					$out = FALSE;
				}
				break;
			case 'explicitDeny':
599
				if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) {
600
601
602
603
604
605
606
607
					$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) {
608
609
							if ((string)$iCfg[1] === (string)$value && $iCfg[4]) {
								switch ((string)$iCfg[4]) {
610
									case 'EXPL_ALLOW':
611
										if (!GeneralUtility::inList($this->groupData['explicit_allowdeny'], ($testValue . ':ALLOW'))) {
612
613
614
615
											$out = FALSE;
										}
										break;
									case 'EXPL_DENY':
616
										if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) {
617
618
619
											$out = FALSE;
										}
										break;
620
621
622
623
624
625
								}
								break;
							}
						}
					}
				}
626
				break;
627
628
629
630
631
632
633
		}
		return $out;
	}

	/**
	 * Checking if a language value (-1, 0 and >0 for sys_language records) is allowed to be edited by the user.
	 *
634
	 * @param int $langValue Language value to evaluate
635
	 * @return bool Returns TRUE if the language value is allowed, otherwise FALSE.
636
637
638
	 */
	public function checkLanguageAccess($langValue) {
		// The users language list must be non-blank - otherwise all languages are allowed.
639
		if (trim($this->groupData['allowed_languages']) !== '') {
640
			$langValue = (int)$langValue;
641
642
643
644
645
646
647
648
649
650
651
652
653
			// 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
654
	 * @return bool
655
656
657
	 */
	public function checkFullLanguagesAccess($table, $record) {
		$recordLocalizationAccess = $this->checkLanguageAccess(0);
658
		if ($recordLocalizationAccess && (BackendUtility::isTableLocalizable($table) || isset($GLOBALS['TCA'][$table]['ctrl']['transForeignTable']))) {
659
660
661
662
663
664
665
666
667
			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'];
			}
668
			$recordLocalizations = BackendUtility::getRecordsByField($l10nTable, $pointerField, $pointerValue, '', '', '', '1');
669
670
			if (is_array($recordLocalizations)) {
				foreach ($recordLocalizations as $localization) {
671
672
					$recordLocalizationAccess = $recordLocalizationAccess
						&& $this->checkLanguageAccess($localization[$GLOBALS['TCA'][$l10nTable]['ctrl']['languageField']]);
673
674
675
676
677
678
679
680
681
682
683
					if (!$recordLocalizationAccess) {
						break;
					}
				}
			}
		}
		return $recordLocalizationAccess;
	}

	/**
	 * Checking if a user has editing access to a record from a $GLOBALS['TCA'] table.
684
685
	 * 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.
686
687
	 * For instance languages settings, authMode selector boxes are evaluated (and maybe more in the future).
	 * It will check for workspace dependent access.
688
	 * The function takes an ID (int) or row (array) as second argument.
689
690
691
	 *
	 * @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.
692
693
694
	 * @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
695
	 * @return bool TRUE if OK, otherwise FALSE
696
697
	 */
	public function recordEditAccessInternals($table, $idOrRow, $newRecord = FALSE, $deletedRecord = FALSE, $checkFullLanguageAccess = FALSE) {
698
699
700
701
702
703
704
705
706
707
708
709
710
		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);
711
712
			}
			if (!is_array($idOrRow)) {
713
714
				$this->errorMsg = 'ERROR: Record could not be fetched.';
				return FALSE;
715
			}
716
717
718
719
720
721
722
723
724
725
726
727
728
		}
		// 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.';
729
730
					return FALSE;
				}
731
732
733
			} else {
				$this->errorMsg = 'ERROR: The "languageField" field named "'
					. $GLOBALS['TCA'][$table]['ctrl']['languageField'] . '" was not found in testing record!';
734
735
				return FALSE;
			}
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
		} 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;
755
756
757
758
						}
					}
				}
			}
759
760
761
762
763
764
		}
		// 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.';
765
766
					return FALSE;
				}
767
768
769
770
			} else {
				$this->errorMsg = 'ERROR: The "editLock" field named "' . $GLOBALS['TCA'][$table]['ctrl']['editlock']
					. '" was not found in testing record!';
				return FALSE;
771
			}
772
773
774
775
776
777
778
779
780
781
782
783
784
		}
		// 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;
785
786
787
				}
			}
		}
788
789
		// Finally, return TRUE if all is well.
		return TRUE;
790
791
792
	}

	/**
793
794
	 * Checks a type of permission against the compiled permission integer,
	 * $compiledPermissions, and in relation to table, $tableName
795
	 *
796
	 * @param int $compiledPermissions Could typically be the "compiled permissions" integer returned by ->calcPerms
797
798
	 * @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)
799
	 * @return bool
800
	 * @access public (used by ClickMenuController)
801
802
803
804
805
806
	 */
	public function isPSet($compiledPermissions, $tableName, $actionType = '') {
		if ($this->isAdmin()) {
			$result = TRUE;
		} elseif ($tableName == 'pages') {
			switch ($actionType) {
807
				case 'edit':
808
					$result = ($compiledPermissions & Permission::PAGE_EDIT) !== 0;
809
810
811
					break;
				case 'new':
					// Create new page OR page content
812
					$result = ($compiledPermissions & Permission::PAGE_NEW + Permission::CONTENT_EDIT) !== 0;
813
814
					break;
				case 'delete':
815
					$result = ($compiledPermissions & Permission::PAGE_DELETE) !== 0;
816
817
					break;
				case 'editcontent':
818
					$result = ($compiledPermissions & Permission::CONTENT_EDIT) !== 0;
819
820
821
					break;
				default:
					$result = FALSE;
822
823
			}
		} else {
824
			$result = ($compiledPermissions & Permission::CONTENT_EDIT) !== 0;
825
826
827
828
829
830
831
		}
		return $result;
	}

	/**
	 * Returns TRUE if the BE_USER is allowed to *create* shortcuts in the backend modules
	 *
832
	 * @return bool
833
834
	 */
	public function mayMakeShortcut() {
835
836
		return $this->getTSConfigVal('options.enableBookmarks')
			&& !$this->getTSConfigVal('options.mayNotCreateEditBookmarks');
837
838
839
840
841
842
	}

	/**
	 * 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
843
844
	 * - 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
845
846
847
848
849
850
851
852
853
	 *
	 * @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)) {
854
855
856
857
858
				$recData = BackendUtility::getRecord(
					$table,
					$recData,
					'pid' . ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] ? ',t3ver_wsid,t3ver_stage' : '')
				);
859
860
			}
			if (is_array($recData)) {
861
862
				// 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.
863
				if ((int)$recData['pid'] === -1) {
864
865
866
					// 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';
867
					} elseif ((int)$recData['t3ver_wsid'] !== $this->workspace) {
868
869
870
871
						// 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?
872
873
874
						return $this->workspaceCheckStageForCurrent(0)
							? FALSE
							: 'User\'s access level did not allow for editing';
875
876
877
878
879
880
881
					}
				} 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
882
883
884
						return $res > 0
							? FALSE
							: 'Stage for versioning root point and users access level did not allow for editing';
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
					} 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)) {
910
				$recData = BackendUtility::getRecord($table, $recData, 'uid,pid,t3ver_wsid,t3ver_stage');
911
912
			}
			if (is_array($recData)) {
913
				if ((int)$recData['pid'] === -1) {
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
					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
929
930
931
	 * 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!
932
	 *
933
	 * @param int $pid PID value to check for. OBSOLETE!
934
935
936
937
	 * @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) {
938
939
940
941
942
943
944
		// 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']
		) {
945
946
947
948
949
950
951
952
953
954
955
			// 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
	 *
956
	 * @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!
957
	 * @param string $table Table name
958
	 * @return bool TRUE if OK.
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
	 */
	public function workspaceCreateNewRecord($pid, $table) {
		if ($res = $this->workspaceAllowLiveRecordsInPID($pid, $table)) {
			// If LIVE records cannot be created in the current PID due to workspace restrictions, prepare creation of placeholder-record
			if ($res < 0) {
				// Stage for versioning root point and users access level did not allow for editing
				return FALSE;
			}
		} elseif (!$GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
			// So, if no live records were allowed, we have to create a new version of this record:
			return FALSE;
		}
		return TRUE;
	}

	/**
	 * Evaluates if auto creation of a version of a record is allowed.
	 *
	 * @param string $table Table of the record
978
979
	 * @param int $id UID of record
	 * @param int $recpid PID of record
980
	 * @return bool TRUE if ok.
981
982
983
984
985
	 */
	public function workspaceAllowAutoCreation($table, $id, $recpid) {
		// Auto-creation of version: In offline workspace, test if versioning is
		// enabled and look for workspace version of input record.
		// If there is no versionized record found we will create one and save to that.
986
		if (
987
			$this->workspace !== 0
988
989
990
			&& $GLOBALS['TCA'][$table]['ctrl']['versioningWS'] && $recpid >= 0
			&& !BackendUtility::getWorkspaceVersionOfRecord($this->workspace, $table, $id, 'uid')
		) {
991
992
993
			// There must be no existing version of this record in workspace.
			return TRUE;
		}
994
		return FALSE;
995
996
997
998
999
1000
	}

	/**
	 * Checks if an element stage allows access for the user in the current workspace
	 * In live workspace (= 0) access is always granted for any stage.
	 * Admins are always allowed.