Passed
Pull Request — 4 (#10222)
by Steve
07:19
created

InheritedPermissions::getJoinTable()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 4
nop 1
dl 0
loc 11
rs 10
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));
0 ignored issues
show
Bug introduced by
It seems like $cachePermissions can also be of type null; however, parameter $array2 of array_diff_key() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

273
            $uncachedIDs = array_keys(array_diff_key($result ?: [], /** @scrutinizer ignore-type */ $cachePermissions));
Loading history...
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
        $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 " . "(\"$typeField\" = ? AND \"$groupJoinTable\".\"{$baseTable}ID\" IS NOT NULL))"
366
                        => [
367
                            self::ANYONE,
368
                            self::LOGGED_IN_USERS,
369
                            self::ONLY_THESE_USERS
370
                        ]
371
                    ])
372
                    ->leftJoin(
373
                        $groupJoinTable,
374
                        "\"$groupJoinTable\".\"{$baseTable}ID\" = \"{$baseTable}\".\"ID\" AND " . "\"$groupJoinTable\".\"GroupID\" IN ($groupIDsSQLList)"
375
                    )->column('ID');
376
            } else {
377
                $uninheritedPermissions = $stageRecords->column('ID');
378
            }
379
        } else {
380
            // Only view pages with ViewType = Anyone if not logged in
381
            $uninheritedPermissions = $stageRecords
382
                ->filter($typeField, self::ANYONE)
383
                ->column('ID');
384
        }
385
386
        if ($uninheritedPermissions) {
387
            // Set all the relevant items in $result to true
388
            $result = array_fill_keys($uninheritedPermissions ?: [], true) + $result;
0 ignored issues
show
introduced by
$uninheritedPermissions is a non-empty array, thus is always true.
Loading history...
389
        }
390
391
        // This looks for any of our subjects who has their permission set to "inherited" in the CMS.
392
        // We group these and run a batch permission check on all parents. This gives us the result
393
        // of whether the user has permission to edit this object.
394
        $groupedByParent = [];
395
        $potentiallyInherited = $stageRecords->filter($typeField, self::INHERIT)
396
            ->sort("\"{$baseTable}\".\"ID\"")
397
            ->dataQuery()
398
            ->query()
399
            ->setSelect([
400
                "\"{$baseTable}\".\"ID\"",
401
                "\"{$baseTable}\".\"ParentID\""
402
            ])
403
            ->execute();
404
405
        foreach ($potentiallyInherited as $item) {
406
            /** @var DataObject|Hierarchy $item */
407
            if ($item['ParentID']) {
408
                if (!isset($groupedByParent[$item['ParentID']])) {
409
                    $groupedByParent[$item['ParentID']] = [];
410
                }
411
                $groupedByParent[$item['ParentID']][] = $item['ID'];
412
            } else {
413
                // Fail over to default permission check for Inherit and ParentID = 0
414
                $result[$item['ID']] = $this->checkDefaultPermissions($type, $member);
415
            }
416
        }
417
418
        // Copy permissions from parent to child
419
        if (!empty($groupedByParent)) {
420
            $actuallyInherited = $this->batchPermissionCheck(
421
                $type,
422
                array_keys($groupedByParent),
423
                $member,
424
                $globalPermission
425
            );
426
            if ($actuallyInherited) {
0 ignored issues
show
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
427
                $parentIDs = array_keys(array_filter($actuallyInherited ?: []));
428
                foreach ($parentIDs as $parentID) {
429
                    // Set all the relevant items in $result to true
430
                    $result = array_fill_keys($groupedByParent[$parentID], true) + $result;
431
                }
432
            }
433
        }
434
        return $result;
435
    }
436
437
    /**
438
     * @param array $ids
439
     * @param Member|null $member
440
     * @param bool $useCached
441
     * @return array
442
     */
443
    public function canEditMultiple($ids, Member $member = null, $useCached = true)
444
    {
445
        return $this->batchPermissionCheck(
446
            self::EDIT,
447
            $ids,
448
            $member,
449
            $this->getGlobalEditPermissions(),
450
            $useCached
451
        );
452
    }
453
454
    /**
455
     * @param array $ids
456
     * @param Member|null $member
457
     * @param bool $useCached
458
     * @return array
459
     */
460
    public function canViewMultiple($ids, Member $member = null, $useCached = true)
461
    {
462
        return $this->batchPermissionCheck(self::VIEW, $ids, $member, [], $useCached);
463
    }
464
465
    /**
466
     * @param array $ids
467
     * @param Member|null $member
468
     * @param bool $useCached
469
     * @return array
470
     */
471
    public function canDeleteMultiple($ids, Member $member = null, $useCached = true)
472
    {
473
        // Validate ids
474
        $ids = array_filter($ids ?: [], 'is_numeric');
475
        if (empty($ids)) {
476
            return [];
477
        }
478
        $result = array_fill_keys($ids ?: [], false);
479
480
        // Validate member permission
481
        if (!$member || !$member->ID) {
482
            return $result;
483
        }
484
        $deletable = [];
485
486
        // Look in the cache for values
487
        $cacheKey = "delete-{$member->ID}";
488
        $cachePermissions = $this->getCachePermissions($cacheKey);
489
        if ($useCached && $cachePermissions) {
490
            $cachedValues = array_intersect_key($cachePermissions[$cacheKey], $result);
491
492
            // If we can't find everything in the cache, then look up the remainder separately
493
            $uncachedIDs = array_keys(array_diff_key($result ?: [], $cachePermissions[$cacheKey]));
494
            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...
495
                $uncachedValues = $this->canDeleteMultiple($uncachedIDs, $member, false);
496
                return $cachedValues + $uncachedValues;
497
            }
498
            return $cachedValues;
499
        }
500
501
        // You can only delete pages that you can edit
502
        $editableIDs = array_keys(array_filter($this->canEditMultiple($ids, $member) ?: []));
503
        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...
504
            // You can only delete pages whose children you can delete
505
            $childRecords = DataObject::get($this->baseClass)
506
                ->filter('ParentID', $editableIDs);
507
508
            // Find out the children that can be deleted
509
            $children = $childRecords->map("ID", "ParentID");
510
            $childIDs = $children->keys();
511
            if ($childIDs) {
0 ignored issues
show
Bug Best Practice introduced by
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 empty(..) or ! empty(...) instead.

Loading history...
512
                $deletableChildren = $this->canDeleteMultiple($childIDs, $member);
513
514
                // Get a list of all the parents that have no undeletable children
515
                $deletableParents = array_fill_keys($editableIDs ?: [], true);
516
                foreach ($deletableChildren as $id => $canDelete) {
517
                    if (!$canDelete) {
518
                        unset($deletableParents[$children[$id]]);
519
                    }
520
                }
521
522
                // Use that to filter the list of deletable parents that have children
523
                $deletableParents = array_keys($deletableParents ?: []);
524
525
                // Also get the $ids that don't have children
526
                $parents = array_unique($children->values() ?: []);
527
                $deletableLeafNodes = array_diff($editableIDs ?: [], $parents);
528
529
                // Combine the two
530
                $deletable = array_merge($deletableParents, $deletableLeafNodes);
531
            } else {
532
                $deletable = $editableIDs;
533
            }
534
        }
535
536
        // Convert the array of deletable IDs into a map of the original IDs with true/false as the value
537
        return array_fill_keys($deletable ?: [], true) + array_fill_keys($ids ?: [], false);
538
    }
539
540
    /**
541
     * @param int $id
542
     * @param Member|null $member
543
     * @return bool|mixed
544
     */
545
    public function canDelete($id, Member $member = null)
546
    {
547
        // No ID: Check default permission
548
        if (!$id) {
549
            return $this->checkDefaultPermissions(self::DELETE, $member);
550
        }
551
552
        // Regular canEdit logic is handled by canEditMultiple
553
        $results = $this->canDeleteMultiple(
554
            [$id],
555
            $member
556
        );
557
558
        // Check if in result
559
        return isset($results[$id]) ? $results[$id] : false;
560
    }
561
562
    /**
563
     * @param int $id
564
     * @param Member|null $member
565
     * @return bool|mixed
566
     */
567
    public function canEdit($id, Member $member = null)
568
    {
569
        // No ID: Check default permission
570
        if (!$id) {
571
            return $this->checkDefaultPermissions(self::EDIT, $member);
572
        }
573
574
        // Regular canEdit logic is handled by canEditMultiple
575
        $results = $this->canEditMultiple(
576
            [$id],
577
            $member
578
        );
579
580
        // Check if in result
581
        return isset($results[$id]) ? $results[$id] : false;
582
    }
583
584
    /**
585
     * @param int $id
586
     * @param Member|null $member
587
     * @return bool|mixed
588
     */
589
    public function canView($id, Member $member = null)
590
    {
591
        // No ID: Check default permission
592
        if (!$id) {
593
            return $this->checkDefaultPermissions(self::VIEW, $member);
594
        }
595
596
        // Regular canView logic is handled by canViewMultiple
597
        $results = $this->canViewMultiple(
598
            [$id],
599
            $member
600
        );
601
602
        // Check if in result
603
        return isset($results[$id]) ? $results[$id] : false;
604
    }
605
606
    /**
607
     * Get field to check for permission type for the given check.
608
     * Defaults to those provided by {@see InheritedPermissionsExtension)
609
     *
610
     * @param string $type
611
     * @return string
612
     */
613
    protected function getPermissionField($type)
614
    {
615
        switch ($type) {
616
            case self::DELETE:
617
                // Delete uses edit type - Drop through
618
            case self::EDIT:
619
                return 'CanEditType';
620
            case self::VIEW:
621
                return 'CanViewType';
622
            default:
623
                throw new InvalidArgumentException("Invalid argument type $type");
624
        }
625
    }
626
627
    /**
628
     * Get join table for type
629
     * Defaults to those provided by {@see InheritedPermissionsExtension)
630
     *
631
     * @param string $type
632
     * @return string
633
     */
634
    protected function getJoinTable($type)
635
    {
636
        switch ($type) {
637
            case self::DELETE:
638
                // Delete uses edit type - Drop through
639
            case self::EDIT:
640
                return $this->getEditorGroupsTable();
641
            case self::VIEW:
642
                return $this->getViewerGroupsTable();
643
            default:
644
                throw new InvalidArgumentException("Invalid argument type $type");
645
        }
646
    }
647
648
    /**
649
     * Determine default permission for a givion check
650
     *
651
     * @param string $type Method to check
652
     * @param Member $member
653
     * @return bool
654
     */
655
    protected function checkDefaultPermissions($type, Member $member = null)
656
    {
657
        $defaultPermissions = $this->getDefaultPermissions();
658
        if (!$defaultPermissions) {
0 ignored issues
show
introduced by
$defaultPermissions is of type SilverStripe\Security\DefaultPermissionChecker, thus it always evaluated to true.
Loading history...
659
            return false;
660
        }
661
        switch ($type) {
662
            case self::VIEW:
663
                return $defaultPermissions->canView($member);
664
            case self::EDIT:
665
                return $defaultPermissions->canEdit($member);
666
            case self::DELETE:
667
                return $defaultPermissions->canDelete($member);
668
            default:
669
                return false;
670
        }
671
    }
672
673
    /**
674
     * Check if this model has versioning
675
     *
676
     * @return bool
677
     */
678
    protected function isVersioned()
679
    {
680
        if (!class_exists(Versioned::class)) {
681
            return false;
682
        }
683
        /** @var Versioned|DataObject $singleton */
684
        $singleton = DataObject::singleton($this->getBaseClass());
685
        return $singleton->hasExtension(Versioned::class) && $singleton->hasStages();
0 ignored issues
show
Bug introduced by
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 ignore-call  annotation

685
        return $singleton->/** @scrutinizer ignore-call */ hasExtension(Versioned::class) && $singleton->hasStages();

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

685
        return $singleton->hasExtension(Versioned::class) && $singleton->/** @scrutinizer ignore-call */ hasStages();
Loading history...
686
    }
687
688
    /**
689
     * @return $this
690
     */
691
    public function clearCache()
692
    {
693
        $this->cachePermissions = [];
694
        return $this;
695
    }
696
697
    /**
698
     * Get table to use for editor groups relation
699
     *
700
     * @return string
701
     */
702
    protected function getEditorGroupsTable()
703
    {
704
        $table = DataObject::getSchema()->tableName($this->baseClass);
705
        return "{$table}_EditorGroups";
706
    }
707
708
    /**
709
     * Get table to use for viewer groups relation
710
     *
711
     * @return string
712
     */
713
    protected function getViewerGroupsTable()
714
    {
715
        $table = DataObject::getSchema()->tableName($this->baseClass);
716
        return "{$table}_ViewerGroups";
717
    }
718
719
    /**
720
     * Gets the permission from cache
721
     *
722
     * @param string $cacheKey
723
     * @return mixed
724
     */
725
    protected function getCachePermissions($cacheKey)
726
    {
727
        // Check local cache
728
        if (isset($this->cachePermissions[$cacheKey])) {
729
            return $this->cachePermissions[$cacheKey];
730
        }
731
732
        // Check persistent cache
733
        if ($this->cacheService) {
734
            $result = $this->cacheService->get($cacheKey);
735
736
            // Warm local cache
737
            if ($result) {
738
                $this->cachePermissions[$cacheKey] = $result;
739
                return $result;
740
            }
741
        }
742
743
        return null;
744
    }
745
746
    /**
747
     * Creates a cache key for a member and type
748
     *
749
     * @param string $type
750
     * @param int $memberID
751
     * @return string
752
     */
753
    protected function generateCacheKey($type, $memberID)
754
    {
755
        $classKey = str_replace('\\', '-', $this->baseClass ?: '');
756
        return "{$type}-{$classKey}-{$memberID}";
757
    }
758
}
759