Completed
Push — fix-2494 ( 3153ee )
by Sam
07:19
created

InheritedPermissions   C

Complexity

Total Complexity 78

Size/Duplication

Total Lines 595
Duplicated Lines 11.76 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
dl 70
loc 595
rs 5.4563
c 0
b 0
f 0
wmc 78
lcom 1
cbo 9

22 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 2
A setDefaultPermissions() 0 5 1
A setGlobalEditPermissions() 0 5 1
A getGlobalEditPermissions() 0 4 1
A getDefaultPermissions() 0 4 1
A getBaseClass() 0 4 1
A prePopulatePermissionCache() 0 16 4
D batchPermissionCheck() 11 91 17
C batchPermissionCheckForStage() 0 78 10
A canEditMultiple() 0 10 1
A canViewMultiple() 0 4 1
C canDeleteMultiple() 11 67 11
A canDelete() 16 16 3
A canEdit() 16 16 3
A canView() 16 16 3
A getPermissionField() 0 13 4
A getJoinTable() 0 13 4
B checkDefaultPermissions() 0 17 5
A isVersioned() 0 8 2
A clearCache() 0 5 1
A getEditorGroupsTable() 0 5 1
A getViewerGroupsTable() 0 5 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
12
/**
13
 * Calculates batch permissions for nested objects for:
14
 *  - canView: Supports 'Anyone' type
15
 *  - canEdit
16
 *  - canDelete: Includes special logic for ensuring parent objects can only be deleted if their children can
17
 *    be deleted also.
18
 */
19
class InheritedPermissions implements PermissionChecker
20
{
21
    use Injectable;
22
23
    /**
24
     * Delete permission
25
     */
26
    const DELETE = 'delete';
27
28
    /**
29
     * View permission
30
     */
31
    const VIEW = 'view';
32
33
    /**
34
     * Edit permission
35
     */
36
    const EDIT = 'edit';
37
38
    /**
39
     * Anyone canView permission
40
     */
41
    const ANYONE = 'Anyone';
42
43
    /**
44
     * Restrict to logged in users
45
     */
46
    const LOGGED_IN_USERS = 'LoggedInUsers';
47
48
    /**
49
     * Restrict to specific groups
50
     */
51
    const ONLY_THESE_USERS = 'OnlyTheseUsers';
52
53
    /**
54
     * Inherit from parent
55
     */
56
    const INHERIT = 'Inherit';
57
58
    /**
59
     * Class name
60
     *
61
     * @var string
62
     */
63
    protected $baseClass = null;
64
65
    /**
66
     * Object for evaluating top level permissions designed as "Inherit"
67
     *
68
     * @var DefaultPermissionChecker
69
     */
70
    protected $defaultPermissions = null;
71
72
    /**
73
     * Global permissions required to edit.
74
     * If empty no global permissions are required
75
     *
76
     * @var array
77
     */
78
    protected $globalEditPermissions = [];
79
80
    /**
81
     * Cache of permissions
82
     *
83
     * @var array
84
     */
85
    protected $cachePermissions = [];
86
87
    /**
88
     * Construct new permissions object
89
     *
90
     * @param string $baseClass Base class
91
     */
92
    public function __construct($baseClass)
93
    {
94
        if (!is_a($baseClass, DataObject::class, true)) {
95
            throw new InvalidArgumentException('Invalid DataObject class: ' . $baseClass);
96
        }
97
        $this->baseClass = $baseClass;
98
        return $this;
0 ignored issues
show
Bug introduced by
Constructors do not have meaningful return values, anything that is returned from here is discarded. Are you sure this is correct?
Loading history...
99
    }
100
101
    /**
102
     * @param DefaultPermissionChecker $callback
103
     * @return $this
104
     */
105
    public function setDefaultPermissions(DefaultPermissionChecker $callback)
106
    {
107
        $this->defaultPermissions = $callback;
108
        return $this;
109
    }
110
111
    /**
112
     * Global permissions required to edit
113
     *
114
     * @param array $permissions
115
     * @return $this
116
     */
117
    public function setGlobalEditPermissions($permissions)
118
    {
119
        $this->globalEditPermissions = $permissions;
120
        return $this;
121
    }
122
123
    /**
124
     * @return array
125
     */
126
    public function getGlobalEditPermissions()
127
    {
128
        return $this->globalEditPermissions;
129
    }
130
131
    /**
132
     * Get root permissions handler, or null if no handler
133
     *
134
     * @return DefaultPermissionChecker|null
135
     */
136
    public function getDefaultPermissions()
137
    {
138
        return $this->defaultPermissions;
139
    }
140
141
    /**
142
     * Get base class
143
     *
144
     * @return string
145
     */
146
    public function getBaseClass()
147
    {
148
        return $this->baseClass;
149
    }
150
151
    /**
152
     * Force pre-calculation of a list of permissions for optimisation
153
     *
154
     * @param string $permission
155
     * @param array $ids
156
     */
157
    public function prePopulatePermissionCache($permission = 'edit', $ids = [])
158
    {
159
        switch ($permission) {
160
            case self::EDIT:
161
                $this->canEditMultiple($ids, Member::currentUser(), false);
162
                break;
163
            case self::VIEW:
164
                $this->canViewMultiple($ids, Member::currentUser(), false);
165
                break;
166
            case self::DELETE:
167
                $this->canDeleteMultiple($ids, Member::currentUser(), false);
168
                break;
169
            default:
170
                throw new InvalidArgumentException("Invalid permission type $permission");
171
        }
172
    }
173
174
    /**
175
     * This method is NOT a full replacement for the individual can*() methods, e.g. {@link canEdit()}. Rather than
176
     * checking (potentially slow) PHP logic, it relies on the database group associations, e.g. the "CanEditType" field
177
     * plus the "SiteTree_EditorGroups" many-many table. By batch checking multiple records, we can combine the queries
178
     * efficiently.
179
     *
180
     * Caches based on $typeField data. To invalidate the cache, use {@link SiteTree::reset()} or set the $useCached
181
     * property to FALSE.
182
     *
183
     * @param string $type Either edit, view, or create
184
     * @param array $ids Array of IDs
185
     * @param Member $member Member
186
     * @param array $globalPermission If the member doesn't have this permission code, don't bother iterating deeper
187
     * @param bool $useCached Enables use of cache. Cache will be populated even if this is false.
188
     * @return array A map of permissions, keys are ID numbers, and values are boolean permission checks
189
     * ID keys to boolean values
190
     */
191
    protected function batchPermissionCheck(
192
        $type,
193
        $ids,
194
        Member $member = null,
195
        $globalPermission = [],
196
        $useCached = true
197
    ) {
198
        // Validate ids
199
        $ids = array_filter($ids, 'is_numeric');
200
        if (empty($ids)) {
201
            return [];
202
        }
203
204
        // Default result: nothing editable
205
        $result = array_fill_keys($ids, false);
206
207
        // Validate member permission
208
        // Only VIEW allows anonymous (Anyone) permissions
209
        $memberID = $member ? (int)$member->ID : 0;
210
        if (!$memberID && $type !== self::VIEW) {
211
            return $result;
212
        }
213
214
        // Look in the cache for values
215
        $cacheKey = "{$type}-{$memberID}";
216 View Code Duplication
        if ($useCached && isset($this->cachePermissions[$cacheKey])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
217
            $cachedValues = array_intersect_key($this->cachePermissions[$cacheKey], $result);
218
219
            // If we can't find everything in the cache, then look up the remainder separately
220
            $uncachedIDs = array_keys(array_diff_key($result, $this->cachePermissions[$cacheKey]));
221
            if ($uncachedIDs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $uncachedIDs of type array<integer|string> 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...
222
                $uncachedValues = $this->batchPermissionCheck($type, $uncachedIDs, $member, $globalPermission, false);
223
                return $cachedValues + $uncachedValues;
224
            }
225
            return $cachedValues;
226
        }
227
228
        // If a member doesn't have a certain permission then they can't edit anything
229
        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...
230
            return $result;
231
        }
232
233
        // Get the groups that the given member belongs to
234
        $groupIDsSQLList = '0';
235
        if ($memberID) {
236
            $groupIDs = $member->Groups()->column("ID");
0 ignored issues
show
Bug introduced by
It seems like $member is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
237
            $groupIDsSQLList = implode(", ", $groupIDs) ?: '0';
238
        }
239
240
        // Check if record is versioned
241
        if ($this->isVersioned()) {
242
            // Check all records for each stage and merge
243
            $combinedStageResult = [];
244
            foreach ([ Versioned::DRAFT, Versioned::LIVE ] as $stage) {
245
                $stageRecords = Versioned::get_by_stage($this->getBaseClass(), $stage)
246
                    ->byIDs($ids);
247
                // Exclude previously calculated records from later stage calculations
248
                if ($combinedStageResult) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $combinedStageResult 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...
249
                    $stageRecords = $stageRecords->exclude('ID', array_keys($combinedStageResult));
250
                }
251
                $stageResult = $this->batchPermissionCheckForStage(
252
                    $type,
253
                    $globalPermission,
254
                    $stageRecords,
255
                    $groupIDsSQLList,
256
                    $member
257
                );
258
                // Note: Draft stage takes precedence over live, but only if draft exists
259
                $combinedStageResult = $combinedStageResult + $stageResult;
260
            }
261
        } else {
262
            // Unstaged result
263
            $stageRecords = DataObject::get($this->getBaseClass())->byIDs($ids);
264
            $combinedStageResult = $this->batchPermissionCheckForStage(
265
                $type,
266
                $globalPermission,
267
                $stageRecords,
268
                $groupIDsSQLList,
269
                $member
270
            );
271
        }
272
273
        // Cache the results
274
        if (empty($this->cachePermissions[$cacheKey])) {
275
            $this->cachePermissions[$cacheKey] = [];
276
        }
277
        if ($combinedStageResult) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $combinedStageResult 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...
278
            $this->cachePermissions[$cacheKey] = $combinedStageResult + $this->cachePermissions[$cacheKey];
279
        }
280
        return $combinedStageResult;
281
    }
282
283
    /**
284
     * @param string $type
285
     * @param array $globalPermission List of global permissions
286
     * @param DataList $stageRecords List of records to check for this stage
287
     * @param string $groupIDsSQLList Group IDs this member belongs to
288
     * @param Member $member
289
     * @return array
290
     */
291
    protected function batchPermissionCheckForStage(
292
        $type,
293
        $globalPermission,
294
        DataList $stageRecords,
295
        $groupIDsSQLList,
296
        Member $member = null
297
    ) {
298
        // Initialise all IDs to false
299
        $result = array_fill_keys($stageRecords->column('ID'), false);
300
301
        // Get the uninherited permissions
302
        $typeField = $this->getPermissionField($type);
303
        if ($member && $member->ID) {
304
            // Determine if this member matches any of the group or other rules
305
            $groupJoinTable = $this->getJoinTable($type);
306
            $baseTable = DataObject::getSchema()->baseDataTable($this->getBaseClass());
307
            $uninheritedPermissions = $stageRecords
308
                ->where([
309
                    "(\"$typeField\" IN (?, ?) OR " .
310
                    "(\"$typeField\" = ? AND \"$groupJoinTable\".\"{$baseTable}ID\" IS NOT NULL))"
311
                    => [
312
                        self::ANYONE,
313
                        self::LOGGED_IN_USERS,
314
                        self::ONLY_THESE_USERS
315
                    ]
316
                ])
317
                ->leftJoin(
318
                    $groupJoinTable,
319
                    "\"$groupJoinTable\".\"{$baseTable}ID\" = \"{$baseTable}\".\"ID\" AND " .
320
                    "\"$groupJoinTable\".\"GroupID\" IN ($groupIDsSQLList)"
321
                )->column('ID');
322
        } else {
323
            // Only view pages with ViewType = Anyone if not logged in
324
            $uninheritedPermissions = $stageRecords
325
                ->filter($typeField, self::ANYONE)
326
                ->column('ID');
327
        }
328
329
        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...
330
            // Set all the relevant items in $result to true
331
            $result = array_fill_keys($uninheritedPermissions, true) + $result;
332
        }
333
334
        // Group $potentiallyInherited by ParentID; we'll look at the permission of all those parents and
335
        // then see which ones the user has permission on
336
        $groupedByParent = [];
337
        $potentiallyInherited = $stageRecords->filter($typeField, self::INHERIT);
338
        foreach ($potentiallyInherited as $item) {
339
            /** @var DataObject|Hierarchy $item */
340
            if ($item->ParentID) {
341
                if (!isset($groupedByParent[$item->ParentID])) {
342
                    $groupedByParent[$item->ParentID] = [];
343
                }
344
                $groupedByParent[$item->ParentID][] = $item->ID;
345
            } else {
346
                // Fail over to default permission check for Inherit and ParentID = 0
347
                $result[$item->ID] = $this->checkDefaultPermissions($type, $member);
348
            }
349
        }
350
351
        // Copy permissions from parent to child
352
        if ($groupedByParent) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $groupedByParent of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
353
            $actuallyInherited = $this->batchPermissionCheck(
354
                $type,
355
                array_keys($groupedByParent),
356
                $member,
357
                $globalPermission
358
            );
359
            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...
360
                $parentIDs = array_keys(array_filter($actuallyInherited));
361
                foreach ($parentIDs as $parentID) {
362
                    // Set all the relevant items in $result to true
363
                    $result = array_fill_keys($groupedByParent[$parentID], true) + $result;
364
                }
365
            }
366
        }
367
        return $result;
368
    }
369
370
    public function canEditMultiple($ids, Member $member = null, $useCached = true)
371
    {
372
        return $this->batchPermissionCheck(
373
            self::EDIT,
374
            $ids,
375
            $member,
376
            $this->getGlobalEditPermissions(),
377
            $useCached
378
        );
379
    }
380
381
    public function canViewMultiple($ids, Member $member = null, $useCached = true)
382
    {
383
        return $this->batchPermissionCheck(self::VIEW, $ids, $member, [], $useCached);
384
    }
385
386
    public function canDeleteMultiple($ids, Member $member = null, $useCached = true)
387
    {
388
        // Validate ids
389
        $ids = array_filter($ids, 'is_numeric');
390
        if (empty($ids)) {
391
            return [];
392
        }
393
        $result = array_fill_keys($ids, false);
394
395
        // Validate member permission
396
        if (!$member || !$member->ID) {
397
            return $result;
398
        }
399
        $deletable = [];
400
401
        // Look in the cache for values
402
        $cacheKey = "delete-{$member->ID}";
403 View Code Duplication
        if ($useCached && isset($this->cachePermissions[$cacheKey])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
404
            $cachedValues = array_intersect_key($this->cachePermissions[$cacheKey], $result);
405
406
            // If we can't find everything in the cache, then look up the remainder separately
407
            $uncachedIDs = array_keys(array_diff_key($result, $this->cachePermissions[$cacheKey]));
408
            if ($uncachedIDs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $uncachedIDs of type array<integer|string> 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...
409
                $uncachedValues = $this->canDeleteMultiple($uncachedIDs, $member, false);
410
                return $cachedValues + $uncachedValues;
411
            }
412
            return $cachedValues;
413
        }
414
415
        // You can only delete pages that you can edit
416
        $editableIDs = array_keys(array_filter($this->canEditMultiple($ids, $member)));
417
        if ($editableIDs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $editableIDs of type array<integer|string> 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...
418
            // You can only delete pages whose children you can delete
419
            $childRecords = DataObject::get($this->baseClass)
420
                ->filter('ParentID', $editableIDs);
421
422
            // Find out the children that can be deleted
423
            $children = $childRecords->map("ID", "ParentID");
424
            $childIDs = $children->keys();
425
            if ($childIDs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $childIDs of type array<integer|string> 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...
426
                $deletableChildren = $this->canDeleteMultiple($childIDs, $member);
427
428
                // Get a list of all the parents that have no undeletable children
429
                $deletableParents = array_fill_keys($editableIDs, true);
430
                foreach ($deletableChildren as $id => $canDelete) {
431
                    if (!$canDelete) {
432
                        unset($deletableParents[$children[$id]]);
433
                    }
434
                }
435
436
                // Use that to filter the list of deletable parents that have children
437
                $deletableParents = array_keys($deletableParents);
438
439
                // Also get the $ids that don't have children
440
                $parents = array_unique($children->values());
441
                $deletableLeafNodes = array_diff($editableIDs, $parents);
442
443
                // Combine the two
444
                $deletable = array_merge($deletableParents, $deletableLeafNodes);
445
            } else {
446
                $deletable = $editableIDs;
447
            }
448
        }
449
450
        // Convert the array of deletable IDs into a map of the original IDs with true/false as the value
451
        return array_fill_keys($deletable, true) + array_fill_keys($ids, false);
452
    }
453
454 View Code Duplication
    public function canDelete($id, Member $member = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
455
    {
456
        // No ID: Check default permission
457
        if (!$id) {
458
            return $this->checkDefaultPermissions(self::DELETE, $member);
459
        }
460
461
        // Regular canEdit logic is handled by canEditMultiple
462
        $results = $this->canDeleteMultiple(
463
            [ $id ],
464
            $member
465
        );
466
467
        // Check if in result
468
        return isset($results[$id]) ? $results[$id] : false;
469
    }
470
471 View Code Duplication
    public function canEdit($id, Member $member = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
472
    {
473
        // No ID: Check default permission
474
        if (!$id) {
475
            return $this->checkDefaultPermissions(self::EDIT, $member);
476
        }
477
478
        // Regular canEdit logic is handled by canEditMultiple
479
        $results = $this->canEditMultiple(
480
            [ $id ],
481
            $member
482
        );
483
484
        // Check if in result
485
        return isset($results[$id]) ? $results[$id] : false;
486
    }
487
488 View Code Duplication
    public function canView($id, Member $member = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
489
    {
490
        // No ID: Check default permission
491
        if (!$id) {
492
            return $this->checkDefaultPermissions(self::VIEW, $member);
493
        }
494
495
        // Regular canView logic is handled by canViewMultiple
496
        $results = $this->canViewMultiple(
497
            [ $id ],
498
            $member
499
        );
500
501
        // Check if in result
502
        return isset($results[$id]) ? $results[$id] : false;
503
    }
504
505
    /**
506
     * Get field to check for permission type for the given check.
507
     * Defaults to those provided by {@see InheritedPermissionsExtension)
508
     *
509
     * @param string $type
510
     * @return string
511
     */
512
    protected function getPermissionField($type)
513
    {
514
        switch ($type) {
515
            case self::DELETE:
516
                // Delete uses edit type - Drop through
517
            case self::EDIT:
518
                return 'CanEditType';
519
            case self::VIEW:
520
                return 'CanViewType';
521
            default:
522
                throw new InvalidArgumentException("Invalid argument type $type");
523
        }
524
    }
525
526
    /**
527
     * Get join table for type
528
     * Defaults to those provided by {@see InheritedPermissionsExtension)
529
     *
530
     * @param string $type
531
     * @return string
532
     */
533
    protected function getJoinTable($type)
534
    {
535
        switch ($type) {
536
            case self::DELETE:
537
                // Delete uses edit type - Drop through
538
            case self::EDIT:
539
                return $this->getEditorGroupsTable();
540
            case self::VIEW:
541
                return $this->getViewerGroupsTable();
542
            default:
543
                throw new InvalidArgumentException("Invalid argument type $type");
544
        }
545
    }
546
547
    /**
548
     * Determine default permission for a givion check
549
     *
550
     * @param string $type Method to check
551
     * @param Member $member
552
     * @return bool
553
     */
554
    protected function checkDefaultPermissions($type, Member $member = null)
555
    {
556
        $defaultPermissions = $this->getDefaultPermissions();
557
        if (!$defaultPermissions) {
558
            return false;
559
        }
560
        switch ($type) {
561
            case self::VIEW:
562
                return $defaultPermissions->canView($member);
563
            case self::EDIT:
564
                return $defaultPermissions->canEdit($member);
565
            case self::DELETE:
566
                return $defaultPermissions->canDelete($member);
567
            default:
568
                return false;
569
        }
570
    }
571
572
    /**
573
     * Check if this model has versioning
574
     *
575
     * @return bool
576
     */
577
    protected function isVersioned()
578
    {
579
        if (!class_exists(Versioned::class)) {
580
            return false;
581
        }
582
        $singleton = DataObject::singleton($this->getBaseClass());
583
        return $singleton->hasExtension(Versioned::class);
584
    }
585
586
    public function clearCache()
587
    {
588
        $this->cachePermissions = [];
589
        return $this;
590
    }
591
592
    /**
593
     * Get table to use for editor groups relation
594
     *
595
     * @return string
596
     */
597
    protected function getEditorGroupsTable()
598
    {
599
        $table = DataObject::getSchema()->tableName($this->baseClass);
600
        return "{$table}_EditorGroups";
601
    }
602
603
    /**
604
     * Get table to use for viewer groups relation
605
     *
606
     * @return string
607
     */
608
    protected function getViewerGroupsTable()
609
    {
610
        $table = DataObject::getSchema()->tableName($this->baseClass);
611
        return "{$table}_ViewerGroups";
612
    }
613
}
614