Source of file ChangeSetItem.php
Size: 17,832 Bytes - Last Modified: 2021-12-23T10:35:47+00:00
/var/www/docs.ssmods.com/process/src/src/ChangeSetItem.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565 | <?php namespace SilverStripe\Versioned; use BadMethodCallException; use InvalidArgumentException; use LogicException; use SilverStripe\Assets\Thumbnail; use SilverStripe\Control\Controller; use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\CMSPreviewable; use SilverStripe\ORM\DataList; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\ManyManyList; use SilverStripe\ORM\SS_List; use SilverStripe\ORM\UnexpectedDataException; use SilverStripe\Security\Member; use SilverStripe\Security\Permission; use SilverStripe\Security\Security; /** * A single line in a changeset * * @property string $Added * @property string $ObjectClass The _base_ data class for the referenced DataObject * @property int $ObjectID The numeric ID for the referenced object * @property int $ChangeSetID ID of parent ChangeSet object * @property int $VersionBefore * @property int $VersionAfter * @method ManyManyList ReferencedBy() List of explicit items that require this change * @method ManyManyList References() List of implicit items required by this change * @method ChangeSet ChangeSet() Parent changeset * @method DataObject Object() The object attached to this item */ class ChangeSetItem extends DataObject implements Thumbnail { const EXPLICITLY = 'explicitly'; const IMPLICITLY = 'implicitly'; /** Represents an object deleted */ const CHANGE_DELETED = 'deleted'; /** Represents an object which was modified */ const CHANGE_MODIFIED = 'modified'; /** Represents an object added */ const CHANGE_CREATED = 'created'; private static $table_name = 'ChangeSetItem'; /** * Represents that an object has not yet been changed, but * should be included in this changeset as soon as any changes exist. * Also used for unversioned objects that have no non-recursive publish. */ const CHANGE_NONE = 'none'; private static $db = [ 'VersionBefore' => 'Int', 'VersionAfter' => 'Int', 'Added' => "Enum('explicitly, implicitly', 'implicitly')" ]; private static $has_one = [ 'ChangeSet' => ChangeSet::class, 'Object' => DataObject::class, ]; private static $many_many = [ 'ReferencedBy' => ChangeSetItem::class, ]; private static $belongs_many_many = [ 'References' => ChangeSetItem::class . '.ReferencedBy', ]; private static $indexes = [ 'ObjectUniquePerChangeSet' => [ 'type' => 'unique', 'columns' => ['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 = $this->getSchema()->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 * @throws UnexpectedDataException */ public function getChangeType() { if (!class_exists($this->ObjectClass)) { throw new UnexpectedDataException("Invalid Class '{$this->ObjectClass}' in ChangeSetItem #{$this->ID}"); } // Unversioned classes have no change type if (!$this->isVersioned()) { return self::CHANGE_NONE; } // 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) { $type = self::CHANGE_NONE; } elseif (!$liveVersion) { $type = self::CHANGE_CREATED; } elseif (!$draftVersion) { $type = self::CHANGE_DELETED; } else { $type = self::CHANGE_MODIFIED; } $this->extend('updateChangeType', $type, $draftVersion, $liveVersion); return $type; } /** * Find version of this object in the given stage. * If the object isn't versioned it will return the normal record. * * @param string $stage * @return DataObject|Versioned|RecursivePublishable Object in this stage (may not be Versioned) * @throws UnexpectedDataException */ protected function getObjectInStage($stage) { if (!class_exists($this->ObjectClass)) { throw new UnexpectedDataException("Invalid Class '{$this->ObjectClass}' in ChangeSetItem #{$this->ID}"); } // Ignore stage for unversioned objects if (!$this->isVersioned()) { return DataObject::get_by_id($this->ObjectClass, $this->ObjectID); } // Get versioned object return Versioned::get_by_stage($this->ObjectClass, $stage)->byID($this->ObjectID); } /** * Find latest version of this object * @return DataObject|Versioned * @throws UnexpectedDataException */ protected function getObjectLatestVersion() { if (!class_exists($this->ObjectClass)) { throw new UnexpectedDataException("Invalid Class '{$this->ObjectClass}' in ChangeSetItem #{$this->ID}"); } // Ignore version for unversioned objects if (!$this->isVersioned()) { return DataObject::get_by_id($this->ObjectClass, $this->ObjectID); } // Get versioned object return Versioned::get_latest_version($this->ObjectClass, $this->ObjectID); } /** * Get all implicit objects for this change * * @return SS_List */ public function findReferenced() { $liveRecord = $this->getObjectInStage(Versioned::LIVE); // For unversioned objects, simply return all owned objects if (!$this->isVersioned()) { return $liveRecord->findOwned(); } // If we have deleted this record, recursively delete live objects on publish if ($this->getChangeType() === ChangeSetItem::CHANGE_DELETED) { if (!$liveRecord) { return ArrayList::create(); } return $liveRecord->findCascadeDeletes(true); } // If changed on stage, include all owned objects for publish /** @var DataObject|RecursivePublishable $draftRecord */ $draftRecord = $this->getObjectInStage(Versioned::DRAFT); if (!$draftRecord) { return ArrayList::create(); } $references = $draftRecord->findOwned(); // When publishing, use cascade_deletes to partially unpublished sets if ($liveRecord) { foreach ($liveRecord->findCascadeDeletes(true) as $next) { /** @var Versioned|DataObject $next */ if ($next->hasExtension(Versioned::class) && $next->hasStages() && $next->isOnLiveOnly()) { $this->mergeRelatedObject($references, ArrayList::create(), $next); } } } return $references; } /** * Publish this item, then close it. * * Note: Unlike Versioned::doPublish() and Versioned::doUnpublish, this action is not recursive. */ public function publish() { if (!class_exists($this->ObjectClass)) { throw new UnexpectedDataException("Invalid Class '{$this->ObjectClass}' in ChangeSetItem #{$this->ID}"); } // Logical checks prior to publish if ($this->VersionBefore || $this->VersionAfter) { throw new BadMethodCallException("This ChangeSetItem has already been published"); } // Skip unversioned records if (!$this->isVersioned()) { $this->VersionBefore = 0; $this->VersionAfter = 0; $this->write(); return; } // 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 ); // Enact change $changeType = $this->getChangeType(); switch ($changeType) { 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(); // Point after version to the published version actually created, not the // version copied from draft. $this->VersionAfter = Versioned::get_versionnumber_by_stage( $this->ObjectClass, Versioned::LIVE, $this->ObjectID, false ); break; } default: throw new LogicException("Invalid change type: {$changeType}"); } $this->write(); } /** * Once this item (and all owned objects) are published, unlink * all disowned objects */ public function unlinkDisownedObjects() { $object = $this->getObjectInStage(Versioned::DRAFT); if ($object) { $object->unlinkDisownedObjects($object, Versioned::LIVE); } } /** Reverts this item, then close it. **/ public function revert() { throw new \RuntimeException('Not implemented'); } 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 = []) { 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) { // No action for unversiond objects so no action to deny if (!$this->isVersioned()) { return true; } // 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 return $object->canDelete($member); } default: { // All other actions are typically editing draft stage return $object->canEdit($member); } } } /** * Check if this ChangeSetItem can be published * * @param Member $member * @return bool */ public function canPublish($member = null) { // No action for unversiond objects so no action to deny // Implicitly added items allow publish if (!$this->isVersioned() || $this->Added === self::IMPLICITLY) { return true; } // 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) { return $object->canUnpublish($member); } break; } default: { /** @var Versioned|DataObject $object */ $object = Versioned::get_by_stage($this->ObjectClass, Versioned::DRAFT)->byID($this->ObjectID); if ($object) { return $object->canPublish($member); } break; } } return true; } /** * Determine if this item has changes * * @return bool */ public function hasChange() { return $this->getChangeType() !== ChangeSetItem::CHANGE_NONE; } /** * Default permissions for this ChangeSetItem * * @param string $perm * @param Member $member * @param array $context * @return bool */ public function can($perm, $member = null, $context = []) { if (!$member) { $member = Security::getCurrentUser(); } // 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()->get('required_permission')); } /** * Get the ChangeSetItems that reference a passed DataObject * * @param DataObject $object * @return DataList */ public static function get_for_object($object) { // Capture changesetitem for both changed and deleted objects $id = $object->isInDB() ? $object->ID : $object->OldID; return static::get_for_object_by_id($id, $object->baseClass()); } /** * Get the ChangeSetItems that reference a passed DataObject * * @param int $objectID The ID of the object * @param string $objectClass The class of the object (or any parent class) * @return DataList */ public static function get_for_object_by_id($objectID, $objectClass) { if (!$objectID) { throw new InvalidArgumentException("Cannot get ChangesetItem for object which was never saved"); } return ChangeSetItem::get()->filter([ 'ObjectID' => $objectID, 'ObjectClass' => static::getSchema()->baseDataClass($objectClass) ]); } /** * Gets the list of modes this record can be previewed in. * * {@link https://tools.ietf.org/html/draft-kelly-json-hal-07#section-5} * * @return array Map of links in acceptable HAL format */ public function getPreviewLinks() { $links = []; // Preview draft $stage = $this->getObjectInStage(Versioned::DRAFT); if ($stage instanceof CMSPreviewable && $stage->canView() && ($link = $stage->PreviewLink())) { $links[Versioned::DRAFT] = [ 'href' => Controller::join_links($link, '?stage=' . Versioned::DRAFT), 'type' => $stage->getMimeType(), ]; } // Preview live if versioned if ($this->isVersioned()) { $live = $this->getObjectInStage(Versioned::LIVE); if ($live instanceof CMSPreviewable && $live->canView() && ($link = $live->PreviewLink())) { $links[Versioned::LIVE] = [ 'href' => Controller::join_links($link, '?stage=' . Versioned::LIVE), 'type' => $live->getMimeType(), ]; } } return $links; } /** * Get edit link for this item * * @return string */ public function CMSEditLink() { $link = $this->getObjectInStage(Versioned::DRAFT); if ($link instanceof CMSPreviewable) { return $link->CMSEditLink(); } return null; } /** * Check if the object attached to this changesetitem is versionable * * @return bool */ public function isVersioned() { if (!$this->ObjectClass || !class_exists($this->ObjectClass)) { return false; } /** @var Versioned|DataObject $singleton */ $singleton = DataObject::singleton($this->ObjectClass); return $singleton->hasExtension(Versioned::class) && $singleton->hasStages(); } } |