BackendUserAuthentication.php 92.2 KB
Newer Older
1
2
3
4
5
6
<?php
namespace TYPO3\CMS\Core\Authentication;

/***************************************************************
 *  Copyright notice
 *
7
 *  (c) 1999-2013 Kasper Skårhøj (kasperYYYY@typo3.com)
8
9
10
11
12
13
14
15
16
17
 *  All rights reserved
 *
 *  This script is part of the TYPO3 project. The TYPO3 project is
 *  free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  The GNU General Public License can be found at
 *  http://www.gnu.org/copyleft/gpl.html.
18
 *  A copy is found in the text file GPL.txt and important notices to the license
19
20
21
22
23
24
25
26
27
28
 *  from the author is found in LICENSE.txt distributed with these scripts.
 *
 *
 *  This script is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  This copyright notice MUST APPEAR in all copies of the script!
 ***************************************************************/
29
30
31
32

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

33
34
35
36
37
38
39
40
41
42
43
44
45
/**
 * 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 {

	/**
46
47
	 * Should be set to the usergroup-column (id-list) in the user-record
	 * @var string
48
49
50
51
52
	 * @todo Define visibility
	 */
	public $usergroup_column = 'usergroup';

	/**
53
54
	 * The name of the group-table
	 * @var string
55
56
57
58
59
	 * @todo Define visibility
	 */
	public $usergroup_table = 'be_groups';

	/**
60
61
	 * holds lists of eg. tables, fields and other values related to the permission-system. See fetchGroupData
	 * @var array
62
	 * @todo Define visibility
63
	 * @internal
64
65
66
67
68
69
	 */
	public $groupData = array(
		'filemounts' => array()
	);

	/**
70
71
	 * This array will hold the groups that the user is a member of
	 * @var array
72
73
74
75
76
	 * @todo Define visibility
	 */
	public $userGroups = array();

	/**
77
78
	 * This array holds the uid's of the groups in the listed order
	 * @var array
79
80
81
82
83
	 * @todo Define visibility
	 */
	public $userGroupsUID = array();

	/**
84
85
	 * This is $this->userGroupsUID imploded to a comma list... Will correspond to the 'usergroup_cached_list'
	 * @var string
86
87
88
89
90
	 * @todo Define visibility
	 */
	public $groupList = '';

	/**
91
92
93
94
95
96
	 * User workspace.
	 * -99 is ERROR (none available)
	 * -1 is offline
	 * 0 is online
	 * >0 is custom workspaces
	 * @var int
97
98
99
100
101
	 * @todo Define visibility
	 */
	public $workspace = -99;

	/**
102
103
	 * Custom workspace record if any
	 * @var array
104
105
106
107
108
	 * @todo Define visibility
	 */
	public $workspaceRec = array();

	/**
109
110
111
112
	 * Used to accumulate data for the user-group.
	 * DON NOT USE THIS EXTERNALLY!
	 * Use $this->groupData instead
	 * @var array
113
	 * @todo Define visibility
114
	 * @internal
115
116
117
118
	 */
	public $dataLists = array(
		'webmount_list' => '',
		'filemount_list' => '',
119
		'file_permissions' => '',
120
121
122
123
124
125
126
127
128
129
130
131
		'modList' => '',
		'tables_select' => '',
		'tables_modify' => '',
		'pagetypes_select' => '',
		'non_exclude_fields' => '',
		'explicit_allowdeny' => '',
		'allowed_languages' => '',
		'workspace_perms' => '',
		'custom_options' => ''
	);

	/**
132
133
	 * For debugging/display of order in which subgroups are included.
	 * @var array
134
135
136
137
138
	 * @todo Define visibility
	 */
	public $includeHierarchy = array();

	/**
139
140
	 * List of group_id's in the order they are processed.
	 * @var array
141
142
143
144
145
	 * @todo Define visibility
	 */
	public $includeGroupArray = array();

	/**
146
147
	 * Set to 'WIN', if windows
	 * @var string
148
149
150
151
152
	 * @todo Define visibility
	 */
	public $OS = '';

	/**
153
154
	 * Used to accumulate the TSconfig data of the user
	 * @var array
155
156
157
158
159
	 * @todo Define visibility
	 */
	public $TSdataArray = array();

	/**
160
161
	 * Contains the non-parsed user TSconfig
	 * @var string
162
163
164
165
166
	 * @todo Define visibility
	 */
	public $userTS_text = '';

	/**
167
168
	 * Contains the parsed user TSconfig
	 * @var array
169
170
171
172
173
	 * @todo Define visibility
	 */
	public $userTS = array();

	/**
174
175
	 * Set internally if the user TSconfig was parsed and needs to be cached.
	 * @var bool
176
177
	 * @todo Define visibility
	 */
178
	public $userTSUpdated = FALSE;
179
180

	/**
181
182
	 * Set this from outside if you want the user TSconfig to ALWAYS be parsed and not fetched from cache.
	 * @var bool
183
184
	 * @todo Define visibility
	 */
185
	public $userTS_dontGetCached = FALSE;
186
187

	/**
188
189
	 * RTE availability errors collected.
	 * @var array
190
191
192
193
194
	 * @todo Define visibility
	 */
	public $RTE_errors = array();

	/**
195
196
	 * Contains last error message
	 * @var string
197
198
199
200
201
	 * @todo Define visibility
	 */
	public $errorMsg = '';

	/**
202
203
	 * Cache for checkWorkspaceCurrent()
	 * @var array|NULL
204
205
206
207
208
	 * @todo Define visibility
	 */
	public $checkWorkspaceCurrent_cache = NULL;

	/**
209
	 * @var \TYPO3\CMS\Core\Resource\ResourceStorage[]
210
211
212
213
214
215
216
217
218
	 */
	protected $fileStorages;

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

	/**
219
220
	 * Table to use for session data
	 * @var string
221
222
223
224
225
	 * @todo Define visibility
	 */
	public $session_table = 'be_sessions';

	/**
226
227
	 * Table in database with user data
	 * @var string
228
229
230
231
232
	 * @todo Define visibility
	 */
	public $user_table = 'be_users';

	/**
233
234
	 * Column for login-name
	 * @var string
235
236
237
238
239
	 * @todo Define visibility
	 */
	public $username_column = 'username';

	/**
240
241
	 * Column for password
	 * @var string
242
243
244
245
246
	 * @todo Define visibility
	 */
	public $userident_column = 'password';

	/**
247
248
	 * Column for user-id
	 * @var string
249
250
251
252
253
	 * @todo Define visibility
	 */
	public $userid_column = 'uid';

	/**
254
	 * @var string
255
256
257
258
259
	 * @todo Define visibility
	 */
	public $lastLogin_column = 'lastlogin';

	/**
260
	 * @var array
261
262
263
264
265
266
267
268
269
270
271
	 * @todo Define visibility
	 */
	public $enablecolumns = array(
		'rootLevel' => 1,
		'deleted' => 'deleted',
		'disabled' => 'disable',
		'starttime' => 'starttime',
		'endtime' => 'endtime'
	);

	/**
272
273
	 * Form field with login-name
	 * @var string
274
275
276
277
278
	 * @todo Define visibility
	 */
	public $formfield_uname = 'username';

	/**
279
280
	 * Form field with password
	 * @var string
281
282
283
284
285
	 * @todo Define visibility
	 */
	public $formfield_uident = 'userident';

	/**
286
287
	 * Form field with a unique value which is used to encrypt the password and username
	 * @var string
288
289
290
291
292
	 * @todo Define visibility
	 */
	public $formfield_chalvalue = 'challenge';

	/**
293
294
	 * Form field with status: *'login', 'logout'
	 * @var string
295
296
297
298
299
	 * @todo Define visibility
	 */
	public $formfield_status = 'login_status';

	/**
300
301
	 * Decides if the writelog() function is called at login and logout
	 * @var bool
302
303
	 * @todo Define visibility
	 */
304
	public $writeStdLog = TRUE;
305
306

	/**
307
308
	 * If the writelog() functions is called if a login-attempt has be tried without success
	 * @var bool
309
310
	 * @todo Define visibility
	 */
311
	public $writeAttemptLog = TRUE;
312
313

	/**
314
315
316
317
	 * 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
318
319
320
321
322
	 * @todo Define visibility
	 */
	public $auth_timeout_field = 6000;

	/**
323
	 * @var bool
324
325
	 * @todo Define visibility
	 */
326
	public $challengeStoredInCookie = TRUE;
327
328

	/**
329
	 * @var int
330
	 */
331
	public $firstMainGroup = 0;
332
333

	/**
334
335
	 * User Config
	 * @var array
336
337
338
339
340
	 * @todo Define visibility
	 */
	public $uc;

	/**
341
342
343
344
345
346
347
	 * 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
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
	 * @todo Define visibility
	 */
	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,
		'noMenuMode' => 0,
		'startModule' => 'help_aboutmodules',
		'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() {
377
		parent::__construct();
378
379
380
381
382
383
384
385
386
387
388
389
		$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.
	 *
	 * @return boolean
	 * @todo Define visibility
	 */
	public function isAdmin() {
390
		return is_array($this->user) && ($this->user['admin'] & 1) == 1;
391
392
393
394
395
396
397
398
399
400
401
402
	}

	/**
	 * 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.
	 *
	 * @param integer $groupId Group ID to look for in $this->groupList
	 * @return boolean
	 * @todo Define visibility
	 */
	public function isMemberOfGroup($groupId) {
403
		$groupId = (int)$groupId;
404
		if ($this->groupList && $groupId) {
405
			return GeneralUtility::inList($this->groupList, $groupId);
406
		}
407
		return FALSE;
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
	}

	/**
	 * 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
	 * @param integer $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.
423
	 * @return boolean
424
425
426
427
428
429
430
431
432
433
	 * @todo Define visibility
	 */
	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.
434
435
436
437
438
	 * 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
439
440
441
442
	 * Otherwise the function will return the uid of the webmount which was first found in the rootline of the input page $id
	 *
	 * @param integer $id Page ID to check
	 * @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!)
443
444
445
	 * @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
446
447
448
449
450
451
	 * @todo Define visibility
	 */
	public function isInWebMount($id, $readPerms = '', $exitOnError = 0) {
		if (!$GLOBALS['TYPO3_CONF_VARS']['BE']['lockBeUserToDBmounts'] || $this->isAdmin()) {
			return 1;
		}
452
		$id = (int)$id;
453
		// Check if input id is an offline version page in which case we will map id to the online version:
454
		$checkRec = BackendUtility::getRecord('pages', $id, 'pid,t3ver_oid');
455
		if ($checkRec['pid'] == -1) {
456
			$id = (int)$checkRec['t3ver_oid'];
457
458
459
460
461
462
		}
		if (!$readPerms) {
			$readPerms = $this->getPagePermsClause(1);
		}
		if ($id > 0) {
			$wM = $this->returnWebmounts();
463
			$rL = BackendUtility::BEgetRootLine($id, ' AND ' . $readPerms);
464
465
466
467
468
469
470
471
472
			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);
		}
473
		return NULL;
474
475
476
477
478
479
480
	}

	/**
	 * Checks access to a backend module with the $MCONF passed as first argument
	 *
	 * @param array $conf $MCONF array of a backend module!
	 * @param boolean $exitOnError If set, an array will issue an error message and exit.
481
	 * @throws \RuntimeException
482
483
484
485
	 * @return boolean 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
	 * @todo Define visibility
	 */
	public function modAccess($conf, $exitOnError) {
486
		if (!BackendUtility::isModuleSetInTBE_MODULES($conf['name'])) {
487
			if ($exitOnError) {
488
				throw new \RuntimeException('Fatal Error: This module "' . $conf['name'] . '" is not enabled in TBE_MODULES', 1294586446);
489
490
491
492
			}
			return FALSE;
		}
		// Workspaces check:
493
494
495
496
497
498
499
500
501
		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);
502
			}
503
			return FALSE;
504
505
506
507
508
509
		}
		// 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
510
		$acs = FALSE;
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
		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)
530
531
	 * 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.
532
533
534
535
536
537
538
539
540
541
	 *
	 * @param integer $perms Permission mask to use, see function description
	 * @return string Part of where clause. Prefix " AND " to this.
	 * @todo Define visibility
	 */
	public function getPagePermsClause($perms) {
		if (is_array($this->user)) {
			if ($this->isAdmin()) {
				return ' 1=1';
			}
542
			$perms = (int)$perms;
543
			// Make sure it's integer.
544
545
			$str = ' (' . '(pages.perms_everybody & ' . $perms . ' = ' . $perms . ')' . ' OR (pages.perms_userid = '
				. $this->user['uid'] . ' AND pages.perms_user & ' . $perms . ' = ' . $perms . ')';
546
547
548
			// User
			if ($this->groupList) {
				// Group (if any is set)
549
550
				$str .= ' OR (pages.perms_groupid in (' . $this->groupList . ') AND pages.perms_group & '
					. $perms . ' = ' . $perms . ')';
551
552
553
554
555
556
557
558
			}
			$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);
559
					$str = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
560
561
562
563
564
565
566
567
568
569
				}
			}
			return $str;
		} else {
			return ' 1=0';
		}
	}

	/**
	 * Returns a combined binary representation of the current users permissions for the page-record, $row.
570
571
	 * 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.
572
573
574
575
576
577
578
579
580
581
582
	 * 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.
	 * @return integer Bitwise representation of the users permissions in relation to input page row, $row
	 * @todo Define visibility
	 */
	public function calcPerms($row) {
		// Return 31 for admin users.
		if ($this->isAdmin()) {
			return 31;
		}
583
584
585
586
		// Return 0 if page is not within the allowed web mount
		if (!$this->isInWebMount($row['uid'])) {
			return 0;
		}
587
		$out = 0;
588
589
590
591
		if (
			isset($row['perms_userid']) && isset($row['perms_user']) && isset($row['perms_groupid'])
			&& isset($row['perms_group']) && isset($row['perms_everybody']) && isset($this->groupList)
		) {
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
			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
				);
609
				$out = GeneralUtility::callUserFunction($_funcRef, $_params, $this);
610
611
612
613
614
615
616
			}
		}
		return $out;
	}

	/**
	 * Returns TRUE if the RTE (Rich Text Editor) can be enabled for the user
617
618
	 * 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]
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
	 * The reasons for a FALSE return can be found in $this->RTE_errors
	 *
	 * @return boolean
	 * @todo Define visibility
	 */
	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:
634
		$RTE = BackendUtility::RTEgetObj();
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
		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
	 * @return boolean TRUE if permission is granted (that is, the value was found in the groupData list - or the BE_USER is "admin")
	 * @todo Define visibility
	 */
	public function check($type, $value) {
		if (isset($this->groupData[$type])) {
658
			if ($this->isAdmin() || GeneralUtility::inList($this->groupData[$type], $value)) {
659
660
661
662
663
664
665
666
667
668
669
670
671
				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)
672
	 * @return boolean Whether access is granted or not
673
674
675
676
677
678
679
680
	 * @todo Define visibility
	 */
	public function checkAuthMode($table, $field, $value, $authMode) {
		// Admin users can do anything:
		if ($this->isAdmin()) {
			return TRUE;
		}
		// Allow all blank values:
681
		if ((string)$value === '') {
682
683
684
685
686
687
688
			return TRUE;
		}
		// Certain characters are not allowed in the value
		if (preg_match('/[:|,]/', $value)) {
			return FALSE;
		}
		// Initialize:
689
		$testValue = $table . ':' . $field . ':' . $value;
690
691
692
		$out = TRUE;
		// Checking value:
		switch ((string) $authMode) {
693
			case 'explicitAllow':
694
				if (!GeneralUtility::inList($this->groupData['explicit_allowdeny'], ($testValue . ':ALLOW'))) {
695
696
697
698
					$out = FALSE;
				}
				break;
			case 'explicitDeny':
699
				if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) {
700
701
702
703
704
705
706
707
					$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) {
708
709
							if ((string)$iCfg[1] === (string)$value && $iCfg[4]) {
								switch ((string)$iCfg[4]) {
710
									case 'EXPL_ALLOW':
711
										if (!GeneralUtility::inList($this->groupData['explicit_allowdeny'], ($testValue . ':ALLOW'))) {
712
713
714
715
											$out = FALSE;
										}
										break;
									case 'EXPL_DENY':
716
										if (GeneralUtility::inList($this->groupData['explicit_allowdeny'], $testValue . ':DENY')) {
717
718
719
											$out = FALSE;
										}
										break;
720
721
722
723
724
725
								}
								break;
							}
						}
					}
				}
726
				break;
727
728
729
730
731
732
733
734
735
736
737
738
739
		}
		return $out;
	}

	/**
	 * Checking if a language value (-1, 0 and >0 for sys_language records) is allowed to be edited by the user.
	 *
	 * @param integer $langValue Language value to evaluate
	 * @return boolean Returns TRUE if the language value is allowed, otherwise FALSE.
	 * @todo Define visibility
	 */
	public function checkLanguageAccess($langValue) {
		// The users language list must be non-blank - otherwise all languages are allowed.
740
		if (trim($this->groupData['allowed_languages']) !== '') {
741
			$langValue = (int)$langValue;
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
			// 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
	 * @return boolean
	 * @todo Define visibility
	 */
	public function checkFullLanguagesAccess($table, $record) {
		$recordLocalizationAccess = $this->checkLanguageAccess(0);
760
		if ($recordLocalizationAccess && (BackendUtility::isTableLocalizable($table) || isset($GLOBALS['TCA'][$table]['ctrl']['transForeignTable']))) {
761
762
763
764
765
766
767
768
769
			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'];
			}
770
			$recordLocalizations = BackendUtility::getRecordsByField($l10nTable, $pointerField, $pointerValue, '', '', '', '1');
771
772
			if (is_array($recordLocalizations)) {
				foreach ($recordLocalizations as $localization) {
773
774
					$recordLocalizationAccess = $recordLocalizationAccess
						&& $this->checkLanguageAccess($localization[$GLOBALS['TCA'][$l10nTable]['ctrl']['languageField']]);
775
776
777
778
779
780
781
782
783
784
785
					if (!$recordLocalizationAccess) {
						break;
					}
				}
			}
		}
		return $recordLocalizationAccess;
	}

	/**
	 * Checking if a user has editing access to a record from a $GLOBALS['TCA'] table.
786
787
	 * 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.
788
789
790
791
792
793
794
795
796
797
798
799
800
	 * For instance languages settings, authMode selector boxes are evaluated (and maybe more in the future).
	 * It will check for workspace dependent access.
	 * The function takes an ID (integer) or row (array) as second argument.
	 *
	 * @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.
	 * @param boolean $newRecord Set, if testing a new (non-existing) record array. Will disable certain checks that doesn't make much sense in that context.
	 * @param boolean $deletedRecord Set, if testing a deleted record array.
	 * @param boolean $checkFullLanguageAccess Set, whenever access to all translations of the record is required
	 * @return boolean TRUE if OK, otherwise FALSE
	 * @todo Define visibility
	 */
	public function recordEditAccessInternals($table, $idOrRow, $newRecord = FALSE, $deletedRecord = FALSE, $checkFullLanguageAccess = FALSE) {
801
802
803
804
805
806
807
808
809
810
811
812
813
		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);
814
815
			}
			if (!is_array($idOrRow)) {
816
817
				$this->errorMsg = 'ERROR: Record could not be fetched.';
				return FALSE;
818
			}
819
820
821
822
823
824
825
826
827
828
829
830
831
		}
		// 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.';
832
833
					return FALSE;
				}
834
835
836
			} else {
				$this->errorMsg = 'ERROR: The "languageField" field named "'
					. $GLOBALS['TCA'][$table]['ctrl']['languageField'] . '" was not found in testing record!';
837
838
				return FALSE;
			}
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
		} 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;
858
859
860
861
						}
					}
				}
			}
862
863
864
865
866
867
		}
		// 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.';
868
869
					return FALSE;
				}
870
871
872
873
			} else {
				$this->errorMsg = 'ERROR: The "editLock" field named "' . $GLOBALS['TCA'][$table]['ctrl']['editlock']
					. '" was not found in testing record!';
				return FALSE;
874
			}
875
876
877
878
879
880
881
882
883
884
885
886
887
		}
		// 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;
888
889
890
				}
			}
		}
891
892
		// Finally, return TRUE if all is well.
		return TRUE;
893
894
895
	}

	/**
896
897
	 * Checks a type of permission against the compiled permission integer,
	 * $compiledPermissions, and in relation to table, $tableName
898
899
900
901
902
903
904
905
906
907
908
909
	 *
	 * @param integer $compiledPermissions Could typically be the "compiled permissions" integer returned by ->calcPerms
	 * @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)
	 * @return boolean
	 * @access public (used by typo3/alt_clickmenu.php)
	 */
	public function isPSet($compiledPermissions, $tableName, $actionType = '') {
		if ($this->isAdmin()) {
			$result = TRUE;
		} elseif ($tableName == 'pages') {
			switch ($actionType) {
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
				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;
925
926
927
928
929
930
931
932
933
934
935
936
937
938
			}
		} else {
			$result = ($compiledPermissions & 16) !== 0;
		}
		return $result;
	}

	/**
	 * Returns TRUE if the BE_USER is allowed to *create* shortcuts in the backend modules
	 *
	 * @return boolean
	 * @todo Define visibility
	 */
	public function mayMakeShortcut() {
939
940
		return $this->getTSConfigVal('options.enableBookmarks')
			&& !$this->getTSConfigVal('options.mayNotCreateEditBookmarks');
941
942
943
944
945
946
	}

	/**
	 * 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
947
948
	 * - 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
949
950
951
952
953
954
955
956
957
958
	 *
	 * @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
	 * @todo Define visibility
	 */
	public function workspaceCannotEditRecord($table, $recData) {
		// Only test offline spaces:
		if ($this->workspace !== 0) {
			if (!is_array($recData)) {
959
960
961
962
963
				$recData = BackendUtility::getRecord(
					$table,
					$recData,
					'pid' . ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] ? ',t3ver_wsid,t3ver_stage' : '')
				);
964
965
			}
			if (is_array($recData)) {
966
967
				// 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.
968
				if ((int)$recData['pid'] === -1) {
969
970
971
					// 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';
972
					} elseif ((int)$recData['t3ver_wsid'] !== $this->workspace) {
973
974
975
976
						// 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?
977
978
979
						return $this->workspaceCheckStageForCurrent(0)
							? FALSE
							: 'User\'s access level did not allow for editing';
980
981
982
983
984
985
986
					}
				} 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
987
988
989
						return $res > 0
							? FALSE
							: 'Stage for versioning root point and users access level did not allow for editing';
990
991
992
993
994
995
996
997
998
999
1000
					} 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;