'Int', 'VersionAfter' => 'Int', 'Added' => "Enum('explicitly, implicitly', 'implicitly')" ); private static $has_one = array( 'ChangeSet' => 'ChangeSet', 'Object' => 'DataObject', ); private static $many_many = array( 'ReferencedBy' => 'ChangeSetItem' ); private static $belongs_many_many = array( 'References' => 'ChangeSetItem.ReferencedBy' ); private static $indexes = array( 'ObjectUniquePerChangeSet' => array( 'type' => 'unique', 'value' => '"ObjectID", "ObjectClass", "ChangeSetID"' ) ); public function onBeforeWrite() { // Make sure ObjectClass refers to the base data class in the case of old or wrong code $this->ObjectClass = ClassInfo::baseDataClass($this->ObjectClass); parent::onBeforeWrite(); } public function getTitle() { // Get title of modified object $object = $this->getObjectLatestVersion(); if($object) { return $object->getTitle(); } return $this->i18n_singular_name() . ' #' . $this->ID; } /** * Get a thumbnail for this object * * @param int $width Preferred width of the thumbnail * @param int $height Preferred height of the thumbnail * @return string URL to the thumbnail, if available */ public function ThumbnailURL($width, $height) { $object = $this->getObjectLatestVersion(); if($object instanceof Thumbnail) { return $object->ThumbnailURL($width, $height); } return null; } /** * Get the type of change: none, created, deleted, modified, manymany * * @return string */ public function getChangeType() { // Get change versions if($this->VersionBefore || $this->VersionAfter) { $draftVersion = $this->VersionAfter; // After publishing draft was written to stage $liveVersion = $this->VersionBefore; // The live version before the publish } else { $draftVersion = Versioned::get_versionnumber_by_stage( $this->ObjectClass, Versioned::DRAFT, $this->ObjectID, false ); $liveVersion = Versioned::get_versionnumber_by_stage( $this->ObjectClass, Versioned::LIVE, $this->ObjectID, false ); } // Version comparisons if ($draftVersion == $liveVersion) { return self::CHANGE_NONE; } elseif (!$liveVersion) { return self::CHANGE_CREATED; } elseif (!$draftVersion) { return self::CHANGE_DELETED; } else { return self::CHANGE_MODIFIED; } } /** * Find version of this object in the given stage * * @param string $stage * @return Versioned|DataObject */ protected function getObjectInStage($stage) { return Versioned::get_by_stage($this->ObjectClass, $stage)->byID($this->ObjectID); } /** * Find latest version of this object * * @return Versioned|DataObject */ protected function getObjectLatestVersion() { return Versioned::get_latest_version($this->ObjectClass, $this->ObjectID); } /** * Get all implicit objects for this change * * @return SS_List */ public function findReferenced() { if($this->getChangeType() === ChangeSetItem::CHANGE_DELETED) { // If deleted from stage, need to look at live record return $this->getObjectInStage(Versioned::LIVE)->findOwners(false); } else { // If changed on stage, look at owned objects there return $this->getObjectInStage(Versioned::DRAFT)->findOwned()->filterByCallback(function ($owned) { /** @var Versioned|DataObject $owned */ return $owned->stagesDiffer(Versioned::DRAFT, Versioned::LIVE); }); } } /** * Publish this item, then close it. * * Note: Unlike Versioned::doPublish() and Versioned::doUnpublish, this action is not recursive. */ public function publish() { // Logical checks prior to publish if(!$this->canPublish()) { throw new Exception("The current member does not have permission to publish this ChangeSetItem."); } if($this->VersionBefore || $this->VersionAfter) { throw new BadMethodCallException("This ChangeSetItem has already been published"); } // Record state changed $this->VersionAfter = Versioned::get_versionnumber_by_stage( $this->ObjectClass, Versioned::DRAFT, $this->ObjectID, false ); $this->VersionBefore = Versioned::get_versionnumber_by_stage( $this->ObjectClass, Versioned::LIVE, $this->ObjectID, false ); switch($this->getChangeType()) { case static::CHANGE_NONE: { break; } case static::CHANGE_DELETED: { // Non-recursive delete $object = $this->getObjectInStage(Versioned::LIVE); $object->deleteFromStage(Versioned::LIVE); break; } case static::CHANGE_MODIFIED: case static::CHANGE_CREATED: { // Non-recursive publish $object = $this->getObjectInStage(Versioned::DRAFT); $object->publishSingle(); break; } } $this->write(); } /** Reverts this item, then close it. **/ public function revert() { user_error('Not implemented', E_USER_ERROR); } public function canView($member = null) { return $this->can(__FUNCTION__, $member); } public function canEdit($member = null) { return $this->can(__FUNCTION__, $member); } public function canCreate($member = null, $context = array()) { return $this->can(__FUNCTION__, $member, $context); } public function canDelete($member = null) { return $this->can(__FUNCTION__, $member); } /** * Check if the BeforeVersion of this changeset can be restored to draft * * @param Member $member * @return bool */ public function canRevert($member) { // Just get the best version as this object may not even exist on either stage anymore. /** @var Versioned|DataObject $object */ $object = $this->getObjectLatestVersion(); if(!$object) { return false; } // Check change type switch($this->getChangeType()) { case static::CHANGE_CREATED: { // Revert creation by deleting from stage if(!$object->canDelete($member)) { return false; } break; } default: { // All other actions are typically editing draft stage if(!$object->canEdit($member)) { return false; } break; } } // If object can be published/unpublished let extensions deny return $this->can(__FUNCTION__, $member); } /** * Check if this ChangeSetItem can be published * * @param Member $member * @return bool */ public function canPublish($member = null) { // Check canMethod to invoke on object switch($this->getChangeType()) { case static::CHANGE_DELETED: { /** @var Versioned|DataObject $object */ $object = Versioned::get_by_stage($this->ObjectClass, Versioned::LIVE)->byID($this->ObjectID); if(!$object || !$object->canUnpublish($member)) { return false; } break; } default: { /** @var Versioned|DataObject $object */ $object = Versioned::get_by_stage($this->ObjectClass, Versioned::DRAFT)->byID($this->ObjectID); if(!$object || !$object->canPublish($member)) { return false; } break; } } // If object can be published/unpublished let extensions deny return $this->can(__FUNCTION__, $member); } /** * Default permissions for this ChangeSetItem * * @param string $perm * @param Member $member * @param array $context * @return bool */ public function can($perm, $member = null, $context = array()) { if(!$member) { $member = Member::currentUser(); } // Allow extensions to bypass default permissions, but only if // each change can be individually published. $extended = $this->extendedCan($perm, $member, $context); if($extended !== null) { return $extended; } // Default permissions return (bool)Permission::checkMember($member, ChangeSet::config()->required_permission); } }