Completed
Push — master ( d7cfe2...c69a56 )
by Daniel
08:24
created

Group::inGroups()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 15
nc 10
nop 2
dl 0
loc 21
rs 8.7624
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Security;
4
5
use SilverStripe\Admin\SecurityAdmin;
6
use SilverStripe\Core\Convert;
7
use SilverStripe\Forms\DropdownField;
8
use SilverStripe\Forms\FieldList;
9
use SilverStripe\Forms\Form;
10
use SilverStripe\Forms\GridField\GridField;
11
use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter;
12
use SilverStripe\Forms\GridField\GridFieldButtonRow;
13
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
14
use SilverStripe\Forms\GridField\GridFieldDetailForm;
15
use SilverStripe\Forms\GridField\GridFieldExportButton;
16
use SilverStripe\Forms\GridField\GridFieldPrintButton;
17
use SilverStripe\Forms\HiddenField;
18
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
19
use SilverStripe\Forms\ListboxField;
20
use SilverStripe\Forms\LiteralField;
21
use SilverStripe\Forms\Tab;
22
use SilverStripe\Forms\TabSet;
23
use SilverStripe\Forms\TextareaField;
24
use SilverStripe\Forms\TextField;
25
use SilverStripe\ORM\ArrayList;
26
use SilverStripe\ORM\DataObject;
27
use SilverStripe\ORM\DataQuery;
28
use SilverStripe\ORM\HasManyList;
29
use SilverStripe\ORM\Hierarchy\Hierarchy;
30
use SilverStripe\ORM\ManyManyList;
31
use SilverStripe\ORM\UnsavedRelationList;
32
33
/**
34
 * A security group.
35
 *
36
 * @property string $Title Name of the group
37
 * @property string $Description Description of the group
38
 * @property string $Code Group code
39
 * @property string $Locked Boolean indicating whether group is locked in security panel
40
 * @property int $Sort
41
 * @property string HtmlEditorConfig
42
 *
43
 * @property int $ParentID ID of parent group
44
 *
45
 * @method Group Parent() Return parent group
46
 * @method HasManyList Permissions() List of group permissions
47
 * @method HasManyList Groups() List of child groups
48
 * @method ManyManyList Roles() List of PermissionRoles
49
 * @mixin Hierarchy
50
 */
51
class Group extends DataObject
52
{
53
54
    private static $db = array(
55
        "Title" => "Varchar(255)",
56
        "Description" => "Text",
57
        "Code" => "Varchar(255)",
58
        "Locked" => "Boolean",
59
        "Sort" => "Int",
60
        "HtmlEditorConfig" => "Text"
61
    );
62
63
    private static $has_one = array(
64
        "Parent" => Group::class,
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
65
    );
66
67
    private static $has_many = array(
68
        "Permissions" => Permission::class,
69
        "Groups" => Group::class,
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
70
    );
71
72
    private static $many_many = array(
73
        "Members" => Member::class,
74
        "Roles" => PermissionRole::class,
75
    );
76
77
    private static $extensions = array(
78
        Hierarchy::class,
79
    );
80
81
    private static $table_name = "Group";
82
83
    public function populateDefaults()
84
    {
85
        parent::populateDefaults();
86
87
        if (!$this->Title) {
88
            $this->Title = _t(__CLASS__.'.NEWGROUP', "New Group");
89
        }
90
    }
91
92
    public function getAllChildren()
93
    {
94
        $doSet = new ArrayList();
95
96
        $children = Group::get()->filter("ParentID", $this->ID);
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
97
        /** @var Group $child */
98
        foreach ($children as $child) {
99
            $doSet->push($child);
100
            $doSet->merge($child->getAllChildren());
101
        }
102
103
        return $doSet;
104
    }
105
106
    /**
107
     * Caution: Only call on instances, not through a singleton.
108
     * The "root group" fields will be created through {@link SecurityAdmin->EditForm()}.
109
     *
110
     * @return FieldList
111
     */
112
    public function getCMSFields()
113
    {
114
        $fields = new FieldList(
115
            new TabSet(
116
                "Root",
117
                new Tab(
118
                    'Members',
119
                    _t(__CLASS__.'.MEMBERS', 'Members'),
120
                    new TextField("Title", $this->fieldLabel('Title')),
121
                    $parentidfield = DropdownField::create(
122
                        'ParentID',
123
                        $this->fieldLabel('Parent'),
124
                        Group::get()->exclude('ID', $this->ID)->map('ID', 'Breadcrumbs')
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
125
                    )->setEmptyString(' '),
126
                    new TextareaField('Description', $this->fieldLabel('Description'))
127
                ),
128
                $permissionsTab = new Tab(
129
                    'Permissions',
130
                    _t(__CLASS__.'.PERMISSIONS', 'Permissions'),
131
                    $permissionsField = new PermissionCheckboxSetField(
132
                        'Permissions',
133
                        false,
134
                        Permission::class,
135
                        'GroupID',
136
                        $this
137
                    )
138
                )
139
            )
140
        );
141
142
        $parentidfield->setDescription(
143
            _t('SilverStripe\\Security\\Group.GroupReminder', 'If you choose a parent group, this group will take all it\'s roles')
144
        );
145
146
        if ($this->ID) {
147
            $group = $this;
148
            $config = GridFieldConfig_RelationEditor::create();
149
            $config->addComponent(new GridFieldButtonRow('after'));
150
            $config->addComponents(new GridFieldExportButton('buttons-after-left'));
151
            $config->addComponents(new GridFieldPrintButton('buttons-after-left'));
152
            /** @var GridFieldAddExistingAutocompleter $autocompleter */
153
            $autocompleter = $config->getComponentByType('SilverStripe\\Forms\\GridField\\GridFieldAddExistingAutocompleter');
154
            /** @skipUpgrade */
155
            $autocompleter
156
                ->setResultsFormat('$Title ($Email)')
157
                ->setSearchFields(array('FirstName', 'Surname', 'Email'));
158
            /** @var GridFieldDetailForm $detailForm */
159
            $detailForm = $config->getComponentByType(GridFieldDetailForm::class);
160
            $detailForm
161
                ->setValidator(Member_Validator::create())
162
                ->setItemEditFormCallback(function ($form) use ($group) {
163
                    /** @var Form $form */
164
                    $record = $form->getRecord();
165
                    $groupsField = $form->Fields()->dataFieldByName('DirectGroups');
166
                    if ($groupsField) {
167
                        // If new records are created in a group context,
168
                        // set this group by default.
169
                        if ($record && !$record->ID) {
170
                            $groupsField->setValue($group->ID);
171
                        } elseif ($record && $record->ID) {
172
                            // TODO Mark disabled once chosen.js supports it
173
                            // $groupsField->setDisabledItems(array($group->ID));
0 ignored issues
show
Unused Code Comprehensibility introduced by
77% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
174
                            $form->Fields()->replaceField(
175
                                'DirectGroups',
176
                                $groupsField->performReadonlyTransformation()
177
                            );
178
                        }
179
                    }
180
                });
181
            $memberList = GridField::create('Members', false, $this->DirectMembers(), $config)
182
                ->addExtraClass('members_grid');
183
            // @todo Implement permission checking on GridField
184
            //$memberList->setPermissions(array('edit', 'delete', 'export', 'add', 'inlineadd'));
0 ignored issues
show
Unused Code Comprehensibility introduced by
78% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
185
            $fields->addFieldToTab('Root.Members', $memberList);
186
        }
187
188
        // Only add a dropdown for HTML editor configurations if more than one is available.
189
        // Otherwise Member->getHtmlEditorConfigForCMS() will default to the 'cms' configuration.
190
        $editorConfigMap = HTMLEditorConfig::get_available_configs_map();
191
        if (count($editorConfigMap) > 1) {
192
            $fields->addFieldToTab(
193
                'Root.Permissions',
194
                new DropdownField(
195
                    'HtmlEditorConfig',
196
                    'HTML Editor Configuration',
197
                    $editorConfigMap
198
                ),
199
                'Permissions'
200
            );
201
        }
202
203
        if (!Permission::check('EDIT_PERMISSIONS')) {
204
            $fields->removeFieldFromTab('Root', 'Permissions');
205
        }
206
207
        // Only show the "Roles" tab if permissions are granted to edit them,
208
        // and at least one role exists
209
        if (Permission::check('APPLY_ROLES') &&
210
            PermissionRole::get()->count() &&
211
            class_exists(SecurityAdmin::class)
212
        ) {
213
            $fields->findOrMakeTab('Root.Roles', _t(__CLASS__.'.ROLES', 'Roles'));
214
            $fields->addFieldToTab(
215
                'Root.Roles',
216
                new LiteralField(
217
                    "",
218
                    "<p>" .
219
                    _t(
220
                        __CLASS__.'.ROLESDESCRIPTION',
221
                        "Roles are predefined sets of permissions, and can be assigned to groups.<br />"
222
                        . "They are inherited from parent groups if required."
223
                    ) . '<br />' .
224
                    sprintf(
225
                        '<a href="%s" class="add-role">%s</a>',
226
                        SecurityAdmin::singleton()->Link('show/root#Root_Roles'),
227
                        // TODO This should include #Root_Roles to switch directly to the tab,
228
                        // but tabstrip.js doesn't display tabs when directly adressed through a URL pragma
229
                        _t('SilverStripe\\Security\\Group.RolesAddEditLink', 'Manage roles')
230
                    ) .
231
                    "</p>"
232
                )
233
            );
234
235
            // Add roles (and disable all checkboxes for inherited roles)
236
            $allRoles = PermissionRole::get();
237
            if (!Permission::check('ADMIN')) {
238
                $allRoles = $allRoles->filter("OnlyAdminCanApply", 0);
239
            }
240
            if ($this->ID) {
241
                $groupRoles = $this->Roles();
242
                $inheritedRoles = new ArrayList();
243
                $ancestors = $this->getAncestors();
244
                foreach ($ancestors as $ancestor) {
245
                    $ancestorRoles = $ancestor->Roles();
246
                    if ($ancestorRoles) {
247
                        $inheritedRoles->merge($ancestorRoles);
248
                    }
249
                }
250
                $groupRoleIDs = $groupRoles->column('ID') + $inheritedRoles->column('ID');
251
                $inheritedRoleIDs = $inheritedRoles->column('ID');
252
            } else {
253
                $groupRoleIDs = array();
254
                $inheritedRoleIDs = array();
255
            }
256
257
            $rolesField = ListboxField::create('Roles', false, $allRoles->map()->toArray())
258
                    ->setDefaultItems($groupRoleIDs)
259
                    ->setAttribute('data-placeholder', _t('SilverStripe\\Security\\Group.AddRole', 'Add a role for this group'))
260
                    ->setDisabledItems($inheritedRoleIDs);
261
            if (!$allRoles->count()) {
262
                $rolesField->setAttribute('data-placeholder', _t('SilverStripe\\Security\\Group.NoRoles', 'No roles found'));
263
            }
264
            $fields->addFieldToTab('Root.Roles', $rolesField);
265
        }
266
267
        $fields->push($idField = new HiddenField("ID"));
268
269
        $this->extend('updateCMSFields', $fields);
270
271
        return $fields;
272
    }
273
274
    /**
275
     * @param bool $includerelations Indicate if the labels returned include relation fields
276
     * @return array
277
     */
278
    public function fieldLabels($includerelations = true)
279
    {
280
        $labels = parent::fieldLabels($includerelations);
281
        $labels['Title'] = _t(__CLASS__.'.GROUPNAME', 'Group name');
282
        $labels['Description'] = _t('SilverStripe\\Security\\Group.Description', 'Description');
283
        $labels['Code'] = _t('SilverStripe\\Security\\Group.Code', 'Group Code', 'Programmatical code identifying a group');
284
        $labels['Locked'] = _t('SilverStripe\\Security\\Group.Locked', 'Locked?', 'Group is locked in the security administration area');
285
        $labels['Sort'] = _t('SilverStripe\\Security\\Group.Sort', 'Sort Order');
286
        if ($includerelations) {
287
            $labels['Parent'] = _t('SilverStripe\\Security\\Group.Parent', 'Parent Group', 'One group has one parent group');
288
            $labels['Permissions'] = _t('SilverStripe\\Security\\Group.has_many_Permissions', 'Permissions', 'One group has many permissions');
289
            $labels['Members'] = _t('SilverStripe\\Security\\Group.many_many_Members', 'Members', 'One group has many members');
290
        }
291
292
        return $labels;
293
    }
294
295
    /**
296
     * Get many-many relation to {@link Member},
297
     * including all members which are "inherited" from children groups of this record.
298
     * See {@link DirectMembers()} for retrieving members without any inheritance.
299
     *
300
     * @param String $filter
301
     * @return ManyManyList
302
     */
303
    public function Members($filter = '')
304
    {
305
        // First get direct members as a base result
306
        $result = $this->DirectMembers();
307
308
        // Unsaved group cannot have child groups because its ID is still 0.
309
        if (!$this->exists()) {
310
            return $result;
311
        }
312
313
        // Remove the default foreign key filter in prep for re-applying a filter containing all children groups.
314
        // Filters are conjunctive in DataQuery by default, so this filter would otherwise overrule any less specific
315
        // ones.
316
        if (!($result instanceof UnsavedRelationList)) {
317
            $result = $result->alterDataQuery(function ($query) {
318
                /** @var DataQuery $query */
319
                $query->removeFilterOn('Group_Members');
320
            });
321
        }
322
        // Now set all children groups as a new foreign key
323
        $groups = Group::get()->byIDs($this->collateFamilyIDs());
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
324
        $result = $result->forForeignID($groups->column('ID'))->where($filter);
325
326
        return $result;
327
    }
328
329
    /**
330
     * Return only the members directly added to this group
331
     */
332
    public function DirectMembers()
333
    {
334
        return $this->getManyManyComponents('Members');
335
    }
336
337
    /**
338
     * Return a set of this record's "family" of IDs - the IDs of
339
     * this record and all its descendants.
340
     *
341
     * @return array
342
     */
343
    public function collateFamilyIDs()
344
    {
345
        if (!$this->exists()) {
346
            throw new \InvalidArgumentException("Cannot call collateFamilyIDs on unsaved Group.");
347
        }
348
349
        $familyIDs = array();
350
        $chunkToAdd = array($this->ID);
351
352
        while ($chunkToAdd) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $chunkToAdd 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...
353
            $familyIDs = array_merge($familyIDs, $chunkToAdd);
354
355
            // Get the children of *all* the groups identified in the previous chunk.
356
            // This minimises the number of SQL queries necessary
357
            $chunkToAdd = Group::get()->filter("ParentID", $chunkToAdd)->column('ID');
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
358
        }
359
360
        return $familyIDs;
361
    }
362
363
    /**
364
     * Returns an array of the IDs of this group and all its parents
365
     *
366
     * @return array
367
     */
368
    public function collateAncestorIDs()
369
    {
370
        $parent = $this;
371
        $items = [];
372
        while ($parent instanceof Group) {
373
            $items[] = $parent->ID;
374
            $parent = $parent->getParent();
0 ignored issues
show
Bug introduced by
The method getParent() does not exist on SilverStripe\Security\Group. Did you maybe mean Parent()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
375
        }
376
        return $items;
377
    }
378
379
    /**
380
     * Check if the group is a child of the given group or any parent groups
381
     *
382
     * @param string|int|Group $group Group instance, Group Code or ID
383
     * @return bool Returns TRUE if the Group is a child of the given group, otherwise FALSE
384
     */
385
    public function inGroup($group)
386
    {
387
        return in_array($this->identifierToGroupID($group), $this->collateAncestorIDs());
388
    }
389
390
    /**
391
     * Check if the group is a child of the given groups or any parent groups
392
     *
393
     * @param (string|int|Group)[] $groups
394
     * @param bool $requireAll set to TRUE if must be in ALL groups, or FALSE if must be in ANY
395
     * @return bool Returns TRUE if the Group is a child of any of the given groups, otherwise FALSE
396
     */
397
    public function inGroups($groups, $requireAll = false)
398
    {
399
        $ancestorIDs = $this->collateAncestorIDs();
400
        $candidateIDs = [];
401
        foreach ($groups as $group) {
402
            $groupID = $this->identifierToGroupID($group);
403
            if ($groupID) {
404
                $candidateIDs[] = $groupID;
405
            } elseif ($requireAll) {
406
                return false;
407
            }
408
        }
409
        if (empty($candidateIDs)) {
410
            return false;
411
        }
412
        $matches = array_intersect($candidateIDs, $ancestorIDs);
413
        if ($requireAll) {
414
            return count($candidateIDs) === count($matches);
415
        }
416
        return !empty($matches);
417
    }
418
419
    /**
420
     * Turn a string|int|Group into a GroupID
421
     *
422
     * @param string|int|Group $groupID Group instance, Group Code or ID
423
     * @return int|null the Group ID or NULL if not found
424
     */
425
    protected function identifierToGroupID($groupID)
426
    {
427
        if (is_numeric($groupID) && Group::get()->byID($groupID)) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
428
            return $groupID;
429
        } elseif (is_string($groupID) && $groupByCode = Group::get()->filter(['Code' => $groupID])->first()) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
430
            return $groupByCode->ID;
431
        } elseif ($groupID instanceof Group && $groupID->exists()) {
432
            return $groupID->ID;
433
        }
434
        return null;
435
    }
436
437
    /**
438
     * This isn't a decendant of SiteTree, but needs this in case
439
     * the group is "reorganised";
440
     */
441
    public function cmsCleanup_parentChanged()
442
    {
443
    }
444
445
    /**
446
     * Override this so groups are ordered in the CMS
447
     */
448
    public function stageChildren()
449
    {
450
        return Group::get()
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
451
            ->filter("ParentID", $this->ID)
452
            ->exclude("ID", $this->ID)
453
            ->sort('"Sort"');
454
    }
455
456
    /**
457
     * @return string
458
     */
459
    public function getTreeTitle()
460
    {
461
        $title = htmlspecialchars($this->Title, ENT_QUOTES);
462
        $this->extend('updateTreeTitle', $title);
463
        return $title;
464
    }
465
466
    /**
467
     * Overloaded to ensure the code is always descent.
468
     *
469
     * @param string
470
     */
471
    public function setCode($val)
472
    {
473
        $this->setField("Code", Convert::raw2url($val));
474
    }
475
476
    public function validate()
477
    {
478
        $result = parent::validate();
479
480
        // Check if the new group hierarchy would add certain "privileged permissions",
481
        // and require an admin to perform this change in case it does.
482
        // This prevents "sub-admin" users with group editing permissions to increase their privileges.
483
        if ($this->Parent()->exists() && !Permission::check('ADMIN')) {
484
            $inheritedCodes = Permission::get()
485
                ->filter('GroupID', $this->Parent()->collateAncestorIDs())
486
                ->column('Code');
487
            $privilegedCodes = Permission::config()->get('privileged_permissions');
488
            if (array_intersect($inheritedCodes, $privilegedCodes)) {
489
                $result->addError(sprintf(
490
                    _t(
491
                        'SilverStripe\\Security\\Group.HierarchyPermsError',
492
                        'Can\'t assign parent group "%s" with privileged permissions (requires ADMIN access)'
493
                    ),
494
                    $this->Parent()->Title
495
                ));
496
            }
497
        }
498
499
        return $result;
500
    }
501
502
    public function onBeforeWrite()
503
    {
504
        parent::onBeforeWrite();
505
506
        // Only set code property when the group has a custom title, and no code exists.
507
        // The "Code" attribute is usually treated as a more permanent identifier than database IDs
508
        // in custom application logic, so can't be changed after its first set.
509
        if (!$this->Code && $this->Title != _t(__CLASS__.'.NEWGROUP', "New Group")) {
510
            $this->setCode($this->Title);
511
        }
512
    }
513
514
    public function onBeforeDelete()
515
    {
516
        parent::onBeforeDelete();
517
518
        // if deleting this group, delete it's children as well
519
        foreach ($this->Groups() as $group) {
520
            $group->delete();
521
        }
522
523
        // Delete associated permissions
524
        foreach ($this->Permissions() as $permission) {
525
            $permission->delete();
526
        }
527
    }
528
529
    /**
530
     * Checks for permission-code CMS_ACCESS_SecurityAdmin.
531
     * If the group has ADMIN permissions, it requires the user to have ADMIN permissions as well.
532
     *
533
     * @param Member $member Member
534
     * @return boolean
535
     */
536
    public function canEdit($member = null)
537
    {
538
        if (!$member) {
539
            $member = Security::getCurrentUser();
540
        }
541
542
        // extended access checks
543
        $results = $this->extend('canEdit', $member);
544
        if ($results && is_array($results)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results 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...
545
            if (!min($results)) {
546
                return false;
547
            }
548
        }
549
550
        if (// either we have an ADMIN
551
            (bool)Permission::checkMember($member, "ADMIN")
552
            || (
553
                // or a privileged CMS user and a group without ADMIN permissions.
554
                // without this check, a user would be able to add himself to an administrators group
555
                // with just access to the "Security" admin interface
556
                Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin") &&
557
                !Permission::get()->filter(array('GroupID' => $this->ID, 'Code' => 'ADMIN'))->exists()
558
            )
559
        ) {
560
            return true;
561
        }
562
563
        return false;
564
    }
565
566
    /**
567
     * Checks for permission-code CMS_ACCESS_SecurityAdmin.
568
     *
569
     * @param Member $member
570
     * @return boolean
571
     */
572
    public function canView($member = null)
573
    {
574
        if (!$member) {
575
            $member = Security::getCurrentUser();
576
        }
577
578
        // extended access checks
579
        $results = $this->extend('canView', $member);
580
        if ($results && is_array($results)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results 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...
581
            if (!min($results)) {
582
                return false;
583
            }
584
        }
585
586
        // user needs access to CMS_ACCESS_SecurityAdmin
587
        if (Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin")) {
588
            return true;
589
        }
590
591
        return false;
592
    }
593
594
    public function canDelete($member = null)
595
    {
596
        if (!$member) {
597
            $member = Security::getCurrentUser();
598
        }
599
600
        // extended access checks
601
        $results = $this->extend('canDelete', $member);
602
        if ($results && is_array($results)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $results 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...
603
            if (!min($results)) {
604
                return false;
605
            }
606
        }
607
608
        return $this->canEdit($member);
609
    }
610
611
    /**
612
     * Returns all of the children for the CMS Tree.
613
     * Filters to only those groups that the current user can edit
614
     */
615
    public function AllChildrenIncludingDeleted()
616
    {
617
        /** @var Hierarchy $extInstance */
618
        $extInstance = $this->getExtensionInstance(Hierarchy::class);
619
        $extInstance->setOwner($this);
620
        $children = $extInstance->AllChildrenIncludingDeleted();
621
        $extInstance->clearOwner();
622
623
        $filteredChildren = new ArrayList();
624
625
        if ($children) {
626
            foreach ($children as $child) {
627
                if ($child->canView()) {
628
                    $filteredChildren->push($child);
629
                }
630
            }
631
        }
632
633
        return $filteredChildren;
634
    }
635
636
    /**
637
     * Add default records to database.
638
     *
639
     * This function is called whenever the database is built, after the
640
     * database tables have all been created.
641
     */
642
    public function requireDefaultRecords()
643
    {
644
        parent::requireDefaultRecords();
645
646
        // Add default author group if no other group exists
647
        $allGroups = Group::get();
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
648
        if (!$allGroups->count()) {
649
            $authorGroup = new Group();
650
            $authorGroup->Code = 'content-authors';
651
            $authorGroup->Title = _t('SilverStripe\\Security\\Group.DefaultGroupTitleContentAuthors', 'Content Authors');
652
            $authorGroup->Sort = 1;
653
            $authorGroup->write();
654
            Permission::grant($authorGroup->ID, 'CMS_ACCESS_CMSMain');
655
            Permission::grant($authorGroup->ID, 'CMS_ACCESS_AssetAdmin');
656
            Permission::grant($authorGroup->ID, 'CMS_ACCESS_ReportAdmin');
657
            Permission::grant($authorGroup->ID, 'SITETREE_REORGANISE');
658
        }
659
660
        // Add default admin group if none with permission code ADMIN exists
661
        $adminGroups = Permission::get_groups_by_permission('ADMIN');
662
        if (!$adminGroups->count()) {
663
            $adminGroup = new Group();
664
            $adminGroup->Code = 'administrators';
665
            $adminGroup->Title = _t('SilverStripe\\Security\\Group.DefaultGroupTitleAdministrators', 'Administrators');
666
            $adminGroup->Sort = 0;
667
            $adminGroup->write();
668
            Permission::grant($adminGroup->ID, 'ADMIN');
669
        }
670
671
        // Members are populated through Member->requireDefaultRecords()
672
    }
673
}
674