Completed
Push — 4 ( 6b0b20...a8f776 )
by Damian
33s
created

InheritedPermissions::canEditMultiple()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 6
nc 1
nop 3
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
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
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
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
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
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
Bug introduced by
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 ignore-call  annotation

289
            $groupIDs = $member->/** @scrutinizer ignore-call */ Groups()->column("ID");

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.

Loading history...
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
        if ($member && $member->ID) {
358
            // Determine if this member matches any of the group or other rules
359
            $groupJoinTable = $this->getJoinTable($type);
360
            $baseTable = DataObject::getSchema()->baseDataTable($this->getBaseClass());
361
            $uninheritedPermissions = $stageRecords
362
                ->where([
363
                    "(\"$typeField\" IN (?, ?) OR " .
364
                    "(\"$typeField\" = ? AND \"$groupJoinTable\".\"{$baseTable}ID\" IS NOT NULL))"
365
                    => [
366
                        self::ANYONE,
367
                        self::LOGGED_IN_USERS,
368
                        self::ONLY_THESE_USERS
369
                    ]
370
                ])
371
                ->leftJoin(
372
                    $groupJoinTable,
373
                    "\"$groupJoinTable\".\"{$baseTable}ID\" = \"{$baseTable}\".\"ID\" AND " .
374
                    "\"$groupJoinTable\".\"GroupID\" IN ($groupIDsSQLList)"
375
                )->column('ID');
376
        } else {
377
            // Only view pages with ViewType = Anyone if not logged in
378
            $uninheritedPermissions = $stageRecords
379
                ->filter($typeField, self::ANYONE)
380
                ->column('ID');
381
        }
382
383
        if ($uninheritedPermissions) {
0 ignored issues
show
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
384
            // Set all the relevant items in $result to true
385
            $result = array_fill_keys($uninheritedPermissions, true) + $result;
386
        }
387
388
        // Group $potentiallyInherited by ParentID; we'll look at the permission of all those parents and
389
        // then see which ones the user has permission on
390
        $groupedByParent = [];
391
        $potentiallyInherited = $stageRecords->filter($typeField, self::INHERIT);
392
        foreach ($potentiallyInherited as $item) {
393
            /** @var DataObject|Hierarchy $item */
394
            if ($item->ParentID) {
0 ignored issues
show
Bug Best Practice introduced by
The property ParentID does not exist on SilverStripe\ORM\DataObject. Since you implemented __get, consider adding a @property annotation.
Loading history...
395
                if (!isset($groupedByParent[$item->ParentID])) {
396
                    $groupedByParent[$item->ParentID] = [];
397
                }
398
                $groupedByParent[$item->ParentID][] = $item->ID;
0 ignored issues
show
Bug introduced by
The property ID does not seem to exist on SilverStripe\ORM\Hierarchy\Hierarchy.
Loading history...
399
            } else {
400
                // Fail over to default permission check for Inherit and ParentID = 0
401
                $result[$item->ID] = $this->checkDefaultPermissions($type, $member);
402
            }
403
        }
404
405
        // Copy permissions from parent to child
406
        if ($groupedByParent) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groupedByParent 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...
407
            $actuallyInherited = $this->batchPermissionCheck(
408
                $type,
409
                array_keys($groupedByParent),
410
                $member,
411
                $globalPermission
412
            );
413
            if ($actuallyInherited) {
414
                $parentIDs = array_keys(array_filter($actuallyInherited));
415
                foreach ($parentIDs as $parentID) {
416
                    // Set all the relevant items in $result to true
417
                    $result = array_fill_keys($groupedByParent[$parentID], true) + $result;
418
                }
419
            }
420
        }
421
        return $result;
422
    }
423
424
    /**
425
     * @param array $ids
426
     * @param Member|null $member
427
     * @param bool $useCached
428
     * @return array
429
     */
430
    public function canEditMultiple($ids, Member $member = null, $useCached = true)
431
    {
432
        return $this->batchPermissionCheck(
433
            self::EDIT,
434
            $ids,
435
            $member,
436
            $this->getGlobalEditPermissions(),
437
            $useCached
438
        );
439
    }
440
441
    /**
442
     * @param array $ids
443
     * @param Member|null $member
444
     * @param bool $useCached
445
     * @return array
446
     */
447
    public function canViewMultiple($ids, Member $member = null, $useCached = true)
448
    {
449
        return $this->batchPermissionCheck(self::VIEW, $ids, $member, [], $useCached);
450
    }
451
452
    /**
453
     * @param array $ids
454
     * @param Member|null $member
455
     * @param bool $useCached
456
     * @return array
457
     */
458
    public function canDeleteMultiple($ids, Member $member = null, $useCached = true)
459
    {
460
        // Validate ids
461
        $ids = array_filter($ids, 'is_numeric');
462
        if (empty($ids)) {
463
            return [];
464
        }
465
        $result = array_fill_keys($ids, false);
466
467
        // Validate member permission
468
        if (!$member || !$member->ID) {
469
            return $result;
470
        }
471
        $deletable = [];
472
473
        // Look in the cache for values
474
        $cacheKey = "delete-{$member->ID}";
475
        $cachePermissions = $this->getCachePermissions($cacheKey);
476
        if ($useCached && $cachePermissions) {
477
            $cachedValues = array_intersect_key($cachePermissions[$cacheKey], $result);
478
479
            // If we can't find everything in the cache, then look up the remainder separately
480
            $uncachedIDs = array_keys(array_diff_key($result, $cachePermissions[$cacheKey]));
481
            if ($uncachedIDs) {
0 ignored issues
show
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
482
                $uncachedValues = $this->canDeleteMultiple($uncachedIDs, $member, false);
483
                return $cachedValues + $uncachedValues;
484
            }
485
            return $cachedValues;
486
        }
487
488
        // You can only delete pages that you can edit
489
        $editableIDs = array_keys(array_filter($this->canEditMultiple($ids, $member)));
490
        if ($editableIDs) {
0 ignored issues
show
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
491
            // You can only delete pages whose children you can delete
492
            $childRecords = DataObject::get($this->baseClass)
493
                ->filter('ParentID', $editableIDs);
494
495
            // Find out the children that can be deleted
496
            $children = $childRecords->map("ID", "ParentID");
497
            $childIDs = $children->keys();
498
            if ($childIDs) {
499
                $deletableChildren = $this->canDeleteMultiple($childIDs, $member);
500
501
                // Get a list of all the parents that have no undeletable children
502
                $deletableParents = array_fill_keys($editableIDs, true);
503
                foreach ($deletableChildren as $id => $canDelete) {
504
                    if (!$canDelete) {
505
                        unset($deletableParents[$children[$id]]);
506
                    }
507
                }
508
509
                // Use that to filter the list of deletable parents that have children
510
                $deletableParents = array_keys($deletableParents);
511
512
                // Also get the $ids that don't have children
513
                $parents = array_unique($children->values());
514
                $deletableLeafNodes = array_diff($editableIDs, $parents);
515
516
                // Combine the two
517
                $deletable = array_merge($deletableParents, $deletableLeafNodes);
518
            } else {
519
                $deletable = $editableIDs;
520
            }
521
        }
522
523
        // Convert the array of deletable IDs into a map of the original IDs with true/false as the value
524
        return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
525
    }
526
527
    /**
528
     * @param int $id
529
     * @param Member|null $member
530
     * @return bool|mixed
531
     */
532
    public function canDelete($id, Member $member = null)
533
    {
534
        // No ID: Check default permission
535
        if (!$id) {
536
            return $this->checkDefaultPermissions(self::DELETE, $member);
537
        }
538
539
        // Regular canEdit logic is handled by canEditMultiple
540
        $results = $this->canDeleteMultiple(
541
            [$id],
542
            $member
543
        );
544
545
        // Check if in result
546
        return isset($results[$id]) ? $results[$id] : false;
547
    }
548
549
    /**
550
     * @param int $id
551
     * @param Member|null $member
552
     * @return bool|mixed
553
     */
554
    public function canEdit($id, Member $member = null)
555
    {
556
        // No ID: Check default permission
557
        if (!$id) {
558
            return $this->checkDefaultPermissions(self::EDIT, $member);
559
        }
560
561
        // Regular canEdit logic is handled by canEditMultiple
562
        $results = $this->canEditMultiple(
563
            [$id],
564
            $member
565
        );
566
567
        // Check if in result
568
        return isset($results[$id]) ? $results[$id] : false;
569
    }
570
571
    /**
572
     * @param int $id
573
     * @param Member|null $member
574
     * @return bool|mixed
575
     */
576
    public function canView($id, Member $member = null)
577
    {
578
        // No ID: Check default permission
579
        if (!$id) {
580
            return $this->checkDefaultPermissions(self::VIEW, $member);
581
        }
582
583
        // Regular canView logic is handled by canViewMultiple
584
        $results = $this->canViewMultiple(
585
            [$id],
586
            $member
587
        );
588
589
        // Check if in result
590
        return isset($results[$id]) ? $results[$id] : false;
591
    }
592
593
    /**
594
     * Get field to check for permission type for the given check.
595
     * Defaults to those provided by {@see InheritedPermissionsExtension)
596
     *
597
     * @param string $type
598
     * @return string
599
     */
600
    protected function getPermissionField($type)
601
    {
602
        switch ($type) {
603
            case self::DELETE:
604
                // Delete uses edit type - Drop through
605
            case self::EDIT:
606
                return 'CanEditType';
607
            case self::VIEW:
608
                return 'CanViewType';
609
            default:
610
                throw new InvalidArgumentException("Invalid argument type $type");
611
        }
612
    }
613
614
    /**
615
     * Get join table for type
616
     * Defaults to those provided by {@see InheritedPermissionsExtension)
617
     *
618
     * @param string $type
619
     * @return string
620
     */
621
    protected function getJoinTable($type)
622
    {
623
        switch ($type) {
624
            case self::DELETE:
625
                // Delete uses edit type - Drop through
626
            case self::EDIT:
627
                return $this->getEditorGroupsTable();
628
            case self::VIEW:
629
                return $this->getViewerGroupsTable();
630
            default:
631
                throw new InvalidArgumentException("Invalid argument type $type");
632
        }
633
    }
634
635
    /**
636
     * Determine default permission for a givion check
637
     *
638
     * @param string $type Method to check
639
     * @param Member $member
640
     * @return bool
641
     */
642
    protected function checkDefaultPermissions($type, Member $member = null)
643
    {
644
        $defaultPermissions = $this->getDefaultPermissions();
645
        if (!$defaultPermissions) {
646
            return false;
647
        }
648
        switch ($type) {
649
            case self::VIEW:
650
                return $defaultPermissions->canView($member);
651
            case self::EDIT:
652
                return $defaultPermissions->canEdit($member);
653
            case self::DELETE:
654
                return $defaultPermissions->canDelete($member);
655
            default:
656
                return false;
657
        }
658
    }
659
660
    /**
661
     * Check if this model has versioning
662
     *
663
     * @return bool
664
     */
665
    protected function isVersioned()
666
    {
667
        if (!class_exists(Versioned::class)) {
668
            return false;
669
        }
670
        $singleton = DataObject::singleton($this->getBaseClass());
671
        return $singleton->hasExtension(Versioned::class);
672
    }
673
674
    /**
675
     * @return $this
676
     */
677
    public function clearCache()
678
    {
679
        $this->cachePermissions = [];
680
        return $this;
681
    }
682
683
    /**
684
     * Get table to use for editor groups relation
685
     *
686
     * @return string
687
     */
688
    protected function getEditorGroupsTable()
689
    {
690
        $table = DataObject::getSchema()->tableName($this->baseClass);
691
        return "{$table}_EditorGroups";
692
    }
693
694
    /**
695
     * Get table to use for viewer groups relation
696
     *
697
     * @return string
698
     */
699
    protected function getViewerGroupsTable()
700
    {
701
        $table = DataObject::getSchema()->tableName($this->baseClass);
702
        return "{$table}_ViewerGroups";
703
    }
704
705
    /**
706
     * Gets the permission from cache
707
     *
708
     * @param string $cacheKey
709
     * @return mixed
710
     */
711
    protected function getCachePermissions($cacheKey)
712
    {
713
        // Check local cache
714
        if (isset($this->cachePermissions[$cacheKey])) {
715
            return $this->cachePermissions[$cacheKey];
716
        }
717
718
        // Check persistent cache
719
        if ($this->cacheService) {
720
            $result = $this->cacheService->get($cacheKey);
721
722
            // Warm local cache
723
            if ($result) {
724
                $this->cachePermissions[$cacheKey] = $result;
725
                return $result;
726
            }
727
        }
728
729
        return null;
730
    }
731
732
    /**
733
     * Creates a cache key for a member and type
734
     *
735
     * @param string $type
736
     * @param int $memberID
737
     * @return string
738
     */
739
    protected function generateCacheKey($type, $memberID)
740
    {
741
        return "{$type}-{$memberID}";
742
    }
743
}
744