Add support for derived MCR slots
Bug: T277203 Change-Id: I1c70abc00c912b283e3e6eb2266633ae3f57673b
This commit is contained in:
parent
82053e3f78
commit
29862dd51d
14 changed files with 596 additions and 24 deletions
|
|
@ -67,6 +67,39 @@ class MutableRevisionRecord extends RevisionRecord {
|
|||
return $rev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a MutableRevisionRecord which is an updated version of $revision with $slots
|
||||
* added.
|
||||
* @param RevisionRecord $revision
|
||||
* @param SlotRecord[] $slots
|
||||
* @return MutableRevisionRecord
|
||||
* @since 1.36
|
||||
*/
|
||||
public static function newUpdatedRevisionRecord(
|
||||
RevisionRecord $revision,
|
||||
array $slots
|
||||
): MutableRevisionRecord {
|
||||
$newRevisionRecord = new MutableRevisionRecord(
|
||||
$revision->getPage(),
|
||||
$revision->getWikiId()
|
||||
);
|
||||
|
||||
$newRevisionRecord->setId( $revision->getId( $revision->getWikiId() ) );
|
||||
$newRevisionRecord->setPageId( $revision->getPageId( $revision->getWikiId() ) );
|
||||
$newRevisionRecord->setParentId( $revision->getParentId( $revision->getWikiId() ) );
|
||||
$newRevisionRecord->setUser( $revision->getUser() );
|
||||
|
||||
foreach ( $revision->getSlots()->getSlots() as $role => $slot ) {
|
||||
$newRevisionRecord->setSlot( $slot );
|
||||
}
|
||||
|
||||
foreach ( $slots as $role => $slot ) {
|
||||
$newRevisionRecord->setSlot( $slot );
|
||||
}
|
||||
|
||||
return $newRevisionRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* @stable to call.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -272,6 +272,16 @@ abstract class RevisionRecord implements WikiAwareEntity {
|
|||
return new RevisionSlots( $this->mSlots->getInheritedSlots() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns primary slots (those that are not derived).
|
||||
*
|
||||
* @return RevisionSlots
|
||||
* @since 1.36
|
||||
*/
|
||||
public function getPrimarySlots() : RevisionSlots {
|
||||
return new RevisionSlots( $this->mSlots->getPrimarySlots() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get revision ID. Depending on the concrete subclass, this may return null if
|
||||
* the revision ID is not known (e.g. because the revision does not yet exist
|
||||
|
|
|
|||
|
|
@ -142,13 +142,13 @@ class RevisionSlots {
|
|||
/**
|
||||
* Computes the total nominal size of the revision's slots, in bogo-bytes.
|
||||
*
|
||||
* @warning This is potentially expensive! It may cause all slot's content to be loaded
|
||||
* @warning This is potentially expensive! It may cause some slots' content to be loaded
|
||||
* and deserialized.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function computeSize() {
|
||||
return array_reduce( $this->getSlots(), static function ( $accu, SlotRecord $slot ) {
|
||||
return array_reduce( $this->getPrimarySlots(), static function ( $accu, SlotRecord $slot ) {
|
||||
return $accu + $slot->getSize();
|
||||
}, 0 );
|
||||
}
|
||||
|
|
@ -184,13 +184,13 @@ class RevisionSlots {
|
|||
* is that slot's hash. For consistency, the combined hash of an empty set of slots
|
||||
* is the hash of the empty string.
|
||||
*
|
||||
* @warning This is potentially expensive! It may cause all slot's content to be loaded
|
||||
* @warning This is potentially expensive! It may cause some slots' content to be loaded
|
||||
* and deserialized, then re-serialized and hashed.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function computeSha1() {
|
||||
$slots = $this->getSlots();
|
||||
$slots = $this->getPrimarySlots();
|
||||
ksort( $slots );
|
||||
|
||||
if ( empty( $slots ) ) {
|
||||
|
|
@ -238,6 +238,21 @@ class RevisionSlots {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all primary slots (those that are not derived).
|
||||
*
|
||||
* @return SlotRecord[]
|
||||
* @since 1.36
|
||||
*/
|
||||
public function getPrimarySlots() : array {
|
||||
return array_filter(
|
||||
$this->getSlots(),
|
||||
static function ( SlotRecord $slot ) {
|
||||
return !$slot->isDerived();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the other RevisionSlots instance has the same content
|
||||
* as this instance. Note that this does not mean that the slots have to be the same:
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ use DBAccessObjectUtils;
|
|||
use FallbackContent;
|
||||
use IDBAccessObject;
|
||||
use InvalidArgumentException;
|
||||
use LogicException;
|
||||
use MediaWiki\Content\IContentHandlerFactory;
|
||||
use MediaWiki\DAO\WikiAwareEntity;
|
||||
use MediaWiki\HookContainer\HookContainer;
|
||||
|
|
@ -45,6 +46,7 @@ use MediaWiki\Permissions\Authority;
|
|||
use MediaWiki\Storage\BlobAccessException;
|
||||
use MediaWiki\Storage\BlobStore;
|
||||
use MediaWiki\Storage\NameTableStore;
|
||||
use MediaWiki\Storage\RevisionSlotsUpdate;
|
||||
use MediaWiki\Storage\SqlBlobStore;
|
||||
use MediaWiki\User\ActorStore;
|
||||
use MediaWiki\User\UserIdentity;
|
||||
|
|
@ -495,6 +497,98 @@ class RevisionStore
|
|||
return $rev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update derived slots in an existing revision into the database, returning the modified
|
||||
* slots on success.
|
||||
*
|
||||
* @param RevisionRecord $revision After this method returns, the $revision object will be
|
||||
* obsolete in that it does not have the new slots.
|
||||
* @param RevisionSlotsUpdate $revisionSlotsUpdate
|
||||
* @param IDatabase $dbw (master connection)
|
||||
*
|
||||
* @return SlotRecord[] the new slot records.
|
||||
* @internal
|
||||
*/
|
||||
public function updateSlotsOn(
|
||||
RevisionRecord $revision,
|
||||
RevisionSlotsUpdate $revisionSlotsUpdate,
|
||||
IDatabase $dbw
|
||||
) : array {
|
||||
$this->checkDatabaseDomain( $dbw );
|
||||
|
||||
// Make sure all modified and removed slots are derived slots
|
||||
foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) {
|
||||
Assert::precondition(
|
||||
$this->slotRoleRegistry->getRoleHandler( $role )->isDerived(),
|
||||
'Trying to modify a slot that is not derived'
|
||||
);
|
||||
}
|
||||
foreach ( $revisionSlotsUpdate->getRemovedRoles() as $role ) {
|
||||
$isDerived = $this->slotRoleRegistry->getRoleHandler( $role )->isDerived();
|
||||
Assert::precondition(
|
||||
$isDerived,
|
||||
'Trying to remove a slot that is not derived'
|
||||
);
|
||||
throw new LogicException( 'Removing derived slots is not yet implemented. See T277394.' );
|
||||
}
|
||||
|
||||
/** @var SlotRecord[] $slotRecords */
|
||||
$slotRecords = $dbw->doAtomicSection(
|
||||
__METHOD__,
|
||||
function ( IDatabase $dbw, $fname ) use (
|
||||
$revision,
|
||||
$revisionSlotsUpdate
|
||||
) {
|
||||
return $this->updateSlotsInternal(
|
||||
$revision,
|
||||
$revisionSlotsUpdate,
|
||||
$dbw
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
foreach ( $slotRecords as $role => $slot ) {
|
||||
Assert::postcondition(
|
||||
$slot->getContent() !== null,
|
||||
$role . ' slot must have content'
|
||||
);
|
||||
Assert::postcondition(
|
||||
$slot->hasRevision(),
|
||||
$role . ' slot must have a revision associated'
|
||||
);
|
||||
}
|
||||
|
||||
return $slotRecords;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param RevisionRecord $revision
|
||||
* @param RevisionSlotsUpdate $revisionSlotsUpdate
|
||||
* @param IDatabase $dbw
|
||||
* @return SlotRecord[]
|
||||
*/
|
||||
private function updateSlotsInternal(
|
||||
RevisionRecord $revision,
|
||||
RevisionSlotsUpdate $revisionSlotsUpdate,
|
||||
IDatabase $dbw
|
||||
) : array {
|
||||
$page = $revision->getPage();
|
||||
$revId = $revision->getId( $this->wikiId );
|
||||
$blobHints = [
|
||||
BlobStore::PAGE_HINT => $page->getId( $this->wikiId ),
|
||||
BlobStore::REVISION_HINT => $revId,
|
||||
BlobStore::PARENT_HINT => $revision->getParentId( $this->wikiId ),
|
||||
];
|
||||
|
||||
$newSlots = [];
|
||||
foreach ( $revisionSlotsUpdate->getModifiedRoles() as $role ) {
|
||||
$slot = $revisionSlotsUpdate->getModifiedSlot( $role );
|
||||
$newSlots[$role] = $this->insertSlotOn( $dbw, $revId, $slot, $page, $blobHints );
|
||||
}
|
||||
|
||||
return $newSlots;
|
||||
}
|
||||
|
||||
private function insertRevisionInternal(
|
||||
RevisionRecord $rev,
|
||||
IDatabase $dbw,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,11 @@ class SlotRecord {
|
|||
*/
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $derived;
|
||||
|
||||
/**
|
||||
* Returns a new SlotRecord just like the given $slot, except that calling getContent()
|
||||
* will fail with an exception.
|
||||
|
|
@ -70,6 +75,19 @@ class SlotRecord {
|
|||
} );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a SlotRecord for a derived slot.
|
||||
*
|
||||
* @param string $role
|
||||
* @param Content $content Initial content
|
||||
*
|
||||
* @return SlotRecord
|
||||
* @since 1.36
|
||||
*/
|
||||
public static function newDerived( string $role, Content $content ) {
|
||||
return self::newUnsaved( $role, $content, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new SlotRecord from an existing SlotRecord, overriding some fields.
|
||||
* The slot's content cannot be overwritten.
|
||||
|
|
@ -79,7 +97,7 @@ class SlotRecord {
|
|||
*
|
||||
* @return SlotRecord
|
||||
*/
|
||||
private static function newDerived( SlotRecord $slot, array $overrides = [] ) {
|
||||
private static function newFromSlotRecord( SlotRecord $slot, array $overrides = [] ) {
|
||||
$row = clone $slot->row;
|
||||
$row->slot_id = null; // never copy the row ID!
|
||||
|
||||
|
|
@ -87,7 +105,7 @@ class SlotRecord {
|
|||
$row->$key = $value;
|
||||
}
|
||||
|
||||
return new SlotRecord( $row, $slot->content );
|
||||
return new SlotRecord( $row, $slot->content, $slot->isDerived() );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -109,7 +127,7 @@ class SlotRecord {
|
|||
$slot->getAddress();
|
||||
|
||||
// NOTE: slot_origin and content_address are copied from $slot.
|
||||
return self::newDerived( $slot, [
|
||||
return self::newFromSlotRecord( $slot, [
|
||||
'slot_revision_id' => null,
|
||||
] );
|
||||
}
|
||||
|
|
@ -125,10 +143,10 @@ class SlotRecord {
|
|||
*
|
||||
* @param string $role
|
||||
* @param Content $content
|
||||
*
|
||||
* @param bool $derived
|
||||
* @return SlotRecord An incomplete proto-slot object, to be used with newSaved() later.
|
||||
*/
|
||||
public static function newUnsaved( $role, Content $content ) {
|
||||
public static function newUnsaved( $role, Content $content, bool $derived = false ) {
|
||||
Assert::parameterType( 'string', $role, '$role' );
|
||||
|
||||
$row = [
|
||||
|
|
@ -143,7 +161,7 @@ class SlotRecord {
|
|||
'model_name' => $content->getModel(),
|
||||
];
|
||||
|
||||
return new SlotRecord( (object)$row, $content );
|
||||
return new SlotRecord( (object)$row, $content, $derived );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -212,7 +230,7 @@ class SlotRecord {
|
|||
$origin = $revisionId;
|
||||
}
|
||||
|
||||
return self::newDerived( $protoSlot, [
|
||||
return self::newFromSlotRecord( $protoSlot, [
|
||||
'slot_revision_id' => $revisionId,
|
||||
'slot_content_id' => $contentId,
|
||||
'slot_origin' => $origin,
|
||||
|
|
@ -232,8 +250,13 @@ class SlotRecord {
|
|||
* callbacks here, for security reasons.
|
||||
* @param Content|callable $content The content object associated with the slot, or a
|
||||
* callback that will return that Content object, given this SlotRecord as a parameter.
|
||||
* @param bool $derived Is this handler for a derived slot? Derived slots allow information that
|
||||
* is derived from the content of a page to be stored even if it is generated
|
||||
* asynchronously or updated later. Their size is not included in the revision size,
|
||||
* their hash does not contribute to the revision hash, and updates are not included
|
||||
* in revision history.
|
||||
*/
|
||||
public function __construct( $row, $content ) {
|
||||
public function __construct( $row, $content, bool $derived = false ) {
|
||||
Assert::parameterType( \stdClass::class, $row, '$row' );
|
||||
Assert::parameterType( 'Content|callable', $content, '$content' );
|
||||
|
||||
|
|
@ -275,6 +298,7 @@ class SlotRecord {
|
|||
|
||||
$this->row = $row;
|
||||
$this->content = $content;
|
||||
$this->derived = $derived;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -652,6 +676,14 @@ class SlotRecord {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Is this a derived slot?
|
||||
* @since 1.36
|
||||
*/
|
||||
public function isDerived() : bool {
|
||||
return $this->derived;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -51,6 +51,11 @@ class SlotRoleHandler {
|
|||
'placement' => 'append'
|
||||
];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $derived;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
|
|
@ -65,11 +70,18 @@ class SlotRoleHandler {
|
|||
* implementation of isAllowedModel(), also the only content model allowed for the
|
||||
* slot. Subclasses may however handle default and allowed models differently.
|
||||
* @param string[] $layout Layout hints, for use by RevisionRenderer. See getOutputLayoutHints.
|
||||
* @param bool $derived Is this handler for a derived slot? Derived slots allow information that
|
||||
* is derived from the content of a page to be stored even if it is generated
|
||||
* asynchronously or updated later. Their size is not included in the revision size,
|
||||
* their hash does not contribute to the revision hash, and updates are not included
|
||||
* in revision history.
|
||||
* @since 1.36 optional $derived parameter added
|
||||
*/
|
||||
public function __construct( $role, $contentModel, $layout = [] ) {
|
||||
public function __construct( $role, $contentModel, $layout = [], bool $derived = false ) {
|
||||
$this->role = $role;
|
||||
$this->contentModel = $contentModel;
|
||||
$this->layout = array_merge( $this->layout, $layout );
|
||||
$this->derived = $derived;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -104,6 +116,14 @@ class SlotRoleHandler {
|
|||
return $this->layout;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool Is this a handler for a derived slot?
|
||||
* @since 1.36
|
||||
*/
|
||||
public function isDerived() : bool {
|
||||
return $this->derived;
|
||||
}
|
||||
|
||||
/**
|
||||
* The message key for the translation of the slot name.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -104,14 +104,16 @@ class SlotRoleRegistry {
|
|||
* for more information.
|
||||
* @param string $model A content model name, see ContentHandler
|
||||
* @param array $layout See SlotRoleHandler getOutputLayoutHints
|
||||
* @param bool $derived see SlotRoleHandler constructor
|
||||
* @since 1.36 optional $derived parameter added
|
||||
*/
|
||||
public function defineRoleWithModel( $role, $model, $layout = [] ) {
|
||||
public function defineRoleWithModel( $role, $model, $layout = [], bool $derived = false ) {
|
||||
$role = strtolower( $role );
|
||||
|
||||
$this->defineRole(
|
||||
$role,
|
||||
static function ( $role ) use ( $model, $layout ) {
|
||||
return new SlotRoleHandler( $role, $model, $layout );
|
||||
static function ( $role ) use ( $model, $layout, $derived ) {
|
||||
return new SlotRoleHandler( $role, $model, $layout, $derived );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -704,7 +704,9 @@ class PageUpdater {
|
|||
Assert::parameterType( 'integer', $flags, '$flags' );
|
||||
|
||||
if ( $this->wasCommitted() ) {
|
||||
throw new RuntimeException( 'saveRevision() has already been called on this PageUpdater!' );
|
||||
throw new RuntimeException(
|
||||
'saveRevision() or updateRevision() has already been called on this PageUpdater!'
|
||||
);
|
||||
}
|
||||
|
||||
// Low-level sanity check
|
||||
|
|
@ -831,6 +833,81 @@ class PageUpdater {
|
|||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates derived slots of an existing article. Does not update RC. Updates all necessary
|
||||
* caches, optionally via the deferred update array. This does not check user permissions.
|
||||
* Does not do a PST.
|
||||
*
|
||||
* Use isUnchanged(), wasSuccessful() and getStatus() to determine the outcome of the
|
||||
* revision update.
|
||||
*
|
||||
* @param int $revId
|
||||
* @since 1.36
|
||||
*/
|
||||
public function updateRevision( int $revId = 0 ) {
|
||||
if ( $this->wasCommitted() ) {
|
||||
throw new RuntimeException(
|
||||
'saveRevision() or updateRevision() has already been called on this PageUpdater!'
|
||||
);
|
||||
}
|
||||
|
||||
// Low-level sanity check
|
||||
if ( $this->getLinkTarget()->getText() === '' ) {
|
||||
throw new RuntimeException( 'Something is trying to edit an article with an empty title' );
|
||||
}
|
||||
|
||||
$status = Status::newGood();
|
||||
$this->checkAllRolesAllowed(
|
||||
$this->slotsUpdate->getModifiedRoles(),
|
||||
$status
|
||||
);
|
||||
$this->checkAllRolesDerived(
|
||||
$this->slotsUpdate->getModifiedRoles(),
|
||||
$status
|
||||
);
|
||||
$this->checkAllRolesDerived(
|
||||
$this->slotsUpdate->getRemovedRoles(),
|
||||
$status
|
||||
);
|
||||
|
||||
if ( $revId === 0 ) {
|
||||
$revision = $this->grabParentRevision();
|
||||
} else {
|
||||
$revision = $this->revisionStore->getRevisionById( $revId, RevisionStore::READ_LATEST );
|
||||
}
|
||||
if ( $revision === null ) {
|
||||
$status->fatal( 'edit-gone-missing' );
|
||||
}
|
||||
|
||||
if ( !$status->isOK() ) {
|
||||
$this->status = $status;
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the given content is allowed in the respective slots of this page
|
||||
foreach ( $this->slotsUpdate->getModifiedRoles() as $role ) {
|
||||
$slot = $this->slotsUpdate->getModifiedSlot( $role );
|
||||
$roleHandler = $this->slotRoleRegistry->getRoleHandler( $role );
|
||||
|
||||
if ( !$roleHandler->isAllowedModel( $slot->getModel(), $this->getTitle() ) ) {
|
||||
$contentHandler = $this->contentHandlerFactory
|
||||
->getContentHandler( $slot->getModel() );
|
||||
$this->status = Status::newFatal(
|
||||
'content-not-allowed-here',
|
||||
ContentHandler::getLocalizedName( $contentHandler->getModelID() ),
|
||||
$this->getTitle()->getPrefixedText(),
|
||||
wfMessage( $roleHandler->getNameMessageKey() )
|
||||
// TODO: defer message lookup to caller
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// do we need PST?
|
||||
|
||||
$this->status = $this->doUpdate( $this->performer->getUser(), $revision );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether saveRevision() has been called on this instance
|
||||
*
|
||||
|
|
@ -1008,10 +1085,72 @@ class PageUpdater {
|
|||
$this->editResult = $this->editResultBuilder->buildEditResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update derived slots in an existing revision. If the revision is the current revision,
|
||||
* this will update page_touched and trigger secondary updates.
|
||||
*
|
||||
* We do not have sufficient information to know whether to or how to update recentchanges
|
||||
* here, so, as opposed to doCreate(), updating recentchanges is left as the responsibility
|
||||
* of the caller.
|
||||
*
|
||||
* @param UserIdentity $user
|
||||
* @param RevisionRecord $revision
|
||||
* @return Status
|
||||
*/
|
||||
private function doUpdate( UserIdentity $user, RevisionRecord $revision ) : Status {
|
||||
$currentRevision = $this->grabParentRevision();
|
||||
if ( !$currentRevision ) {
|
||||
// Article gone missing
|
||||
return Status::newFatal( 'edit-gone-missing' );
|
||||
}
|
||||
|
||||
$dbw = $this->getDBConnectionRef( DB_MASTER );
|
||||
$dbw->startAtomic( __METHOD__ );
|
||||
|
||||
$slots = $this->revisionStore->updateslotsOn( $revision, $this->slotsUpdate, $dbw );
|
||||
|
||||
$dbw->endAtomic( __METHOD__ );
|
||||
|
||||
// Return the slots and revision to the caller
|
||||
$newRevisionRecord = MutableRevisionRecord::newUpdatedRevisionRecord( $revision, $slots );
|
||||
$status = Status::newGood( [
|
||||
'revision-record' => $newRevisionRecord,
|
||||
'slots' => $slots,
|
||||
] );
|
||||
|
||||
$isCurrent = $revision->getId( $this->getWikiId() ) ===
|
||||
$currentRevision->getId( $this->getWikiId() );
|
||||
|
||||
if ( $isCurrent ) {
|
||||
// Update page_touched
|
||||
$this->getTitle()->invalidateCache( $newRevisionRecord->getTimestamp() );
|
||||
|
||||
$this->buildEditResult( $newRevisionRecord, false );
|
||||
|
||||
// Do secondary updates once the main changes have been committed...
|
||||
$wikiPage = $this->getWikiPage(); // TODO: use for legacy hooks only!
|
||||
DeferredUpdates::addUpdate(
|
||||
$this->getAtomicSectionUpdate(
|
||||
$dbw,
|
||||
$wikiPage,
|
||||
$newRevisionRecord,
|
||||
$user,
|
||||
$revision->getComment(),
|
||||
EDIT_INTERNAL,
|
||||
$status,
|
||||
[ 'changed' => false, ]
|
||||
),
|
||||
DeferredUpdates::PRESEND
|
||||
);
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CommentStoreComment $summary The edit summary
|
||||
* @param UserIdentity $user The revision's author
|
||||
* @param int $flags EXIT_XXX constants
|
||||
* @param int $flags EDIT_XXX constants
|
||||
*
|
||||
* @throws MWException
|
||||
* @return Status
|
||||
|
|
@ -1193,7 +1332,7 @@ class PageUpdater {
|
|||
/**
|
||||
* @param CommentStoreComment $summary The edit summary
|
||||
* @param UserIdentity $user The revision's author
|
||||
* @param int $flags EXIT_XXX constants
|
||||
* @param int $flags EDIT_XXX constants
|
||||
*
|
||||
* @throws DBUnexpectedError
|
||||
* @throws MWException
|
||||
|
|
@ -1457,6 +1596,10 @@ class PageUpdater {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $roles
|
||||
* @param Status $status
|
||||
*/
|
||||
private function checkAllRolesAllowed( array $roles, Status $status ) {
|
||||
$allowedRoles = $this->getAllowedSlotRoles();
|
||||
|
||||
|
|
@ -1470,6 +1613,30 @@ class PageUpdater {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $roles
|
||||
* @param Status $status
|
||||
*/
|
||||
private function checkAllRolesDerived( array $roles, Status $status ) {
|
||||
$notDerived = array_filter(
|
||||
$roles,
|
||||
function ( $role ) {
|
||||
return !$this->slotRoleRegistry->getRoleHandler( $role )->isDerived();
|
||||
}
|
||||
);
|
||||
if ( $notDerived ) {
|
||||
$status->error(
|
||||
'edit-slots-not-derived',
|
||||
count( $notDerived ),
|
||||
implode( ', ', $notDerived )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $roles
|
||||
* @param Status $status
|
||||
*/
|
||||
private function checkNoRolesRequired( array $roles, Status $status ) {
|
||||
$requiredRoles = $this->getRequiredSlotRoles();
|
||||
|
||||
|
|
@ -1483,6 +1650,10 @@ class PageUpdater {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $roles
|
||||
* @param Status $status
|
||||
*/
|
||||
private function checkAllRequiredRoles( array $roles, Status $status ) {
|
||||
$requiredRoles = $this->getRequiredSlotRoles();
|
||||
|
||||
|
|
|
|||
|
|
@ -333,9 +333,9 @@ class DifferenceEngine extends ContextSource {
|
|||
return [];
|
||||
}
|
||||
|
||||
$newSlots = $this->mNewRevisionRecord->getSlots()->getSlots();
|
||||
$newSlots = $this->mNewRevisionRecord->getPrimarySlots()->getSlots();
|
||||
if ( $this->mOldRevisionRecord ) {
|
||||
$oldSlots = $this->mOldRevisionRecord->getSlots()->getSlots();
|
||||
$oldSlots = $this->mOldRevisionRecord->getPrimarySlots()->getSlots();
|
||||
} else {
|
||||
$oldSlots = [];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ class SlotRecordTest extends \MediaWikiIntegrationTestCase {
|
|||
$this->assertSame( 33, $record->getContentId() );
|
||||
$this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
|
||||
$this->assertSame( 'myRole', $record->getRole() );
|
||||
$this->assertFalse( $record->isDerived() );
|
||||
}
|
||||
|
||||
public function testConstructionDeferred() {
|
||||
|
|
@ -83,6 +84,7 @@ class SlotRecordTest extends \MediaWikiIntegrationTestCase {
|
|||
$this->assertSame( 'tt:456', $record->getAddress() );
|
||||
$this->assertSame( CONTENT_FORMAT_WIKITEXT, $record->getFormat() );
|
||||
$this->assertSame( 'myRole', $record->getRole() );
|
||||
$this->assertFalse( $record->isDerived() );
|
||||
}
|
||||
|
||||
public function testNewUnsaved() {
|
||||
|
|
@ -98,6 +100,7 @@ class SlotRecordTest extends \MediaWikiIntegrationTestCase {
|
|||
$this->assertNotEmpty( $record->getSha1() );
|
||||
$this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
|
||||
$this->assertSame( 'myRole', $record->getRole() );
|
||||
$this->assertFalse( $record->isDerived() );
|
||||
}
|
||||
|
||||
public function provideInvalidConstruction() {
|
||||
|
|
@ -205,6 +208,7 @@ class SlotRecordTest extends \MediaWikiIntegrationTestCase {
|
|||
$this->assertTrue( $inherited->isInherited() );
|
||||
$this->assertTrue( $inherited->hasOrigin() );
|
||||
$this->assertFalse( $inherited->hasRevision() );
|
||||
$this->assertFalse( $inherited->isDerived() );
|
||||
|
||||
// make sure we didn't mess with the internal state of $parent
|
||||
$this->assertFalse( $parent->isInherited() );
|
||||
|
|
@ -224,6 +228,7 @@ class SlotRecordTest extends \MediaWikiIntegrationTestCase {
|
|||
$this->assertTrue( $saved->isInherited() );
|
||||
$this->assertTrue( $saved->hasRevision() );
|
||||
$this->assertSame( 10, $saved->getRevision() );
|
||||
$this->assertFalse( $saved->isDerived() );
|
||||
|
||||
// make sure we didn't mess with the internal state of $parent or $inherited
|
||||
$this->assertSame( 7, $parent->getRevision() );
|
||||
|
|
@ -247,11 +252,13 @@ class SlotRecordTest extends \MediaWikiIntegrationTestCase {
|
|||
$this->assertSame( 'A', $saved->getContent()->getText() );
|
||||
$this->assertSame( 10, $saved->getRevision() );
|
||||
$this->assertSame( 10, $saved->getOrigin() );
|
||||
$this->assertFalse( $saved->isDerived() );
|
||||
|
||||
// make sure we didn't mess with the internal state of $unsaved
|
||||
$this->assertFalse( $unsaved->hasAddress() );
|
||||
$this->assertFalse( $unsaved->hasContentId() );
|
||||
$this->assertFalse( $unsaved->hasRevision() );
|
||||
$this->assertFalse( $unsaved->isDerived() );
|
||||
}
|
||||
|
||||
public function provideNewSaved_LogicException() {
|
||||
|
|
@ -412,4 +419,54 @@ class SlotRecordTest extends \MediaWikiIntegrationTestCase {
|
|||
$this->assertSame( $sameContent, $b->hasSameContent( $a ) );
|
||||
}
|
||||
|
||||
public function testNewDerived() {
|
||||
$this->getServiceContainer()->getSlotRoleRegistry()->defineRoleWithModel(
|
||||
'derivedslot',
|
||||
CONTENT_MODEL_WIKITEXT,
|
||||
[],
|
||||
true
|
||||
);
|
||||
$record = SlotRecord::newDerived( 'derivedslot', new WikitextContent( 'A' ) );
|
||||
|
||||
$this->assertFalse( $record->hasAddress() );
|
||||
$this->assertFalse( $record->hasContentId() );
|
||||
$this->assertFalse( $record->hasRevision() );
|
||||
$this->assertFalse( $record->isInherited() );
|
||||
$this->assertFalse( $record->hasOrigin() );
|
||||
$this->assertSame( 'A', $record->getContent()->getText() );
|
||||
$this->assertSame( 1, $record->getSize() );
|
||||
$this->assertNotEmpty( $record->getSha1() );
|
||||
$this->assertSame( CONTENT_MODEL_WIKITEXT, $record->getModel() );
|
||||
$this->assertSame( 'derivedslot', $record->getRole() );
|
||||
$this->assertTrue( $record->isDerived() );
|
||||
}
|
||||
|
||||
public function testCopyDerived() {
|
||||
$this->getServiceContainer()->getSlotRoleRegistry()->defineRoleWithModel(
|
||||
'derivedslot',
|
||||
CONTENT_MODEL_WIKITEXT,
|
||||
[],
|
||||
true
|
||||
);
|
||||
$unsaved = SlotRecord::newDerived( 'derivedslot', new WikitextContent( 'A' ) );
|
||||
$saved = SlotRecord::newSaved( 10, 20, 'theNewAddress', $unsaved );
|
||||
|
||||
$this->assertFalse( $saved->isInherited() );
|
||||
$this->assertTrue( $saved->hasOrigin() );
|
||||
$this->assertTrue( $saved->hasRevision() );
|
||||
$this->assertTrue( $saved->hasAddress() );
|
||||
$this->assertTrue( $saved->hasContentId() );
|
||||
$this->assertSame( 'theNewAddress', $saved->getAddress() );
|
||||
$this->assertSame( 20, $saved->getContentId() );
|
||||
$this->assertSame( 'A', $saved->getContent()->getText() );
|
||||
$this->assertSame( 10, $saved->getRevision() );
|
||||
$this->assertSame( 10, $saved->getOrigin() );
|
||||
$this->assertTrue( $saved->isDerived() );
|
||||
|
||||
// make sure we didn't mess with the internal state of $unsaved
|
||||
$this->assertFalse( $unsaved->hasAddress() );
|
||||
$this->assertFalse( $unsaved->hasContentId() );
|
||||
$this->assertFalse( $unsaved->hasRevision() );
|
||||
$this->assertTrue( $unsaved->isDerived() );
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use CommentStoreComment;
|
|||
use Content;
|
||||
use DeferredUpdates;
|
||||
use FormatJson;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use MediaWiki\Permissions\SimpleAuthority;
|
||||
use MediaWiki\Revision\RenderedRevision;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
|
|
@ -23,6 +22,7 @@ use Title;
|
|||
use User;
|
||||
use Wikimedia\AtEase\AtEase;
|
||||
use WikiPage;
|
||||
use WikitextContent;
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\Storage\PageUpdater
|
||||
|
|
@ -41,7 +41,7 @@ class PageUpdaterTest extends MediaWikiIntegrationTestCase {
|
|||
protected function setUp() : void {
|
||||
parent::setUp();
|
||||
|
||||
$slotRoleRegistry = MediaWikiServices::getInstance()->getSlotRoleRegistry();
|
||||
$slotRoleRegistry = $this->getServiceContainer()->getSlotRoleRegistry();
|
||||
|
||||
if ( !$slotRoleRegistry->isDefinedRole( 'aux' ) ) {
|
||||
$slotRoleRegistry->defineRoleWithModel(
|
||||
|
|
@ -50,6 +50,15 @@ class PageUpdaterTest extends MediaWikiIntegrationTestCase {
|
|||
);
|
||||
}
|
||||
|
||||
if ( !$slotRoleRegistry->isDefinedRole( 'derivedslot' ) ) {
|
||||
$slotRoleRegistry->defineRoleWithModel(
|
||||
'derivedslot',
|
||||
CONTENT_MODEL_WIKITEXT,
|
||||
[],
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
$this->tablesUsed[] = 'logging';
|
||||
$this->tablesUsed[] = 'recentchanges';
|
||||
$this->tablesUsed[] = 'change_tag';
|
||||
|
|
@ -631,7 +640,7 @@ class PageUpdaterTest extends MediaWikiIntegrationTestCase {
|
|||
* @covers \MediaWiki\Storage\PageUpdater::setRcPatrolStatus()
|
||||
*/
|
||||
public function testSetRcPatrolStatus( $patrolled ) {
|
||||
$revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
|
||||
$revisionStore = $this->getServiceContainer()->getRevisionStore();
|
||||
|
||||
$user = $this->getTestUser()->getUser();
|
||||
$authority = $this->newAuthority( $user );
|
||||
|
|
@ -725,6 +734,88 @@ class PageUpdaterTest extends MediaWikiIntegrationTestCase {
|
|||
$this->assertTrue( $main1->getContent()->equals( $main3->getContent() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\Storage\PageUpdater::setSlot()
|
||||
* @covers \MediaWiki\Storage\PageUpdater::updateRevision()
|
||||
*/
|
||||
public function testUpdatingDerivedSlot() {
|
||||
$user = $this->getTestUser()->getUser();
|
||||
$title = $this->getDummyTitle( __METHOD__ );
|
||||
$page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title );
|
||||
|
||||
$updater = $page->newPageUpdater( $user );
|
||||
$summary = CommentStoreComment::newUnsavedComment( 'one' );
|
||||
$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum' ) );
|
||||
$updater->saveRevision( $summary, EDIT_NEW );
|
||||
|
||||
$updater = $page->newPageUpdater( $user );
|
||||
$content = new WikitextContent( 'A' );
|
||||
$derived = SlotRecord::newDerived( 'derivedslot', $content );
|
||||
$updater->setSlot( $derived );
|
||||
$updater->updateRevision();
|
||||
|
||||
$status = $updater->getStatus();
|
||||
$this->assertTrue( $status->isOK() );
|
||||
$rev = $status->getValue()['revision-record'];
|
||||
$slot = $rev->getSlot( 'derivedslot' );
|
||||
$this->assertTrue( $slot->getContent()->equals( $content ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\Storage\PageUpdater::setSlot()
|
||||
* @covers \MediaWiki\Storage\PageUpdater::updateRevision()
|
||||
*/
|
||||
public function testUpdatingDerivedSlotCurrentRevision() {
|
||||
$user = $this->getTestUser()->getUser();
|
||||
$title = $this->getDummyTitle( __METHOD__ );
|
||||
$page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title );
|
||||
|
||||
$updater = $page->newPageUpdater( $user );
|
||||
$summary = CommentStoreComment::newUnsavedComment( 'one' );
|
||||
$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum' ) );
|
||||
$rev1 = $updater->saveRevision( $summary, EDIT_NEW );
|
||||
|
||||
$updater = $page->newPageUpdater( $user );
|
||||
$content = new WikitextContent( 'A' );
|
||||
$derived = SlotRecord::newDerived( 'derivedslot', $content );
|
||||
$updater->setSlot( $derived );
|
||||
$updater->updateRevision( $rev1->getId( $rev1->getWikiId() ) );
|
||||
|
||||
$rev2 = $updater->getStatus()->getValue()['revision-record'];
|
||||
$slot = $rev2->getSlot( 'derivedslot' );
|
||||
$this->assertTrue( $slot->getContent()->equals( $content ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\Storage\PageUpdater::setSlot()
|
||||
* @covers \MediaWiki\Storage\PageUpdater::updateRevision()
|
||||
*/
|
||||
public function testUpdatingDerivedSlotOldRevision() {
|
||||
$user = $this->getTestUser()->getUser();
|
||||
$title = $this->getDummyTitle( __METHOD__ );
|
||||
$page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title );
|
||||
|
||||
$updater = $page->newPageUpdater( $user );
|
||||
$summary = CommentStoreComment::newUnsavedComment( 'one' );
|
||||
$updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum' ) );
|
||||
$rev1 = $updater->saveRevision( $summary, EDIT_NEW );
|
||||
|
||||
$updater = $page->newPageUpdater( $user );
|
||||
$summary = CommentStoreComment::newUnsavedComment( 'two' );
|
||||
$updater->setContent( SlotRecord::MAIN, new TextContent( 'Something different' ) );
|
||||
$updater->saveRevision( $summary, EDIT_UPDATE );
|
||||
|
||||
$updater = $page->newPageUpdater( $user );
|
||||
$content = new WikitextContent( 'A' );
|
||||
$derived = SlotRecord::newDerived( 'derivedslot', $content );
|
||||
$updater->setSlot( $derived );
|
||||
$updater->updateRevision( $rev1->getId( $rev1->getWikiId() ) );
|
||||
|
||||
$rev3 = $updater->getStatus()->getValue()['revision-record'];
|
||||
$slot = $rev3->getSlot( 'derivedslot' );
|
||||
$this->assertTrue( $slot->getContent()->equals( $content ) );
|
||||
}
|
||||
|
||||
// TODO: MCR: test adding multiple slots, inheriting parent slots, and removing slots.
|
||||
|
||||
public function testSetUseAutomaticEditSummaries() {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,17 @@ class DifferenceEngineTest extends MediaWikiIntegrationTestCase {
|
|||
}
|
||||
|
||||
$this->setMwGlobals( [ 'wgDiffEngine' => 'php' ] );
|
||||
|
||||
$slotRoleRegistry = $this->getServiceContainer()->getSlotRoleRegistry();
|
||||
|
||||
if ( !$slotRoleRegistry->isDefinedRole( 'derivedslot' ) ) {
|
||||
$slotRoleRegistry->defineRoleWithModel(
|
||||
'derivedslot',
|
||||
CONTENT_MODEL_WIKITEXT,
|
||||
[],
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -247,6 +258,10 @@ class DifferenceEngineTest extends MediaWikiIntegrationTestCase {
|
|||
ContentHandler::makeContent( 'aaa', null, CONTENT_MODEL_TEXT ) );
|
||||
$slot2 = SlotRecord::newUnsaved( 'slot',
|
||||
ContentHandler::makeContent( 'bbb', null, CONTENT_MODEL_TEXT ) );
|
||||
$slot3 = SlotRecord::newDerived( 'derivedslot',
|
||||
ContentHandler::makeContent( 'aaa', null, CONTENT_MODEL_TEXT ) );
|
||||
$slot4 = SlotRecord::newDerived( 'derivedslot',
|
||||
ContentHandler::makeContent( 'bbb', null, CONTENT_MODEL_TEXT ) );
|
||||
|
||||
return [
|
||||
'revision vs. null' => [
|
||||
|
|
@ -274,6 +289,11 @@ class DifferenceEngineTest extends MediaWikiIntegrationTestCase {
|
|||
$this->getRevisionRecord( $main1, $slot1 ),
|
||||
"slotLine 1:\nLine 1:\n- +aaa",
|
||||
],
|
||||
'ignored difference in derived slot' => [
|
||||
$this->getRevisionRecord( $main1, $slot3 ),
|
||||
$this->getRevisionRecord( $main1, $slot4 ),
|
||||
'',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -134,6 +134,18 @@ class RevisionSlotsTest extends MediaWikiUnitTestCase {
|
|||
$this->assertEquals( [ 'main' => $mainSlot, 'aux' => $auxSlot ], $slots->getSlots() );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\Revision\RevisionSlots::getSlots
|
||||
*/
|
||||
public function testGetNonDerivedSlots() {
|
||||
$mainSlot = SlotRecord::newUnsaved( SlotRecord::MAIN, new WikitextContent( 'A' ) );
|
||||
$auxSlot = SlotRecord::newDerived( 'aux', new WikitextContent( 'B' ) );
|
||||
$slotsArray = [ $mainSlot, $auxSlot ];
|
||||
$slots = $this->newRevisionSlots( $slotsArray );
|
||||
|
||||
$this->assertEquals( [ 'main' => $mainSlot ], $slots->getPrimarySlots() );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\Revision\RevisionSlots::getInheritedSlots
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -41,6 +41,21 @@ class SlotRoleHandlerTest extends \MediaWikiUnitTestCase {
|
|||
$this->assertArrayHasKey( 'placement', $hints );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\Revision\SlotRoleHandler::__construct
|
||||
* @covers \MediaWiki\Revision\SlotRoleHandler::isDerived
|
||||
*/
|
||||
public function testDerived() {
|
||||
$handler = new SlotRoleHandler( 'foo', 'FooModel', [ 'frob' => 'niz' ] );
|
||||
$this->assertFalse( $handler->isDerived() );
|
||||
|
||||
$handler = new SlotRoleHandler( 'foo', 'FooModel', [ 'frob' => 'niz' ], false );
|
||||
$this->assertFalse( $handler->isDerived() );
|
||||
|
||||
$handler = new SlotRoleHandler( 'foo', 'FooModel', [ 'frob' => 'niz' ], true );
|
||||
$this->assertTrue( $handler->isDerived() );
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \MediaWiki\Revision\SlotRoleHandler::isAllowedModel()
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue