Total Complexity | 96 |
Total Lines | 738 |
Duplicated Lines | 0 % |
Changes | 2 | ||
Bugs | 0 | Features | 0 |
Complex classes like InheritedPermissions often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use InheritedPermissions, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
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() |
||
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() |
||
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 = []) |
||
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( |
||
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) { |
||
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) { |
||
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) { |
||
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) { |
||
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) { |
||
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) |
||
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) { |
||
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(); |
||
688 | } |
||
689 | |||
690 | /** |
||
691 | * @return $this |
||
692 | */ |
||
693 | public function clearCache() |
||
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) |
||
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) |
||
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.