Completed
Pull Request — master (#6927)
by Damian
11:37 queued 03:26
created

PermissionCheckboxSetField::__construct()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 4
nop 5
dl 0
loc 20
rs 9.2
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Security;
4
5
use SilverStripe\Core\Config\Config;
6
use SilverStripe\Forms\FormField;
7
use SilverStripe\ORM\DataObject;
8
use SilverStripe\ORM\SS_List;
9
use SilverStripe\ORM\ArrayList;
10
use SilverStripe\ORM\DataObjectInterface;
11
use SilverStripe\View\Requirements;
12
use InvalidArgumentException;
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
Documentation introduced by
The property filterField does not exist on object<SilverStripe\Secu...issionCheckboxSetField>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
54
        $this->managedClass = $managedClass;
0 ignored issues
show
Documentation introduced by
The property managedClass does not exist on object<SilverStripe\Secu...issionCheckboxSetField>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

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);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface SilverStripe\ORM\SS_List as the method push() does only exist in the following implementations of said interface: SilverStripe\Forms\FieldList, SilverStripe\ORM\ArrayList, SilverStripe\ORM\UnsavedRelationList.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
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) {
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();
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 "%s" on group "%s"',
161
                                        'A permission inherited from a role on a certain group',
162
                                        array('roletitle' => $role->dbObject('Title')->forTemplate(), 'grouptitle' => $parent->dbObject('Title')->forTemplate())
163
                                    );
164
                                }
165
                            }
166
                        }
167
                        if ($parent->Permissions()->Count()) {
168
                            foreach ($parent->Permissions() as $permission) {
169
                                if (!isset($inheritedCodes[$permission->Code])) {
170
                                    $inheritedCodes[$permission->Code] = array();
171
                                }
172
                                $inheritedCodes[$permission->Code][] =
173
                                _t(
174
                                    'SilverStripe\\Security\\PermissionCheckboxSetField.FromGroup',
175
                                    'inherited from group "{title}"',
176
                                    'A permission inherited from a certain group',
177
                                    array('title' => $parent->dbObject('Title')->forTemplate())
178
                                );
179
                            }
180
                        }
181
                    }
182
                }
183
            }
184
        }
185
186
        $odd = 0;
187
        $options = '';
188
        $globalHidden = (array)Config::inst()->get('SilverStripe\\Security\\Permission', 'hidden_permissions');
189
        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...
190
            $privilegedPermissions = Permission::config()->privileged_permissions;
0 ignored issues
show
Documentation introduced by
The property privileged_permissions does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
191
192
            // loop through all available categorized permissions and see if they're assigned for the given groups
193
            foreach ($this->source as $categoryName => $permissions) {
194
                $options .= "<li><h5>$categoryName</h5></li>";
195
                foreach ($permissions as $code => $permission) {
196
                    if (in_array($code, $this->hiddenPermissions)) {
197
                        continue;
198
                    }
199
                    if (in_array($code, $globalHidden)) {
200
                        continue;
201
                    }
202
203
                    $value = $permission['name'];
204
205
                    $odd = ($odd + 1) % 2;
206
                    $extraClass = $odd ? 'odd' : 'even';
207
                    $extraClass .= ' val' . str_replace(' ', '', $code);
208
                    $itemID = $this->ID() . '_' . preg_replace('/[^a-zA-Z0-9]+/', '', $code);
209
                    $disabled = $inheritMessage = '';
210
                    $checked = (isset($uninheritedCodes[$code]) || isset($inheritedCodes[$code]))
211
                        ? ' checked="checked"'
212
                        : '';
213
                    $title = $permission['help']
214
                        ? 'title="' . htmlentities($permission['help'], ENT_COMPAT, 'UTF-8') . '" '
215
                        : '';
216
217
                    if (isset($inheritedCodes[$code])) {
218
                        // disable inherited codes, as any saving logic would be too complicate to express in this
219
                        // interface
220
                        $disabled = ' disabled="true"';
221
                        $inheritMessage = ' (' . join(', ', $inheritedCodes[$code]) . ')';
222
                    } elseif ($this->records && $this->records->Count() > 1 && isset($uninheritedCodes[$code])) {
223
                        // If code assignments are collected from more than one "source group",
224
                        // show its origin automatically
225
                        $inheritMessage = ' (' . join(', ', $uninheritedCodes[$code]).')';
226
                    }
227
228
                    // Disallow modification of "privileged" permissions unless currently logged-in user is an admin
229
                    if (!Permission::check('ADMIN') && in_array($code, $privilegedPermissions)) {
230
                        $disabled = ' disabled="true"';
231
                    }
232
233
                    // If the field is readonly, always mark as "disabled"
234
                    if ($this->readonly) {
235
                        $disabled = ' disabled="true"';
236
                    }
237
238
                    $inheritMessage = '<small>' . $inheritMessage . '</small>';
239
                    $icon = ($checked) ? 'check-mark-circle' : 'cancel-circled';
240
241
                    // Inherited codes are shown as a gray x
242
                    if (Permission::check('ADMIN') && $code != 'ADMIN') {
243
                        $icon = 'disable-circled';
244
                    }
245
246
                    // If the field is readonly, add a span that will replace the disabled checkbox input
247
                    if ($this->readonly) {
248
                        $options .= "<li class=\"$extraClass\">"
249
                            . "<input id=\"$itemID\"$disabled name=\"$this->name[$code]\" type=\"checkbox\""
250
                            . " value=\"$code\"$checked class=\"checkbox\" />"
251
                            . "<label {$title}for=\"$itemID\">"
252
                            . "<span class=\"font-icon-$icon\"></span>"
253
                            . "$value$inheritMessage</label>"
254
                            . "</li>\n";
255
                    } else {
256
                        $options .= "<li class=\"$extraClass\">"
257
                            . "<input id=\"$itemID\"$disabled name=\"$this->name[$code]\" type=\"checkbox\""
258
                            . " value=\"$code\"$checked class=\"checkbox\" />"
259
                            . "<label {$title}for=\"$itemID\">$value$inheritMessage</label>"
260
                            . "</li>\n";
261
                    }
262
                }
263
            }
264
        }
265
        if ($this->readonly) {
266
            return
267
                "<ul id=\"{$this->ID()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" .
268
                "<li class=\"help\">" .
269
                _t(
270
                    'SilverStripe\\Security\\Permission.UserPermissionsIntro',
271
                    'Assigning groups to this user will adjust the permissions they have.'
272
                    . ' See the groups section for details of permissions on individual groups.'
273
                ) .
274
                "</li>" .
275
                $options .
276
                "</ul>\n";
277
        } else {
278
            return
279
                "<ul id=\"{$this->ID()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" .
280
                $options .
281
                "</ul>\n";
282
        }
283
    }
284
285
    /**
286
     * Update the permission set associated with $record DataObject
287
     *
288
     * @param DataObjectInterface $record
289
     */
290
    public function saveInto(DataObjectInterface $record)
291
    {
292
        $fieldname = $this->name;
293
        $managedClass = $this->managedClass;
0 ignored issues
show
Documentation introduced by
The property managedClass does not exist on object<SilverStripe\Secu...issionCheckboxSetField>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
294
295
        // Remove all "privileged" permissions if the currently logged-in user is not an admin
296
        $privilegedPermissions = Permission::config()->privileged_permissions;
0 ignored issues
show
Documentation introduced by
The property privileged_permissions does not exist on object<SilverStripe\Core\Config\Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
297
        if (!Permission::check('ADMIN')) {
298
            foreach ($this->value as $id => $bool) {
299
                if (in_array($id, $privilegedPermissions)) {
300
                    unset($this->value[$id]);
301
                }
302
            }
303
        }
304
305
        // remove all permissions and re-add them afterwards
306
        $permissions = $record->$fieldname();
307
        foreach ($permissions as $permission) {
308
            $permission->delete();
309
        }
310
311
        $schema = DataObject::getSchema();
312
        if ($fieldname && $record && (
313
            $schema->hasManyComponent(get_class($record), $fieldname)
0 ignored issues
show
Bug Best Practice introduced by
The expression $schema->hasManyComponen...s($record), $fieldname) of type string|null 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...
314
            || $schema->manyManyComponent(get_class($record), $fieldname)
315
        )) {
316
            if (!$record->ID) {
0 ignored issues
show
Bug introduced by
Accessing ID on the interface SilverStripe\ORM\DataObjectInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
317
                $record->write(); // We need a record ID to write permissions
318
            }
319
320
            if ($this->value) {
321
                foreach ($this->value as $id => $bool) {
322
                    if ($bool) {
323
                        $perm = new $managedClass();
324
                        $perm->{$this->filterField} = $record->ID;
0 ignored issues
show
Bug introduced by
Accessing ID on the interface SilverStripe\ORM\DataObjectInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
325
                        $perm->Code = $id;
326
                        $perm->write();
327
                    }
328
                }
329
            }
330
        }
331
    }
332
333
    /**
334
     * @return PermissionCheckboxSetField_Readonly
335
     */
336
    public function performReadonlyTransformation()
337
    {
338
        $readonly = new PermissionCheckboxSetField_Readonly(
339
            $this->name,
340
            $this->title,
341
            $this->managedClass,
342
            $this->filterField,
343
            $this->records
344
        );
345
346
        return $readonly;
347
    }
348
}
349