Completed
Pull Request — master (#5741)
by Damian
12:40
created

PermissionCheckboxSetField::getHiddenPermissions()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
namespace SilverStripe\Security;
4
5
6
7
use SilverStripe\ORM\FieldType\DBHTMLText;
8
use SilverStripe\ORM\SS_List;
9
use SilverStripe\ORM\ArrayList;
10
use SilverStripe\ORM\FieldType\DBField;
11
use SilverStripe\ORM\DataObjectInterface;
12
use FormField;
13
use InvalidArgumentException;
14
use Requirements;
15
use Config;
16
17
18
19
/**
20
 * Shows a categorized list of available permissions (through {@link Permission::get_codes()}).
21
 * Permissions which are assigned to a given {@link Group} record
22
 * (either directly, inherited from parent groups, or through a {@link PermissionRole})
23
 * will be checked automatically. All checkboxes for "inherited" permissions will be readonly.
24
 *
25
 * The field can gets its assignment data either from {@link Group} or {@link PermissionRole} records.
26
 *
27
 * @package framework
28
 * @subpackage security
29
 */
30
class PermissionCheckboxSetField extends FormField {
31
32
	/**
33
	 * @var array Filter certain permission codes from the output.
34
	 * Useful to simplify the interface
35
	 */
36
	protected $hiddenPermissions = array();
37
38
	/**
39
	 * @var SS_List
40
	 */
41
	protected $records = null;
42
43
	/**
44
	 * @var array Array Nested array in same notation as {@link CheckboxSetField}.
45
	 */
46
	protected $source = null;
47
48
	/**
49
	 * @param String $name
50
	 * @param String $title
51
	 * @param String $managedClass
52
	 * @param String $filterField
53
	 * @param Group|SS_List $records One or more {@link Group} or {@link PermissionRole} records
54
	 *  used to determine permission checkboxes.
55
	 *  Caution: saveInto() can only be used with a single record, all inherited permissions will be marked readonly.
56
	 *  Setting multiple groups only makes sense in a readonly context. (Optional)
57
	 */
58
	public function __construct($name, $title, $managedClass, $filterField, $records = null) {
59
		$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...
60
		$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...
61
62
		if($records instanceof SS_List) {
63
			$this->records = $records;
64
		} elseif($records instanceof Group) {
65
			$this->records = new ArrayList(array($records));
66
		} elseif($records) {
67
			throw new InvalidArgumentException(
68
				'$record should be either a Group record, or a SS_List of Group records');
69
		}
70
71
		// Get all available codes in the system as a categorized nested array
72
		$this->source = Permission::get_codes(true);
73
74
		parent::__construct($name, $title);
75
	}
76
77
	/**
78
	 * @param array $codes
79
	 */
80
	public function setHiddenPermissions($codes) {
81
		$this->hiddenPermissions = $codes;
82
	}
83
84
	/**
85
	 * @return array
86
	 */
87
	public function getHiddenPermissions() {
88
		return $this->hiddenPermissions;
89
	}
90
91
	/**
92
	 * @param array $properties
93
	 * @return DBHTMLText
94
	 */
95
	public function Field($properties = array()) {
96
		Requirements::css(FRAMEWORK_DIR . '/client/dist/styles/CheckboxSetField.css');
97
		Requirements::javascript(FRAMEWORK_DIR . '/client/dist/js/PermissionCheckboxSetField.js');
98
99
		$uninheritedCodes = array();
100
		$inheritedCodes = array();
101
		$records = ($this->records) ? $this->records : new ArrayList();
102
103
		// Get existing values from the form record (assuming the formfield name is a join field on the record)
104
		if(is_object($this->form)) {
105
			$record = $this->form->getRecord();
106
			if(
107
				$record
108
				&& ($record instanceof Group || $record instanceof PermissionRole)
109
				&& !$records->find('ID', $record->ID)
110
			) {
111
				$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: 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...
112
			}
113
		}
114
115
		// Get all 'inherited' codes not directly assigned to the group (which is stored in $values)
116
		foreach($records as $record) {
117
			// Get all uninherited permissions
118
			$relationMethod = $this->name;
119
			foreach($record->$relationMethod() as $permission) {
120
				if(!isset($uninheritedCodes[$permission->Code])) $uninheritedCodes[$permission->Code] = array();
121
				$uninheritedCodes[$permission->Code][] = _t(
122
					'PermissionCheckboxSetField.AssignedTo', 'assigned to "{title}"',
123
					array('title' => $record->dbObject('Title')->forTemplate())
124
				);
125
			}
126
127
			// Special case for Group records (not PermissionRole):
128
			// Determine inherited assignments
129
			if(is_a($record, 'SilverStripe\\Security\\Group')) {
130
				// Get all permissions from roles
131
				if ($record->Roles()->Count()) {
132
					foreach($record->Roles() as $role) {
133
						foreach($role->Codes() as $code) {
134
							if (!isset($inheritedCodes[$code->Code])) $inheritedCodes[$code->Code] = array();
135
							$inheritedCodes[$code->Code][] = _t(
136
								'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()) continue;
150
						foreach($parent->Roles() as $role) {
151
							if ($role->Codes()) {
152
								foreach($role->Codes() as $code) {
153
									if (!isset($inheritedCodes[$code->Code])) $inheritedCodes[$code->Code] = array();
154
									$inheritedCodes[$code->Code][] = _t(
155
										'PermissionCheckboxSetField.FromRoleOnGroup',
156
										'inherited from role "%s" on group "%s"',
157
										'A permission inherited from a role on a certain group',
158
										array('roletitle' => $role->dbObject('Title')->forTemplate(), 'grouptitle' => $parent->dbObject('Title')->forTemplate())
159
									);
160
								}
161
							}
162
						}
163
						if ($parent->Permissions()->Count()) {
164
							foreach($parent->Permissions() as $permission) {
165
								if (!isset($inheritedCodes[$permission->Code])) {
166
									$inheritedCodes[$permission->Code] = array();
167
								}
168
								$inheritedCodes[$permission->Code][] =
169
								_t(
170
									'PermissionCheckboxSetField.FromGroup',
171
									'inherited from group "{title}"',
172
									'A permission inherited from a certain group',
173
									array('title' => $parent->dbObject('Title')->forTemplate())
174
								);
175
							}
176
						}
177
					}
178
				}
179
			}
180
		}
181
182
		$odd = 0;
183
		$options = '';
184
		$globalHidden = (array)Config::inst()->get('SilverStripe\\Security\\Permission', 'hidden_permissions');
185
		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...
186
			$privilegedPermissions = Permission::config()->privileged_permissions;
0 ignored issues
show
Documentation introduced by
The property privileged_permissions does not exist on object<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...
187
188
			// loop through all available categorized permissions and see if they're assigned for the given groups
189
			foreach($this->source as $categoryName => $permissions) {
190
				$options .= "<li><h5>$categoryName</h5></li>";
191
				foreach($permissions as $code => $permission) {
192
					if(in_array($code, $this->hiddenPermissions)) continue;
193
					if(in_array($code, $globalHidden)) continue;
194
195
					$value = $permission['name'];
196
197
					$odd = ($odd + 1) % 2;
198
					$extraClass = $odd ? 'odd' : 'even';
199
					$extraClass .= ' val' . str_replace(' ', '', $code);
200
					$itemID = $this->ID() . '_' . preg_replace('/[^a-zA-Z0-9]+/', '', $code);
201
					$checked = $disabled = $inheritMessage = '';
202
					$checked = (isset($uninheritedCodes[$code]) || isset($inheritedCodes[$code]))
203
						? ' checked="checked"'
204
						: '';
205
					$title = $permission['help']
206
						? 'title="' . htmlentities($permission['help'], ENT_COMPAT, 'UTF-8') . '" '
207
						: '';
208
209
					if (isset($inheritedCodes[$code])) {
210
						// disable inherited codes, as any saving logic would be too complicate to express in this
211
						// interface
212
						$disabled = ' disabled="true"';
213
						$inheritMessage = ' (' . join(', ', $inheritedCodes[$code]) . ')';
214
					} elseif($this->records && $this->records->Count() > 1 && isset($uninheritedCodes[$code])) {
215
						// If code assignments are collected from more than one "source group",
216
						// show its origin automatically
217
						$inheritMessage = ' (' . join(', ', $uninheritedCodes[$code]).')';
218
					}
219
220
					// Disallow modification of "privileged" permissions unless currently logged-in user is an admin
221
					if(!Permission::check('ADMIN') && in_array($code, $privilegedPermissions)) {
222
						$disabled = ' disabled="true"';
223
					}
224
225
					// If the field is readonly, always mark as "disabled"
226
					if($this->readonly) $disabled = ' disabled="true"';
227
228
					$inheritMessage = '<small>' . $inheritMessage . '</small>';
229
					$icon = ($checked) ? 'accept' : 'decline';
230
231
					// If the field is readonly, add a span that will replace the disabled checkbox input
232
					if($this->readonly) {
233
						$options .= "<li class=\"$extraClass\">"
234
							. "<input id=\"$itemID\"$disabled name=\"$this->name[$code]\" type=\"checkbox\""
235
							. " value=\"$code\"$checked class=\"checkbox\" />"
236
							. "<label {$title}for=\"$itemID\">"
237
							. "<span class=\"ui-button-icon-primary ui-icon btn-icon-$icon\"></span>"
238
							. "$value$inheritMessage</label>"
239
							. "</li>\n";
240
					} else {
241
						$options .= "<li class=\"$extraClass\">"
242
							. "<input id=\"$itemID\"$disabled name=\"$this->name[$code]\" type=\"checkbox\""
243
							. " value=\"$code\"$checked class=\"checkbox\" />"
244
							. "<label {$title}for=\"$itemID\">$value$inheritMessage</label>"
245
							. "</li>\n";
246
					}
247
				}
248
			}
249
		}
250
		if($this->readonly) {
251
			return DBField::create_field('HTMLText',
252
				"<ul id=\"{$this->ID()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" .
253
				"<li class=\"help\">" .
254
				_t(
255
					'Permissions.UserPermissionsIntro',
256
					'Assigning groups to this user will adjust the permissions they have.'
257
					. ' See the groups section for details of permissions on individual groups.'
258
				) .
259
				"</li>" .
260
				$options .
261
				"</ul>\n"
262
			);
263
		} else {
264
			return DBField::create_field('HTMLText',
265
			    "<ul id=\"{$this->ID()}\" class=\"optionset checkboxsetfield{$this->extraClass()}\">\n" .
266
				$options .
267
				"</ul>\n"
268
			);
269
		}
270
	}
271
272
	/**
273
	 * Update the permission set associated with $record DataObject
274
	 *
275
	 * @param DataObjectInterface $record
276
	 */
277
	public function saveInto(DataObjectInterface $record) {
278
		$fieldname = $this->name;
279
		$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...
280
281
		// Remove all "privileged" permissions if the currently logged-in user is not an admin
282
		$privilegedPermissions = Permission::config()->privileged_permissions;
0 ignored issues
show
Documentation introduced by
The property privileged_permissions does not exist on object<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...
283
		if(!Permission::check('ADMIN')) {
284
			foreach($this->value as $id => $bool) {
285
				if(in_array($id, $privilegedPermissions)) {
286
					unset($this->value[$id]);
287
				}
288
			}
289
		}
290
291
		// remove all permissions and re-add them afterwards
292
		$permissions = $record->$fieldname();
293
		foreach ( $permissions as $permission ) {
294
			$permission->delete();
295
		}
296
297
		if($fieldname && $record && ($record->hasManyComponent($fieldname) || $record->manyManyComponent($fieldname))) {
298
299
			if(!$record->ID) $record->write(); // We need a record ID to write permissions
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...
300
301
			$idList = array();
302
			if($this->value) foreach($this->value as $id => $bool) {
303
				if($bool) {
304
					$perm = new $managedClass();
305
					$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...
306
					$perm->Code = $id;
307
					$perm->write();
308
				}
309
			}
310
		}
311
	}
312
313
	/**
314
	 * @return PermissionCheckboxSetField_Readonly
315
	 */
316
	public function performReadonlyTransformation() {
317
		$readonly = new PermissionCheckboxSetField_Readonly(
318
			$this->name,
319
			$this->title,
320
			$this->managedClass,
321
			$this->filterField,
322
			$this->records
323
		);
324
325
		return $readonly;
326
	}
327
328
	/**
329
	 * Retrieves all permission codes for the currently set records
330
	 *
331
	 * @return array
332
	 */
333
	public function getAssignedPermissionCodes() {
334
		if(!$this->records) return false;
335
336
		// TODO
337
338
		return $codes;
0 ignored issues
show
Bug introduced by
The variable $codes does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
339
	}
340
}
341
342
/**
343
 * Readonly version of a {@link PermissionCheckboxSetField} -
344
 * uses the same structure, but has all checkboxes disabled.
345
 *
346
 * @package framework
347
 * @subpackage security
348
 */
349
class PermissionCheckboxSetField_Readonly extends PermissionCheckboxSetField {
350
351
	protected $readonly = true;
352
353
	public function saveInto(DataObjectInterface $record) {
354
		return false;
355
	}
356
}
357