Skip to content

Commit

Permalink
API Add new Owner relation for handling permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
GuySartorelli committed Dec 5, 2023
1 parent 9debde6 commit cd50e9b
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 5 deletions.
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class Page extends SiteTree
];

private static $has_many = [
'HasManyLinks' => Link::class
'HasManyLinks' => Link::class . '.Owner',
];

public function getCMSFields()
Expand All @@ -63,8 +63,6 @@ class Page extends SiteTree
}
```

Note that you also need to add a `has_one` relation on the `Link` model to match your `has_many` here. See [official docs about `has_many`](https://docs.silverstripe.org/en/developer_guides/model/relations/#has-many)

## Migrating from Shae Dawson's Linkable module

https://github.com/sheadawson/silverstripe-linkable
Expand Down
6 changes: 6 additions & 0 deletions src/Models/FileLink.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
use SilverStripe\Assets\File;
use SilverStripe\Forms\FieldList;

/**
* A link to a File in the CMS
*
* @property int $FileID
* @method File File()
*/
class FileLink extends Link
{
private static string $table_name = 'LinkField_FileLink';
Expand Down
91 changes: 89 additions & 2 deletions src/Models/Link.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@
use SilverStripe\Forms\RequiredFields;
use SilverStripe\LinkField\Type\Registry;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataObjectSchema;
use SilverStripe\ORM\FieldType\DBHTMLText;

/**
* A Link Data Object. This class should be a subclass, and you should never directly interact with a plain Link
* instance
* A Link Data Object. This class should be treated as abstract. You should never directly interact with a plain Link
* instance.
*
* Note that links should be added via a has_one or has_many relation, NEVER a many_many relation. This is because
* some functionality such as the can* methods rely on having a single Owner.
*
* @property string $Title
* @property bool $OpenInNew
Expand All @@ -30,6 +34,17 @@ class Link extends DataObject
'OpenInNew' => 'Boolean',
];

private static array $has_one = [
// Note that this handles one-to-many relations AND one-to-one relations.
// Any has_one pointing at Link will be intentionally double handled - this allows us to use the owner
// for permission checks and to link back to the owner from reports, etc.
// See also the Owner method.
'Owner' => [
'class' => DataObject::class,
DataObjectSchema::HASONE_IS_MULTIRECIPROCAL => true,
],
];

/**
* In-memory only property used to change link type
* This case is relevant for CMS edit form which doesn't use React driven UI
Expand Down Expand Up @@ -270,6 +285,78 @@ public function getURL(): string
return '';
}

/**
* Get the owner of this link, if there is one.
*
* Returns null if the reciprocal relation is a has_one which no longer contains this link.
*/
public function Owner(): ?DataObject
{
$owner = $this->getComponent('Owner');
// Since the has_one is being stored in two places, double check the owner
// actually still owns this record. If not, return null.
$ownerRelationType = $owner->getRelationType($this->OwnerRelation);
if ($ownerRelationType === 'has_one') {
$idField = "{$this->OwnerRelation}ID";
if ($owner->$idField !== $this->ID) {
return null;
}
}
return $owner;
}


public function canView($member = null)
{
return $this->canPerformAction(__FUNCTION__, $member);
}

public function canEdit($member = null)
{
return $this->canPerformAction(__FUNCTION__, $member);
}

public function canDelete($member = null, $context = [])
{
return $this->canPerformAction(__FUNCTION__, $member);
}

public function canCreate($member = null, $context = [])
{
return $this->canPerformAction(__FUNCTION__, $member);
}

public function can($perm, $member = null, $context = [])
{
$check = ucfirst(strtolower($perm));
return match ($check) {
'View', 'Create', 'Edit', 'Delete' => $this->{"can$check"}($member, $context),
default => parent::can($perm, $member, $context)
};
}

private function canPerformAction(string $canMethod, $member, $context = [])
{
// Allow extensions to override permission checks
$results = $this->extendedCan($canMethod, $member, $context);
if (isset($results)) {
return $results;
}

// If we have an owner, rely on it to tell us what we can and can't do
$owner = $this->Owner();
if ($owner && $owner->exists()) {
// Can delete or create links if you can edit its owner.
if ($canMethod === 'canCreate' || $canMethod === 'canDelete') {
$canMethod = 'canEdit';
}
return $owner->$canMethod($member, $context);
}

// Default to DataObject's permission checks
return parent::$canMethod($member, $context);
}

/**
* Get all link types except the generic one
*
Expand Down

0 comments on commit cd50e9b

Please sign in to comment.