Passed
Pull Request — 4.10 (#10333)
by Guy
06:12
created

Group::validate()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 39
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 23
nc 6
nop 0
dl 0
loc 39
rs 9.2408
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
    public function getAllChildren()
89
    {
90
        $doSet = new ArrayList();
91
92
        $children = Group::get()->filter("ParentID", $this->ID);
93
        /** @var Group $child */
94
        foreach ($children as $child) {
95
            $doSet->push($child);
96
            $doSet->merge($child->getAllChildren());
97
        }
98
99
        return $doSet;
100
    }
101
102
    private function getDecodedBreadcrumbs()
103
    {
104
        $list = Group::get()->exclude('ID', $this->ID);
105
        $groups = ArrayList::create();
106
        foreach ($list as $group) {
107
            $groups->push(['ID' => $group->ID, 'Title' => $group->getBreadcrumbs(' » ')]);
108
        }
109
        return $groups;
110
    }
111
112
    /**
113
     * Caution: Only call on instances, not through a singleton.
114
     * The "root group" fields will be created through {@link SecurityAdmin->EditForm()}.
115
     *
116
     * @skipUpgrade
117
     * @return FieldList
118
     */
119
    public function getCMSFields()
120
    {
121
        $fields = new FieldList(
122
            new TabSet(
123
                "Root",
124
                new Tab(
125
                    'Members',
126
                    _t(__CLASS__ . '.MEMBERS', 'Members'),
127
                    new TextField("Title", $this->fieldLabel('Title')),
128
                    $parentidfield = DropdownField::create(
129
                        'ParentID',
130
                        $this->fieldLabel('Parent'),
131
                        $this->getDecodedBreadcrumbs()
132
                    )->setEmptyString(' '),
133
                    new TextareaField('Description', $this->fieldLabel('Description'))
134
                ),
135
                $permissionsTab = new Tab(
136
                    'Permissions',
137
                    _t(__CLASS__ . '.PERMISSIONS', 'Permissions'),
138
                    $permissionsField = new PermissionCheckboxSetField(
139
                        'Permissions',
140
                        false,
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type string expected by parameter $title of SilverStripe\Security\Pe...SetField::__construct(). ( Ignorable by Annotation )

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

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

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

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

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

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

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

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

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

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

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

386
            /** @scrutinizer ignore-call */ 
387
            $parent = $parent->getParent();
Loading history...
387
        }
388
        return $items;
389
    }
390
391
    /**
392
     * Check if the group is a child of the given group or any parent groups
393
     *
394
     * @param string|int|Group $group Group instance, Group Code or ID
395
     * @return bool Returns TRUE if the Group is a child of the given group, otherwise FALSE
396
     */
397
    public function inGroup($group)
398
    {
399
        return in_array($this->identifierToGroupID($group), $this->collateAncestorIDs());
400
    }
401
402
    /**
403
     * Check if the group is a child of the given groups or any parent groups
404
     *
405
     * @param (string|int|Group)[] $groups
406
     * @param bool $requireAll set to TRUE if must be in ALL groups, or FALSE if must be in ANY
407
     * @return bool Returns TRUE if the Group is a child of any of the given groups, otherwise FALSE
408
     */
409
    public function inGroups($groups, $requireAll = false)
410
    {
411
        $ancestorIDs = $this->collateAncestorIDs();
412
        $candidateIDs = [];
413
        foreach ($groups as $group) {
414
            $groupID = $this->identifierToGroupID($group);
415
            if ($groupID) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groupID of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
416
                $candidateIDs[] = $groupID;
417
            } elseif ($requireAll) {
418
                return false;
419
            }
420
        }
421
        if (empty($candidateIDs)) {
422
            return false;
423
        }
424
        $matches = array_intersect($candidateIDs, $ancestorIDs);
425
        if ($requireAll) {
426
            return count($candidateIDs) === count($matches);
427
        }
428
        return !empty($matches);
429
    }
430
431
    /**
432
     * Turn a string|int|Group into a GroupID
433
     *
434
     * @param string|int|Group $groupID Group instance, Group Code or ID
435
     * @return int|null the Group ID or NULL if not found
436
     */
437
    protected function identifierToGroupID($groupID)
438
    {
439
        if (is_numeric($groupID) && Group::get()->byID($groupID)) {
0 ignored issues
show
Bug introduced by
It seems like $groupID can also be of type string; however, parameter $id of SilverStripe\ORM\DataList::byID() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

439
        if (is_numeric($groupID) && Group::get()->byID(/** @scrutinizer ignore-type */ $groupID)) {
Loading history...
440
            return $groupID;
441
        } elseif (is_string($groupID) && $groupByCode = Group::get()->filter(['Code' => $groupID])->first()) {
442
            return $groupByCode->ID;
443
        } elseif ($groupID instanceof Group && $groupID->exists()) {
444
            return $groupID->ID;
445
        }
446
        return null;
447
    }
448
449
    /**
450
     * This isn't a descendant of SiteTree, but needs this in case
451
     * the group is "reorganised";
452
     */
453
    public function cmsCleanup_parentChanged()
454
    {
455
    }
456
457
    /**
458
     * Override this so groups are ordered in the CMS
459
     */
460
    public function stageChildren()
461
    {
462
        return Group::get()
463
            ->filter("ParentID", $this->ID)
464
            ->exclude("ID", $this->ID)
465
            ->sort('"Sort"');
466
    }
467
468
    /**
469
     * @return string
470
     */
471
    public function getTreeTitle()
472
    {
473
        $title = htmlspecialchars($this->Title, ENT_QUOTES);
474
        $this->extend('updateTreeTitle', $title);
475
        return $title;
476
    }
477
478
    /**
479
     * Overloaded to ensure the code is always descent.
480
     *
481
     * @param string $val
482
     */
483
    public function setCode($val)
484
    {
485
        $this->setField('Code', Convert::raw2url($val));
486
    }
487
488
    public function validate()
489
    {
490
        $result = parent::validate();
491
492
        // Check if the new group hierarchy would add certain "privileged permissions",
493
        // and require an admin to perform this change in case it does.
494
        // This prevents "sub-admin" users with group editing permissions to increase their privileges.
495
        if ($this->Parent()->exists() && !Permission::check('ADMIN')) {
496
            $inheritedCodes = Permission::get()
497
                ->filter('GroupID', $this->Parent()->collateAncestorIDs())
498
                ->column('Code');
499
            $privilegedCodes = Permission::config()->get('privileged_permissions');
500
            if (array_intersect($inheritedCodes, $privilegedCodes)) {
501
                $result->addError(
502
                    _t(
503
                        'SilverStripe\\Security\\Group.HierarchyPermsError',
504
                        'Can\'t assign parent group "{group}" with privileged permissions (requires ADMIN access)',
505
                        ['group' => $this->Parent()->Title]
506
                    )
507
                );
508
            }
509
        }
510
511
        $currentGroups = Group::get()
512
            ->filter('ID:not', $this->ID)
513
            ->map('Code', 'Title')
514
            ->toArray();
515
516
        if (in_array($this->Title, $currentGroups)) {
517
            $result->addError(
518
                _t(
519
                    'SilverStripe\\Security\\Group.ValidationIdentifierAlreadyExists',
520
                    'A Group ({group}) already exists with the same {identifier}',
521
                    ['group' => $this->Title, 'identifier' => 'Title']
522
                )
523
            );
524
        }
525
526
        return $result;
527
    }
528
529
    public function getCMSCompositeValidator(): CompositeValidator
530
    {
531
        $validator = parent::getCMSCompositeValidator();
532
533
        $validator->addValidator(RequiredFields::create([
534
            'Title'
535
        ]));
536
537
        return $validator;
538
    }
539
540
    public function onBeforeWrite()
541
    {
542
        parent::onBeforeWrite();
543
544
        // Only set code property when the group has a custom title, and no code exists.
545
        // The "Code" attribute is usually treated as a more permanent identifier than database IDs
546
        // in custom application logic, so can't be changed after its first set.
547
        if (!$this->Code && $this->Title != _t(__CLASS__ . '.NEWGROUP', "New Group")) {
548
            $this->setCode($this->Title);
549
        }
550
551
        // Make sure the code for this group is unique.
552
        $this->dedupeCode();
0 ignored issues
show
Deprecated Code introduced by
The function SilverStripe\Security\Group::dedupeCode() has been deprecated: 5.0 We should move this to validate() and throw 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

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

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