Passed
Pull Request — 4.10 (#10333)
by Guy
18:59 queued 04:44
created

Group   F

Complexity

Total Complexity 89

Size/Duplication

Total Lines 665
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 304
dl 0
loc 665
rs 2
c 0
b 0
f 0
wmc 89

24 Methods

Rating   Name   Duplication   Size   Complexity  
D getCMSFields() 0 163 17
A getDecodedBreadcrumbs() 0 8 2
A inGroups() 0 20 6
A Members() 0 25 3
A fieldLabels() 0 15 2
A setCode() 0 3 1
A stageChildren() 0 6 1
A DirectMembers() 0 3 1
A cmsCleanup_parentChanged() 0 2 1
A getTreeTitle() 0 5 1
B identifierToGroupID() 0 10 7
A inGroup() 0 3 1
A collateAncestorIDs() 0 9 2
A getAllChildren() 0 12 2
A collateFamilyIDs() 0 18 3
A onBeforeDelete() 0 12 3
A requireDefaultRecords() 0 27 3
A canDelete() 0 15 5
A validate() 0 39 5
B canEdit() 0 28 8
A getCMSCompositeValidator() 0 9 1
A onBeforeWrite() 0 25 4
A canView() 0 20 6
A AllChildrenIncludingDeleted() 0 16 4

How to fix   Complexity   

Complex Class

Complex classes like Group 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.

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 Group, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Security;
4
5
use SilverStripe\Admin\SecurityAdmin;
0 ignored issues
show
Bug introduced by
The type SilverStripe\Admin\SecurityAdmin was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
6
use SilverStripe\Core\Convert;
7
use SilverStripe\Forms\CompositeValidator;
8
use SilverStripe\Forms\DropdownField;
9
use SilverStripe\Forms\FieldList;
10
use SilverStripe\Forms\Form;
11
use SilverStripe\Forms\GridField\GridField;
12
use SilverStripe\Forms\GridField\GridFieldAddExistingAutocompleter;
13
use SilverStripe\Forms\GridField\GridFieldButtonRow;
14
use SilverStripe\Forms\GridField\GridFieldConfig_RelationEditor;
15
use SilverStripe\Forms\GridField\GridFieldDeleteAction;
16
use SilverStripe\Forms\GridField\GridFieldDetailForm;
17
use SilverStripe\Forms\GridField\GridFieldExportButton;
18
use SilverStripe\Forms\GridField\GridFieldGroupDeleteAction;
19
use SilverStripe\Forms\GridField\GridFieldPageCount;
20
use SilverStripe\Forms\GridField\GridFieldPrintButton;
21
use SilverStripe\Forms\HiddenField;
22
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
23
use SilverStripe\Forms\ListboxField;
24
use SilverStripe\Forms\LiteralField;
25
use SilverStripe\Forms\RequiredFields;
26
use SilverStripe\Forms\Tab;
27
use SilverStripe\Forms\TabSet;
28
use SilverStripe\Forms\TextareaField;
29
use SilverStripe\Forms\TextField;
30
use SilverStripe\ORM\ArrayList;
31
use SilverStripe\ORM\DataObject;
32
use SilverStripe\ORM\DataQuery;
33
use SilverStripe\ORM\HasManyList;
34
use SilverStripe\ORM\Hierarchy\Hierarchy;
35
use SilverStripe\ORM\ManyManyList;
36
use SilverStripe\ORM\UnsavedRelationList;
37
38
/**
39
 * A security group.
40
 *
41
 * @property string $Title Name of the group
42
 * @property string $Description Description of the group
43
 * @property string $Code Group code
44
 * @property string $Locked Boolean indicating whether group is locked in security panel
45
 * @property int $Sort
46
 * @property string HtmlEditorConfig
47
 *
48
 * @property int $ParentID ID of parent group
49
 *
50
 * @method Group Parent() Return parent group
51
 * @method HasManyList Permissions() List of group permissions
52
 * @method HasManyList Groups() List of child groups
53
 * @method ManyManyList Roles() List of PermissionRoles
54
 * @mixin Hierarchy
55
 */
56
class Group extends DataObject
57
{
58
59
    private static $db = [
0 ignored issues
show
introduced by
The private property $db is not used, and could be removed.
Loading history...
60
        "Title" => "Varchar(255)",
61
        "Description" => "Text",
62
        "Code" => "Varchar(255)",
63
        "Locked" => "Boolean",
64
        "Sort" => "Int",
65
        "HtmlEditorConfig" => "Text"
66
    ];
67
68
    private static $has_one = [
0 ignored issues
show
introduced by
The private property $has_one is not used, and could be removed.
Loading history...
69
        "Parent" => Group::class,
70
    ];
71
72
    private static $has_many = [
0 ignored issues
show
introduced by
The private property $has_many is not used, and could be removed.
Loading history...
73
        "Permissions" => Permission::class,
74
        "Groups" => Group::class,
75
    ];
76
77
    private static $many_many = [
0 ignored issues
show
introduced by
The private property $many_many is not used, and could be removed.
Loading history...
78
        "Members" => Member::class,
79
        "Roles" => PermissionRole::class,
80
    ];
81
82
    private static $extensions = [
0 ignored issues
show
introduced by
The private property $extensions is not used, and could be removed.
Loading history...
83
        Hierarchy::class,
84
    ];
85
86
    private static $table_name = "Group";
0 ignored issues
show
introduced by
The private property $table_name is not used, and could be removed.
Loading history...
87
88
    public function getAllChildren()
89
    {
90
        $doSet = new ArrayList();
91
92
        $children = Group::get()->filter("ParentID", $this->ID);
93
        /** @var Group $child */
94
        foreach ($children as $child) {
95
            $doSet->push($child);
96
            $doSet->merge($child->getAllChildren());
97
        }
98
99
        return $doSet;
100
    }
101
102
    private function getDecodedBreadcrumbs()
103
    {
104
        $list = Group::get()->exclude('ID', $this->ID);
105
        $groups = ArrayList::create();
106
        foreach ($list as $group) {
107
            $groups->push(['ID' => $group->ID, 'Title' => $group->getBreadcrumbs(' » ')]);
108
        }
109
        return $groups;
110
    }
111
112
    /**
113
     * Caution: Only call on instances, not through a singleton.
114
     * The "root group" fields will be created through {@link SecurityAdmin->EditForm()}.
115
     *
116
     * @skipUpgrade
117
     * @return FieldList
118
     */
119
    public function getCMSFields()
120
    {
121
        $fields = new FieldList(
122
            new TabSet(
123
                "Root",
124
                new Tab(
125
                    'Members',
126
                    _t(__CLASS__ . '.MEMBERS', 'Members'),
127
                    new TextField("Title", $this->fieldLabel('Title')),
128
                    $parentidfield = DropdownField::create(
129
                        'ParentID',
130
                        $this->fieldLabel('Parent'),
131
                        $this->getDecodedBreadcrumbs()
132
                    )->setEmptyString(' '),
133
                    new TextareaField('Description', $this->fieldLabel('Description'))
134
                ),
135
                $permissionsTab = new Tab(
136
                    'Permissions',
137
                    _t(__CLASS__ . '.PERMISSIONS', 'Permissions'),
138
                    $permissionsField = new PermissionCheckboxSetField(
139
                        'Permissions',
140
                        false,
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type string expected by parameter $title of SilverStripe\Security\Pe...SetField::__construct(). ( Ignorable by Annotation )

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

140
                        /** @scrutinizer ignore-type */ false,
Loading history...
141
                        Permission::class,
142
                        'GroupID',
143
                        $this
144
                    )
145
                )
146
            )
147
        );
148
149
        $parentidfield->setDescription(
150
            _t('SilverStripe\\Security\\Group.GroupReminder', 'If you choose a parent group, this group will take all it\'s roles')
151
        );
152
153
        if ($this->ID) {
154
            $group = $this;
155
            $config = GridFieldConfig_RelationEditor::create();
156
            $config->addComponent(new GridFieldButtonRow('after'));
157
            $config->addComponents(new GridFieldExportButton('buttons-after-left'));
158
            $config->addComponents(new GridFieldPrintButton('buttons-after-left'));
159
            $config->removeComponentsByType(GridFieldDeleteAction::class);
160
            $config->addComponent(new GridFieldGroupDeleteAction($this->ID), GridFieldPageCount::class);
161
162
            /** @var GridFieldAddExistingAutocompleter $autocompleter */
163
            $autocompleter = $config->getComponentByType(GridFieldAddExistingAutocompleter::class);
164
            /** @skipUpgrade */
165
            $autocompleter
166
                ->setResultsFormat('$Title ($Email)')
167
                ->setSearchFields(['FirstName', 'Surname', 'Email']);
168
            /** @var GridFieldDetailForm $detailForm */
169
            $detailForm = $config->getComponentByType(GridFieldDetailForm::class);
170
            $detailForm
171
                ->setItemEditFormCallback(function ($form) use ($group) {
172
                    /** @var Form $form */
173
                    $record = $form->getRecord();
174
                    $form->setValidator($record->getValidator());
0 ignored issues
show
Bug introduced by
The method getValidator() 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

174
                    $form->setValidator($record->/** @scrutinizer ignore-call */ getValidator());
Loading history...
175
                    $groupsField = $form->Fields()->dataFieldByName('DirectGroups');
176
                    if ($groupsField) {
177
                        // If new records are created in a group context,
178
                        // set this group by default.
179
                        if ($record && !$record->ID) {
180
                            $groupsField->setValue($group->ID);
181
                        } elseif ($record && $record->ID) {
182
                            // TODO Mark disabled once chosen.js supports it
183
                            // $groupsField->setDisabledItems(array($group->ID));
184
                            $form->Fields()->replaceField(
185
                                'DirectGroups',
186
                                $groupsField->performReadonlyTransformation()
187
                            );
188
                        }
189
                    }
190
                });
191
            $memberList = GridField::create('Members', false, $this->DirectMembers(), $config)
192
                ->addExtraClass('members_grid');
193
            // @todo Implement permission checking on GridField
194
            //$memberList->setPermissions(array('edit', 'delete', 'export', 'add', 'inlineadd'));
195
            $fields->addFieldToTab('Root.Members', $memberList);
196
        }
197
198
        // Only add a dropdown for HTML editor configurations if more than one is available.
199
        // Otherwise Member->getHtmlEditorConfigForCMS() will default to the 'cms' configuration.
200
        $editorConfigMap = HTMLEditorConfig::get_available_configs_map();
201
        if (count($editorConfigMap) > 1) {
202
            $fields->addFieldToTab(
203
                'Root.Permissions',
204
                new DropdownField(
205
                    'HtmlEditorConfig',
206
                    'HTML Editor Configuration',
207
                    $editorConfigMap
208
                ),
209
                'Permissions'
210
            );
211
        }
212
213
        if (!Permission::check('EDIT_PERMISSIONS')) {
214
            $fields->removeFieldFromTab('Root', 'Permissions');
215
        }
216
217
        // Only show the "Roles" tab if permissions are granted to edit them,
218
        // and at least one role exists
219
        if (Permission::check('APPLY_ROLES') &&
220
            PermissionRole::get()->count() &&
221
            class_exists(SecurityAdmin::class)
222
        ) {
223
            $fields->findOrMakeTab('Root.Roles', _t(__CLASS__ . '.ROLES', 'Roles'));
224
            $fields->addFieldToTab(
225
                'Root.Roles',
226
                new LiteralField(
227
                    "",
228
                    "<p>" .
229
                    _t(
230
                        __CLASS__ . '.ROLESDESCRIPTION',
231
                        "Roles are predefined sets of permissions, and can be assigned to groups.<br />"
232
                        . "They are inherited from parent groups if required."
233
                    ) . '<br />' .
234
                    sprintf(
235
                        '<a href="%s" class="add-role">%s</a>',
236
                        SecurityAdmin::singleton()->Link('show/root#Root_Roles'),
237
                        // TODO This should include #Root_Roles to switch directly to the tab,
238
                        // but tabstrip.js doesn't display tabs when directly adressed through a URL pragma
239
                        _t('SilverStripe\\Security\\Group.RolesAddEditLink', 'Manage roles')
240
                    ) .
241
                    "</p>"
242
                )
243
            );
244
245
            // Add roles (and disable all checkboxes for inherited roles)
246
            $allRoles = PermissionRole::get();
247
            if (!Permission::check('ADMIN')) {
248
                $allRoles = $allRoles->filter("OnlyAdminCanApply", 0);
249
            }
250
            if ($this->ID) {
251
                $groupRoles = $this->Roles();
252
                $inheritedRoles = new ArrayList();
253
                $ancestors = $this->getAncestors();
0 ignored issues
show
Bug introduced by
The method getAncestors() does not exist on SilverStripe\Security\Group. 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

253
                /** @scrutinizer ignore-call */ 
254
                $ancestors = $this->getAncestors();
Loading history...
254
                foreach ($ancestors as $ancestor) {
255
                    $ancestorRoles = $ancestor->Roles();
256
                    if ($ancestorRoles) {
257
                        $inheritedRoles->merge($ancestorRoles);
258
                    }
259
                }
260
                $groupRoleIDs = $groupRoles->column('ID') + $inheritedRoles->column('ID');
261
                $inheritedRoleIDs = $inheritedRoles->column('ID');
262
            } else {
263
                $groupRoleIDs = [];
264
                $inheritedRoleIDs = [];
265
            }
266
267
            $rolesField = ListboxField::create('Roles', false, $allRoles->map()->toArray())
268
                    ->setDefaultItems($groupRoleIDs)
269
                    ->setAttribute('data-placeholder', _t('SilverStripe\\Security\\Group.AddRole', 'Add a role for this group'))
270
                    ->setDisabledItems($inheritedRoleIDs);
271
            if (!$allRoles->count()) {
272
                $rolesField->setAttribute('data-placeholder', _t('SilverStripe\\Security\\Group.NoRoles', 'No roles found'));
273
            }
274
            $fields->addFieldToTab('Root.Roles', $rolesField);
275
        }
276
277
        $fields->push($idField = new HiddenField("ID"));
278
279
        $this->extend('updateCMSFields', $fields);
280
281
        return $fields;
282
    }
283
284
    /**
285
     * @param bool $includerelations Indicate if the labels returned include relation fields
286
     * @return array
287
     * @skipUpgrade
288
     */
289
    public function fieldLabels($includerelations = true)
290
    {
291
        $labels = parent::fieldLabels($includerelations);
292
        $labels['Title'] = _t(__CLASS__ . '.GROUPNAME', 'Group name');
293
        $labels['Description'] = _t('SilverStripe\\Security\\Group.Description', 'Description');
294
        $labels['Code'] = _t('SilverStripe\\Security\\Group.Code', 'Group Code', 'Programmatical code identifying a group');
295
        $labels['Locked'] = _t('SilverStripe\\Security\\Group.Locked', 'Locked?', 'Group is locked in the security administration area');
296
        $labels['Sort'] = _t('SilverStripe\\Security\\Group.Sort', 'Sort Order');
297
        if ($includerelations) {
298
            $labels['Parent'] = _t('SilverStripe\\Security\\Group.Parent', 'Parent Group', 'One group has one parent group');
299
            $labels['Permissions'] = _t('SilverStripe\\Security\\Group.has_many_Permissions', 'Permissions', 'One group has many permissions');
300
            $labels['Members'] = _t('SilverStripe\\Security\\Group.many_many_Members', 'Members', 'One group has many members');
301
        }
302
303
        return $labels;
304
    }
305
306
    /**
307
     * Get many-many relation to {@link Member},
308
     * including all members which are "inherited" from children groups of this record.
309
     * See {@link DirectMembers()} for retrieving members without any inheritance.
310
     *
311
     * @param string $filter
312
     * @return ManyManyList
313
     */
314
    public function Members($filter = '')
315
    {
316
        // First get direct members as a base result
317
        $result = $this->DirectMembers();
318
319
        // Unsaved group cannot have child groups because its ID is still 0.
320
        if (!$this->exists()) {
321
            return $result;
322
        }
323
324
        // Remove the default foreign key filter in prep for re-applying a filter containing all children groups.
325
        // Filters are conjunctive in DataQuery by default, so this filter would otherwise overrule any less specific
326
        // ones.
327
        if (!($result instanceof UnsavedRelationList)) {
328
            $result = $result->alterDataQuery(function ($query) {
329
                /** @var DataQuery $query */
330
                $query->removeFilterOn('Group_Members');
331
            });
332
        }
333
334
        // Now set all children groups as a new foreign key
335
        $familyIDs = $this->collateFamilyIDs();
336
        $result = $result->forForeignID($familyIDs);
337
338
        return $result->where($filter);
0 ignored issues
show
Bug introduced by
The method where() does not exist on SilverStripe\ORM\UnsavedRelationList. 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

338
        return $result->/** @scrutinizer ignore-call */ where($filter);
Loading history...
Bug introduced by
The method where() does not exist on SilverStripe\ORM\Relation. Since it exists in all sub-types, consider adding an abstract or default implementation to SilverStripe\ORM\Relation. ( Ignorable by Annotation )

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

338
        return $result->/** @scrutinizer ignore-call */ where($filter);
Loading history...
339
    }
340
341
    /**
342
     * Return only the members directly added to this group
343
     */
344
    public function DirectMembers()
345
    {
346
        return $this->getManyManyComponents('Members');
347
    }
348
349
    /**
350
     * Return a set of this record's "family" of IDs - the IDs of
351
     * this record and all its descendants.
352
     *
353
     * @return array
354
     */
355
    public function collateFamilyIDs()
356
    {
357
        if (!$this->exists()) {
358
            throw new \InvalidArgumentException("Cannot call collateFamilyIDs on unsaved Group.");
359
        }
360
361
        $familyIDs = [];
362
        $chunkToAdd = [$this->ID];
363
364
        while ($chunkToAdd) {
365
            $familyIDs = array_merge($familyIDs, $chunkToAdd);
366
367
            // Get the children of *all* the groups identified in the previous chunk.
368
            // This minimises the number of SQL queries necessary
369
            $chunkToAdd = Group::get()->filter("ParentID", $chunkToAdd)->column('ID');
370
        }
371
372
        return $familyIDs;
373
    }
374
375
    /**
376
     * Returns an array of the IDs of this group and all its parents
377
     *
378
     * @return array
379
     */
380
    public function collateAncestorIDs()
381
    {
382
        $parent = $this;
383
        $items = [];
384
        while ($parent instanceof Group) {
385
            $items[] = $parent->ID;
386
            $parent = $parent->getParent();
0 ignored issues
show
Bug introduced by
The method getParent() does not exist on SilverStripe\Security\Group. 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

386
            /** @scrutinizer ignore-call */ 
387
            $parent = $parent->getParent();
Loading history...
387
        }
388
        return $items;
389
    }
390
391
    /**
392
     * Check if the group is a child of the given group or any parent groups
393
     *
394
     * @param string|int|Group $group Group instance, Group Code or ID
395
     * @return bool Returns TRUE if the Group is a child of the given group, otherwise FALSE
396
     */
397
    public function inGroup($group)
398
    {
399
        return in_array($this->identifierToGroupID($group), $this->collateAncestorIDs());
400
    }
401
402
    /**
403
     * Check if the group is a child of the given groups or any parent groups
404
     *
405
     * @param (string|int|Group)[] $groups
406
     * @param bool $requireAll set to TRUE if must be in ALL groups, or FALSE if must be in ANY
407
     * @return bool Returns TRUE if the Group is a child of any of the given groups, otherwise FALSE
408
     */
409
    public function inGroups($groups, $requireAll = false)
410
    {
411
        $ancestorIDs = $this->collateAncestorIDs();
412
        $candidateIDs = [];
413
        foreach ($groups as $group) {
414
            $groupID = $this->identifierToGroupID($group);
415
            if ($groupID) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groupID of type integer|null is loosely compared to true; 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...
416
                $candidateIDs[] = $groupID;
417
            } elseif ($requireAll) {
418
                return false;
419
            }
420
        }
421
        if (empty($candidateIDs)) {
422
            return false;
423
        }
424
        $matches = array_intersect($candidateIDs, $ancestorIDs);
425
        if ($requireAll) {
426
            return count($candidateIDs) === count($matches);
427
        }
428
        return !empty($matches);
429
    }
430
431
    /**
432
     * Turn a string|int|Group into a GroupID
433
     *
434
     * @param string|int|Group $groupID Group instance, Group Code or ID
435
     * @return int|null the Group ID or NULL if not found
436
     */
437
    protected function identifierToGroupID($groupID)
438
    {
439
        if (is_numeric($groupID) && Group::get()->byID($groupID)) {
0 ignored issues
show
Bug introduced by
It seems like $groupID can also be of type string; however, parameter $id of SilverStripe\ORM\DataList::byID() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

439
        if (is_numeric($groupID) && Group::get()->byID(/** @scrutinizer ignore-type */ $groupID)) {
Loading history...
440
            return $groupID;
441
        } elseif (is_string($groupID) && $groupByCode = Group::get()->filter(['Code' => $groupID])->first()) {
442
            return $groupByCode->ID;
443
        } elseif ($groupID instanceof Group && $groupID->exists()) {
444
            return $groupID->ID;
445
        }
446
        return null;
447
    }
448
449
    /**
450
     * This isn't a descendant of SiteTree, but needs this in case
451
     * the group is "reorganised";
452
     */
453
    public function cmsCleanup_parentChanged()
454
    {
455
    }
456
457
    /**
458
     * Override this so groups are ordered in the CMS
459
     */
460
    public function stageChildren()
461
    {
462
        return Group::get()
463
            ->filter("ParentID", $this->ID)
464
            ->exclude("ID", $this->ID)
465
            ->sort('"Sort"');
466
    }
467
468
    /**
469
     * @return string
470
     */
471
    public function getTreeTitle()
472
    {
473
        $title = htmlspecialchars($this->Title, ENT_QUOTES);
474
        $this->extend('updateTreeTitle', $title);
475
        return $title;
476
    }
477
478
    /**
479
     * Overloaded to ensure the code is always descent.
480
     *
481
     * @param string $val
482
     */
483
    public function setCode($val)
484
    {
485
        $this->setField('Code', Convert::raw2url($val));
486
    }
487
488
    public function validate()
489
    {
490
        $result = parent::validate();
491
492
        // Check if the new group hierarchy would add certain "privileged permissions",
493
        // and require an admin to perform this change in case it does.
494
        // This prevents "sub-admin" users with group editing permissions to increase their privileges.
495
        if ($this->Parent()->exists() && !Permission::check('ADMIN')) {
496
            $inheritedCodes = Permission::get()
497
                ->filter('GroupID', $this->Parent()->collateAncestorIDs())
498
                ->column('Code');
499
            $privilegedCodes = Permission::config()->get('privileged_permissions');
500
            if (array_intersect($inheritedCodes, $privilegedCodes)) {
501
                $result->addError(
502
                    _t(
503
                        'SilverStripe\\Security\\Group.HierarchyPermsError',
504
                        'Can\'t assign parent group "{group}" with privileged permissions (requires ADMIN access)',
505
                        ['group' => $this->Parent()->Title]
506
                    )
507
                );
508
            }
509
        }
510
511
        $currentGroups = Group::get()
512
            ->filter('ID:not', $this->ID)
513
            ->map('Code', 'Title')
514
            ->toArray();
515
516
        if (in_array($this->Title, $currentGroups)) {
517
            $result->addError(
518
                _t(
519
                    'SilverStripe\\Security\\Group.ValidationIdentifierAlreadyExists',
520
                    'A Group ({group}) already exists with the same {identifier}',
521
                    ['group' => $this->Title, 'identifier' => 'Title']
522
                )
523
            );
524
        }
525
526
        return $result;
527
    }
528
529
    public function getCMSCompositeValidator(): CompositeValidator
530
    {
531
        $validator = parent::getCMSCompositeValidator();
532
533
        $validator->addValidator(RequiredFields::create([
534
            'Title'
535
        ]));
536
537
        return $validator;
538
    }
539
540
    public function onBeforeWrite()
541
    {
542
        parent::onBeforeWrite();
543
544
        // Only set code property when the group has a custom title, and no code exists.
545
        // The "Code" attribute is usually treated as a more permanent identifier than database IDs
546
        // in custom application logic, so can't be changed after its first set.
547
        if (!$this->Code && $this->Title != _t(__CLASS__ . '.NEWGROUP', "New Group")) {
548
            $this->setCode($this->Title);
549
        }
550
551
        // Make sure the code for this group is unique.
552
        // TODO in a future major version we should move this to validate() and throw a
553
        // validation error for duplicates.
554
        $currentGroups = Group::get()
555
            ->exclude('ID', $this->ID)
556
            ->map('Code', 'Title')
557
            ->toArray();
558
        $code = $this->Code;
559
        $count = 2;
560
        while (isset($currentGroups[$code])) {
561
            $code = $this->Code . '-' . $count;
562
            $count++;
563
        }
564
        $this->setField("Code", $code);
565
    }
566
567
    public function onBeforeDelete()
568
    {
569
        parent::onBeforeDelete();
570
571
        // if deleting this group, delete it's children as well
572
        foreach ($this->Groups() as $group) {
573
            $group->delete();
574
        }
575
576
        // Delete associated permissions
577
        foreach ($this->Permissions() as $permission) {
578
            $permission->delete();
579
        }
580
    }
581
582
    /**
583
     * Checks for permission-code CMS_ACCESS_SecurityAdmin.
584
     * If the group has ADMIN permissions, it requires the user to have ADMIN permissions as well.
585
     *
586
     * @param Member $member Member
587
     * @return boolean
588
     */
589
    public function canEdit($member = null)
590
    {
591
        if (!$member) {
592
            $member = Security::getCurrentUser();
593
        }
594
595
        // extended access checks
596
        $results = $this->extend('canEdit', $member);
597
        if ($results && is_array($results)) {
598
            if (!min($results)) {
599
                return false;
600
            }
601
        }
602
603
        if (// either we have an ADMIN
604
            (bool)Permission::checkMember($member, "ADMIN")
605
            || (
606
                // or a privileged CMS user and a group without ADMIN permissions.
607
                // without this check, a user would be able to add himself to an administrators group
608
                // with just access to the "Security" admin interface
609
                Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin") &&
610
                !Permission::get()->filter(['GroupID' => $this->ID, 'Code' => 'ADMIN'])->exists()
611
            )
612
        ) {
613
            return true;
614
        }
615
616
        return false;
617
    }
618
619
    /**
620
     * Checks for permission-code CMS_ACCESS_SecurityAdmin.
621
     *
622
     * @param Member $member
623
     * @return boolean
624
     */
625
    public function canView($member = null)
626
    {
627
        if (!$member) {
628
            $member = Security::getCurrentUser();
629
        }
630
631
        // extended access checks
632
        $results = $this->extend('canView', $member);
633
        if ($results && is_array($results)) {
634
            if (!min($results)) {
635
                return false;
636
            }
637
        }
638
639
        // user needs access to CMS_ACCESS_SecurityAdmin
640
        if (Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin")) {
641
            return true;
642
        }
643
644
        return false;
645
    }
646
647
    public function canDelete($member = null)
648
    {
649
        if (!$member) {
650
            $member = Security::getCurrentUser();
651
        }
652
653
        // extended access checks
654
        $results = $this->extend('canDelete', $member);
655
        if ($results && is_array($results)) {
656
            if (!min($results)) {
657
                return false;
658
            }
659
        }
660
661
        return $this->canEdit($member);
662
    }
663
664
    /**
665
     * Returns all of the children for the CMS Tree.
666
     * Filters to only those groups that the current user can edit
667
     *
668
     * @return ArrayList
669
     */
670
    public function AllChildrenIncludingDeleted()
671
    {
672
        $children = parent::AllChildrenIncludingDeleted();
0 ignored issues
show
introduced by
The method AllChildrenIncludingDeleted() does not exist on SilverStripe\ORM\DataObject. Maybe you want to declare this class abstract? ( Ignorable by Annotation )

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

672
        /** @scrutinizer ignore-call */ 
673
        $children = parent::AllChildrenIncludingDeleted();
Loading history...
673
674
        $filteredChildren = new ArrayList();
675
676
        if ($children) {
677
            foreach ($children as $child) {
678
                /** @var DataObject $child */
679
                if ($child->canView()) {
680
                    $filteredChildren->push($child);
681
                }
682
            }
683
        }
684
685
        return $filteredChildren;
686
    }
687
688
    /**
689
     * Add default records to database.
690
     *
691
     * This function is called whenever the database is built, after the
692
     * database tables have all been created.
693
     */
694
    public function requireDefaultRecords()
695
    {
696
        parent::requireDefaultRecords();
697
698
        // Add default author group if no other group exists
699
        $allGroups = Group::get();
700
        if (!$allGroups->count()) {
701
            $authorGroup = new Group();
702
            $authorGroup->Code = 'content-authors';
703
            $authorGroup->Title = _t(__CLASS__ . '.DefaultGroupTitleContentAuthors', 'Content Authors');
704
            $authorGroup->Sort = 1;
705
            $authorGroup->write();
706
            Permission::grant($authorGroup->ID, 'CMS_ACCESS_CMSMain');
707
            Permission::grant($authorGroup->ID, 'CMS_ACCESS_AssetAdmin');
708
            Permission::grant($authorGroup->ID, 'CMS_ACCESS_ReportAdmin');
709
            Permission::grant($authorGroup->ID, 'SITETREE_REORGANISE');
710
        }
711
712
        // Add default admin group if none with permission code ADMIN exists
713
        $adminGroups = Permission::get_groups_by_permission('ADMIN');
714
        if (!$adminGroups->count()) {
715
            $adminGroup = new Group();
716
            $adminGroup->Code = 'administrators';
717
            $adminGroup->Title = _t(__CLASS__ . '.DefaultGroupTitleAdministrators', 'Administrators');
718
            $adminGroup->Sort = 0;
719
            $adminGroup->write();
720
            Permission::grant($adminGroup->ID, 'ADMIN');
721
        }
722
723
        // Members are populated through Member->requireDefaultRecords()
724
    }
725
}
726