Passed
Push — 4.1.1 ( 01ed8a )
by Robbie
09:45
created

InheritedPermissions   F

Complexity

Total Complexity 95

Size/Duplication

Total Lines 720
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 720
rs 1.263
c 0
b 0
f 0
wmc 95

26 Methods

Rating   Name   Duplication   Size   Complexity  
A getJoinTable() 0 11 4
A getPermissionField() 0 11 4
A canDelete() 0 15 3
A canEditMultiple() 0 8 1
C canDeleteMultiple() 0 67 11
A canView() 0 15 3
A canEdit() 0 15 3
B checkDefaultPermissions() 0 15 5
A canViewMultiple() 0 3 1
A clearCache() 0 4 1
A getCachePermissions() 0 19 4
A getEditorGroupsTable() 0 4 1
A generateCacheKey() 0 3 1
A getViewerGroupsTable() 0 4 1
A getGlobalEditPermissions() 0 3 1
B flushMemberCache() 0 16 7
A setDefaultPermissions() 0 4 1
A prePopulatePermissionCache() 0 14 4
D batchPermissionCheck() 0 92 17
A setGlobalEditPermissions() 0 4 1
A getBaseClass() 0 3 1
A getDefaultPermissions() 0 3 1
A __construct() 0 10 2
A __destruct() 0 9 4
C batchPermissionCheckForStage() 0 75 10
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
        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 " . "(\"$typeField\" = ? AND \"$groupJoinTable\".\"{$baseTable}ID\" IS NOT NULL))"
364
                    => [
365
                        self::ANYONE,
366
                        self::LOGGED_IN_USERS,
367
                        self::ONLY_THESE_USERS
368
                    ]
369
                ])
370
                ->leftJoin(
371
                    $groupJoinTable,
372
                    "\"$groupJoinTable\".\"{$baseTable}ID\" = \"{$baseTable}\".\"ID\" AND " . "\"$groupJoinTable\".\"GroupID\" IN ($groupIDsSQLList)"
373
                )->column('ID');
374
        } else {
375
            // Only view pages with ViewType = Anyone if not logged in
376
            $uninheritedPermissions = $stageRecords
377
                ->filter($typeField, self::ANYONE)
378
                ->column('ID');
379
        }
380
381
        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...
382
            // Set all the relevant items in $result to true
383
            $result = array_fill_keys($uninheritedPermissions, true) + $result;
384
        }
385
386
        // Group $potentiallyInherited by ParentID; we'll look at the permission of all those parents and
387
        // then see which ones the user has permission on
388
        $groupedByParent = [];
389
        $potentiallyInherited = $stageRecords->filter($typeField, self::INHERIT);
390
        foreach ($potentiallyInherited as $item) {
391
            /** @var DataObject|Hierarchy $item */
392
            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...
393
                if (!isset($groupedByParent[$item->ParentID])) {
394
                    $groupedByParent[$item->ParentID] = [];
395
                }
396
                $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...
397
            } else {
398
                // Fail over to default permission check for Inherit and ParentID = 0
399
                $result[$item->ID] = $this->checkDefaultPermissions($type, $member);
400
            }
401
        }
402
403
        // Copy permissions from parent to child
404
        if ($groupedByParent) {
405
            $actuallyInherited = $this->batchPermissionCheck(
406
                $type,
407
                array_keys($groupedByParent),
408
                $member,
409
                $globalPermission
410
            );
411
            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...
412
                $parentIDs = array_keys(array_filter($actuallyInherited));
413
                foreach ($parentIDs as $parentID) {
414
                    // Set all the relevant items in $result to true
415
                    $result = array_fill_keys($groupedByParent[$parentID], true) + $result;
416
                }
417
            }
418
        }
419
        return $result;
420
    }
421
422
    /**
423
     * @param array $ids
424
     * @param Member|null $member
425
     * @param bool $useCached
426
     * @return array
427
     */
428
    public function canEditMultiple($ids, Member $member = null, $useCached = true)
429
    {
430
        return $this->batchPermissionCheck(
431
            self::EDIT,
432
            $ids,
433
            $member,
434
            $this->getGlobalEditPermissions(),
435
            $useCached
436
        );
437
    }
438
439
    /**
440
     * @param array $ids
441
     * @param Member|null $member
442
     * @param bool $useCached
443
     * @return array
444
     */
445
    public function canViewMultiple($ids, Member $member = null, $useCached = true)
446
    {
447
        return $this->batchPermissionCheck(self::VIEW, $ids, $member, [], $useCached);
448
    }
449
450
    /**
451
     * @param array $ids
452
     * @param Member|null $member
453
     * @param bool $useCached
454
     * @return array
455
     */
456
    public function canDeleteMultiple($ids, Member $member = null, $useCached = true)
457
    {
458
        // Validate ids
459
        $ids = array_filter($ids, 'is_numeric');
460
        if (empty($ids)) {
461
            return [];
462
        }
463
        $result = array_fill_keys($ids, false);
464
465
        // Validate member permission
466
        if (!$member || !$member->ID) {
467
            return $result;
468
        }
469
        $deletable = [];
470
471
        // Look in the cache for values
472
        $cacheKey = "delete-{$member->ID}";
473
        $cachePermissions = $this->getCachePermissions($cacheKey);
474
        if ($useCached && $cachePermissions) {
475
            $cachedValues = array_intersect_key($cachePermissions[$cacheKey], $result);
476
477
            // If we can't find everything in the cache, then look up the remainder separately
478
            $uncachedIDs = array_keys(array_diff_key($result, $cachePermissions[$cacheKey]));
479
            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...
480
                $uncachedValues = $this->canDeleteMultiple($uncachedIDs, $member, false);
481
                return $cachedValues + $uncachedValues;
482
            }
483
            return $cachedValues;
484
        }
485
486
        // You can only delete pages that you can edit
487
        $editableIDs = array_keys(array_filter($this->canEditMultiple($ids, $member)));
488
        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...
489
            // You can only delete pages whose children you can delete
490
            $childRecords = DataObject::get($this->baseClass)
491
                ->filter('ParentID', $editableIDs);
492
493
            // Find out the children that can be deleted
494
            $children = $childRecords->map("ID", "ParentID");
495
            $childIDs = $children->keys();
496
            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...
497
                $deletableChildren = $this->canDeleteMultiple($childIDs, $member);
498
499
                // Get a list of all the parents that have no undeletable children
500
                $deletableParents = array_fill_keys($editableIDs, true);
501
                foreach ($deletableChildren as $id => $canDelete) {
502
                    if (!$canDelete) {
503
                        unset($deletableParents[$children[$id]]);
504
                    }
505
                }
506
507
                // Use that to filter the list of deletable parents that have children
508
                $deletableParents = array_keys($deletableParents);
509
510
                // Also get the $ids that don't have children
511
                $parents = array_unique($children->values());
512
                $deletableLeafNodes = array_diff($editableIDs, $parents);
513
514
                // Combine the two
515
                $deletable = array_merge($deletableParents, $deletableLeafNodes);
516
            } else {
517
                $deletable = $editableIDs;
518
            }
519
        }
520
521
        // Convert the array of deletable IDs into a map of the original IDs with true/false as the value
522
        return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
523
    }
524
525
    /**
526
     * @param int $id
527
     * @param Member|null $member
528
     * @return bool|mixed
529
     */
530
    public function canDelete($id, Member $member = null)
531
    {
532
        // No ID: Check default permission
533
        if (!$id) {
534
            return $this->checkDefaultPermissions(self::DELETE, $member);
535
        }
536
537
        // Regular canEdit logic is handled by canEditMultiple
538
        $results = $this->canDeleteMultiple(
539
            [$id],
540
            $member
541
        );
542
543
        // Check if in result
544
        return isset($results[$id]) ? $results[$id] : false;
545
    }
546
547
    /**
548
     * @param int $id
549
     * @param Member|null $member
550
     * @return bool|mixed
551
     */
552
    public function canEdit($id, Member $member = null)
553
    {
554
        // No ID: Check default permission
555
        if (!$id) {
556
            return $this->checkDefaultPermissions(self::EDIT, $member);
557
        }
558
559
        // Regular canEdit logic is handled by canEditMultiple
560
        $results = $this->canEditMultiple(
561
            [$id],
562
            $member
563
        );
564
565
        // Check if in result
566
        return isset($results[$id]) ? $results[$id] : false;
567
    }
568
569
    /**
570
     * @param int $id
571
     * @param Member|null $member
572
     * @return bool|mixed
573
     */
574
    public function canView($id, Member $member = null)
575
    {
576
        // No ID: Check default permission
577
        if (!$id) {
578
            return $this->checkDefaultPermissions(self::VIEW, $member);
579
        }
580
581
        // Regular canView logic is handled by canViewMultiple
582
        $results = $this->canViewMultiple(
583
            [$id],
584
            $member
585
        );
586
587
        // Check if in result
588
        return isset($results[$id]) ? $results[$id] : false;
589
    }
590
591
    /**
592
     * Get field to check for permission type for the given check.
593
     * Defaults to those provided by {@see InheritedPermissionsExtension)
594
     *
595
     * @param string $type
596
     * @return string
597
     */
598
    protected function getPermissionField($type)
599
    {
600
        switch ($type) {
601
            case self::DELETE:
602
                // Delete uses edit type - Drop through
603
            case self::EDIT:
604
                return 'CanEditType';
605
            case self::VIEW:
606
                return 'CanViewType';
607
            default:
608
                throw new InvalidArgumentException("Invalid argument type $type");
609
        }
610
    }
611
612
    /**
613
     * Get join table for type
614
     * Defaults to those provided by {@see InheritedPermissionsExtension)
615
     *
616
     * @param string $type
617
     * @return string
618
     */
619
    protected function getJoinTable($type)
620
    {
621
        switch ($type) {
622
            case self::DELETE:
623
                // Delete uses edit type - Drop through
624
            case self::EDIT:
625
                return $this->getEditorGroupsTable();
626
            case self::VIEW:
627
                return $this->getViewerGroupsTable();
628
            default:
629
                throw new InvalidArgumentException("Invalid argument type $type");
630
        }
631
    }
632
633
    /**
634
     * Determine default permission for a givion check
635
     *
636
     * @param string $type Method to check
637
     * @param Member $member
638
     * @return bool
639
     */
640
    protected function checkDefaultPermissions($type, Member $member = null)
641
    {
642
        $defaultPermissions = $this->getDefaultPermissions();
643
        if (!$defaultPermissions) {
0 ignored issues
show
introduced by
$defaultPermissions is of type SilverStripe\Security\DefaultPermissionChecker, thus it always evaluated to true.
Loading history...
644
            return false;
645
        }
646
        switch ($type) {
647
            case self::VIEW:
648
                return $defaultPermissions->canView($member);
649
            case self::EDIT:
650
                return $defaultPermissions->canEdit($member);
651
            case self::DELETE:
652
                return $defaultPermissions->canDelete($member);
653
            default:
654
                return false;
655
        }
656
    }
657
658
    /**
659
     * Check if this model has versioning
660
     *
661
     * @return bool
662
     */
663
    protected function isVersioned()
664
    {
665
        if (!class_exists(Versioned::class)) {
666
            return false;
667
        }
668
        /** @var Versioned|DataObject $singleton */
669
        $singleton = DataObject::singleton($this->getBaseClass());
670
        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

670
        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

670
        return $singleton->hasExtension(Versioned::class) && $singleton->/** @scrutinizer ignore-call */ hasStages();
Loading history...
671
    }
672
673
    /**
674
     * @return $this
675
     */
676
    public function clearCache()
677
    {
678
        $this->cachePermissions = [];
679
        return $this;
680
    }
681
682
    /**
683
     * Get table to use for editor groups relation
684
     *
685
     * @return string
686
     */
687
    protected function getEditorGroupsTable()
688
    {
689
        $table = DataObject::getSchema()->tableName($this->baseClass);
690
        return "{$table}_EditorGroups";
691
    }
692
693
    /**
694
     * Get table to use for viewer groups relation
695
     *
696
     * @return string
697
     */
698
    protected function getViewerGroupsTable()
699
    {
700
        $table = DataObject::getSchema()->tableName($this->baseClass);
701
        return "{$table}_ViewerGroups";
702
    }
703
704
    /**
705
     * Gets the permission from cache
706
     *
707
     * @param string $cacheKey
708
     * @return mixed
709
     */
710
    protected function getCachePermissions($cacheKey)
711
    {
712
        // Check local cache
713
        if (isset($this->cachePermissions[$cacheKey])) {
714
            return $this->cachePermissions[$cacheKey];
715
        }
716
717
        // Check persistent cache
718
        if ($this->cacheService) {
719
            $result = $this->cacheService->get($cacheKey);
720
721
            // Warm local cache
722
            if ($result) {
723
                $this->cachePermissions[$cacheKey] = $result;
724
                return $result;
725
            }
726
        }
727
728
        return null;
729
    }
730
731
    /**
732
     * Creates a cache key for a member and type
733
     *
734
     * @param string $type
735
     * @param int $memberID
736
     * @return string
737
     */
738
    protected function generateCacheKey($type, $memberID)
739
    {
740
        return "{$type}-{$memberID}";
741
    }
742
}
743