Completed
Push — hash-nonce ( 07e2e8 )
by Sam
08:52
created

Permission   D

Complexity

Total Complexity 96

Size/Duplication

Total Lines 675
Duplicated Lines 7.11 %

Coupling/Cohesion

Components 3
Dependencies 10

Importance

Changes 0
Metric Value
dl 48
loc 675
rs 4.5578
c 0
b 0
f 0
wmc 96
lcom 3
cbo 10

19 Methods

Rating   Name   Duplication   Size   Complexity  
A check() 0 10 3
A flush_permission_cache() 0 3 1
F checkMember() 0 115 27
B permissions_for_member() 0 31 2
D groupList() 0 32 9
B grant() 24 24 4
B deny() 24 24 4
B get_members_by_permission() 0 22 5
A get_groups_by_permission() 0 15 2
C get_codes() 0 77 20
A sort_permissions() 0 9 3
A add_to_hidden_permissions() 0 5 2
A remove_from_hidden_permissions() 0 5 2
A declare_permissions() 0 4 1
A get_declared_permissions_list() 0 14 3
A get_label_for_permission() 0 5 2
A traverse_declared_permissions() 0 13 4
A onBeforeWrite() 0 6 1
A get_template_global_variables() 0 5 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Permission often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Permission, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Represents a permission assigned to a group.
4
 * @package framework
5
 * @subpackage security
6
 *
7
 * @property string Code
8
 * @property int Arg
9
 * @property int Type
10
 *
11
 * @property int GroupID
12
 *
13
 * @method Group Group()
14
 */
15
class Permission extends DataObject implements TemplateGlobalProvider {
16
17
	// the (1) after Type specifies the DB default value which is needed for
18
	// upgrades from older SilverStripe versions
19
	private static $db = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
20
		"Code" => "Varchar(255)",
21
		"Arg" => "Int",
22
		"Type" => "Int(1)"
23
	);
24
	private static $has_one = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
25
		"Group" => "Group"
26
	);
27
	private static $indexes = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
28
		"Code" => true
29
	);
30
	private static $defaults = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
31
		"Type" => 1
32
	);
33
	private static $has_many = array();
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
34
35
	private static $many_many = array();
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
36
37
	private static $belongs_many_many = array();
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
38
39
	/**
40
	 * This is the value to use for the "Type" field if a permission should be
41
	 * granted.
42
	 */
43
	const GRANT_PERMISSION = 1;
44
45
	/**
46
	 * This is the value to use for the "Type" field if a permission should be
47
	 * denied.
48
	 */
49
	const DENY_PERMISSION = -1;
50
51
	/**
52
	 * This is the value to use for the "Type" field if a permission should be
53
	 * inherited.
54
	 */
55
	const INHERIT_PERMISSION = 0;
56
57
58
	/**
59
	 * Method to globally disable "strict" checking, which means a permission
60
	 * will be granted if the key does not exist at all.
61
	 *
62
	 * @var bool
63
	 */
64
	private static $declared_permissions = null;
65
66
	/**
67
	 * Linear list of declared permissions in the system.
68
	 *
69
	 * @var array
70
	 */
71
	private static $declared_permissions_list = null;
72
73
	/**
74
	 * @config
75
	 * @var $strict_checking Boolean Method to globally disable "strict" checking,
76
	 * which means a permission will be granted if the key does not exist at all.
77
	 */
78
	private static $strict_checking = true;
79
80
	/**
81
	 * Set to false to prevent the 'ADMIN' permission from implying all
82
	 * permissions in the system
83
	 *
84
	 * @config
85
	 * @var bool
86
	 */
87
	private static $admin_implies_all = true;
88
89
	/**
90
	 * a list of permission codes which doesn't appear in the Permission list
91
	 * when make the {@link PermissionCheckboxSetField}
92
	 * @config
93
	 * @var array;
94
	 */
95
	private static $hidden_permissions = array();
96
97
	/**
98
	 * @config These permissions can only be applied by ADMIN users, to prevent
99
	 * privilege escalation on group assignments and inheritance.
100
	 * @var array
101
	 */
102
	private static $privileged_permissions = array(
103
		'ADMIN',
104
		'APPLY_ROLES',
105
		'EDIT_PERMISSIONS'
106
	);
107
108
	/**
109
	 * Check that the current member has the given permission.
110
	 *
111
	 * @param string|array $code Code of the permission to check (case-sensitive)
112
	 * @param string $arg Optional argument (e.g. a permissions for a specific page)
113
	 * @param int|Member $member Optional member instance or ID. If set to NULL, the permssion
114
	 *  will be checked for the current user
115
	 * @param bool $strict Use "strict" checking (which means a permission
116
	 *  will be granted if the key does not exist at all)?
117
	 * @return int|bool The ID of the permission record if the permission
118
	 *  exists; FALSE otherwise. If "strict" checking is
119
	 *  disabled, TRUE will be returned if the permission does not exist at all.
120
	 */
121
	public static function check($code, $arg = "any", $member = null, $strict = true) {
122
		if(!$member) {
123
			if(!Member::currentUserID()) {
124
				return false;
125
			}
126
			$member = Member::currentUserID();
127
		}
128
129
		return self::checkMember($member, $code, $arg, $strict);
130
	}
131
132
	/**
133
	 * Permissions cache.  The format is a map, where the keys are member IDs, and the values are
134
	 * arrays of permission codes.
135
	 */
136
	private static $cache_permissions = array();
137
138
	/**
139
	 * Flush the permission cache, for example if you have edited group membership or a permission record.
140
	 * @todo Call this whenever Group_Members is added to or removed from
141
	 */
142
	public static function flush_permission_cache() {
143
		self::$cache_permissions = array();
144
	}
145
146
	/**
147
	 * Check that the given member has the given permission.
148
	 *
149
	 * @param int|Member memberID The ID of the member to check. Leave blank for the current member.
150
	 *  Alternatively you can use a member object.
151
	 * @param string|array $code Code of the permission to check (case-sensitive)
152
	 * @param string $arg Optional argument (e.g. a permissions for a specific page)
153
	 * @param bool $strict Use "strict" checking (which means a permission
154
	 *  will be granted if the key does not exist at all)?
155
	 * @return int|bool The ID of the permission record if the permission
156
	 *  exists; FALSE otherwise. If "strict" checking is
157
	 *  disabled, TRUE will be returned if the permission does not exist at all.
158
	 */
159
	public static function checkMember($member, $code, $arg = "any", $strict = true) {
160
		if(!$member) {
161
			$memberID = $member = Member::currentUserID();
0 ignored issues
show
Unused Code introduced by
$member is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
162
		} else {
163
			$memberID = (is_object($member)) ? $member->ID : $member;
164
		}
165
166
		if (!$memberID) {
167
			return false;
168
		}
169
170
		// Turn the code into an array as we may need to add other permsissions to the set we check
171
		if(!is_array($code)) $code = array($code);
172
173
		if($arg == 'any') {
174
			$adminImpliesAll = (bool)Config::inst()->get('Permission', 'admin_implies_all');
175
			// Cache the permissions in memory
176
			if(!isset(self::$cache_permissions[$memberID])) {
177
				self::$cache_permissions[$memberID] = self::permissions_for_member($memberID);
178
			}
179
			foreach ($code as $permCode) {
180
				if ($permCode === 'CMS_ACCESS') {
181
					foreach (self::$cache_permissions[$memberID] as $perm) {
182
						//if they have admin rights OR they have an explicit access to the CMS then give permission
183
						if (($adminImpliesAll && $perm == 'ADMIN') || substr($perm, 0, 11) === 'CMS_ACCESS_') {
184
							return true;
185
						}
186
					}
187
				}
188
				elseif (substr($permCode, 0, 11) === 'CMS_ACCESS_') {
189
					//cms_access_leftandmain means access to all CMS areas
190
					$code[] = 'CMS_ACCESS_LeftAndMain';
191
					break;
192
				}
193
			}
194
195
			// if ADMIN has all privileges, then we need to push that code in
196
			if($adminImpliesAll) {
197
				$code[] = "ADMIN";
198
			}
199
200
			// Multiple $code values - return true if at least one matches, ie, intersection exists
201
			return (bool)array_intersect($code, self::$cache_permissions[$memberID]);
202
		}
203
204
		// Code filters
205
		$codeParams = is_array($code) ? $code : array($code);
206
		$codeClause = DB::placeholders($codeParams);
207
		$adminParams = (self::$admin_implies_all) ? array('ADMIN') : array();
208
		$adminClause = (self::$admin_implies_all) ?  ", ?" : '';
209
210
		// The following code should only be used if you're not using the "any" arg.  This is kind
211
		// of obselete functionality and could possibly be deprecated.
212
		$groupParams = self::groupList($memberID);
213
		if(empty($groupParams)) return false;
214
		$groupClause = DB::placeholders($groupParams);
215
216
		// Arg component
217
		$argClause = "";
218
		$argParams = array();
219
		switch($arg) {
220
			case "any":
221
				break;
222
			case "all":
223
				$argClause = " AND \"Arg\" = ?";
224
				$argParams = array(-1);
225
				break;
226
			default:
227
				if(is_numeric($arg)) {
228
					$argClause = "AND \"Arg\" IN (?, ?) ";
229
					$argParams = array(-1, $arg);
230
				} else {
231
					user_error("Permission::checkMember: bad arg '$arg'", E_USER_ERROR);
232
				}
233
		}
234
		$adminFilter = (Config::inst()->get('Permission', 'admin_implies_all')) ?  ",'ADMIN'" : '';
0 ignored issues
show
Unused Code introduced by
$adminFilter is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
235
236
		// Raw SQL for efficiency
237
		$permission = DB::prepared_query(
238
			"SELECT \"ID\"
239
			FROM \"Permission\"
240
			WHERE (
241
				\"Code\" IN ($codeClause $adminClause)
242
				AND \"Type\" = ?
243
				AND \"GroupID\" IN ($groupClause)
244
				$argClause
245
			)",
246
			array_merge(
247
				$codeParams,
248
				$adminParams,
249
				array(self::GRANT_PERMISSION),
250
				$groupParams,
251
				$argParams
252
			)
253
		)->value();
254
255
		if($permission) return $permission;
256
257
		// Strict checking disabled?
258
		if(!Config::inst()->get('Permission', 'strict_checking') || !$strict) {
259
			$hasPermission = DB::prepared_query(
260
				"SELECT COUNT(*)
261
				FROM \"Permission\"
262
				WHERE (
263
					\"Code\" IN ($codeClause) AND
264
					\"Type\" = ?
265
				)",
266
				array_merge($codeParams, array(self::GRANT_PERMISSION))
267
			)->value();
268
269
			if(!$hasPermission) return;
270
		}
271
272
		return false;
273
	}
274
275
	/**
276
	 * Get all the 'any' permission codes available to the given member.
277
	 *
278
	 * @return array
279
	 */
280
	public static function permissions_for_member($memberID) {
281
		$groupList = self::groupList($memberID);
282
283
		if($groupList) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groupList of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
284
			$groupCSV = implode(", ", $groupList);
285
286
			$allowed = array_unique(DB::query("
287
				SELECT \"Code\"
288
				FROM \"Permission\"
289
				WHERE \"Type\" = " . self::GRANT_PERMISSION . " AND \"GroupID\" IN ($groupCSV)
290
291
				UNION
292
293
				SELECT \"Code\"
294
				FROM \"PermissionRoleCode\" PRC
295
				INNER JOIN \"PermissionRole\" PR ON PRC.\"RoleID\" = PR.\"ID\"
296
				INNER JOIN \"Group_Roles\" GR ON GR.\"PermissionRoleID\" = PR.\"ID\"
297
				WHERE \"GroupID\" IN ($groupCSV)
298
			")->column());
299
300
			$denied = array_unique(DB::query("
301
				SELECT \"Code\"
302
				FROM \"Permission\"
303
				WHERE \"Type\" = " . self::DENY_PERMISSION . " AND \"GroupID\" IN ($groupCSV)
304
			")->column());
305
306
			return array_diff($allowed, $denied);
307
		}
308
309
		return array();
310
	}
311
312
313
	/**
314
	 * Get the list of groups that the given member belongs to.
315
	 *
316
	 * Call without an argument to get the groups that the current member
317
	 * belongs to. In this case, the results will be session-cached.
318
	 *
319
	 * @param int $memberID The ID of the member. Leave blank for the current
320
	 *                      member.
321
	 * @return array Returns a list of group IDs to which the member belongs
322
	 *               to or NULL.
323
	 */
324
	public static function groupList($memberID = null) {
325
		// Default to current member, with session-caching
326
		if(!$memberID) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $memberID of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
327
			$member = Member::currentUser();
328
			if($member && isset($_SESSION['Permission_groupList'][$member->ID]))
329
				return $_SESSION['Permission_groupList'][$member->ID];
330
		} else {
331
			$member = DataObject::get_by_id("Member", $memberID);
332
		}
333
334
		if($member) {
335
			// Build a list of the IDs of the groups.  Most of the heavy lifting
336
			// is done by Member::Groups
337
			// NOTE: This isn't effecient; but it's called once per session so
338
			// it's a low priority to fix.
339
			$groups = $member->Groups();
340
			$groupList = array();
341
342
			if($groups) {
343
				foreach($groups as $group)
344
					$groupList[] = $group->ID;
345
			}
346
347
348
			// Session caching
349
			if(!$memberID) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $memberID of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
350
				$_SESSION['Permission_groupList'][$member->ID] = $groupList;
351
			}
352
353
			return isset($groupList) ? $groupList : null;
354
		}
355
	}
356
357
358
	/**
359
	 * Grant the given permission code/arg to the given group
360
	 *
361
	 * @param int $groupID The ID of the group
362
	 * @param string $code The permission code
363
	 * @param string Optional: The permission argument (e.g. a page ID).
364
	 * @returns Permission Returns the new permission object.
365
	 */
366 View Code Duplication
	public static function grant($groupID, $code, $arg = "any") {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
367
		$perm = new Permission();
368
		$perm->GroupID = $groupID;
369
		$perm->Code = $code;
370
		$perm->Type = self::GRANT_PERMISSION;
371
372
		// Arg component
373
		switch($arg) {
374
			case "any":
375
				break;
376
			case "all":
377
				$perm->Arg = -1;
378
			default:
379
				if(is_numeric($arg)) {
380
					$perm->Arg = $arg;
381
				} else {
382
					user_error("Permission::checkMember: bad arg '$arg'",
383
										E_USER_ERROR);
384
				}
385
		}
386
387
		$perm->write();
388
		return $perm;
389
	}
390
391
392
	/**
393
	 * Deny the given permission code/arg to the given group
394
	 *
395
	 * @param int $groupID The ID of the group
396
	 * @param string $code The permission code
397
	 * @param string Optional: The permission argument (e.g. a page ID).
398
	 * @returns Permission Returns the new permission object.
399
	 */
400 View Code Duplication
	public static function deny($groupID, $code, $arg = "any") {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
401
		$perm = new Permission();
402
		$perm->GroupID = $groupID;
403
		$perm->Code = $code;
404
		$perm->Type = self::DENY_PERMISSION;
405
406
		// Arg component
407
		switch($arg) {
408
			case "any":
409
				break;
410
			case "all":
411
				$perm->Arg = -1;
412
			default:
413
				if(is_numeric($arg)) {
414
					$perm->Arg = $arg;
415
				} else {
416
					user_error("Permission::checkMember: bad arg '$arg'",
417
										E_USER_ERROR);
418
				}
419
		}
420
421
		$perm->write();
422
		return $perm;
423
	}
424
425
	/**
426
	 * Returns all members for a specific permission.
427
	 *
428
	 * @param $code String|array Either a single permission code, or a list of permission codes
429
	 * @return SS_List Returns a set of member that have the specified
430
	 *                       permission.
431
	 */
432
	public static function get_members_by_permission($code) {
433
		$toplevelGroups = self::get_groups_by_permission($code);
434
		if (!$toplevelGroups) return new ArrayList();
435
436
		$groupIDs = array();
437
		foreach($toplevelGroups as $group) {
438
			$familyIDs = $group->collateFamilyIDs();
439
			if(is_array($familyIDs)) {
440
				$groupIDs = array_merge($groupIDs, array_values($familyIDs));
441
			}
442
		}
443
444
		if(empty($groupIDs)) return new ArrayList();
445
446
		$groupClause = DB::placeholders($groupIDs);
447
		$members = Member::get()
448
			->where(array("\"Group\".\"ID\" IN ($groupClause)" => $groupIDs))
449
			->leftJoin("Group_Members", '"Member"."ID" = "Group_Members"."MemberID"')
450
			->leftJoin("Group", '"Group_Members"."GroupID" = "Group"."ID"');
451
452
		return $members;
453
	}
454
455
	/**
456
	 * Return all of the groups that have one of the given permission codes
457
	 * @param $codes array|string Either a single permission code, or an array of permission codes
458
	 * @return SS_List The matching group objects
459
	 */
460
	public static function get_groups_by_permission($codes) {
461
		$codeParams = is_array($codes) ? $codes : array($codes);
462
		$codeClause = DB::placeholders($codeParams);
463
464
		// Via Roles are groups that have the permission via a role
465
		return DataObject::get('Group')
466
			->where(array(
467
				"\"PermissionRoleCode\".\"Code\" IN ($codeClause) OR \"Permission\".\"Code\" IN ($codeClause)"
468
				=> array_merge($codeParams, $codeParams)
469
			))
470
			->leftJoin('Permission', "\"Permission\".\"GroupID\" = \"Group\".\"ID\"")
471
			->leftJoin('Group_Roles', "\"Group_Roles\".\"GroupID\" = \"Group\".\"ID\"")
472
			->leftJoin('PermissionRole', "\"Group_Roles\".\"PermissionRoleID\" = \"PermissionRole\".\"ID\"")
473
			->leftJoin('PermissionRoleCode', "\"PermissionRoleCode\".\"RoleID\" = \"PermissionRole\".\"ID\"");
474
	}
475
476
477
	/**
478
	 * Get a list of all available permission codes, both defined through the
479
	 * {@link PermissionProvider} interface, and all not explicitly defined codes existing
480
	 * as a {@link Permission} database record. By default, the results are
481
	 * grouped as denoted by {@link Permission_Group}.
482
	 *
483
	 * @param bool $grouped Group results into an array of permission groups.
484
	 * @return array Returns an array of all available permission codes. The
485
	 *  array indicies are the permission codes as used in
486
	 *  {@link Permission::check()}. The value is a description
487
	 *  suitable for using in an interface.
488
	 */
489
	public static function get_codes($grouped = true) {
490
		$classes = ClassInfo::implementorsOf('PermissionProvider');
491
492
		$allCodes = array();
493
		$adminCategory = _t('Permission.AdminGroup', 'Administrator');
494
		$allCodes[$adminCategory]['ADMIN'] = array(
495
			'name' => _t('Permission.FULLADMINRIGHTS', 'Full administrative rights'),
496
			'help' => _t(
497
				'Permission.FULLADMINRIGHTS_HELP',
498
				'Implies and overrules all other assigned permissions.'
499
			),
500
			'sort' => 100000
501
		);
502
503
		if($classes) foreach($classes as $class) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $classes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
504
			$SNG = singleton($class);
505
			if($SNG instanceof TestOnly) continue;
506
507
			$someCodes = $SNG->providePermissions();
508
			if($someCodes) {
509
				foreach($someCodes as $k => $v) {
510
					if (is_array($v)) {
511
						// There must be a category and name key.
512
						if (!isset($v['category'])) user_error("The permission $k must have a category key",
513
							E_USER_WARNING);
514
						if (!isset($v['name'])) user_error("The permission $k must have a name key",
515
							E_USER_WARNING);
516
517
						if (!isset($allCodes[$v['category']])) $allCodes[$v['category']] = array();
518
519
						$allCodes[$v['category']][$k] = array(
520
							'name' => $v['name'],
521
							'help' => isset($v['help']) ? $v['help'] : null,
522
							'sort' => isset($v['sort']) ? $v['sort'] : 0
523
						);
524
525
					} else {
526
						$allCodes['Other'][$k] = array(
527
							'name' => $v,
528
							'help' => null,
529
							'sort' => 0
530
						);
531
					}
532
				}
533
			}
534
		}
535
536
		$flatCodeArray = array();
537
		foreach($allCodes as $category) foreach($category as $code => $permission) $flatCodeArray[] = $code;
538
		$otherPerms = DB::query("SELECT DISTINCT \"Code\" From \"Permission\" WHERE \"Code\" != ''")->column();
539
540
		if($otherPerms) foreach($otherPerms as $otherPerm) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $otherPerms of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
541
			if(!in_array($otherPerm, $flatCodeArray))
542
				$allCodes['Other'][$otherPerm] = array(
543
					'name' => $otherPerm,
544
					'help' => null,
545
					'sort' => 0
546
				);
547
		}
548
549
		// Don't let people hijack ADMIN rights
550
		if(!Permission::check("ADMIN")) unset($allCodes['ADMIN']);
551
552
		ksort($allCodes);
553
554
		$returnCodes = array();
555
		foreach($allCodes as $category => $permissions) {
556
			if($grouped) {
557
				uasort($permissions, array(__CLASS__, 'sort_permissions'));
558
				$returnCodes[$category] = $permissions;
559
			} else {
560
				$returnCodes = array_merge($returnCodes, $permissions);
561
			}
562
		}
563
564
		return $returnCodes;
565
	}
566
567
	/**
568
	 * Sort permissions based on their sort value, or name
569
	 *
570
	 */
571
	public static function sort_permissions($a, $b) {
572
		if ($a['sort'] == $b['sort']) {
573
			// Same sort value, do alpha instead
574
			return strcmp($a['name'], $b['name']);
575
		} else {
576
			// Just numeric.
577
			return $a['sort'] < $b['sort'] ? -1 : 1;
578
		}
579
	}
580
581
	/**
582
	 * add a permission represented by the $code to the {@link slef::$hidden_permissions} list
583
	 *
584
	 * @deprecated 4.0 Use "Permission.hidden_permissions" config setting instead
585
	 * @param $code string - the permissions code
586
	 * @return void
587
	 */
588
	public static function add_to_hidden_permissions($code){
0 ignored issues
show
Unused Code introduced by
The parameter $code is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
589
		if(is_string($codes)) $codes = array($codes);
0 ignored issues
show
Bug introduced by
The variable $codes seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
590
		Deprecation::notice('4.0', 'Use "Permission.hidden_permissions" config setting instead');
591
		Config::inst()->update('Permission', 'hidden_permissions', $codes);
0 ignored issues
show
Bug introduced by
The variable $codes does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
592
	}
593
594
	/**
595
	 * remove a permission represented by the $code from the {@link slef::$hidden_permissions} list
596
	 *
597
	 * @deprecated 4.0 Use "Permission.hidden_permissions" config setting instead
598
	 * @param $code string - the permissions code
599
	 * @return void
600
	 */
601
	public static function remove_from_hidden_permissions($code){
0 ignored issues
show
Unused Code introduced by
The parameter $code is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
602
		if(is_string($codes)) $codes = array($codes);
0 ignored issues
show
Bug introduced by
The variable $codes seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
603
		Deprecation::notice('4.0', 'Use "Permission.hidden_permissions" config setting instead');
604
		Config::inst()->remove('Permission', 'hidden_permissions', $codes);
0 ignored issues
show
Bug introduced by
The variable $codes does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
605
	}
606
607
	/**
608
	 * Declare an array of permissions for the system.
609
	 *
610
	 * Permissions can be grouped by nesting arrays. Scalar values are always
611
	 * treated as permissions.
612
	 *
613
	 * @deprecated 4.0 Use "Permission.declared_permissions" config setting instead
614
	 * @param array $permArray A (possibly nested) array of permissions to
615
	 *                         declare for the system.
616
	 */
617
	public static function declare_permissions($permArray) {
618
		Deprecation::notice('4.0', 'Use "Permission.declared_permissions" config setting instead');
619
		self::config()->declared_permissions = $permArray;
0 ignored issues
show
Documentation introduced by
The property declared_permissions does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
620
	}
621
622
623
	/**
624
	 * Get a linear list of the permissions in the system.
625
	 *
626
	 * @return array Linear list of declared permissions in the system.
627
	 */
628
	public static function get_declared_permissions_list() {
629
		if(!self::$declared_permissions)
630
			return null;
631
632
		if(self::$declared_permissions_list)
0 ignored issues
show
Bug Best Practice introduced by
The expression self::$declared_permissions_list of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
633
			return self::$declared_permissions_list;
634
635
		self::$declared_permissions_list = array();
636
637
		self::traverse_declared_permissions(self::$declared_permissions,
0 ignored issues
show
Documentation introduced by
self::$declared_permissions is of type boolean, but the function expects a object<aeeay>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
638
																				self::$declared_permissions_list);
639
640
		return self::$declared_permissions_list;
641
	}
642
643
	/**
644
	 * Look up the human-readable title for the permission as defined by <code>Permission::declare_permissions</code>
645
	 *
646
	 * @param $perm Permission code
647
	 * @return Label for the given permission, or the permission itself if the label doesn't exist
648
	 */
649
	public static function get_label_for_permission($perm) {
650
		$list = self::get_declared_permissions_list();
651
		if(array_key_exists($perm, $list)) return $list[$perm];
652
		return $perm;
653
	}
654
655
	/**
656
	 * Recursively traverse the nested list of declared permissions and create
657
	 * a linear list.
658
	 *
659
	 * @param aeeay $declared Nested structure of permissions.
660
	 * @param $list List of permissions in the structure. The result will be
661
	 *              written to this array.
662
	 */
663
	protected static function traverse_declared_permissions($declared, &$list) {
664
		if(!is_array($declared))
665
			return;
666
667
		foreach($declared as $perm => $value) {
668
			if($value instanceof Permission_Group) {
669
				$list[] = $value->getName();
670
				self::traverse_declared_permissions($value->getPermissions(), $list);
0 ignored issues
show
Documentation introduced by
$value->getPermissions() is of type string, but the function expects a object<aeeay>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
671
			} else {
672
				$list[$perm] = $value;
673
			}
674
		}
675
	}
676
677
	public function onBeforeWrite() {
678
		parent::onBeforeWrite();
679
680
		// Just in case we've altered someone's permissions
681
		Permission::flush_permission_cache();
682
	}
683
684
	public static function get_template_global_variables() {
685
		return array(
686
			'HasPerm' => 'check'
687
		);
688
	}
689
}
690
691
692
/**
693
 * Permission_Group class
694
 *
695
 * This class is used to group permissions together for showing on an
696
 * interface.
697
 * @package framework
698
 * @subpackage security
699
 */
700
class Permission_Group {
701
702
	/**
703
	 * Name of the permission group (can be used as label in an interface)
704
	 * @var string
705
	 */
706
	protected $name;
707
708
	/**
709
	 * Associative array of permissions in this permission group. The array
710
	 * indicies are the permission codes as used in
711
	 * {@link Permission::check()}. The value is suitable for using in an
712
	 * interface.
713
	 * @var string
714
	 */
715
	protected $permissions = array();
716
717
718
	/**
719
	 * Constructor
720
	 *
721
	 * @param string $name Text that could be used as label used in an
722
	 *                     interface
723
	 * @param array $permissions Associative array of permissions in this
724
	 *                           permission group. The array indicies are the
725
	 *                           permission codes as used in
726
	 *                           {@link Permission::check()}. The value is
727
	 *                           suitable for using in an interface.
728
	 */
729
	public function __construct($name, $permissions) {
730
		$this->name = $name;
731
		$this->permissions = $permissions;
0 ignored issues
show
Documentation Bug introduced by
It seems like $permissions of type array is incompatible with the declared type string of property $permissions.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
732
	}
733
734
	/**
735
	 * Get the name of the permission group
736
	 *
737
	 * @return string Name (label) of the permission group
738
	 */
739
	public function getName() {
740
		return $this->name;
741
	}
742
743
744
	/**
745
	 * Get permissions
746
	 *
747
	 * @return array Associative array of permissions in this permission
748
	 *               group. The array indicies are the permission codes as
749
	 *               used in {@link Permission::check()}. The value is
750
	 *               suitable for using in an interface.
751
	 */
752
	public function getPermissions() {
753
		return $this->permissions;
754
	}
755
}
756
757
758