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; |
|
|
|
|
54
|
|
|
$this->managedClass = $managedClass; |
|
|
|
|
55
|
|
|
|
56
|
|
|
if ($records instanceof SS_List) { |
57
|
|
|
$this->records = $records; |
58
|
|
|
} elseif ($records instanceof Group) { |
59
|
|
|
$this->records = new ArrayList(array($records)); |
60
|
|
|
} elseif ($records) { |
|
|
|
|
61
|
|
|
throw new InvalidArgumentException( |
62
|
|
|
'$record should be either a Group record, or a SS_List of Group records' |
63
|
|
|
); |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
// Get all available codes in the system as a categorized nested array |
67
|
|
|
$this->source = Permission::get_codes(true); |
68
|
|
|
|
69
|
|
|
parent::__construct($name, $title); |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* @param array $codes |
74
|
|
|
*/ |
75
|
|
|
public function setHiddenPermissions($codes) |
76
|
|
|
{ |
77
|
|
|
$this->hiddenPermissions = $codes; |
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* @return array |
82
|
|
|
*/ |
83
|
|
|
public function getHiddenPermissions() |
84
|
|
|
{ |
85
|
|
|
return $this->hiddenPermissions; |
86
|
|
|
} |
87
|
|
|
|
88
|
|
|
/** |
89
|
|
|
* @param array $properties |
90
|
|
|
* @return string |
91
|
|
|
*/ |
92
|
|
|
public function Field($properties = array()) |
93
|
|
|
{ |
94
|
|
|
$uninheritedCodes = array(); |
95
|
|
|
$inheritedCodes = array(); |
96
|
|
|
$records = ($this->records) ? $this->records : new ArrayList(); |
97
|
|
|
|
98
|
|
|
// Get existing values from the form record (assuming the formfield name is a join field on the record) |
99
|
|
|
if (is_object($this->form)) { |
100
|
|
|
$record = $this->form->getRecord(); |
101
|
|
|
if ($record |
102
|
|
|
&& ($record instanceof Group || $record instanceof PermissionRole) |
103
|
|
|
&& !$records->find('ID', $record->ID) |
104
|
|
|
) { |
105
|
|
|
$records->push($record); |
|
|
|
|
106
|
|
|
} |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
// Get all 'inherited' codes not directly assigned to the group (which is stored in $values) |
110
|
|
|
foreach ($records as $record) { |
111
|
|
|
// Get all uninherited permissions |
112
|
|
|
$relationMethod = $this->name; |
113
|
|
|
foreach ($record->$relationMethod() as $permission) { |
114
|
|
|
if (!isset($uninheritedCodes[$permission->Code])) { |
115
|
|
|
$uninheritedCodes[$permission->Code] = array(); |
116
|
|
|
} |
117
|
|
|
$uninheritedCodes[$permission->Code][] = _t( |
118
|
|
|
'SilverStripe\\Security\\PermissionCheckboxSetField.AssignedTo', |
119
|
|
|
'assigned to "{title}"', |
120
|
|
|
array('title' => $record->dbObject('Title')->forTemplate()) |
121
|
|
|
); |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
// Special case for Group records (not PermissionRole): |
125
|
|
|
// Determine inherited assignments |
126
|
|
|
if ($record instanceof Group) { |
127
|
|
|
// Get all permissions from roles |
128
|
|
|
if ($record->Roles()->count()) { |
129
|
|
|
foreach ($record->Roles() as $role) { |
130
|
|
|
/** @var PermissionRole $role */ |
131
|
|
|
foreach ($role->Codes() as $code) { |
|
|
|
|
132
|
|
|
if (!isset($inheritedCodes[$code->Code])) { |
133
|
|
|
$inheritedCodes[$code->Code] = array(); |
134
|
|
|
} |
135
|
|
|
$inheritedCodes[$code->Code][] = _t( |
136
|
|
|
'SilverStripe\\Security\\PermissionCheckboxSetField.FromRole', |
137
|
|
|
'inherited from role "{title}"', |
138
|
|
|
'A permission inherited from a certain permission role', |
139
|
|
|
array('title' => $role->dbObject('Title')->forTemplate()) |
140
|
|
|
); |
141
|
|
|
} |
142
|
|
|
} |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
// Get from parent groups |
146
|
|
|
$parentGroups = $record->getAncestors(); |
|
|
|
|
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) { |
|
|
|
|
193
|
|
|
$privilegedPermissions = Permission::config()->privileged_permissions; |
|
|
|
|
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; |
|
|
|
|
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) { |
|
|
|
|
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
|
|
|
|