1 | <?php |
||||||
2 | |||||||
3 | namespace SilverStripe\Security; |
||||||
4 | |||||||
5 | use InvalidArgumentException; |
||||||
6 | use SilverStripe\Core\Injector\Injectable; |
||||||
7 | use SilverStripe\ORM\DataList; |
||||||
8 | use SilverStripe\ORM\DataObject; |
||||||
9 | use SilverStripe\ORM\Hierarchy\Hierarchy; |
||||||
10 | use SilverStripe\Versioned\Versioned; |
||||||
11 | use Psr\SimpleCache\CacheInterface; |
||||||
12 | use SilverStripe\Core\Cache\MemberCacheFlusher; |
||||||
13 | |||||||
14 | /** |
||||||
15 | * Calculates batch permissions for nested objects for: |
||||||
16 | * - canView: Supports 'Anyone' type |
||||||
17 | * - canEdit |
||||||
18 | * - canDelete: Includes special logic for ensuring parent objects can only be deleted if their children can |
||||||
19 | * be deleted also. |
||||||
20 | */ |
||||||
21 | class InheritedPermissions implements PermissionChecker, MemberCacheFlusher |
||||||
22 | { |
||||||
23 | use Injectable; |
||||||
24 | |||||||
25 | /** |
||||||
26 | * Delete permission |
||||||
27 | */ |
||||||
28 | const DELETE = 'delete'; |
||||||
29 | |||||||
30 | /** |
||||||
31 | * View permission |
||||||
32 | */ |
||||||
33 | const VIEW = 'view'; |
||||||
34 | |||||||
35 | /** |
||||||
36 | * Edit permission |
||||||
37 | */ |
||||||
38 | const EDIT = 'edit'; |
||||||
39 | |||||||
40 | /** |
||||||
41 | * Anyone canView permission |
||||||
42 | */ |
||||||
43 | const ANYONE = 'Anyone'; |
||||||
44 | |||||||
45 | /** |
||||||
46 | * Restrict to logged in users |
||||||
47 | */ |
||||||
48 | const LOGGED_IN_USERS = 'LoggedInUsers'; |
||||||
49 | |||||||
50 | /** |
||||||
51 | * Restrict to specific groups |
||||||
52 | */ |
||||||
53 | const ONLY_THESE_USERS = 'OnlyTheseUsers'; |
||||||
54 | |||||||
55 | /** |
||||||
56 | * Inherit from parent |
||||||
57 | */ |
||||||
58 | const INHERIT = 'Inherit'; |
||||||
59 | |||||||
60 | /** |
||||||
61 | * Class name |
||||||
62 | * |
||||||
63 | * @var string |
||||||
64 | */ |
||||||
65 | protected $baseClass = null; |
||||||
66 | |||||||
67 | /** |
||||||
68 | * Object for evaluating top level permissions designed as "Inherit" |
||||||
69 | * |
||||||
70 | * @var DefaultPermissionChecker |
||||||
71 | */ |
||||||
72 | protected $defaultPermissions = null; |
||||||
73 | |||||||
74 | /** |
||||||
75 | * Global permissions required to edit. |
||||||
76 | * If empty no global permissions are required |
||||||
77 | * |
||||||
78 | * @var array |
||||||
79 | */ |
||||||
80 | protected $globalEditPermissions = []; |
||||||
81 | |||||||
82 | /** |
||||||
83 | * Cache of permissions |
||||||
84 | * |
||||||
85 | * @var array |
||||||
86 | */ |
||||||
87 | protected $cachePermissions = []; |
||||||
88 | |||||||
89 | /** |
||||||
90 | * @var CacheInterface |
||||||
91 | */ |
||||||
92 | protected $cacheService; |
||||||
93 | |||||||
94 | /** |
||||||
95 | * Construct new permissions object |
||||||
96 | * |
||||||
97 | * @param string $baseClass Base class |
||||||
98 | * @param CacheInterface $cache |
||||||
99 | */ |
||||||
100 | public function __construct($baseClass, CacheInterface $cache = null) |
||||||
101 | { |
||||||
102 | if (!is_a($baseClass, DataObject::class, true)) { |
||||||
103 | throw new InvalidArgumentException('Invalid DataObject class: ' . $baseClass); |
||||||
104 | } |
||||||
105 | |||||||
106 | $this->baseClass = $baseClass; |
||||||
107 | $this->cacheService = $cache; |
||||||
108 | |||||||
109 | return $this; |
||||||
110 | } |
||||||
111 | |||||||
112 | /** |
||||||
113 | * Commits the cache |
||||||
114 | */ |
||||||
115 | public function __destruct() |
||||||
116 | { |
||||||
117 | // Ensure back-end cache is updated |
||||||
118 | if (!empty($this->cachePermissions) && $this->cacheService) { |
||||||
119 | foreach ($this->cachePermissions as $key => $permissions) { |
||||||
120 | $this->cacheService->set($key, $permissions); |
||||||
121 | } |
||||||
122 | // Prevent double-destruct |
||||||
123 | $this->cachePermissions = []; |
||||||
124 | } |
||||||
125 | } |
||||||
126 | |||||||
127 | /** |
||||||
128 | * Clear the cache for this instance only |
||||||
129 | * |
||||||
130 | * @param array $memberIDs A list of member IDs |
||||||
131 | */ |
||||||
132 | public function flushMemberCache($memberIDs = null) |
||||||
133 | { |
||||||
134 | if (!$this->cacheService) { |
||||||
135 | return; |
||||||
136 | } |
||||||
137 | |||||||
138 | // Hard flush, e.g. flush=1 |
||||||
139 | if (!$memberIDs) { |
||||||
140 | $this->cacheService->clear(); |
||||||
141 | } |
||||||
142 | |||||||
143 | if ($memberIDs && is_array($memberIDs)) { |
||||||
144 | foreach ([self::VIEW, self::EDIT, self::DELETE] as $type) { |
||||||
145 | foreach ($memberIDs as $memberID) { |
||||||
146 | $key = $this->generateCacheKey($type, $memberID); |
||||||
147 | $this->cacheService->delete($key); |
||||||
148 | } |
||||||
149 | } |
||||||
150 | } |
||||||
151 | } |
||||||
152 | |||||||
153 | /** |
||||||
154 | * @param DefaultPermissionChecker $callback |
||||||
155 | * @return $this |
||||||
156 | */ |
||||||
157 | public function setDefaultPermissions(DefaultPermissionChecker $callback) |
||||||
158 | { |
||||||
159 | $this->defaultPermissions = $callback; |
||||||
160 | return $this; |
||||||
161 | } |
||||||
162 | |||||||
163 | /** |
||||||
164 | * Global permissions required to edit |
||||||
165 | * |
||||||
166 | * @param array $permissions |
||||||
167 | * @return $this |
||||||
168 | */ |
||||||
169 | public function setGlobalEditPermissions($permissions) |
||||||
170 | { |
||||||
171 | $this->globalEditPermissions = $permissions; |
||||||
172 | return $this; |
||||||
173 | } |
||||||
174 | |||||||
175 | /** |
||||||
176 | * @return array |
||||||
177 | */ |
||||||
178 | public function getGlobalEditPermissions() |
||||||
179 | { |
||||||
180 | return $this->globalEditPermissions; |
||||||
181 | } |
||||||
182 | |||||||
183 | /** |
||||||
184 | * Get root permissions handler, or null if no handler |
||||||
185 | * |
||||||
186 | * @return DefaultPermissionChecker|null |
||||||
187 | */ |
||||||
188 | public function getDefaultPermissions() |
||||||
189 | { |
||||||
190 | return $this->defaultPermissions; |
||||||
191 | } |
||||||
192 | |||||||
193 | /** |
||||||
194 | * Get base class |
||||||
195 | * |
||||||
196 | * @return string |
||||||
197 | */ |
||||||
198 | public function getBaseClass() |
||||||
199 | { |
||||||
200 | return $this->baseClass; |
||||||
201 | } |
||||||
202 | |||||||
203 | /** |
||||||
204 | * Force pre-calculation of a list of permissions for optimisation |
||||||
205 | * |
||||||
206 | * @param string $permission |
||||||
207 | * @param array $ids |
||||||
208 | */ |
||||||
209 | public function prePopulatePermissionCache($permission = 'edit', $ids = []) |
||||||
210 | { |
||||||
211 | switch ($permission) { |
||||||
212 | case self::EDIT: |
||||||
213 | $this->canEditMultiple($ids, Security::getCurrentUser(), false); |
||||||
214 | break; |
||||||
215 | case self::VIEW: |
||||||
216 | $this->canViewMultiple($ids, Security::getCurrentUser(), false); |
||||||
217 | break; |
||||||
218 | case self::DELETE: |
||||||
219 | $this->canDeleteMultiple($ids, Security::getCurrentUser(), false); |
||||||
220 | break; |
||||||
221 | default: |
||||||
222 | throw new InvalidArgumentException("Invalid permission type $permission"); |
||||||
223 | } |
||||||
224 | } |
||||||
225 | |||||||
226 | /** |
||||||
227 | * This method is NOT a full replacement for the individual can*() methods, e.g. {@link canEdit()}. Rather than |
||||||
228 | * checking (potentially slow) PHP logic, it relies on the database group associations, e.g. the "CanEditType" field |
||||||
229 | * plus the "SiteTree_EditorGroups" many-many table. By batch checking multiple records, we can combine the queries |
||||||
230 | * efficiently. |
||||||
231 | * |
||||||
232 | * Caches based on $typeField data. To invalidate the cache, use {@link SiteTree::reset()} or set the $useCached |
||||||
233 | * property to FALSE. |
||||||
234 | * |
||||||
235 | * @param string $type Either edit, view, or create |
||||||
236 | * @param array $ids Array of IDs |
||||||
237 | * @param Member $member Member |
||||||
238 | * @param array $globalPermission If the member doesn't have this permission code, don't bother iterating deeper |
||||||
239 | * @param bool $useCached Enables use of cache. Cache will be populated even if this is false. |
||||||
240 | * @return array A map of permissions, keys are ID numbers, and values are boolean permission checks |
||||||
241 | * ID keys to boolean values |
||||||
242 | */ |
||||||
243 | protected function batchPermissionCheck( |
||||||
244 | $type, |
||||||
245 | $ids, |
||||||
246 | Member $member = null, |
||||||
247 | $globalPermission = [], |
||||||
248 | $useCached = true |
||||||
249 | ) { |
||||||
250 | // Validate ids |
||||||
251 | $ids = array_filter($ids, 'is_numeric'); |
||||||
252 | if (empty($ids)) { |
||||||
253 | return []; |
||||||
254 | } |
||||||
255 | |||||||
256 | // Default result: nothing editable |
||||||
257 | $result = array_fill_keys($ids, false); |
||||||
258 | |||||||
259 | // Validate member permission |
||||||
260 | // Only VIEW allows anonymous (Anyone) permissions |
||||||
261 | $memberID = $member ? (int)$member->ID : 0; |
||||||
262 | if (!$memberID && $type !== self::VIEW) { |
||||||
263 | return $result; |
||||||
264 | } |
||||||
265 | |||||||
266 | // Look in the cache for values |
||||||
267 | $cacheKey = $this->generateCacheKey($type, $memberID); |
||||||
268 | $cachePermissions = $this->getCachePermissions($cacheKey); |
||||||
269 | if ($useCached && $cachePermissions) { |
||||||
270 | $cachedValues = array_intersect_key($cachePermissions, $result); |
||||||
271 | |||||||
272 | // If we can't find everything in the cache, then look up the remainder separately |
||||||
273 | $uncachedIDs = array_keys(array_diff_key($result, $cachePermissions)); |
||||||
274 | if ($uncachedIDs) { |
||||||
0 ignored issues
–
show
|
|||||||
275 | $uncachedValues = $this->batchPermissionCheck($type, $uncachedIDs, $member, $globalPermission, false); |
||||||
276 | return $cachedValues + $uncachedValues; |
||||||
277 | } |
||||||
278 | return $cachedValues; |
||||||
279 | } |
||||||
280 | |||||||
281 | // If a member doesn't have a certain permission then they can't edit anything |
||||||
282 | if ($globalPermission && !Permission::checkMember($member, $globalPermission)) { |
||||||
0 ignored issues
–
show
The expression
$globalPermission 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 ![]() |
|||||||
283 | return $result; |
||||||
284 | } |
||||||
285 | |||||||
286 | // Get the groups that the given member belongs to |
||||||
287 | $groupIDsSQLList = '0'; |
||||||
288 | if ($memberID) { |
||||||
289 | $groupIDs = $member->Groups()->column("ID"); |
||||||
0 ignored issues
–
show
The method
Groups() does not exist on null .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() |
|||||||
290 | $groupIDsSQLList = implode(", ", $groupIDs) ?: '0'; |
||||||
291 | } |
||||||
292 | |||||||
293 | // Check if record is versioned |
||||||
294 | if ($this->isVersioned()) { |
||||||
295 | // Check all records for each stage and merge |
||||||
296 | $combinedStageResult = []; |
||||||
297 | foreach ([Versioned::DRAFT, Versioned::LIVE] as $stage) { |
||||||
298 | $stageRecords = Versioned::get_by_stage($this->getBaseClass(), $stage) |
||||||
299 | ->byIDs($ids); |
||||||
300 | // Exclude previously calculated records from later stage calculations |
||||||
301 | if ($combinedStageResult) { |
||||||
302 | $stageRecords = $stageRecords->exclude('ID', array_keys($combinedStageResult)); |
||||||
303 | } |
||||||
304 | $stageResult = $this->batchPermissionCheckForStage( |
||||||
305 | $type, |
||||||
306 | $globalPermission, |
||||||
307 | $stageRecords, |
||||||
308 | $groupIDsSQLList, |
||||||
309 | $member |
||||||
310 | ); |
||||||
311 | // Note: Draft stage takes precedence over live, but only if draft exists |
||||||
312 | $combinedStageResult = $combinedStageResult + $stageResult; |
||||||
313 | } |
||||||
314 | } else { |
||||||
315 | // Unstaged result |
||||||
316 | $stageRecords = DataObject::get($this->getBaseClass())->byIDs($ids); |
||||||
317 | $combinedStageResult = $this->batchPermissionCheckForStage( |
||||||
318 | $type, |
||||||
319 | $globalPermission, |
||||||
320 | $stageRecords, |
||||||
321 | $groupIDsSQLList, |
||||||
322 | $member |
||||||
323 | ); |
||||||
324 | } |
||||||
325 | |||||||
326 | // Cache the results |
||||||
327 | if (empty($this->cachePermissions[$cacheKey])) { |
||||||
328 | $this->cachePermissions[$cacheKey] = []; |
||||||
329 | } |
||||||
330 | if ($combinedStageResult) { |
||||||
331 | $this->cachePermissions[$cacheKey] = $combinedStageResult + $this->cachePermissions[$cacheKey]; |
||||||
332 | } |
||||||
333 | |||||||
334 | return $combinedStageResult; |
||||||
335 | } |
||||||
336 | |||||||
337 | /** |
||||||
338 | * @param string $type |
||||||
339 | * @param array $globalPermission List of global permissions |
||||||
340 | * @param DataList $stageRecords List of records to check for this stage |
||||||
341 | * @param string $groupIDsSQLList Group IDs this member belongs to |
||||||
342 | * @param Member $member |
||||||
343 | * @return array |
||||||
344 | */ |
||||||
345 | protected function batchPermissionCheckForStage( |
||||||
346 | $type, |
||||||
347 | $globalPermission, |
||||||
348 | DataList $stageRecords, |
||||||
349 | $groupIDsSQLList, |
||||||
350 | Member $member = null |
||||||
351 | ) { |
||||||
352 | // Initialise all IDs to false |
||||||
353 | $result = array_fill_keys($stageRecords->column('ID'), false); |
||||||
354 | |||||||
355 | // Get the uninherited permissions |
||||||
356 | $typeField = $this->getPermissionField($type); |
||||||
357 | $baseTable = DataObject::getSchema()->baseDataTable($this->getBaseClass()); |
||||||
358 | |||||||
359 | if ($member && $member->ID) { |
||||||
360 | if (!Permission::checkMember($member, 'ADMIN')) { |
||||||
361 | // Determine if this member matches any of the group or other rules |
||||||
362 | $groupJoinTable = $this->getJoinTable($type); |
||||||
363 | $uninheritedPermissions = $stageRecords |
||||||
364 | ->where([ |
||||||
365 | "(\"$typeField\" IN (?, ?) OR " |
||||||
366 | . "(\"$typeField\" = ? AND \"$groupJoinTable\".\"{$baseTable}ID\" IS NOT NULL))" |
||||||
367 | => [ |
||||||
368 | self::ANYONE, |
||||||
369 | self::LOGGED_IN_USERS, |
||||||
370 | self::ONLY_THESE_USERS |
||||||
371 | ] |
||||||
372 | ]) |
||||||
373 | ->leftJoin( |
||||||
374 | $groupJoinTable, |
||||||
375 | "\"$groupJoinTable\".\"{$baseTable}ID\" = \"{$baseTable}\".\"ID\" AND " |
||||||
376 | . "\"$groupJoinTable\".\"GroupID\" IN ($groupIDsSQLList)" |
||||||
377 | )->column('ID'); |
||||||
378 | } else { |
||||||
379 | $uninheritedPermissions = $stageRecords->column('ID'); |
||||||
380 | } |
||||||
381 | } else { |
||||||
382 | // Only view pages with ViewType = Anyone if not logged in |
||||||
383 | $uninheritedPermissions = $stageRecords |
||||||
384 | ->filter($typeField, self::ANYONE) |
||||||
385 | ->column('ID'); |
||||||
386 | } |
||||||
387 | |||||||
388 | if ($uninheritedPermissions) { |
||||||
0 ignored issues
–
show
The expression
$uninheritedPermissions 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 ![]() |
|||||||
389 | // Set all the relevant items in $result to true |
||||||
390 | $result = array_fill_keys($uninheritedPermissions, true) + $result; |
||||||
391 | } |
||||||
392 | |||||||
393 | // This looks for any of our subjects who has their permission set to "inherited" in the CMS. |
||||||
394 | // We group these and run a batch permission check on all parents. This gives us the result |
||||||
395 | // of whether the user has permission to edit this object. |
||||||
396 | $groupedByParent = []; |
||||||
397 | $potentiallyInherited = $stageRecords->filter($typeField, self::INHERIT) |
||||||
398 | ->sort("\"{$baseTable}\".\"ID\"") |
||||||
399 | ->dataQuery() |
||||||
400 | ->query() |
||||||
401 | ->setSelect([ |
||||||
402 | "\"{$baseTable}\".\"ID\"", |
||||||
403 | "\"{$baseTable}\".\"ParentID\"" |
||||||
404 | ]) |
||||||
405 | ->execute(); |
||||||
406 | |||||||
407 | foreach ($potentiallyInherited as $item) { |
||||||
408 | /** @var DataObject|Hierarchy $item */ |
||||||
409 | if ($item['ParentID']) { |
||||||
410 | if (!isset($groupedByParent[$item['ParentID']])) { |
||||||
411 | $groupedByParent[$item['ParentID']] = []; |
||||||
412 | } |
||||||
413 | $groupedByParent[$item['ParentID']][] = $item['ID']; |
||||||
414 | } else { |
||||||
415 | // Fail over to default permission check for Inherit and ParentID = 0 |
||||||
416 | $result[$item['ID']] = $this->checkDefaultPermissions($type, $member); |
||||||
417 | } |
||||||
418 | } |
||||||
419 | |||||||
420 | // Copy permissions from parent to child |
||||||
421 | if (!empty($groupedByParent)) { |
||||||
422 | $actuallyInherited = $this->batchPermissionCheck( |
||||||
423 | $type, |
||||||
424 | array_keys($groupedByParent), |
||||||
425 | $member, |
||||||
426 | $globalPermission |
||||||
427 | ); |
||||||
428 | if ($actuallyInherited) { |
||||||
0 ignored issues
–
show
The expression
$actuallyInherited 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 ![]() |
|||||||
429 | $parentIDs = array_keys(array_filter($actuallyInherited)); |
||||||
430 | foreach ($parentIDs as $parentID) { |
||||||
431 | // Set all the relevant items in $result to true |
||||||
432 | $result = array_fill_keys($groupedByParent[$parentID], true) + $result; |
||||||
433 | } |
||||||
434 | } |
||||||
435 | } |
||||||
436 | return $result; |
||||||
437 | } |
||||||
438 | |||||||
439 | /** |
||||||
440 | * @param array $ids |
||||||
441 | * @param Member|null $member |
||||||
442 | * @param bool $useCached |
||||||
443 | * @return array |
||||||
444 | */ |
||||||
445 | public function canEditMultiple($ids, Member $member = null, $useCached = true) |
||||||
446 | { |
||||||
447 | return $this->batchPermissionCheck( |
||||||
448 | self::EDIT, |
||||||
449 | $ids, |
||||||
450 | $member, |
||||||
451 | $this->getGlobalEditPermissions(), |
||||||
452 | $useCached |
||||||
453 | ); |
||||||
454 | } |
||||||
455 | |||||||
456 | /** |
||||||
457 | * @param array $ids |
||||||
458 | * @param Member|null $member |
||||||
459 | * @param bool $useCached |
||||||
460 | * @return array |
||||||
461 | */ |
||||||
462 | public function canViewMultiple($ids, Member $member = null, $useCached = true) |
||||||
463 | { |
||||||
464 | return $this->batchPermissionCheck(self::VIEW, $ids, $member, [], $useCached); |
||||||
465 | } |
||||||
466 | |||||||
467 | /** |
||||||
468 | * @param array $ids |
||||||
469 | * @param Member|null $member |
||||||
470 | * @param bool $useCached |
||||||
471 | * @return array |
||||||
472 | */ |
||||||
473 | public function canDeleteMultiple($ids, Member $member = null, $useCached = true) |
||||||
474 | { |
||||||
475 | // Validate ids |
||||||
476 | $ids = array_filter($ids, 'is_numeric'); |
||||||
477 | if (empty($ids)) { |
||||||
478 | return []; |
||||||
479 | } |
||||||
480 | $result = array_fill_keys($ids, false); |
||||||
481 | |||||||
482 | // Validate member permission |
||||||
483 | if (!$member || !$member->ID) { |
||||||
484 | return $result; |
||||||
485 | } |
||||||
486 | $deletable = []; |
||||||
487 | |||||||
488 | // Look in the cache for values |
||||||
489 | $cacheKey = "delete-{$member->ID}"; |
||||||
490 | $cachePermissions = $this->getCachePermissions($cacheKey); |
||||||
491 | if ($useCached && $cachePermissions) { |
||||||
492 | $cachedValues = array_intersect_key($cachePermissions[$cacheKey], $result); |
||||||
493 | |||||||
494 | // If we can't find everything in the cache, then look up the remainder separately |
||||||
495 | $uncachedIDs = array_keys(array_diff_key($result, $cachePermissions[$cacheKey])); |
||||||
496 | if ($uncachedIDs) { |
||||||
0 ignored issues
–
show
The expression
$uncachedIDs 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 ![]() |
|||||||
497 | $uncachedValues = $this->canDeleteMultiple($uncachedIDs, $member, false); |
||||||
498 | return $cachedValues + $uncachedValues; |
||||||
499 | } |
||||||
500 | return $cachedValues; |
||||||
501 | } |
||||||
502 | |||||||
503 | // You can only delete pages that you can edit |
||||||
504 | $editableIDs = array_keys(array_filter($this->canEditMultiple($ids, $member))); |
||||||
505 | if ($editableIDs) { |
||||||
0 ignored issues
–
show
The expression
$editableIDs 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 ![]() |
|||||||
506 | // You can only delete pages whose children you can delete |
||||||
507 | $childRecords = DataObject::get($this->baseClass) |
||||||
508 | ->filter('ParentID', $editableIDs); |
||||||
509 | |||||||
510 | // Find out the children that can be deleted |
||||||
511 | $children = $childRecords->map("ID", "ParentID"); |
||||||
512 | $childIDs = $children->keys(); |
||||||
513 | if ($childIDs) { |
||||||
0 ignored issues
–
show
The expression
$childIDs 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 ![]() |
|||||||
514 | $deletableChildren = $this->canDeleteMultiple($childIDs, $member); |
||||||
515 | |||||||
516 | // Get a list of all the parents that have no undeletable children |
||||||
517 | $deletableParents = array_fill_keys($editableIDs, true); |
||||||
518 | foreach ($deletableChildren as $id => $canDelete) { |
||||||
519 | if (!$canDelete) { |
||||||
520 | unset($deletableParents[$children[$id]]); |
||||||
521 | } |
||||||
522 | } |
||||||
523 | |||||||
524 | // Use that to filter the list of deletable parents that have children |
||||||
525 | $deletableParents = array_keys($deletableParents); |
||||||
526 | |||||||
527 | // Also get the $ids that don't have children |
||||||
528 | $parents = array_unique($children->values()); |
||||||
529 | $deletableLeafNodes = array_diff($editableIDs, $parents); |
||||||
530 | |||||||
531 | // Combine the two |
||||||
532 | $deletable = array_merge($deletableParents, $deletableLeafNodes); |
||||||
533 | } else { |
||||||
534 | $deletable = $editableIDs; |
||||||
535 | } |
||||||
536 | } |
||||||
537 | |||||||
538 | // Convert the array of deletable IDs into a map of the original IDs with true/false as the value |
||||||
539 | return array_fill_keys($deletable, true) + array_fill_keys($ids, false); |
||||||
540 | } |
||||||
541 | |||||||
542 | /** |
||||||
543 | * @param int $id |
||||||
544 | * @param Member|null $member |
||||||
545 | * @return bool|mixed |
||||||
546 | */ |
||||||
547 | public function canDelete($id, Member $member = null) |
||||||
548 | { |
||||||
549 | // No ID: Check default permission |
||||||
550 | if (!$id) { |
||||||
551 | return $this->checkDefaultPermissions(self::DELETE, $member); |
||||||
552 | } |
||||||
553 | |||||||
554 | // Regular canEdit logic is handled by canEditMultiple |
||||||
555 | $results = $this->canDeleteMultiple( |
||||||
556 | [$id], |
||||||
557 | $member |
||||||
558 | ); |
||||||
559 | |||||||
560 | // Check if in result |
||||||
561 | return isset($results[$id]) ? $results[$id] : false; |
||||||
562 | } |
||||||
563 | |||||||
564 | /** |
||||||
565 | * @param int $id |
||||||
566 | * @param Member|null $member |
||||||
567 | * @return bool|mixed |
||||||
568 | */ |
||||||
569 | public function canEdit($id, Member $member = null) |
||||||
570 | { |
||||||
571 | // No ID: Check default permission |
||||||
572 | if (!$id) { |
||||||
573 | return $this->checkDefaultPermissions(self::EDIT, $member); |
||||||
574 | } |
||||||
575 | |||||||
576 | // Regular canEdit logic is handled by canEditMultiple |
||||||
577 | $results = $this->canEditMultiple( |
||||||
578 | [$id], |
||||||
579 | $member |
||||||
580 | ); |
||||||
581 | |||||||
582 | // Check if in result |
||||||
583 | return isset($results[$id]) ? $results[$id] : false; |
||||||
584 | } |
||||||
585 | |||||||
586 | /** |
||||||
587 | * @param int $id |
||||||
588 | * @param Member|null $member |
||||||
589 | * @return bool|mixed |
||||||
590 | */ |
||||||
591 | public function canView($id, Member $member = null) |
||||||
592 | { |
||||||
593 | // No ID: Check default permission |
||||||
594 | if (!$id) { |
||||||
595 | return $this->checkDefaultPermissions(self::VIEW, $member); |
||||||
596 | } |
||||||
597 | |||||||
598 | // Regular canView logic is handled by canViewMultiple |
||||||
599 | $results = $this->canViewMultiple( |
||||||
600 | [$id], |
||||||
601 | $member |
||||||
602 | ); |
||||||
603 | |||||||
604 | // Check if in result |
||||||
605 | return isset($results[$id]) ? $results[$id] : false; |
||||||
606 | } |
||||||
607 | |||||||
608 | /** |
||||||
609 | * Get field to check for permission type for the given check. |
||||||
610 | * Defaults to those provided by {@see InheritedPermissionsExtension) |
||||||
611 | * |
||||||
612 | * @param string $type |
||||||
613 | * @return string |
||||||
614 | */ |
||||||
615 | protected function getPermissionField($type) |
||||||
616 | { |
||||||
617 | switch ($type) { |
||||||
618 | case self::DELETE: |
||||||
619 | // Delete uses edit type - Drop through |
||||||
620 | case self::EDIT: |
||||||
621 | return 'CanEditType'; |
||||||
622 | case self::VIEW: |
||||||
623 | return 'CanViewType'; |
||||||
624 | default: |
||||||
625 | throw new InvalidArgumentException("Invalid argument type $type"); |
||||||
626 | } |
||||||
627 | } |
||||||
628 | |||||||
629 | /** |
||||||
630 | * Get join table for type |
||||||
631 | * Defaults to those provided by {@see InheritedPermissionsExtension) |
||||||
632 | * |
||||||
633 | * @param string $type |
||||||
634 | * @return string |
||||||
635 | */ |
||||||
636 | protected function getJoinTable($type) |
||||||
637 | { |
||||||
638 | switch ($type) { |
||||||
639 | case self::DELETE: |
||||||
640 | // Delete uses edit type - Drop through |
||||||
641 | case self::EDIT: |
||||||
642 | return $this->getEditorGroupsTable(); |
||||||
643 | case self::VIEW: |
||||||
644 | return $this->getViewerGroupsTable(); |
||||||
645 | default: |
||||||
646 | throw new InvalidArgumentException("Invalid argument type $type"); |
||||||
647 | } |
||||||
648 | } |
||||||
649 | |||||||
650 | /** |
||||||
651 | * Determine default permission for a givion check |
||||||
652 | * |
||||||
653 | * @param string $type Method to check |
||||||
654 | * @param Member $member |
||||||
655 | * @return bool |
||||||
656 | */ |
||||||
657 | protected function checkDefaultPermissions($type, Member $member = null) |
||||||
658 | { |
||||||
659 | $defaultPermissions = $this->getDefaultPermissions(); |
||||||
660 | if (!$defaultPermissions) { |
||||||
0 ignored issues
–
show
|
|||||||
661 | return false; |
||||||
662 | } |
||||||
663 | switch ($type) { |
||||||
664 | case self::VIEW: |
||||||
665 | return $defaultPermissions->canView($member); |
||||||
666 | case self::EDIT: |
||||||
667 | return $defaultPermissions->canEdit($member); |
||||||
668 | case self::DELETE: |
||||||
669 | return $defaultPermissions->canDelete($member); |
||||||
670 | default: |
||||||
671 | return false; |
||||||
672 | } |
||||||
673 | } |
||||||
674 | |||||||
675 | /** |
||||||
676 | * Check if this model has versioning |
||||||
677 | * |
||||||
678 | * @return bool |
||||||
679 | */ |
||||||
680 | protected function isVersioned() |
||||||
681 | { |
||||||
682 | if (!class_exists(Versioned::class)) { |
||||||
683 | return false; |
||||||
684 | } |
||||||
685 | /** @var Versioned|DataObject $singleton */ |
||||||
686 | $singleton = DataObject::singleton($this->getBaseClass()); |
||||||
687 | return $singleton->hasExtension(Versioned::class) && $singleton->hasStages(); |
||||||
0 ignored issues
–
show
The method
hasExtension() does not exist on SilverStripe\Versioned\Versioned .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces. This is most likely a typographical error or the method has been renamed. ![]() The method
hasStages() 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
![]() |
|||||||
688 | } |
||||||
689 | |||||||
690 | /** |
||||||
691 | * @return $this |
||||||
692 | */ |
||||||
693 | public function clearCache() |
||||||
694 | { |
||||||
695 | $this->cachePermissions = []; |
||||||
696 | return $this; |
||||||
697 | } |
||||||
698 | |||||||
699 | /** |
||||||
700 | * Get table to use for editor groups relation |
||||||
701 | * |
||||||
702 | * @return string |
||||||
703 | */ |
||||||
704 | protected function getEditorGroupsTable() |
||||||
705 | { |
||||||
706 | $table = DataObject::getSchema()->tableName($this->baseClass); |
||||||
707 | return "{$table}_EditorGroups"; |
||||||
708 | } |
||||||
709 | |||||||
710 | /** |
||||||
711 | * Get table to use for viewer groups relation |
||||||
712 | * |
||||||
713 | * @return string |
||||||
714 | */ |
||||||
715 | protected function getViewerGroupsTable() |
||||||
716 | { |
||||||
717 | $table = DataObject::getSchema()->tableName($this->baseClass); |
||||||
718 | return "{$table}_ViewerGroups"; |
||||||
719 | } |
||||||
720 | |||||||
721 | /** |
||||||
722 | * Gets the permission from cache |
||||||
723 | * |
||||||
724 | * @param string $cacheKey |
||||||
725 | * @return mixed |
||||||
726 | */ |
||||||
727 | protected function getCachePermissions($cacheKey) |
||||||
728 | { |
||||||
729 | // Check local cache |
||||||
730 | if (isset($this->cachePermissions[$cacheKey])) { |
||||||
731 | return $this->cachePermissions[$cacheKey]; |
||||||
732 | } |
||||||
733 | |||||||
734 | // Check persistent cache |
||||||
735 | if ($this->cacheService) { |
||||||
736 | $result = $this->cacheService->get($cacheKey); |
||||||
737 | |||||||
738 | // Warm local cache |
||||||
739 | if ($result) { |
||||||
740 | $this->cachePermissions[$cacheKey] = $result; |
||||||
741 | return $result; |
||||||
742 | } |
||||||
743 | } |
||||||
744 | |||||||
745 | return null; |
||||||
746 | } |
||||||
747 | |||||||
748 | /** |
||||||
749 | * Creates a cache key for a member and type |
||||||
750 | * |
||||||
751 | * @param string $type |
||||||
752 | * @param int $memberID |
||||||
753 | * @return string |
||||||
754 | */ |
||||||
755 | protected function generateCacheKey($type, $memberID) |
||||||
756 | { |
||||||
757 | $classKey = str_replace('\\', '-', $this->baseClass); |
||||||
758 | return "{$type}-{$classKey}-{$memberID}"; |
||||||
759 | } |
||||||
760 | } |
||||||
761 |
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.