BackendUserAuthentication.php 92.2 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
19
20
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Backend\Utility\BackendUtility;

21
22
23
24
25
26
27
28
29
30
31
32
33
/**
 * 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 {

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

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

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

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

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

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

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

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

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

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

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

	/**
123
124
	 * Set to 'WIN', if windows
	 * @var string
125
	 * @deprecated since TYPO3 CMS 7, will be removed in TYPO3 CMS 8, use the constant TYPO3_OS directly
126
127
128
129
	 */
	public $OS = '';

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	/**
218
	 * @var string
219
220
221
222
	 */
	public $lastLogin_column = 'lastlogin';

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

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

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

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

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

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

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

	/**
270
271
272
273
	 * 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
274
275
276
277
	 */
	public $auth_timeout_field = 6000;

	/**
278
	 * @var bool
279
	 */
280
	public $challengeStoredInCookie = TRUE;
281
282

	/**
283
	 * @var int
284
	 */
285
	public $firstMainGroup = 0;
286
287

	/**
288
289
	 * User Config
	 * @var array
290
291
292
293
	 */
	public $uc;

	/**
294
295
296
297
298
299
300
	 * 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
301
302
303
304
305
306
307
308
	 */
	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
309
		'startModule' => 'help_AboutmodulesAboutmodules',
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
		'hideSubmoduleIcons' => 0,
		'titleLen' => 50,
		'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
		$this->name = self::getCookieName();
		$this->loginType = 'BE';
329
		$this->OS = TYPO3_OS;
330
331
332
333
334
335
	}

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

	/**
	 * 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.
	 *
347
	 * @param int $groupId Group ID to look for in $this->groupList
348
	 * @return bool
349
350
	 */
	public function isMemberOfGroup($groupId) {
351
		$groupId = (int)$groupId;
352
		if ($this->groupList && $groupId) {
353
			return GeneralUtility::inList($this->groupList, $groupId);
354
		}
355
		return FALSE;
356
357
358
359
360
361
362
363
364
365
366
367
368
369
	}

	/**
	 * 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
370
	 * @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.
371
	 * @return bool
372
373
374
375
376
377
378
379
380
	 */
	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.
381
382
383
384
385
	 * 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
386
387
	 * Otherwise the function will return the uid of the webmount which was first found in the rootline of the input page $id
	 *
388
	 * @param int $id Page ID to check
389
	 * @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!)
390
391
392
	 * @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
393
394
395
396
397
	 */
	public function isInWebMount($id, $readPerms = '', $exitOnError = 0) {
		if (!$GLOBALS['TYPO3_CONF_VARS']['BE']['lockBeUserToDBmounts'] || $this->isAdmin()) {
			return 1;
		}
398
		$id = (int)$id;
399
		// Check if input id is an offline version page in which case we will map id to the online version:
400
		$checkRec = BackendUtility::getRecord('pages', $id, 'pid,t3ver_oid');
401
		if ($checkRec['pid'] == -1) {
402
			$id = (int)$checkRec['t3ver_oid'];
403
404
405
406
407
408
		}
		if (!$readPerms) {
			$readPerms = $this->getPagePermsClause(1);
		}
		if ($id > 0) {
			$wM = $this->returnWebmounts();
409
			$rL = BackendUtility::BEgetRootLine($id, ' AND ' . $readPerms);
410
411
412
413
414
415
416
417
418
			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);
		}
419
		return NULL;
420
421
422
423
424
425
	}

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

	/**
	 * Returns a combined binary representation of the current users permissions for the page-record, $row.
514
515
	 * 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.
516
517
518
	 * 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.
519
	 * @return int Bitwise representation of the users permissions in relation to input page row, $row
520
521
522
523
524
525
	 */
	public function calcPerms($row) {
		// Return 31 for admin users.
		if ($this->isAdmin()) {
			return 31;
		}
526
527
528
529
		// Return 0 if page is not within the allowed web mount
		if (!$this->isInWebMount($row['uid'])) {
			return 0;
		}
530
		$out = 0;
531
532
533
534
		if (
			isset($row['perms_userid']) && isset($row['perms_user']) && isset($row['perms_groupid'])
			&& isset($row['perms_group']) && isset($row['perms_everybody']) && isset($this->groupList)
		) {
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
			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
				);
552
				$out = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
553
554
555
556
557
558
559
			}
		}
		return $out;
	}

	/**
	 * Returns TRUE if the RTE (Rich Text Editor) can be enabled for the user
560
561
	 * 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]
562
563
	 * The reasons for a FALSE return can be found in $this->RTE_errors
	 *
564
	 * @return bool
565
566
567
568
569
570
571
572
573
574
575
	 */
	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:
576
		$RTE = BackendUtility::RTEgetObj();
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
		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
595
	 * @return bool TRUE if permission is granted (that is, the value was found in the groupData list - or the BE_USER is "admin")
596
597
598
	 */
	public function check($type, $value) {
		if (isset($this->groupData[$type])) {
599
			if ($this->isAdmin() || GeneralUtility::inList($this->groupData[$type], $value)) {
600
601
602
603
604
605
606
607
608
609
610
611
612
				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)
613
	 * @return bool Whether access is granted or not
614
615
616
617
618
619
620
	 */
	public function checkAuthMode($table, $field, $value, $authMode) {
		// Admin users can do anything:
		if ($this->isAdmin()) {
			return TRUE;
		}
		// Allow all blank values:
621
		if ((string)$value === '') {
622
623
624
625
626
627
628
			return TRUE;
		}
		// Certain characters are not allowed in the value
		if (preg_match('/[:|,]/', $value)) {
			return FALSE;
		}
		// Initialize:
629
		$testValue = $table . ':' . $field . ':' . $value;
630
631
		$out = TRUE;
		// Checking value:
632
		switch ((string)$authMode) {
633
			case 'explicitAllow':
634
				if (!GeneralUtility::inList($this->groupData['explicit_allowdeny'], ($testValue . ':ALLOW'))) {
635
636
637
638
					$out = FALSE;
				}
				break;
			case 'explicitDeny':
639
				if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) {
640
641
642
643
644
645
646
647
					$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) {
648
649
							if ((string)$iCfg[1] === (string)$value && $iCfg[4]) {
								switch ((string)$iCfg[4]) {
650
									case 'EXPL_ALLOW':
651
										if (!GeneralUtility::inList($this->groupData['explicit_allowdeny'], ($testValue . ':ALLOW'))) {
652
653
654
655
											$out = FALSE;
										}
										break;
									case 'EXPL_DENY':
656
										if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) {
657
658
659
											$out = FALSE;
										}
										break;
660
661
662
663
664
665
								}
								break;
							}
						}
					}
				}
666
				break;
667
668
669
670
671
672
673
		}
		return $out;
	}

	/**
	 * Checking if a language value (-1, 0 and >0 for sys_language records) is allowed to be edited by the user.
	 *
674
	 * @param int $langValue Language value to evaluate
675
	 * @return bool Returns TRUE if the language value is allowed, otherwise FALSE.
676
677
678
	 */
	public function checkLanguageAccess($langValue) {
		// The users language list must be non-blank - otherwise all languages are allowed.
679
		if (trim($this->groupData['allowed_languages']) !== '') {
680
			$langValue = (int)$langValue;
681
682
683
684
685
686
687
688
689
690
691
692
693
			// 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
694
	 * @return bool
695
696
697
	 */
	public function checkFullLanguagesAccess($table, $record) {
		$recordLocalizationAccess = $this->checkLanguageAccess(0);
698
		if ($recordLocalizationAccess && (BackendUtility::isTableLocalizable($table) || isset($GLOBALS['TCA'][$table]['ctrl']['transForeignTable']))) {
699
700
701
702
703
704
705
706
707
			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'];
			}
708
			$recordLocalizations = BackendUtility::getRecordsByField($l10nTable, $pointerField, $pointerValue, '', '', '', '1');
709
710
			if (is_array($recordLocalizations)) {
				foreach ($recordLocalizations as $localization) {
711
712
					$recordLocalizationAccess = $recordLocalizationAccess
						&& $this->checkLanguageAccess($localization[$GLOBALS['TCA'][$l10nTable]['ctrl']['languageField']]);
713
714
715
716
717
718
719
720
721
722
723
					if (!$recordLocalizationAccess) {
						break;
					}
				}
			}
		}
		return $recordLocalizationAccess;
	}

	/**
	 * Checking if a user has editing access to a record from a $GLOBALS['TCA'] table.
724
725
	 * 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.
726
727
	 * For instance languages settings, authMode selector boxes are evaluated (and maybe more in the future).
	 * It will check for workspace dependent access.
728
	 * The function takes an ID (int) or row (array) as second argument.
729
730
731
	 *
	 * @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.
732
733
734
	 * @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
735
	 * @return bool TRUE if OK, otherwise FALSE
736
737
	 */
	public function recordEditAccessInternals($table, $idOrRow, $newRecord = FALSE, $deletedRecord = FALSE, $checkFullLanguageAccess = FALSE) {
738
739
740
741
742
743
744
745
746
747
748
749
750
		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);
751
752
			}
			if (!is_array($idOrRow)) {
753
754
				$this->errorMsg = 'ERROR: Record could not be fetched.';
				return FALSE;
755
			}
756
757
758
759
760
761
762
763
764
765
766
767
768
		}
		// 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.';
769
770
					return FALSE;
				}
771
772
773
			} else {
				$this->errorMsg = 'ERROR: The "languageField" field named "'
					. $GLOBALS['TCA'][$table]['ctrl']['languageField'] . '" was not found in testing record!';
774
775
				return FALSE;
			}
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
		} 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;
795
796
797
798
						}
					}
				}
			}
799
800
801
802
803
804
		}
		// 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.';
805
806
					return FALSE;
				}
807
808
809
810
			} else {
				$this->errorMsg = 'ERROR: The "editLock" field named "' . $GLOBALS['TCA'][$table]['ctrl']['editlock']
					. '" was not found in testing record!';
				return FALSE;
811
			}
812
813
814
815
816
817
818
819
820
821
822
823
824
		}
		// 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;
825
826
827
				}
			}
		}
828
829
		// Finally, return TRUE if all is well.
		return TRUE;
830
831
832
	}

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

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

	/**
	 * 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
883
884
	 * - 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
885
886
887
888
889
890
891
892
893
	 *
	 * @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)) {
894
895
896
897
898
				$recData = BackendUtility::getRecord(
					$table,
					$recData,
					'pid' . ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] ? ',t3ver_wsid,t3ver_stage' : '')
				);
899
900
			}
			if (is_array($recData)) {
901
902
				// 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.
903
				if ((int)$recData['pid'] === -1) {
904
905
906
					// 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';
907
					} elseif ((int)$recData['t3ver_wsid'] !== $this->workspace) {
908
909
910
911
						// 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?
912
913
914
						return $this->workspaceCheckStageForCurrent(0)
							? FALSE
							: 'User\'s access level did not allow for editing';
915
916
917
918
919
920
921
					}
				} 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
922
923
924
						return $res > 0
							? FALSE
							: 'Stage for versioning root point and users access level did not allow for editing';
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
					} 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)) {
950
				$recData = BackendUtility::getRecord($table, $recData, 'uid,pid,t3ver_wsid,t3ver_stage');
951
952
			}
			if (is_array($recData)) {
953
				if ((int)$recData['pid'] === -1) {
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
					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
969
970
971
	 * 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!
972
	 *
973
	 * @param int $pid PID value to check for. OBSOLETE!
974
975
976
977
	 * @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) {
978
979
980
981
982
983
984
		// 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']
		) {
985
986
987
988
989
990
991
992
993
994
995
			// 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
	 *
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) {