Passed
Pull Request — 4 (#10330)
by
unknown
06:45
created

Permission::grant()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 31
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 21
nc 8
nop 3
dl 0
loc 31
rs 8.9617
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Security;
4
5
use SilverStripe\Core\ClassInfo;
6
use SilverStripe\Core\Resettable;
7
use SilverStripe\Dev\TestOnly;
8
use SilverStripe\i18n\i18nEntityProvider;
9
use SilverStripe\ORM\ArrayList;
10
use SilverStripe\ORM\DataObject;
11
use SilverStripe\ORM\DB;
12
use SilverStripe\ORM\SS_List;
13
use SilverStripe\View\TemplateGlobalProvider;
14
15
/**
16
 * Represents a permission assigned to a group.
17
 *
18
 * @property string Code
19
 * @property int Arg
20
 * @property int Type
21
 * @property int GroupID
22
 * @method Group Group()
23
 */
24
class Permission extends DataObject implements TemplateGlobalProvider, Resettable, i18nEntityProvider
25
{
26
27
    // the (1) after Type specifies the DB default value which is needed for
28
    // upgrades from older SilverStripe versions
29
    private static $db = [
0 ignored issues
show
introduced by
The private property $db is not used, and could be removed.
Loading history...
30
        "Code" => "Varchar(255)",
31
        "Arg" => "Int",
32
        "Type" => "Int(1)"
33
    ];
34
35
    private static $has_one = [
0 ignored issues
show
introduced by
The private property $has_one is not used, and could be removed.
Loading history...
36
        "Group" => Group::class,
37
    ];
38
39
    private static $indexes = [
0 ignored issues
show
introduced by
The private property $indexes is not used, and could be removed.
Loading history...
40
        "Code" => true
41
    ];
42
43
    private static $defaults = [
0 ignored issues
show
introduced by
The private property $defaults is not used, and could be removed.
Loading history...
44
        "Type" => 1
45
    ];
46
47
    private static $table_name = "Permission";
0 ignored issues
show
introduced by
The private property $table_name is not used, and could be removed.
Loading history...
48
49
    /**
50
     * This is the value to use for the "Type" field if a permission should be
51
     * granted.
52
     */
53
    const GRANT_PERMISSION = 1;
54
55
    /**
56
     * This is the value to use for the "Type" field if a permission should be
57
     * denied.
58
     */
59
    const DENY_PERMISSION = -1;
60
61
    /**
62
     * This is the value to use for the "Type" field if a permission should be
63
     * inherited.
64
     */
65
    const INHERIT_PERMISSION = 0;
66
67
68
    /**
69
     * Method to globally disable "strict" checking, which means a permission
70
     * will be granted if the key does not exist at all.
71
     *
72
     * @deprecated 4.4.0
73
     * @var array
74
     */
75
    private static $declared_permissions = null;
76
77
    /**
78
     * Linear list of declared permissions in the system.
79
     *
80
     * @deprecated 4.4.0
81
     * @var array
82
     */
83
    private static $declared_permissions_list = null;
84
85
    /**
86
     * @config
87
     * @var $strict_checking Boolean Method to globally disable "strict" checking,
0 ignored issues
show
Documentation Bug introduced by
The doc comment $strict_checking at position 0 could not be parsed: Unknown type name '$strict_checking' at position 0 in $strict_checking.
Loading history...
88
     * which means a permission will be granted if the key does not exist at all.
89
     */
90
    private static $strict_checking = true;
91
92
    /**
93
     * Set to false to prevent the 'ADMIN' permission from implying all
94
     * permissions in the system
95
     *
96
     * @config
97
     * @var bool
98
     */
99
    private static $admin_implies_all = true;
100
101
    /**
102
     * a list of permission codes which doesn't appear in the Permission list
103
     * when make the {@link PermissionCheckboxSetField}
104
     * @config
105
     * @var array;
106
     */
107
    private static $hidden_permissions = [];
0 ignored issues
show
introduced by
The private property $hidden_permissions is not used, and could be removed.
Loading history...
108
109
    /**
110
     * @config These permissions can only be applied by ADMIN users, to prevent
111
     * privilege escalation on group assignments and inheritance.
112
     * @var array
113
     */
114
    private static $privileged_permissions = [
0 ignored issues
show
introduced by
The private property $privileged_permissions is not used, and could be removed.
Loading history...
115
        'ADMIN',
116
        'APPLY_ROLES',
117
        'EDIT_PERMISSIONS'
118
    ];
119
120
    /**
121
     * Check that the current member has the given permission.
122
     *
123
     * @param string|array $code Code of the permission to check (case-sensitive)
124
     * @param string $arg Optional argument (e.g. a permissions for a specific page)
125
     * @param int|Member $member Optional member instance or ID. If set to NULL, the permssion
126
     *  will be checked for the current user
127
     * @param bool $strict Use "strict" checking (which means a permission
128
     *  will be granted if the key does not exist at all)?
129
     * @return int|bool The ID of the permission record if the permission
130
     *  exists; FALSE otherwise. If "strict" checking is
131
     *  disabled, TRUE will be returned if the permission does not exist at all.
132
     */
133
    public static function check($code, $arg = "any", $member = null, $strict = true)
134
    {
135
        if (!$member) {
136
            if (!Security::getCurrentUser()) {
137
                return false;
138
            }
139
            $member = Security::getCurrentUser();
140
        }
141
142
        return self::checkMember($member, $code, $arg, $strict);
143
    }
144
145
    /**
146
     * Permissions cache.  The format is a map, where the keys are member IDs, and the values are
147
     * arrays of permission codes.
148
     */
149
    private static $cache_permissions = [];
150
151
    /**
152
     * Flush the permission cache, for example if you have edited group membership or a permission record.
153
     * @todo Call this whenever Group_Members is added to or removed from
154
     */
155
    public static function reset()
156
    {
157
        self::$cache_permissions = [];
158
    }
159
160
    /**
161
     * Check that the given member has the given permission.
162
     *
163
     * @param int|Member $member The ID of the member to check. Leave blank for the current member.
164
     *  Alternatively you can use a member object.
165
     * @param string|array $code Code of the permission to check (case-sensitive)
166
     * @param string $arg Optional argument (e.g. a permissions for a specific page)
167
     * @param bool $strict Use "strict" checking (which means a permission
168
     *  will be granted if the key does not exist at all)?
169
     * @return int|bool The ID of the permission record if the permission
170
     *  exists; FALSE otherwise. If "strict" checking is
171
     *  disabled, TRUE will be returned if the permission does not exist at all.
172
     */
173
    public static function checkMember($member, $code, $arg = "any", $strict = true)
174
    {
175
        if (!$member) {
176
            $member = Security::getCurrentUser();
177
        }
178
        $memberID = ($member instanceof Member) ? $member->ID : $member;
179
180
        if (!$memberID) {
181
            return false;
182
        }
183
184
        // Turn the code into an array as we may need to add other permsissions to the set we check
185
        if (!is_array($code)) {
186
            $code = [$code];
187
        }
188
189
        // Check if admin should be treated as holding all permissions
190
        $adminImpliesAll = (bool)static::config()->admin_implies_all;
0 ignored issues
show
Bug Best Practice introduced by
The property admin_implies_all does not exist on SilverStripe\Core\Config\Config_ForClass. Since you implemented __get, consider adding a @property annotation.
Loading history...
191
192
        if ($arg == 'any') {
193
            // Cache the permissions in memory
194
            if (!isset(self::$cache_permissions[$memberID])) {
195
                self::$cache_permissions[$memberID] = self::permissions_for_member($memberID);
196
            }
197
            foreach ($code as $permCode) {
198
                if ($permCode === 'CMS_ACCESS') {
199
                    foreach (self::$cache_permissions[$memberID] as $perm) {
200
                        //if they have admin rights OR they have an explicit access to the CMS then give permission
201
                        if (($adminImpliesAll && $perm == 'ADMIN') || substr($perm ?? '', 0, 11) === 'CMS_ACCESS_') {
202
                            return true;
203
                        }
204
                    }
205
                } elseif (substr($permCode ?? '', 0, 11) === 'CMS_ACCESS_' && !in_array('CMS_ACCESS_LeftAndMain', $code ?? [])) {
206
                    //cms_access_leftandmain means access to all CMS areas
207
                    $code[] = 'CMS_ACCESS_LeftAndMain';
208
                }
209
            }
210
211
            // if ADMIN has all privileges, then we need to push that code in
212
            if ($adminImpliesAll) {
213
                $code[] = "ADMIN";
214
            }
215
216
            // Multiple $code values - return true if at least one matches, ie, intersection exists
217
            return (bool)array_intersect($code ?? [], self::$cache_permissions[$memberID]);
218
        }
219
220
        // Code filters
221
        $codeParams = is_array($code) ? $code : [$code];
0 ignored issues
show
introduced by
The condition is_array($code) is always true.
Loading history...
222
        $codeClause = DB::placeholders($codeParams);
223
        $adminParams = $adminImpliesAll ? ['ADMIN'] : [];
224
        $adminClause = $adminImpliesAll ?  ", ?" : '';
225
226
        // The following code should only be used if you're not using the "any" arg.  This is kind
227
        // of obsolete functionality and could possibly be deprecated.
228
        $groupParams = self::groupList($memberID);
229
        if (empty($groupParams)) {
230
            return false;
231
        }
232
        $groupClause = DB::placeholders($groupParams);
233
234
        // Arg component
235
        $argClause = "";
236
        $argParams = [];
237
        switch ($arg) {
238
            case "any":
239
                break;
240
            case "all":
241
                $argClause = " AND \"Arg\" = ?";
242
                $argParams = [-1];
243
                break;
244
            default:
245
                if (is_numeric($arg)) {
246
                    $argClause = "AND \"Arg\" IN (?, ?) ";
247
                    $argParams = [-1, $arg];
248
                } else {
249
                    throw new \InvalidArgumentException("Permission::checkMember: bad arg '$arg'");
250
                }
251
        }
252
253
        // Raw SQL for efficiency
254
        $permission = DB::prepared_query(
255
            "SELECT \"ID\"
256
			FROM \"Permission\"
257
			WHERE (
258
				\"Code\" IN ($codeClause $adminClause)
259
				AND \"Type\" = ?
260
				AND \"GroupID\" IN ($groupClause)
261
				$argClause
262
			)",
263
            array_merge(
264
                $codeParams,
265
                $adminParams,
266
                [self::GRANT_PERMISSION],
267
                $groupParams,
268
                $argParams
269
            )
270
        )->value();
271
272
        if ($permission) {
273
            return $permission;
274
        }
275
276
        // Strict checking disabled?
277
        if (!static::config()->strict_checking || !$strict) {
0 ignored issues
show
Bug Best Practice introduced by
The property strict_checking does not exist on SilverStripe\Core\Config\Config_ForClass. Since you implemented __get, consider adding a @property annotation.
Loading history...
278
            $hasPermission = DB::prepared_query(
279
                "SELECT COUNT(*)
280
				FROM \"Permission\"
281
				WHERE (
282
					\"Code\" IN ($codeClause) AND
283
					\"Type\" = ?
284
				)",
285
                array_merge($codeParams, [self::GRANT_PERMISSION])
286
            )->value();
287
288
            if (!$hasPermission) {
289
                return false;
290
            }
291
        }
292
293
        return false;
294
    }
295
296
    /**
297
     * Get all the 'any' permission codes available to the given member.
298
     *
299
     * @param int $memberID
300
     * @return array
301
     */
302
    public static function permissions_for_member($memberID)
303
    {
304
        $groupList = self::groupList($memberID);
305
306
        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...
307
            $groupCSV = implode(", ", $groupList);
308
309
            $allowed = array_unique(DB::query("
310
				SELECT \"Code\"
311
				FROM \"Permission\"
312
				WHERE \"Type\" = " . self::GRANT_PERMISSION . " AND \"GroupID\" IN ($groupCSV)
313
314
				UNION
315
316
				SELECT \"Code\"
317
				FROM \"PermissionRoleCode\" PRC
318
				INNER JOIN \"PermissionRole\" PR ON PRC.\"RoleID\" = PR.\"ID\"
319
				INNER JOIN \"Group_Roles\" GR ON GR.\"PermissionRoleID\" = PR.\"ID\"
320
				WHERE \"GroupID\" IN ($groupCSV)
321
			")->column() ?? []);
322
323
            $denied = array_unique(DB::query("
324
				SELECT \"Code\"
325
				FROM \"Permission\"
326
				WHERE \"Type\" = " . self::DENY_PERMISSION . " AND \"GroupID\" IN ($groupCSV)
327
			")->column() ?? []);
328
329
            return array_diff($allowed ?? [], $denied);
330
        }
331
332
        return [];
333
    }
334
335
336
    /**
337
     * Get the list of groups that the given member belongs to.
338
     *
339
     * Call without an argument to get the groups that the current member
340
     * belongs to. In this case, the results will be session-cached.
341
     *
342
     * @param int $memberID The ID of the member. Leave blank for the current
343
     *                      member.
344
     * @return array Returns a list of group IDs to which the member belongs
345
     *               to or NULL.
346
     */
347
    public static function groupList($memberID = null)
348
    {
349
        // Default to current member, with session-caching
350
        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 0. 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...
351
            $member = Security::getCurrentUser();
352
            if ($member && isset($_SESSION['Permission_groupList'][$member->ID])) {
353
                return $_SESSION['Permission_groupList'][$member->ID];
354
            }
355
        } else {
356
            $member = DataObject::get_by_id("SilverStripe\\Security\\Member", $memberID);
357
        }
358
359
        if ($member) {
360
            // Build a list of the IDs of the groups.  Most of the heavy lifting
361
            // is done by Member::Groups
362
            // NOTE: This isn't efficient; but it's called once per session so
363
            // it's a low priority to fix.
364
            $groups = $member->Groups();
0 ignored issues
show
Bug introduced by
The method Groups() does not exist on SilverStripe\ORM\DataObject. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

364
            /** @scrutinizer ignore-call */ 
365
            $groups = $member->Groups();
Loading history...
365
            $groupList = [];
366
367
            if ($groups) {
368
                foreach ($groups as $group) {
369
                    $groupList[] = $group->ID;
370
                }
371
            }
372
373
374
            // Session caching
375
            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 0. 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...
376
                $_SESSION['Permission_groupList'][$member->ID] = $groupList;
377
            }
378
379
            return isset($groupList) ? $groupList : null;
380
        }
381
        return null;
382
    }
383
384
385
    /**
386
     * Grant the given permission code/arg to the given group
387
     *
388
     * @param int $groupID The ID of the group
389
     * @param string $code The permission code
390
     * @param string $arg Optional: The permission argument (e.g. a page ID).
391
     * @returns Permission Returns the new permission object.
392
     */
393
    public static function grant($groupID, $code, $arg = "any")
394
    {
395
        $permissions = Permission::get()->filter(['GroupID' => $groupID, 'Code' => $code]);
396
        
397
        if ($permissions && $permissions->count() > 0) {
398
            $perm = $permissions->last();
399
        } else {
400
            $perm = new Permission();
401
            $perm->GroupID = $groupID;
402
            $perm->Code = $code;
403
        }
404
405
        $perm->Type = self::GRANT_PERMISSION;
0 ignored issues
show
Bug Best Practice introduced by
The property Type does not exist on SilverStripe\ORM\DataObject. Since you implemented __set, consider adding a @property annotation.
Loading history...
406
407
        // Arg component
408
        switch ($arg) {
409
            case "any":
410
                break;
411
            case "all":
412
                $perm->Arg = -1;
0 ignored issues
show
Bug Best Practice introduced by
The property Arg does not exist on SilverStripe\ORM\DataObject. Since you implemented __set, consider adding a @property annotation.
Loading history...
413
                break;
414
            default:
415
                if (is_numeric($arg)) {
416
                    $perm->Arg = $arg;
0 ignored issues
show
Documentation Bug introduced by
The property $Arg was declared of type integer, but $arg is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
417
                } else {
418
                    throw new \InvalidArgumentException("Permission::checkMember: bad arg '$arg'");
419
                }
420
        }
421
422
        $perm->write();
423
        return $perm;
424
    }
425
426
427
    /**
428
     * Deny the given permission code/arg to the given group
429
     *
430
     * @param int $groupID The ID of the group
431
     * @param string $code The permission code
432
     * @param string $arg Optional: The permission argument (e.g. a page ID).
433
     * @returns Permission Returns the new permission object.
434
     */
435
    public static function deny($groupID, $code, $arg = "any")
436
    {
437
        $permissions = Permission::get()->filter(['GroupID' => $groupID, 'Code' => $code]);
438
439
        if ($permissions && $permissions->count() > 0) {
440
            $perm = $permissions->last();
441
        } else {
442
            $perm = new Permission();
443
            $perm->GroupID = $groupID;
444
            $perm->Code = $code;
445
        }
446
447
        $perm->Type = self::DENY_PERMISSION;
0 ignored issues
show
Bug Best Practice introduced by
The property Type does not exist on SilverStripe\ORM\DataObject. Since you implemented __set, consider adding a @property annotation.
Loading history...
448
449
        // Arg component
450
        switch ($arg) {
451
            case "any":
452
                break;
453
            case "all":
454
                $perm->Arg = -1;
0 ignored issues
show
Bug Best Practice introduced by
The property Arg does not exist on SilverStripe\ORM\DataObject. Since you implemented __set, consider adding a @property annotation.
Loading history...
455
                break;
456
            default:
457
                if (is_numeric($arg)) {
458
                    $perm->Arg = $arg;
0 ignored issues
show
Documentation Bug introduced by
The property $Arg was declared of type integer, but $arg is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
459
                } else {
460
                    throw new \InvalidArgumentException("Permission::checkMember: bad arg '$arg'");
461
                }
462
        }
463
464
        $perm->write();
465
        return $perm;
466
    }
467
468
    /**
469
     * Returns all members for a specific permission.
470
     *
471
     * @param string|array $code Either a single permission code, or a list of permission codes
472
     * @return SS_List Returns a set of member that have the specified
473
     *                       permission.
474
     */
475
    public static function get_members_by_permission($code)
476
    {
477
        $toplevelGroups = self::get_groups_by_permission($code);
478
        if (!$toplevelGroups) {
0 ignored issues
show
introduced by
$toplevelGroups is of type SilverStripe\ORM\DataList, thus it always evaluated to true.
Loading history...
479
            return new ArrayList();
480
        }
481
482
        $groupIDs = [];
483
        foreach ($toplevelGroups as $group) {
484
            $familyIDs = $group->collateFamilyIDs();
485
            if (is_array($familyIDs)) {
486
                $groupIDs = array_merge($groupIDs, array_values($familyIDs ?? []));
487
            }
488
        }
489
490
        if (empty($groupIDs)) {
491
            return new ArrayList();
492
        }
493
494
        $groupClause = DB::placeholders($groupIDs);
495
        /** @skipUpgrade */
496
        $members = Member::get()
497
            ->where(["\"Group\".\"ID\" IN ($groupClause)" => $groupIDs])
498
            ->leftJoin("Group_Members", '"Member"."ID" = "Group_Members"."MemberID"')
499
            ->leftJoin("Group", '"Group_Members"."GroupID" = "Group"."ID"');
500
501
        return $members;
502
    }
503
504
    /**
505
     * Return all of the groups that have one of the given permission codes
506
     * @param array|string $codes Either a single permission code, or an array of permission codes
507
     * @return SS_List The matching group objects
508
     */
509
    public static function get_groups_by_permission($codes)
510
    {
511
        $codeParams = is_array($codes) ? $codes : [$codes];
512
        $codeClause = DB::placeholders($codeParams);
513
514
        // Via Roles are groups that have the permission via a role
515
        /** @skipUpgrade */
516
        return Group::get()
517
            ->where([
518
                "\"PermissionRoleCode\".\"Code\" IN ($codeClause) OR \"Permission\".\"Code\" IN ($codeClause)"
519
                => array_merge($codeParams, $codeParams)
520
            ])
521
            ->leftJoin('Permission', "\"Permission\".\"GroupID\" = \"Group\".\"ID\"")
522
            ->leftJoin('Group_Roles', "\"Group_Roles\".\"GroupID\" = \"Group\".\"ID\"")
523
            ->leftJoin('PermissionRole', "\"Group_Roles\".\"PermissionRoleID\" = \"PermissionRole\".\"ID\"")
524
            ->leftJoin('PermissionRoleCode', "\"PermissionRoleCode\".\"RoleID\" = \"PermissionRole\".\"ID\"");
525
    }
526
527
528
    /**
529
     * Get a list of all available permission codes, both defined through the
530
     * {@link PermissionProvider} interface, and all not explicitly defined codes existing
531
     * as a {@link Permission} database record. By default, the results are
532
     * grouped as denoted by {@link Permission_Group}.
533
     *
534
     * @param bool $grouped Group results into an array of permission groups.
535
     * @return array Returns an array of all available permission codes. The
536
     *  array indices are the permission codes as used in
537
     *  {@link Permission::check()}. The value is a description
538
     *  suitable for using in an interface.
539
     */
540
    public static function get_codes($grouped = true)
541
    {
542
        $classes = ClassInfo::implementorsOf('SilverStripe\\Security\\PermissionProvider');
543
544
        $allCodes = [];
545
        $adminCategory = _t(__CLASS__ . '.AdminGroup', 'Administrator');
546
        $allCodes[$adminCategory]['ADMIN'] = [
547
            'name' => _t(__CLASS__ . '.FULLADMINRIGHTS', 'Full administrative rights'),
548
            'help' => _t(
549
                'SilverStripe\\Security\\Permission.FULLADMINRIGHTS_HELP',
550
                'Implies and overrules all other assigned permissions.'
551
            ),
552
            'sort' => 100000
553
        ];
554
555
        if ($classes) {
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...
556
            foreach ($classes as $class) {
557
                $SNG = singleton($class);
558
                if ($SNG instanceof TestOnly) {
559
                    continue;
560
                }
561
562
                $someCodes = $SNG->providePermissions();
563
                if ($someCodes) {
564
                    foreach ($someCodes as $k => $v) {
565
                        if (is_array($v)) {
566
                            // There must be a category and name key.
567
                            if (!isset($v['category'])) {
568
                                user_error(
569
                                    "The permission $k must have a category key",
570
                                    E_USER_WARNING
571
                                );
572
                            }
573
                            if (!isset($v['name'])) {
574
                                user_error(
575
                                    "The permission $k must have a name key",
576
                                    E_USER_WARNING
577
                                );
578
                            }
579
580
                            if (!isset($allCodes[$v['category']])) {
581
                                $allCodes[$v['category']] = [];
582
                            }
583
584
                            $allCodes[$v['category']][$k] = [
585
                            'name' => $v['name'],
586
                            'help' => isset($v['help']) ? $v['help'] : null,
587
                            'sort' => isset($v['sort']) ? $v['sort'] : 0
588
                            ];
589
                        } else {
590
                            $allCodes['Other'][$k] = [
591
                            'name' => $v,
592
                            'help' => null,
593
                            'sort' => 0
594
                            ];
595
                        }
596
                    }
597
                }
598
            }
599
        }
600
601
        $flatCodeArray = [];
602
        foreach ($allCodes as $category) {
603
            foreach ($category as $code => $permission) {
604
                $flatCodeArray[] = $code;
605
            }
606
        }
607
        $otherPerms = DB::query("SELECT DISTINCT \"Code\" From \"Permission\" WHERE \"Code\" != ''")->column();
608
609
        if ($otherPerms) {
610
            foreach ($otherPerms as $otherPerm) {
611
                if (!in_array($otherPerm, $flatCodeArray ?? [])) {
612
                    $allCodes['Other'][$otherPerm] = [
613
                    'name' => $otherPerm,
614
                    'help' => null,
615
                    'sort' => 0
616
                    ];
617
                }
618
            }
619
        }
620
621
        // Don't let people hijack ADMIN rights
622
        if (!Permission::check("ADMIN")) {
623
            unset($allCodes['ADMIN']);
624
        }
625
626
        ksort($allCodes);
627
628
        $returnCodes = [];
629
        foreach ($allCodes as $category => $permissions) {
630
            if ($grouped) {
631
                uasort($permissions, [__CLASS__, 'sort_permissions']);
632
                $returnCodes[$category] = $permissions;
633
            } else {
634
                $returnCodes = array_merge($returnCodes, $permissions);
635
            }
636
        }
637
638
        return $returnCodes;
639
    }
640
641
    /**
642
     * Sort permissions based on their sort value, or name
643
     *
644
     * @param array $a
645
     * @param array $b
646
     * @return int
647
     */
648
    public static function sort_permissions($a, $b)
649
    {
650
        if ($a['sort'] == $b['sort']) {
651
            // Same sort value, do alpha instead
652
            return strcmp($a['name'] ?? '', $b['name'] ?? '');
653
        } else {
654
            // Just numeric.
655
            return $a['sort'] < $b['sort'] ? -1 : 1;
656
        }
657
    }
658
659
    /**
660
     * Get a linear list of the permissions in the system.
661
     *
662
     * @return array Linear list of declared permissions in the system.
663
     * @deprecated 4.4.0
664
     */
665
    public static function get_declared_permissions_list()
666
    {
667
        if (!self::$declared_permissions) {
0 ignored issues
show
Bug Best Practice introduced by
The expression self::declared_permissions 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...
668
            return null;
669
        }
670
671
        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...
672
            return self::$declared_permissions_list;
673
        }
674
675
        self::$declared_permissions_list = [];
676
677
        self::traverse_declared_permissions(self::$declared_permissions, self::$declared_permissions_list);
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Security\Pe..._declared_permissions() has been deprecated: 4.4.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

677
        /** @scrutinizer ignore-deprecated */ self::traverse_declared_permissions(self::$declared_permissions, self::$declared_permissions_list);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
678
679
        return self::$declared_permissions_list;
680
    }
681
682
    /**
683
     * Look up the human-readable title for the permission as defined by <code>Permission::declare_permissions</code>
684
     *
685
     * @param string $perm Permission code
686
     * @return string Label for the given permission, or the permission itself if the label doesn't exist
687
     * @deprecated 4.4.0
688
     */
689
    public static function get_label_for_permission($perm)
690
    {
691
        $list = self::get_declared_permissions_list();
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Security\Pe...ared_permissions_list() has been deprecated: 4.4.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

691
        $list = /** @scrutinizer ignore-deprecated */ self::get_declared_permissions_list();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
692
        if (array_key_exists($perm, $list ?? [])) {
693
            return $list[$perm];
694
        }
695
        return $perm;
696
    }
697
698
    /**
699
     * Recursively traverse the nested list of declared permissions and create
700
     * a linear list.
701
     *
702
     * @param array $declared Nested structure of permissions.
703
     * @param array $list List of permissions in the structure. The result will be
704
     *              written to this array.
705
     * @deprecated 4.4.0
706
     */
707
    protected static function traverse_declared_permissions($declared, &$list)
708
    {
709
        if (!is_array($declared)) {
0 ignored issues
show
introduced by
The condition is_array($declared) is always true.
Loading history...
710
            return;
711
        }
712
713
        foreach ($declared as $perm => $value) {
714
            if ($value instanceof Permission_Group) {
715
                $list[] = $value->getName();
716
                self::traverse_declared_permissions($value->getPermissions(), $list);
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Security\Pe..._declared_permissions() has been deprecated: 4.4.0 ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

716
                /** @scrutinizer ignore-deprecated */ self::traverse_declared_permissions($value->getPermissions(), $list);

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
717
            } else {
718
                $list[$perm] = $value;
719
            }
720
        }
721
    }
722
723
    public function onBeforeWrite()
724
    {
725
        parent::onBeforeWrite();
726
727
        // Just in case we've altered someone's permissions
728
        Permission::reset();
729
    }
730
731
    public static function get_template_global_variables()
732
    {
733
        return [
734
            'HasPerm' => 'check'
735
        ];
736
    }
737
738
    public function provideI18nEntities()
739
    {
740
        $keys = parent::provideI18nEntities();
741
742
        // Localise all permission categories
743
        $keys[__CLASS__ . '.AdminGroup'] = 'Administrator';
744
        $keys[__CLASS__ . '.CMS_ACCESS_CATEGORY'] = 'CMS Access';
745
        $keys[__CLASS__ . '.CONTENT_CATEGORY'] = 'Content permissions';
746
        $keys[__CLASS__ . '.PERMISSIONS_CATEGORY'] = 'Roles and access permissions';
747
        return $keys;
748
    }
749
}
750