InheritedPermissions   F
last analyzed

Complexity

Total Complexity 96

Size/Duplication

Total Lines 738
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 262
c 2
b 0
f 0
dl 0
loc 738
rs 2
wmc 96

26 Methods

Rating   Name   Duplication   Size   Complexity  
A getGlobalEditPermissions() 0 3 1
A setDefaultPermissions() 0 4 1
A setGlobalEditPermissions() 0 4 1
A getBaseClass() 0 3 1
A getDefaultPermissions() 0 3 1
B flushMemberCache() 0 16 7
A prePopulatePermissionCache() 0 14 4
C batchPermissionCheck() 0 92 17
A __construct() 0 10 2
A __destruct() 0 9 4
A getJoinTable() 0 11 4
A getPermissionField() 0 11 4
A canDelete() 0 15 3
A clearCache() 0 4 1
A getCachePermissions() 0 19 4
A getEditorGroupsTable() 0 4 1
A canEditMultiple() 0 8 1
C batchPermissionCheckForStage() 0 92 11
B canDeleteMultiple() 0 67 11
A canView() 0 15 3
A canEdit() 0 15 3
A checkDefaultPermissions() 0 15 5
A canViewMultiple() 0 3 1
A generateCacheKey() 0 4 1
A getViewerGroupsTable() 0 4 1
A isVersioned() 0 8 3

How to fix   Complexity   

Complex Class

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
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
        $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
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...
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
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...
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
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...
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
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...
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
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...
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
introduced by
$defaultPermissions is of type SilverStripe\Security\DefaultPermissionChecker, thus it always evaluated to true.
Loading history...
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
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

687
        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

687
        return $singleton->hasExtension(Versioned::class) && $singleton->/** @scrutinizer ignore-call */ hasStages();
Loading history...
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