Passed
Pull Request — 4 (#10222)
by Steve
07:01
created

Group   F

Complexity

Total Complexity 97

Size/Duplication

Total Lines 675
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 312
dl 0
loc 675
rs 2
c 2
b 0
f 1
wmc 97

24 Methods

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

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
    private static $indexes = [
0 ignored issues
show
introduced by
The private property $indexes is not used, and could be removed.
Loading history...
89
        'Title' => true,
90
        'Code' => true,
91
        'Sort' => true,
92
    ];
93
94
    public function getAllChildren()
95
    {
96
        $doSet = new ArrayList();
97
98
        $children = Group::get()->filter("ParentID", $this->ID);
99
        /** @var Group $child */
100
        foreach ($children as $child) {
101
            $doSet->push($child);
102
            $doSet->merge($child->getAllChildren());
103
        }
104
105
        return $doSet;
106
    }
107
108
    private function getDecodedBreadcrumbs()
109
    {
110
        $list = Group::get()->exclude('ID', $this->ID);
111
        $groups = ArrayList::create();
112
        foreach ($list as $group) {
113
            $groups->push(['ID' => $group->ID, 'Title' => $group->getBreadcrumbs(' » ')]);
114
        }
115
        return $groups;
116
    }
117
118
    /**
119
     * Caution: Only call on instances, not through a singleton.
120
     * The "root group" fields will be created through {@link SecurityAdmin->EditForm()}.
121
     *
122
     * @skipUpgrade
123
     * @return FieldList
124
     */
125
    public function getCMSFields()
126
    {
127
        $fields = new FieldList(
128
            new TabSet(
129
                "Root",
130
                new Tab(
131
                    'Members',
132
                    _t(__CLASS__ . '.MEMBERS', 'Members'),
133
                    new TextField("Title", $this->fieldLabel('Title')),
134
                    $parentidfield = DropdownField::create(
135
                        'ParentID',
136
                        $this->fieldLabel('Parent'),
137
                        $this->getDecodedBreadcrumbs()
138
                    )->setEmptyString(' '),
139
                    new TextareaField('Description', $this->fieldLabel('Description'))
140
                ),
141
                $permissionsTab = new Tab(
142
                    'Permissions',
143
                    _t(__CLASS__ . '.PERMISSIONS', 'Permissions'),
144
                    $permissionsField = new PermissionCheckboxSetField(
145
                        'Permissions',
146
                        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

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

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

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

344
        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

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

392
            /** @scrutinizer ignore-call */ 
393
            $parent = $parent->getParent();
Loading history...
393
        }
394
        return $items;
395
    }
396
397
    /**
398
     * Check if the group is a child of the given group or any parent groups
399
     *
400
     * @param string|int|Group $group Group instance, Group Code or ID
401
     * @return bool Returns TRUE if the Group is a child of the given group, otherwise FALSE
402
     */
403
    public function inGroup($group)
404
    {
405
        return in_array($this->identifierToGroupID($group), $this->collateAncestorIDs() ?: []);
406
    }
407
408
    /**
409
     * Check if the group is a child of the given groups or any parent groups
410
     *
411
     * @param (string|int|Group)[] $groups
412
     * @param bool $requireAll set to TRUE if must be in ALL groups, or FALSE if must be in ANY
413
     * @return bool Returns TRUE if the Group is a child of any of the given groups, otherwise FALSE
414
     */
415
    public function inGroups($groups, $requireAll = false)
416
    {
417
        $ancestorIDs = $this->collateAncestorIDs();
418
        $candidateIDs = [];
419
        foreach ($groups as $group) {
420
            $groupID = $this->identifierToGroupID($group);
421
            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...
422
                $candidateIDs[] = $groupID;
423
            } elseif ($requireAll) {
424
                return false;
425
            }
426
        }
427
        if (empty($candidateIDs)) {
428
            return false;
429
        }
430
        $matches = array_intersect($candidateIDs ?: [], $ancestorIDs);
431
        if ($requireAll) {
432
            return count($candidateIDs ?: []) === count($matches ?: []);
433
        }
434
        return !empty($matches);
435
    }
436
437
    /**
438
     * Turn a string|int|Group into a GroupID
439
     *
440
     * @param string|int|Group $groupID Group instance, Group Code or ID
441
     * @return int|null the Group ID or NULL if not found
442
     */
443
    protected function identifierToGroupID($groupID)
444
    {
445
        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

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

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