silverstripe /
silverstripe-framework
| 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
Loading history...
|
|||||||
| 54 | $this->managedClass = $managedClass; |
||||||
|
0 ignored issues
–
show
|
|||||||
| 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
|
|||||||
| 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
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
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
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
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
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
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
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 Loading history...
|
|||||||
| 193 | $privilegedPermissions = Permission::config()->privileged_permissions; |
||||||
|
0 ignored issues
–
show
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
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
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 |