Completed
Push — master ( c17796...052b15 )
by Damian
01:29
created

PermissionCheckboxSetField   F

Complexity

Total Complexity 72

Size/Duplication

Total Lines 332
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 332
rs 2.5423
c 0
b 0
f 0
wmc 72

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 4
F Field() 0 195 49
A setHiddenPermissions() 0 3 1
A getHiddenPermissions() 0 3 1
C saveInto() 0 39 16
A performReadonlyTransformation() 0 11 1

How to fix   Complexity   

Complex Class

Complex classes like PermissionCheckboxSetField 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 PermissionCheckboxSetField, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace SilverStripe\Security;
4
5
use InvalidArgumentException;
6
use SilverStripe\Core\Config\Config;
7
use SilverStripe\Forms\FormField;
8
use SilverStripe\ORM\ArrayList;
9
use SilverStripe\ORM\DataObject;
10
use SilverStripe\ORM\DataObjectInterface;
11
use SilverStripe\ORM\SS_List;
12
use Traversable;
13
14
/**
15
 * Shows a categorized list of available permissions (through {@link Permission::get_codes()}).
16
 * Permissions which are assigned to a given {@link Group} record
17
 * (either directly, inherited from parent groups, or through a {@link PermissionRole})
18
 * will be checked automatically. All checkboxes for "inherited" permissions will be readonly.
19
 *
20
 * The field can gets its assignment data either from {@link Group} or {@link PermissionRole} records.
21
 */
22
class PermissionCheckboxSetField extends FormField
23
{
24
25
    /**
26
     * @var array Filter certain permission codes from the output.
27
     * Useful to simplify the interface
28
     */
29
    protected $hiddenPermissions = array();
30
31
    /**
32
     * @var SS_List
33
     */
34
    protected $records = null;
35
36
    /**
37
     * @var array Array Nested array in same notation as {@link CheckboxSetField}.
38
     */
39
    protected $source = null;
40
41
    /**
42
     * @param String $name
43
     * @param String $title
44
     * @param String $managedClass
45
     * @param String $filterField
46
     * @param Group|SS_List $records One or more {@link Group} or {@link PermissionRole} records
47
     *  used to determine permission checkboxes.
48
     *  Caution: saveInto() can only be used with a single record, all inherited permissions will be marked readonly.
49
     *  Setting multiple groups only makes sense in a readonly context. (Optional)
50
     */
51
    public function __construct($name, $title, $managedClass, $filterField, $records = null)
52
    {
53
        $this->filterField = $filterField;
0 ignored issues
show
Bug Best Practice introduced by
The property filterField does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
54
        $this->managedClass = $managedClass;
0 ignored issues
show
Bug Best Practice introduced by
The property managedClass does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
55
56
        if ($records instanceof SS_List) {
57
            $this->records = $records;
58
        } elseif ($records instanceof Group) {
59
            $this->records = new ArrayList(array($records));
60
        } elseif ($records) {
61
            throw new InvalidArgumentException(
62
                '$record should be either a Group record, or a SS_List of Group records'
63
            );
64
        }
65
66
        // Get all available codes in the system as a categorized nested array
67
        $this->source = Permission::get_codes(true);
68
69
        parent::__construct($name, $title);
70
    }
71
72
    /**
73
     * @param array $codes
74
     */
75
    public function setHiddenPermissions($codes)
76
    {
77
        $this->hiddenPermissions = $codes;
78
    }
79
80
    /**
81
     * @return array
82
     */
83
    public function getHiddenPermissions()
84
    {
85
        return $this->hiddenPermissions;
86
    }
87
88
    /**
89
     * @param array $properties
90
     * @return string
91
     */
92
    public function Field($properties = array())
93
    {
94
        $uninheritedCodes = array();
95
        $inheritedCodes = array();
96
        $records = ($this->records) ? $this->records : new ArrayList();
97
98
        // Get existing values from the form record (assuming the formfield name is a join field on the record)
99
        if (is_object($this->form)) {
100
            $record = $this->form->getRecord();
101
            if ($record
102
                && ($record instanceof Group || $record instanceof PermissionRole)
103
                && !$records->find('ID', $record->ID)
104
            ) {
105
                $records->push($record);
106
            }
107
        }
108
109
        // Get all 'inherited' codes not directly assigned to the group (which is stored in $values)
110
        foreach ($records as $record) {
111
            // Get all uninherited permissions
112
            $relationMethod = $this->name;
113
            foreach ($record->$relationMethod() as $permission) {
114
                if (!isset($uninheritedCodes[$permission->Code])) {
115
                    $uninheritedCodes[$permission->Code] = array();
116
                }
117
                $uninheritedCodes[$permission->Code][] = _t(
118
                    'SilverStripe\\Security\\PermissionCheckboxSetField.AssignedTo',
119
                    'assigned to "{title}"',
120
                    array('title' => $record->dbObject('Title')->forTemplate())
121
                );
122
            }
123
124
            // Special case for Group records (not PermissionRole):
125
            // Determine inherited assignments
126
            if ($record instanceof Group) {
127
                // Get all permissions from roles
128
                if ($record->Roles()->count()) {
129
                    foreach ($record->Roles() as $role) {
130
                        /** @var PermissionRole $role */
131
                        foreach ($role->Codes() as $code) {
132
                            if (!isset($inheritedCodes[$code->Code])) {
133
                                $inheritedCodes[$code->Code] = array();
134
                            }
135
                            $inheritedCodes[$code->Code][] = _t(
136
                                'SilverStripe\\Security\\PermissionCheckboxSetField.FromRole',
137
                                'inherited from role "{title}"',
138
                                'A permission inherited from a certain permission role',
139
                                array('title' => $role->dbObject('Title')->forTemplate())
140
                            );
141
                        }
142
                    }
143
                }
144
145
                // Get from parent groups
146
                $parentGroups = $record->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

146
                /** @scrutinizer ignore-call */ 
147
                $parentGroups = $record->getAncestors();
Loading history...
147
                if ($parentGroups) {
148
                    foreach ($parentGroups as $parent) {
149
                        if (!$parent->Roles()->Count()) {
150
                            continue;
151
                        }
152
                        foreach ($parent->Roles() as $role) {
153
                            if ($role->Codes()) {
154
                                foreach ($role->Codes() as $code) {
155
                                    if (!isset($inheritedCodes[$code->Code])) {
156
                                        $inheritedCodes[$code->Code] = array();
157
                                    }
158
                                    $inheritedCodes[$code->Code][] = _t(
159
                                        'SilverStripe\\Security\\PermissionCheckboxSetField.FromRoleOnGroup',
160
                                        'inherited from role "{roletitle}" on group "{grouptitle}"',
161
                                        'A permission inherited from a role on a certain group',
162
                                        array(
163
                                            'roletitle' => $role->dbObject('Title')->forTemplate(),
164
                                            'grouptitle' => $parent->dbObject('Title')->forTemplate()
165
                                        )
166
                                    );
167
                                }
168
                            }
169
                        }
170
                        if ($parent->Permissions()->Count()) {
171
                            foreach ($parent->Permissions() as $permission) {
172
                                if (!isset($inheritedCodes[$permission->Code])) {
173
                                    $inheritedCodes[$permission->Code] = array();
174
                                }
175
                                $inheritedCodes[$permission->Code][] =
176
                                _t(
177
                                    'SilverStripe\\Security\\PermissionCheckboxSetField.FromGroup',
178
                                    'inherited from group "{title}"',
179
                                    'A permission inherited from a certain group',
180
                                    array('title' => $parent->dbObject('Title')->forTemplate())
181
                                );
182
                            }
183
                        }
184
                    }
185
                }
186
            }
187
        }
188
189
        $odd = 0;
190
        $options = '';
191
        $globalHidden = (array)Config::inst()->get('SilverStripe\\Security\\Permission', 'hidden_permissions');
192
        if ($this->source) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->source of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
193
            $privilegedPermissions = Permission::config()->privileged_permissions;
0 ignored issues
show
Bug Best Practice introduced by
The property privileged_permissions does not exist on SilverStripe\Core\Config\Config_ForClass. Since you implemented __get, consider adding a @property annotation.
Loading history...
194
195
            // loop through all available categorized permissions and see if they're assigned for the given groups
196
            foreach ($this->source as $categoryName => $permissions) {
197
                $options .= "<li><h5>$categoryName</h5></li>";
198
                foreach ($permissions as $code => $permission) {
199
                    if (in_array($code, $this->hiddenPermissions)) {
200
                        continue;
201
                    }
202
                    if (in_array($code, $globalHidden)) {
203
                        continue;
204
                    }
205
206
                    $value = $permission['name'];
207
208
                    $odd = ($odd + 1) % 2;
209
                    $extraClass = $odd ? 'odd' : 'even';
210
                    $extraClass .= ' val' . str_replace(' ', '', $code);
211
                    $itemID = $this->ID() . '_' . preg_replace('/[^a-zA-Z0-9]+/', '', $code);
212
                    $disabled = $inheritMessage = '';
213
                    $checked = (isset($uninheritedCodes[$code]) || isset($inheritedCodes[$code]))
214
                        ? ' checked="checked"'
215
                        : '';
216
                    $title = $permission['help']
217
                        ? 'title="' . htmlentities($permission['help'], ENT_COMPAT, 'UTF-8') . '" '
218
                        : '';
219
220
                    if (isset($inheritedCodes[$code])) {
221
                        // disable inherited codes, as any saving logic would be too complicate to express in this
222
                        // interface
223
                        $disabled = ' disabled="true"';
224
                        $inheritMessage = ' (' . join(', ', $inheritedCodes[$code]) . ')';
225
                    } elseif ($this->records && $this->records->Count() > 1 && isset($uninheritedCodes[$code])) {
226
                        // If code assignments are collected from more than one "source group",
227
                        // show its origin automatically
228
                        $inheritMessage = ' (' . join(', ', $uninheritedCodes[$code]).')';
229
                    }
230
231
                    // Disallow modification of "privileged" permissions unless currently logged-in user is an admin
232
                    if (!Permission::check('ADMIN') && in_array($code, $privilegedPermissions)) {
233
                        $disabled = ' disabled="true"';
234
                    }
235
236
                    // If the field is readonly, always mark as "disabled"
237
                    if ($this->readonly) {
238
                        $disabled = ' disabled="true"';
239
                    }
240
241
                    $inheritMessage = '<small>' . $inheritMessage . '</small>';
242
243
                    // If the field is readonly, add a span that will replace the disabled checkbox input
244
                    if ($this->readonly) {
245
                        $icon = ($checked) ? 'check-mark-circle' : 'cancel-circled';
246
                        $record = is_object($this->form) ? $this->form->getRecord() : false;
247
                        // Inherited codes are shown as a gray x
248
                        if ($record && $record instanceof Member &&
249
                            Permission::checkMember($record, 'ADMIN') && $code != 'ADMIN') {
250
                            $icon = 'plus-circled';
251
                        }
252
253
                        $options .= "<li class=\"$extraClass\">"
254
                            . "<input id=\"$itemID\"$disabled name=\"$this->name[$code]\" type=\"checkbox\""
255
                            . " value=\"$code\"$checked class=\"checkbox\" />"
256
                            . "<label {$title}for=\"$itemID\">"
257
                            . "<span class=\"font-icon-$icon\"></span>"
258
                            . "{$value}{$inheritMessage}</label>"
259
                            . "</li>\n";
260
                    } else {
261
                        $options .= "<li class=\"$extraClass\">"
262
                            . "<input id=\"$itemID\"$disabled name=\"$this->name[$code]\" type=\"checkbox\""
263
                            . " value=\"$code\"$checked class=\"checkbox\" />"
264
                            . "<label {$title}for=\"$itemID\">{$value}{$inheritMessage}</label>"
265
                            . "</li>\n";
266
                    }
267
                }
268
            }
269
        }
270
        if ($this->readonly) {
271
            return
272
                "<ul id=\"{$this->ID()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" .
273
                "<li class=\"help\">" .
274
                _t(
275
                    'SilverStripe\\Security\\Permission.UserPermissionsIntro',
276
                    'Assigning groups to this user will adjust the permissions they have.'
277
                    . ' See the groups section for details of permissions on individual groups.'
278
                ) .
279
                "</li>" .
280
                $options .
281
                "</ul>\n";
282
        } else {
283
            return
284
                "<ul id=\"{$this->ID()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" .
285
                $options .
286
                "</ul>\n";
287
        }
288
    }
289
290
    /**
291
     * Update the permission set associated with $record DataObject
292
     *
293
     * @param DataObjectInterface $record
294
     */
295
    public function saveInto(DataObjectInterface $record)
296
    {
297
        $fieldname = $this->name;
298
        $managedClass = $this->managedClass;
299
300
        // Remove all "privileged" permissions if the currently logged-in user is not an admin
301
        $privilegedPermissions = Permission::config()->privileged_permissions;
0 ignored issues
show
Bug Best Practice introduced by
The property privileged_permissions does not exist on SilverStripe\Core\Config\Config_ForClass. Since you implemented __get, consider adding a @property annotation.
Loading history...
302
        if ((is_array($this->value) || $this->value instanceof Traversable)
303
            && !Permission::check('ADMIN')
304
        ) {
305
            foreach ($this->value as $id => $bool) {
306
                if (in_array($id, $privilegedPermissions)) {
307
                    unset($this->value[$id]);
308
                }
309
            }
310
        }
311
312
        // remove all permissions and re-add them afterwards
313
        $permissions = $record->$fieldname();
314
        foreach ($permissions as $permission) {
315
            $permission->delete();
316
        }
317
318
        $schema = DataObject::getSchema();
319
        if ($fieldname && $record && (
320
            $schema->hasManyComponent(get_class($record), $fieldname)
0 ignored issues
show
Bug Best Practice introduced by
The expression $schema->hasManyComponen...s($record), $fieldname) of type null|string is loosely compared to true; this is ambiguous if the string can be empty. 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 string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
321
            || $schema->manyManyComponent(get_class($record), $fieldname)
322
        )) {
323
            if (!$record->ID) {
324
                $record->write(); // We need a record ID to write permissions
325
            }
326
327
            if (is_array($this->value) || $this->value instanceof Traversable) {
328
                foreach ($this->value as $id => $bool) {
329
                    if ($bool) {
330
                        $perm = new $managedClass();
331
                        $perm->{$this->filterField} = $record->ID;
332
                        $perm->Code = $id;
333
                        $perm->write();
334
                    }
335
                }
336
            }
337
        }
338
    }
339
340
    /**
341
     * @return PermissionCheckboxSetField_Readonly
342
     */
343
    public function performReadonlyTransformation()
344
    {
345
        $readonly = new PermissionCheckboxSetField_Readonly(
346
            $this->name,
347
            $this->title,
348
            $this->managedClass,
349
            $this->filterField,
350
            $this->records
351
        );
352
353
        return $readonly;
354
    }
355
}
356