PermissionCheckboxSetField::Field()   F
last analyzed

Complexity

Conditions 49
Paths 3408

Size

Total Lines 195
Code Lines 124

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 49
eloc 124
nc 3408
nop 1
dl 0
loc 195
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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) {
0 ignored issues
show
introduced by
$records is of type null, thus it always evaluated to false.
Loading history...
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);
0 ignored issues
show
Bug introduced by
The method push() does not exist on SilverStripe\ORM\SS_List. It seems like you code against a sub-type of said class. However, the method does not exist in SilverStripe\ORM\Sortable or SilverStripe\ORM\Filterable or SilverStripe\ORM\Relation or SilverStripe\ORM\Limitable or SilverStripe\ORM\Relation or SilverStripe\ORM\Relation or SilverStripe\ORM\Relation. Are you sure you never get one of those? ( Ignorable by Annotation )

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

105
                $records->/** @scrutinizer ignore-call */ 
106
                          push($record);
Loading history...
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) {
0 ignored issues
show
Bug introduced by
The method Codes() 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

131
                        foreach ($role->/** @scrutinizer ignore-call */ Codes() as $code) {
Loading history...
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
            $message = _t(
272
                'SilverStripe\\Security\\Permission.UserPermissionsIntro',
273
                'Assigning groups to this user will adjust the permissions they have.'
274
                . ' See the groups section for details of permissions on individual groups.'
275
            );
276
277
            return
278
                "<ul id=\"{$this->ID()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" .
279
                "<li class=\"help\">" . $message . "</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)
321
            || $schema->manyManyComponent(get_class($record), $fieldname)
322
        )) {
323
            if (!$record->ID) {
0 ignored issues
show
Bug Best Practice introduced by
The property ID does not exist on SilverStripe\ORM\DataObjectInterface. Since you implemented __get, consider adding a @property annotation.
Loading history...
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