* @param object|string|int $type
* @return mixed
*/
- private static function convertSelectType( $type ) {
+ protected static function convertSelectType( $type ) {
switch ( $type ) {
case 'fromdb':
return self::READ_NORMAL;
return $ret;
}
+ /**
+ * Helper method for checking whether two revisions have differences that go
+ * beyond the main slot.
+ *
+ * MCR migration note: this method should go away!
+ *
+ * @deprecated Use only as a stop-gap before refactoring to support MCR.
+ *
+ * @param Revision $a
+ * @param Revision $b
+ * @return bool
+ */
+ public static function hasDifferencesOutsideMainSlot( Revision $a, Revision $b ) {
+ $aSlots = $a->getRevisionRecord()->getSlots();
+ $bSlots = $b->getRevisionRecord()->getSlots();
+ $changedRoles = $aSlots->getRolesWithDifferentContent( $bSlots );
+
+ return ( $changedRoles !== [ 'main' ] );
+ }
+
/**
* Get the content that needs to be saved in order to undo all revisions
* between $undo and $undoafter. Revisions must belong to the same page,
* must exist and must not be deleted
+ *
* @param Revision $undo
* @param Revision $undoafter Must be an earlier revision than $undo
* @return Content|bool Content on success, false on failure
* @since 1.21
* Before we had the Content object, this was done in getUndoText
*/
- public function getUndoContent( Revision $undo, Revision $undoafter = null ) {
+ public function getUndoContent( Revision $undo, Revision $undoafter ) {
+ // TODO: MCR: replace this with a method that returns a RevisionSlotsUpdate
+
+ if ( self::hasDifferencesOutsideMainSlot( $undo, $undoafter ) ) {
+ // Cannot yet undo edits that involve anything other the main slot.
+ return false;
+ }
+
$handler = $undo->getContentHandler();
return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
}
* error will be returned. These two conditions are also possible with
* auto-detection due to MediaWiki's performance-optimised locking strategy.
*
- * @param bool|int $baseRevId The revision ID this edit was based off, if any.
- * This is not the parent revision ID, rather the revision ID for older
- * content used as the source for a rollback, for example.
+ * @param bool|int $originalRevId: The ID of an original revision that the edit
+ * restores or repeats. The new revision is expected to have the exact same content as
+ * the given original revision. This is used with rollbacks and with dummy "null" revisions
+ * which are created to record things like page moves.
* @param User $user The user doing the edit
* @param string $serialFormat IGNORED.
* @param array|null $tags Change tags to apply to this edit
* @throws MWException
*/
public function doEditContent(
- Content $content, $summary, $flags = 0, $baseRevId = false,
+ Content $content, $summary, $flags = 0, $originalRevId = false,
User $user = null, $serialFormat = null, $tags = [], $undidRevId = 0
) {
global $wgUser, $wgUseNPPatrol, $wgUseRCPatrol;
// used by this PageUpdater. However, there is no guarantee for this.
$updater = $this->newPageUpdater( $user );
$updater->setContent( 'main', $content );
- $updater->setBaseRevisionId( $baseRevId );
+ $updater->setOriginalRevisionId( $originalRevId );
$updater->setUndidRevisionId( $undidRevId );
$needsPatrol = $wgUseRCPatrol || ( $wgUseNPPatrol && !$this->exists() );
// Note array_intersect() preserves keys from the first arg, and we're
// assuming $revQuery has `revision` primary and isn't using subtables
// for anything we care about.
- $tablesFlat = [];
- array_walk_recursive(
- $revQuery['tables'],
- function ( $a ) use ( &$tablesFlat ) {
- $tablesFlat[] = $a;
- }
- );
-
$res = $dbw->select(
array_intersect(
- $tablesFlat,
+ $revQuery['tables'],
[ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
),
'1',
* Callers are responsible for permission checks
* (with ChangeTags::canAddTagsAccompanyingChange)
*
- * @return array
+ * @return array An array of error messages, as returned by Status::getErrorsArray()
*/
public function commitRollback( $fromP, $summary, $bot,
&$resultDetails, User $guser, $tags = null
return [ [ 'readonlytext' ] ];
}
- // Get the last editor
- $current = $this->getRevision();
+ // Begin revision creation cycle by creating a PageUpdater.
+ // If the page is changed concurrently after grabParentRevision(), the rollback will fail.
+ $updater = $this->newPageUpdater( $guser );
+ $current = $updater->grabParentRevision();
+
if ( is_null( $current ) ) {
// Something wrong... no page?
return [ [ 'notanarticle' ] ];
}
+ $currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC );
+ $legacyCurrent = new Revision( $current );
$from = str_replace( '_', ' ', $fromP );
+
// User name given should match up with the top revision.
- // If the user was deleted then $from should be empty.
- if ( $from != $current->getUserText() ) {
- $resultDetails = [ 'current' => $current ];
+ // If the revision's user is not visible, then $from should be empty.
+ if ( $from !== ( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' ) ) {
+ $resultDetails = [ 'current' => $legacyCurrent ];
return [ [ 'alreadyrolled',
htmlspecialchars( $this->mTitle->getPrefixedText() ),
htmlspecialchars( $fromP ),
- htmlspecialchars( $current->getUserText() )
+ htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
] ];
}
// Get the last edit not by this person...
// Note: these may not be public values
- $userId = intval( $current->getUser( Revision::RAW ) );
- $userName = $current->getUserText( Revision::RAW );
- if ( $userId ) {
- $user = User::newFromId( $userId );
- $user->setName( $userName );
- } else {
- $user = User::newFromName( $current->getUserText( Revision::RAW ), false );
- }
-
- $actorWhere = ActorMigration::newMigration()->getWhere( $dbw, 'rev_user', $user );
+ $actorWhere = ActorMigration::newMigration()->getWhere(
+ $dbw,
+ 'rev_user',
+ $current->getUser( RevisionRecord::RAW )
+ );
$s = $dbw->selectRow(
[ 'revision' ] + $actorWhere['tables'],
[ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
[
- 'rev_page' => $current->getPage(),
+ 'rev_page' => $current->getPageId(),
'NOT(' . $actorWhere['conds'] . ')',
],
__METHOD__,
if ( $s === false ) {
// No one else ever edited this page
return [ [ 'cantrollback' ] ];
- } elseif ( $s->rev_deleted & Revision::DELETED_TEXT
- || $s->rev_deleted & Revision::DELETED_USER
+ } elseif ( $s->rev_deleted & RevisionRecord::DELETED_TEXT
+ || $s->rev_deleted & RevisionRecord::DELETED_USER
) {
// Only admins can see this text
return [ [ 'notvisiblerev' ] ];
}
// Generate the edit summary if necessary
- $target = Revision::newFromId( $s->rev_id, Revision::READ_LATEST );
+ $target = $this->getRevisionStore()->getRevisionById(
+ $s->rev_id,
+ RevisionStore::READ_LATEST
+ );
if ( empty( $summary ) ) {
- if ( $from == '' ) { // no public user name
+ if ( !$currentEditorForPublic ) { // no public user name
$summary = wfMessage( 'revertpage-nouser' );
} else {
$summary = wfMessage( 'revertpage' );
}
}
+ $legacyTarget = new Revision( $target );
+ $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
// Allow the custom summary to use the same args as the default message
$args = [
- $target->getUserText(), $from, $s->rev_id,
+ $targetEditorForPublic ? $targetEditorForPublic->getName() : null,
+ $currentEditorForPublic ? $currentEditorForPublic->getName() : null,
+ $s->rev_id,
$wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
- $current->getId(), $wgContLang->timeanddate( $current->getTimestamp() )
+ $current->getId(),
+ $wgContLang->timeanddate( $current->getTimestamp() )
];
if ( $summary instanceof Message ) {
$summary = $summary->params( $args )->inContentLanguage()->text();
$flags |= EDIT_FORCE_BOT;
}
- $targetContent = $target->getContent();
- $changingContentModel = $targetContent->getModel() !== $current->getContentModel();
+ // TODO: MCR: also log model changes in other slots, in case that becomes possible!
+ $currentContent = $current->getContent( 'main' );
+ $targetContent = $target->getContent( 'main' );
+ $changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
if ( in_array( 'mw-rollback', ChangeTags::getSoftwareTags() ) ) {
$tags[] = 'mw-rollback';
}
- // Actually store the edit
- $status = $this->doEditContent(
- $targetContent,
- $summary,
- $flags,
- $target->getId(),
- $guser,
- null,
- $tags
+ // Build rollback revision:
+ // Restore old content
+ // TODO: MCR: test this once we can store multiple slots
+ foreach ( $target->getSlots()->getSlots() as $slot ) {
+ $updater->inheritSlot( $slot );
+ }
+
+ // Remove extra slots
+ // TODO: MCR: test this once we can store multiple slots
+ foreach ( $current->getSlotRoles() as $role ) {
+ if ( !$target->hasSlot( $role ) ) {
+ $updater->removeSlot( $role );
+ }
+ }
+
+ $updater->setOriginalRevisionId( $target->getId() );
+ $updater->setUndidRevisionId( $current->getId() );
+ $updater->addTags( $tags );
+
+ // TODO: this logic should not be in the storage layer, it's here for compatibility
+ // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
+ // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
+ if ( $wgUseRCPatrol && $this->getTitle()->userCan( 'autopatrol', $guser ) ) {
+ $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
+ }
+
+ // Actually store the rollback
+ $rev = $updater->saveRevision(
+ CommentStoreComment::newUnsavedComment( $summary ),
+ $flags
);
// Set patrolling and bot flag on the edits, which gets rollbacked.
}
if ( count( $set ) ) {
- $actorWhere = ActorMigration::newMigration()->getWhere( $dbw, 'rc_user', $user, false );
+ $actorWhere = ActorMigration::newMigration()->getWhere(
+ $dbw,
+ 'rc_user',
+ $current->getUser( RevisionRecord::RAW ),
+ false
+ );
$dbw->update( 'recentchanges', $set,
[ /* WHERE */
- 'rc_cur_id' => $current->getPage(),
+ 'rc_cur_id' => $current->getPageId(),
'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
$actorWhere['conds'], // No tables/joins are needed for rc_user
],
);
}
- if ( !$status->isOK() ) {
- return $status->getErrorsArray();
+ if ( !$updater->wasSuccessful() ) {
+ return $updater->getStatus()->getErrorsArray();
}
- // raise error, when the edit is an edit without a new version
- $statusRev = $status->value['revision'] ?? null;
- if ( !( $statusRev instanceof Revision ) ) {
- $resultDetails = [ 'current' => $current ];
+ // Report if the edit was not created because it did not change the content.
+ if ( $updater->isUnchanged() ) {
+ $resultDetails = [ 'current' => $legacyCurrent ];
return [ [ 'alreadyrolled',
htmlspecialchars( $this->mTitle->getPrefixedText() ),
htmlspecialchars( $fromP ),
- htmlspecialchars( $current->getUserText() )
+ htmlspecialchars( $targetEditorForPublic ? $targetEditorForPublic->getName() : '' )
] ];
}
$log->setTarget( $this->mTitle );
$log->setComment( $summary );
$log->setParameters( [
- '4::oldmodel' => $current->getContentModel(),
+ '4::oldmodel' => $currentContent->getModel(),
'5::newmodel' => $targetContent->getModel(),
] );
$log->publish( $logId );
}
- $revId = $statusRev->getId();
+ $revId = $rev->getId();
- Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $target, $current ] );
+ Hooks::run( 'ArticleRollbackComplete', [ $this, $guser, $legacyTarget, $legacyCurrent ] );
$resultDetails = [
'summary' => $summary,
- 'current' => $current,
- 'target' => $target,
+ 'current' => $legacyCurrent,
+ 'target' => $legacyTarget,
'newid' => $revId,
'tags' => $tags
];
+ // TODO: make this return a Status object and wrap $resultDetails in that.
return [];
}
*/
public function updateCategoryCounts( array $added, array $deleted, $id = 0 ) {
$id = $id ?: $this->getId();
- $ns = $this->getTitle()->getNamespace();
+ $type = MWNamespace::getCategoryLinkType( $this->getTitle()->getNamespace() );
$addFields = [ 'cat_pages = cat_pages + 1' ];
$removeFields = [ 'cat_pages = cat_pages - 1' ];
- if ( $ns == NS_CATEGORY ) {
- $addFields[] = 'cat_subcats = cat_subcats + 1';
- $removeFields[] = 'cat_subcats = cat_subcats - 1';
- } elseif ( $ns == NS_FILE ) {
- $addFields[] = 'cat_files = cat_files + 1';
- $removeFields[] = 'cat_files = cat_files - 1';
+ if ( $type !== 'page' ) {
+ $addFields[] = "cat_{$type}s = cat_{$type}s + 1";
+ $removeFields[] = "cat_{$type}s = cat_{$type}s - 1";
}
$dbw = wfGetDB( DB_MASTER );
$insertRows[] = [
'cat_title' => $cat,
'cat_pages' => 1,
- 'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
- 'cat_files' => ( $ns == NS_FILE ) ? 1 : 0,
+ 'cat_subcats' => ( $type === 'subcat' ) ? 1 : 0,
+ 'cat_files' => ( $type === 'file' ) ? 1 : 0,
];
}
$dbw->upsert(