Group   F
last analyzed

Complexity

Total Complexity 86

Size/Duplication

Total Lines 628
Duplicated Lines 0 %

Importance

Changes 5
Bugs 1 Features 1
Metric Value
eloc 283
c 5
b 1
f 1
dl 0
loc 628
rs 2
wmc 86

23 Methods

Rating   Name   Duplication   Size   Complexity  
A onBeforeDelete() 0 12 3
A requireDefaultRecords() 0 27 3
A inGroups() 0 20 6
A canDelete() 0 15 5
A Members() 0 25 3
A validate() 0 24 4
B canEdit() 0 28 8
A setCode() 0 3 1
A stageChildren() 0 6 1
A onBeforeWrite() 0 9 3
A DirectMembers() 0 3 1
A cmsCleanup_parentChanged() 0 2 1
A getTreeTitle() 0 5 1
B identifierToGroupID() 0 10 7
A canView() 0 20 6
A AllChildrenIncludingDeleted() 0 16 4
A inGroup() 0 3 1
A collateAncestorIDs() 0 9 2
A collateFamilyIDs() 0 18 3
A getAllChildren() 0 12 2
D getCMSFields() 0 163 17
A getDecodedBreadcrumbs() 0 8 2
A fieldLabels() 0 19 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\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\GridFieldDeleteAction;
15
use SilverStripe\Forms\GridField\GridFieldDetailForm;
16
use SilverStripe\Forms\GridField\GridFieldExportButton;
17
use SilverStripe\Forms\GridField\GridFieldGroupDeleteAction;
18
use SilverStripe\Forms\GridField\GridFieldPageCount;
19
use SilverStripe\Forms\GridField\GridFieldPrintButton;
20
use SilverStripe\Forms\HiddenField;
21
use SilverStripe\Forms\HTMLEditor\HTMLEditorConfig;
22
use SilverStripe\Forms\ListboxField;
23
use SilverStripe\Forms\LiteralField;
24
use SilverStripe\Forms\Tab;
25
use SilverStripe\Forms\TabSet;
26
use SilverStripe\Forms\TextareaField;
27
use SilverStripe\Forms\TextField;
28
use SilverStripe\ORM\ArrayList;
29
use SilverStripe\ORM\DataObject;
30
use SilverStripe\ORM\DataQuery;
31
use SilverStripe\ORM\HasManyList;
32
use SilverStripe\ORM\Hierarchy\Hierarchy;
33
use SilverStripe\ORM\ManyManyList;
34
use SilverStripe\ORM\UnsavedRelationList;
35
36
/**
37
 * A security group.
38
 *
39
 * @property string $Title Name of the group
40
 * @property string $Description Description of the group
41
 * @property string $Code Group code
42
 * @property string $Locked Boolean indicating whether group is locked in security panel
43
 * @property int $Sort
44
 * @property string HtmlEditorConfig
45
 *
46
 * @property int $ParentID ID of parent group
47
 *
48
 * @method Group Parent() Return parent group
49
 * @method HasManyList Permissions() List of group permissions
50
 * @method HasManyList Groups() List of child groups
51
 * @method ManyManyList Roles() List of PermissionRoles
52
 * @mixin Hierarchy
53
 */
54
class Group extends DataObject
55
{
56
57
    private static $db = array(
0 ignored issues
show
introduced by
The private property $db is not used, and could be removed.
Loading history...
58
        "Title" => "Varchar(255)",
59
        "Description" => "Text",
60
        "Code" => "Varchar(255)",
61
        "Locked" => "Boolean",
62
        "Sort" => "Int",
63
        "HtmlEditorConfig" => "Text"
64
    );
65
66
    private static $has_one = array(
0 ignored issues
show
introduced by
The private property $has_one is not used, and could be removed.
Loading history...
67
        "Parent" => Group::class,
68
    );
69
70
    private static $has_many = array(
0 ignored issues
show
introduced by
The private property $has_many is not used, and could be removed.
Loading history...
71
        "Permissions" => Permission::class,
72
        "Groups" => Group::class,
73
    );
74
75
    private static $many_many = array(
0 ignored issues
show
introduced by
The private property $many_many is not used, and could be removed.
Loading history...
76
        "Members" => Member::class,
77
        "Roles" => PermissionRole::class,
78
    );
79
80
    private static $extensions = array(
0 ignored issues
show
introduced by
The private property $extensions is not used, and could be removed.
Loading history...
81
        Hierarchy::class,
82
    );
83
84
    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...
85
86
    public function getAllChildren()
87
    {
88
        $doSet = new ArrayList();
89
90
        $children = Group::get()->filter("ParentID", $this->ID);
91
        /** @var Group $child */
92
        foreach ($children as $child) {
93
            $doSet->push($child);
94
            $doSet->merge($child->getAllChildren());
0 ignored issues
show
Bug introduced by
The method getAllChildren() 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

94
            $doSet->merge($child->/** @scrutinizer ignore-call */ getAllChildren());
Loading history...
95
        }
96
97
        return $doSet;
98
    }
99
100
    private function getDecodedBreadcrumbs()
101
    {
102
        $list = Group::get()->exclude('ID', $this->ID);
103
        $groups = ArrayList::create();
104
        foreach ($list as $group) {
105
            $groups->push(['ID' => $group->ID, 'Title' => $group->getBreadcrumbs(' » ')]);
0 ignored issues
show
Bug introduced by
The method getBreadcrumbs() 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

105
            $groups->push(['ID' => $group->ID, 'Title' => $group->/** @scrutinizer ignore-call */ getBreadcrumbs(' » ')]);
Loading history...
106
        }
107
        return $groups;
108
    }
109
110
    /**
111
     * Caution: Only call on instances, not through a singleton.
112
     * The "root group" fields will be created through {@link SecurityAdmin->EditForm()}.
113
     *
114
     * @skipUpgrade
115
     * @return FieldList
116
     */
117
    public function getCMSFields()
118
    {
119
        $fields = new FieldList(
120
            new TabSet(
121
                "Root",
122
                new Tab(
123
                    'Members',
124
                    _t(__CLASS__ . '.MEMBERS', 'Members'),
125
                    new TextField("Title", $this->fieldLabel('Title')),
126
                    $parentidfield = DropdownField::create(
127
                        'ParentID',
128
                        $this->fieldLabel('Parent'),
129
                        $this->getDecodedBreadcrumbs()
130
                    )->setEmptyString(' '),
131
                    new TextareaField('Description', $this->fieldLabel('Description'))
132
                ),
133
                $permissionsTab = new Tab(
134
                    'Permissions',
135
                    _t(__CLASS__ . '.PERMISSIONS', 'Permissions'),
136
                    $permissionsField = new PermissionCheckboxSetField(
137
                        'Permissions',
138
                        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

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

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

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

340
        return $result->/** @scrutinizer ignore-call */ where($filter);
Loading history...
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

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

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

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

633
        /** @scrutinizer ignore-call */ 
634
        $children = parent::AllChildrenIncludingDeleted();
Loading history...
634
635
        $filteredChildren = new ArrayList();
636
637
        if ($children) {
638
            foreach ($children as $child) {
639
                /** @var DataObject $child */
640
                if ($child->canView()) {
641
                    $filteredChildren->push($child);
642
                }
643
            }
644
        }
645
646
        return $filteredChildren;
647
    }
648
649
    /**
650
     * Add default records to database.
651
     *
652
     * This function is called whenever the database is built, after the
653
     * database tables have all been created.
654
     */
655
    public function requireDefaultRecords()
656
    {
657
        parent::requireDefaultRecords();
658
659
        // Add default author group if no other group exists
660
        $allGroups = Group::get();
661
        if (!$allGroups->count()) {
662
            $authorGroup = new Group();
663
            $authorGroup->Code = 'content-authors';
664
            $authorGroup->Title = _t(__CLASS__ . '.DefaultGroupTitleContentAuthors', 'Content Authors');
665
            $authorGroup->Sort = 1;
666
            $authorGroup->write();
667
            Permission::grant($authorGroup->ID, 'CMS_ACCESS_CMSMain');
668
            Permission::grant($authorGroup->ID, 'CMS_ACCESS_AssetAdmin');
669
            Permission::grant($authorGroup->ID, 'CMS_ACCESS_ReportAdmin');
670
            Permission::grant($authorGroup->ID, 'SITETREE_REORGANISE');
671
        }
672
673
        // Add default admin group if none with permission code ADMIN exists
674
        $adminGroups = Permission::get_groups_by_permission('ADMIN');
675
        if (!$adminGroups->count()) {
676
            $adminGroup = new Group();
677
            $adminGroup->Code = 'administrators';
678
            $adminGroup->Title = _t(__CLASS__ . '.DefaultGroupTitleAdministrators', 'Administrators');
679
            $adminGroup->Sort = 0;
680
            $adminGroup->write();
681
            Permission::grant($adminGroup->ID, 'ADMIN');
682
        }
683
684
        // Members are populated through Member->requireDefaultRecords()
685
    }
686
}
687