diff --git a/_config/config.yml b/_config/config.yml index 553ede50..5a8444be 100644 --- a/_config/config.yml +++ b/_config/config.yml @@ -1,6 +1,13 @@ --- Name: linkfield --- +# Adding this here for simplicity for testing the PR, but +# we'd have to make a determination as to whether this is +# automatically applied, or is documented as necessary +# for has_many links only. +SilverStripe\ORM\DataObject: + extensions: + - SilverStripe\LinkField\Extensions\DataObjectWithLinksExtension SilverStripe\Admin\LeftAndMain: extensions: diff --git a/src/Extensions/DataObjectWithLinksExtension.php b/src/Extensions/DataObjectWithLinksExtension.php new file mode 100644 index 00000000..14062794 --- /dev/null +++ b/src/Extensions/DataObjectWithLinksExtension.php @@ -0,0 +1,20 @@ +dataClass(), Link::class, true)) { + $list = $list->filter('OwnerRelation', $relation); + } + } +} diff --git a/src/Form/JsonField.php b/src/Form/JsonField.php index 83d433c9..5a891cd8 100644 --- a/src/Form/JsonField.php +++ b/src/Form/JsonField.php @@ -44,6 +44,10 @@ public function saveInto(DataObjectInterface $record) $dataValue = $this->dataValue(); $value = is_string($dataValue) ? $this->parseString($this->dataValue()) : $dataValue; + $value['OwnerID'] = $record->ID; + $value['OwnerClass'] = $record->ClassName; + $value['OwnerRelation'] = $this->getName(); + if ($class = DataObject::getSchema()->hasOneComponent(get_class($record), $fieldname)) { /** @var JsonData|DataObject $jsonDataObject */ diff --git a/src/Form/MultiLinkField.php b/src/Form/MultiLinkField.php index 8ae2cf17..c05a0146 100644 --- a/src/Form/MultiLinkField.php +++ b/src/Form/MultiLinkField.php @@ -9,7 +9,8 @@ use SilverStripe\ORM\SS_List; /** - * Allows CMS users to edit a list of links. + * Allows CMS users to edit a list of links in a has_many relation. + * Explicitly doesn't support many_many. */ class MultiLinkField extends JsonField { @@ -88,9 +89,17 @@ public function saveInto(DataObjectInterface $record) /** @var HasMany|Link[] $links */ if ($links = $record->$fieldname()) { + /** @var Link $linkDO */ foreach ($links as $linkDO) { $linkData = $this->shiftLinkDataByID($value, $linkDO->ID); if ($linkData) { + // @TODO move all the JSON stuff into the field. The model shouldn't care that + // the form field represents its data as JSON temporarily. + // We should just be calling $linkDO->update($data) here with the data already + // explicitly as an associative array. + // Also, I'm assuming this IS always an associative array, even though the Link + // model doesn't assume that. + $linkData['OwnerRelation'] = $this->getName(); $linkDO->setData($linkData); $linkDO->write(); } else { @@ -98,9 +107,11 @@ public function saveInto(DataObjectInterface $record) } } + // Guy's note: I'm assuming these are explicitly new, and above is explicitly existing links foreach ($value as $linkData) { unset($linkData['ID']); $linkDO = Link::create(); + $linkData['OwnerRelation'] = $this->getName(); $linkDO = $linkDO->setData($linkData); $links->add($linkDO); $linkDO->write(); diff --git a/src/Models/Link.php b/src/Models/Link.php index f31edfd9..8419b0aa 100644 --- a/src/Models/Link.php +++ b/src/Models/Link.php @@ -22,6 +22,9 @@ * A Link Data Object. This class should be a subclass, and 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 */ @@ -30,10 +33,16 @@ class Link extends DataObject implements JsonData, Type private static $table_name = 'LinkField_Link'; private static array $db = [ + 'OwnerRelation' => 'Varchar', 'Title' => 'Varchar', 'OpenInNew' => 'Boolean', ]; + private static $has_one = [ + // See also the OwnerRelation field added in $db + 'Owner' => DataObject::class + ]; + /** * In-memory only property used to change link type * This case is relevant for CMS edit form which doesn't use React driven UI @@ -41,10 +50,6 @@ class Link extends DataObject implements JsonData, Type */ private ?string $linkType = null; - private static $has_one = [ - 'Owner' => DataObject::class - ]; - private static $icon = 'link'; public function defineLinkTypeRequirements() @@ -292,4 +297,79 @@ protected function FallbackTitle(): string { return ''; } + + /** + * This is entirely optional but is something we have the power to do now. + * We can also do checks like this in onBeforeWrite for example. + */ + public function Owner() + { + $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 = []) + { + $delegateToExistingMethods = [ + 'view', + 'edit', + 'create', + 'delete', + ]; + + $owner = $this->Owner(); + if ($owner && $owner->exists() && !in_array(strtolower($perm), $delegateToExistingMethods)) { + return $owner->can($perm, $member, $context); + } + + return parent::can($perm, $member, $context); + } + + private function canPerformAction(string $canMethod, $member, $context = []) + { + $results = $this->extendedCan($canMethod, $member, $context); + if (isset($results)) { + return $results; + } + + $owner = $this->Owner(); + if ($owner && $owner->exists()) { + // Can delete or create links if you can edit its owner. + if ($canMethod === 'canView' ||$canMethod === 'canCreate' || $canMethod === 'canDelete') { + $canMethod = 'canEdit'; + } + return $owner->$canMethod($member, $context); + } + + return parent::$canMethod($member, $context); + } }