Passed
Push — 4.11 ( dccaa9...972a77 )
by Guy
07:24 queued 12s
created

Group::canEdit()   B

Complexity

Conditions 8
Paths 10

Size

Total Lines 28
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 13
nc 10
nop 1
dl 0
loc 28
rs 8.4444
c 0
b 0
f 0
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($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
        $this->setField('Code', Convert::raw2url($val));
492
    }
493
494
    public function validate()
495
    {
496
        $result = parent::validate();
497
498
        // Check if the new group hierarchy would add certain "privileged permissions",
499
        // and require an admin to perform this change in case it does.
500
        // This prevents "sub-admin" users with group editing permissions to increase their privileges.
501
        if ($this->Parent()->exists() && !Permission::check('ADMIN')) {
502
            $inheritedCodes = Permission::get()
503
                ->filter('GroupID', $this->Parent()->collateAncestorIDs())
504
                ->column('Code');
505
            $privilegedCodes = Permission::config()->get('privileged_permissions');
506
            if (array_intersect($inheritedCodes ?? [], $privilegedCodes)) {
507
                $result->addError(
508
                    _t(
509
                        'SilverStripe\\Security\\Group.HierarchyPermsError',
510
                        'Can\'t assign parent group "{group}" with privileged permissions (requires ADMIN access)',
511
                        ['group' => $this->Parent()->Title]
512
                    )
513
                );
514
            }
515
        }
516
517
        $currentGroups = Group::get()
518
            ->filter('ID:not', $this->ID)
519
            ->map('Code', 'Title')
520
            ->toArray();
521
522
        if (in_array($this->Title, $currentGroups)) {
523
            $result->addError(
524
                _t(
525
                    'SilverStripe\\Security\\Group.ValidationIdentifierAlreadyExists',
526
                    'A Group ({group}) already exists with the same {identifier}',
527
                    ['group' => $this->Title, 'identifier' => 'Title']
528
                )
529
            );
530
        }
531
532
        return $result;
533
    }
534
535
    public function getCMSCompositeValidator(): CompositeValidator
536
    {
537
        $validator = parent::getCMSCompositeValidator();
538
539
        $validator->addValidator(RequiredFields::create([
540
            'Title'
541
        ]));
542
543
        return $validator;
544
    }
545
546
    public function onBeforeWrite()
547
    {
548
        parent::onBeforeWrite();
549
550
        // Only set code property when the group has a custom title, and no code exists.
551
        // The "Code" attribute is usually treated as a more permanent identifier than database IDs
552
        // in custom application logic, so can't be changed after its first set.
553
        if (!$this->Code && $this->Title != _t(__CLASS__ . '.NEWGROUP', "New Group")) {
554
            $this->setCode($this->Title);
555
        }
556
557
        // Make sure the code for this group is unique.
558
        $this->dedupeCode();
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Security\Group::dedupeCode() has been deprecated: 5.0 Remove deduping in favour of throwing a validation error for duplicates. ( Ignorable by Annotation )

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

558
        /** @scrutinizer ignore-deprecated */ $this->dedupeCode();

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

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

Loading history...
559
    }
560
561
    public function onBeforeDelete()
562
    {
563
        parent::onBeforeDelete();
564
565
        // if deleting this group, delete it's children as well
566
        foreach ($this->Groups() as $group) {
567
            $group->delete();
568
        }
569
570
        // Delete associated permissions
571
        foreach ($this->Permissions() as $permission) {
572
            $permission->delete();
573
        }
574
    }
575
576
    /**
577
     * Checks for permission-code CMS_ACCESS_SecurityAdmin.
578
     * If the group has ADMIN permissions, it requires the user to have ADMIN permissions as well.
579
     *
580
     * @param Member $member Member
581
     * @return boolean
582
     */
583
    public function canEdit($member = null)
584
    {
585
        if (!$member) {
586
            $member = Security::getCurrentUser();
587
        }
588
589
        // extended access checks
590
        $results = $this->extend('canEdit', $member);
591
        if ($results && is_array($results)) {
592
            if (!min($results)) {
593
                return false;
594
            }
595
        }
596
597
        if (// either we have an ADMIN
598
            (bool)Permission::checkMember($member, "ADMIN")
599
            || (
600
                // or a privileged CMS user and a group without ADMIN permissions.
601
                // without this check, a user would be able to add himself to an administrators group
602
                // with just access to the "Security" admin interface
603
                Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin") &&
604
                !Permission::get()->filter(['GroupID' => $this->ID, 'Code' => 'ADMIN'])->exists()
605
            )
606
        ) {
607
            return true;
608
        }
609
610
        return false;
611
    }
612
613
    /**
614
     * Checks for permission-code CMS_ACCESS_SecurityAdmin.
615
     *
616
     * @param Member $member
617
     * @return boolean
618
     */
619
    public function canView($member = null)
620
    {
621
        if (!$member) {
622
            $member = Security::getCurrentUser();
623
        }
624
625
        // extended access checks
626
        $results = $this->extend('canView', $member);
627
        if ($results && is_array($results)) {
628
            if (!min($results)) {
629
                return false;
630
            }
631
        }
632
633
        // user needs access to CMS_ACCESS_SecurityAdmin
634
        if (Permission::checkMember($member, "CMS_ACCESS_SecurityAdmin")) {
635
            return true;
636
        }
637
638
        return false;
639
    }
640
641
    public function canDelete($member = null)
642
    {
643
        if (!$member) {
644
            $member = Security::getCurrentUser();
645
        }
646
647
        // extended access checks
648
        $results = $this->extend('canDelete', $member);
649
        if ($results && is_array($results)) {
650
            if (!min($results)) {
651
                return false;
652
            }
653
        }
654
655
        return $this->canEdit($member);
656
    }
657
658
    /**
659
     * Returns all of the children for the CMS Tree.
660
     * Filters to only those groups that the current user can edit
661
     *
662
     * @return ArrayList
663
     */
664
    public function AllChildrenIncludingDeleted()
665
    {
666
        $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

666
        /** @scrutinizer ignore-call */ 
667
        $children = parent::AllChildrenIncludingDeleted();
Loading history...
667
668
        $filteredChildren = new ArrayList();
669
670
        if ($children) {
671
            foreach ($children as $child) {
672
                /** @var DataObject $child */
673
                if ($child->canView()) {
674
                    $filteredChildren->push($child);
675
                }
676
            }
677
        }
678
679
        return $filteredChildren;
680
    }
681
682
    /**
683
     * Add default records to database.
684
     *
685
     * This function is called whenever the database is built, after the
686
     * database tables have all been created.
687
     */
688
    public function requireDefaultRecords()
689
    {
690
        parent::requireDefaultRecords();
691
692
        // Add default author group if no other group exists
693
        $allGroups = Group::get();
694
        if (!$allGroups->count()) {
695
            $authorGroup = new Group();
696
            $authorGroup->Code = 'content-authors';
697
            $authorGroup->Title = _t(__CLASS__ . '.DefaultGroupTitleContentAuthors', 'Content Authors');
698
            $authorGroup->Sort = 1;
699
            $authorGroup->write();
700
            Permission::grant($authorGroup->ID, 'CMS_ACCESS_CMSMain');
701
            Permission::grant($authorGroup->ID, 'CMS_ACCESS_AssetAdmin');
702
            Permission::grant($authorGroup->ID, 'CMS_ACCESS_ReportAdmin');
703
            Permission::grant($authorGroup->ID, 'SITETREE_REORGANISE');
704
        }
705
706
        // Add default admin group if none with permission code ADMIN exists
707
        $adminGroups = Permission::get_groups_by_permission('ADMIN');
708
        if (!$adminGroups->count()) {
709
            $adminGroup = new Group();
710
            $adminGroup->Code = 'administrators';
711
            $adminGroup->Title = _t(__CLASS__ . '.DefaultGroupTitleAdministrators', 'Administrators');
712
            $adminGroup->Sort = 0;
713
            $adminGroup->write();
714
            Permission::grant($adminGroup->ID, 'ADMIN');
715
        }
716
717
        // Members are populated through Member->requireDefaultRecords()
718
    }
719
720
    /**
721
     * Code needs to be unique as it is used to identify a specific group. Ensure no duplicate
722
     * codes are created.
723
     *
724
     * @deprecated 5.0 Remove deduping in favour of throwing a validation error for duplicates.
725
     */
726
    private function dedupeCode(): void
727
    {
728
        $currentGroups = Group::get()
729
            ->exclude('ID', $this->ID)
730
            ->map('Code', 'Title')
731
            ->toArray();
732
        $code = $this->Code;
733
        $count = 2;
734
        while (isset($currentGroups[$code])) {
735
            $code = $this->Code . '-' . $count;
736
            $count++;
737
        }
738
        $this->setField('Code', $code);
739
    }
740
}
741