Add support for derived MCR slots

Bug: T277203
Change-Id: I1c70abc00c912b283e3e6eb2266633ae3f57673b
This commit is contained in:
Cindy Cicalese 2021-03-06 23:41:19 -05:00
parent 82053e3f78
commit 29862dd51d
14 changed files with 596 additions and 24 deletions

View file

@ -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.
*

View file

@ -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

View file

@ -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:

View file

@ -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,

View file

@ -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;
}
}
/**

View file

@ -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.
*

View file

@ -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 );
}
);
}

View file

@ -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();

View file

@ -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 = [];
}

View file

@ -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() );
}
}

View file

@ -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() {

View file

@ -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 ),
'',
],
];
}

View file

@ -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
*/

View file

@ -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()
*/