* @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;
* Revision::FOR_PUBLIC to be displayed to all users
* Revision::FOR_THIS_USER to be displayed to $wgUser
* Revision::RAW get the text regardless of permissions
- * @param User $user User object to check for, only if FOR_THIS_USER is passed
+ * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
* to the $audience parameter
* @return Content|null The content of the current revision
*
* Revision::FOR_PUBLIC to be displayed to all users
* Revision::FOR_THIS_USER to be displayed to the given user
* Revision::RAW get the text regardless of permissions
- * @param User $user User object to check for, only if FOR_THIS_USER is passed
+ * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
* to the $audience parameter
* @return int User ID for the user that made the last article revision
*/
* Revision::FOR_PUBLIC to be displayed to all users
* Revision::FOR_THIS_USER to be displayed to the given user
* Revision::RAW get the text regardless of permissions
- * @param User $user User object to check for, only if FOR_THIS_USER is passed
+ * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
* to the $audience parameter
* @return User|null
*/
* Revision::FOR_PUBLIC to be displayed to all users
* Revision::FOR_THIS_USER to be displayed to the given user
* Revision::RAW get the text regardless of permissions
- * @param User $user User object to check for, only if FOR_THIS_USER is passed
+ * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
* to the $audience parameter
* @return string Username of the user that made the last article revision
*/
* Revision::FOR_PUBLIC to be displayed to all users
* Revision::FOR_THIS_USER to be displayed to the given user
* Revision::RAW get the text regardless of permissions
- * @param User $user User object to check for, only if FOR_THIS_USER is passed
+ * @param User|null $user User object to check for, only if FOR_THIS_USER is passed
* to the $audience parameter
* @return string Comment stored for the last article revision
*/
* @param IDatabase $dbw
* @param Revision $revision For ID number, and text used to set
* length and redirect status fields
- * @param int $lastRevision If given, will not overwrite the page field
+ * @param int|null $lastRevision If given, will not overwrite the page field
* when different from the currently set value.
* Giving 0 indicates the new page flag should be set on.
- * @param bool $lastRevIsRedirect If given, will optimize adding and
+ * @param bool|null $lastRevIsRedirect If given, will optimize adding and
* removing rows in redirect table.
* @return bool Success; false if the page row was missing or page_latest changed
*/
* to perform updates, if the edit was already saved.
* @param RevisionSlotsUpdate|null $forUpdate The new content to be saved by the edit (pre PST),
* if the edit was not yet saved.
+ * @param bool $forEdit Only re-use if the cached DerivedPageDataUpdater has the current
+ * revision as the edit's parent revision. This ensures that the same
+ * DerivedPageDataUpdater cannot be re-used for two consecutive edits.
*
* @return DerivedPageDataUpdater
*/
private function getDerivedDataUpdater(
User $forUser = null,
RevisionRecord $forRevision = null,
- RevisionSlotsUpdate $forUpdate = null
+ RevisionSlotsUpdate $forUpdate = null,
+ $forEdit = false
) {
if ( !$forRevision && !$forUpdate ) {
// NOTE: can't re-use an existing derivedDataUpdater if we don't know what the caller is
&& !$this->derivedDataUpdater->isReusableFor(
$forUser,
$forRevision,
- $forUpdate
+ $forUpdate,
+ $forEdit ? $this->getLatest() : null
)
) {
$this->derivedDataUpdater = null;
* @since 1.32
*
* @param User $user
+ * @param RevisionSlotsUpdate|null $forUpdate If given, allows any cached ParserOutput
+ * that may already have been returned via getDerivedDataUpdater to be re-used.
*
* @return PageUpdater
*/
- public function newPageUpdater( User $user ) {
+ public function newPageUpdater( User $user, RevisionSlotsUpdate $forUpdate = null ) {
global $wgAjaxEditStash, $wgUseAutomaticEditSummaries, $wgPageCreationLog;
$pageUpdater = new PageUpdater(
$user,
$this, // NOTE: eventually, PageUpdater should not know about WikiPage
- $this->getDerivedDataUpdater( $user ),
+ $this->getDerivedDataUpdater( $user, null, $forUpdate, true ),
$this->getDBLoadBalancer(),
$this->getRevisionStore()
);
* 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 User|null $user The user doing the edit
+ * @param string|null $serialFormat IGNORED.
* @param array|null $tags Change tags to apply to this edit
* Callers are responsible for permission checks
* (with ChangeTags::canAddTagsAccompanyingChange)
$flags = ( $flags & ~EDIT_MINOR );
}
+ $slotsUpdate = new RevisionSlotsUpdate();
+ $slotsUpdate->modifyContent( 'main', $content );
+
// NOTE: while doEditContent() executes, callbacks to getDerivedDataUpdater and
// prepareContentForEdit will generally use the DerivedPageDataUpdater that is also
// used by this PageUpdater. However, there is no guarantee for this.
- $updater = $this->newPageUpdater( $user );
+ $updater = $this->newPageUpdater( $user, $slotsUpdate );
$updater->setContent( 'main', $content );
$updater->setOriginalRevisionId( $originalRevId );
$updater->setUndidRevisionId( $undidRevId );
/**
* Get parser options suitable for rendering the primary article wikitext
*
- * @see ContentHandler::makeParserOptions
+ * @see ParserOptions::newCanonical
*
* @param IContextSource|User|string $context One of the following:
* - IContextSource: Use the User and the Language of the provided
* @return ParserOptions
*/
public function makeParserOptions( $context ) {
- $options = $this->getContentHandler()->makeParserOptions( $context );
+ $options = ParserOptions::newCanonical( $context );
if ( $this->getTitle()->isConversionTable() ) {
// @todo ConversionTable should become a separate content model, so
$updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
if ( !$updater->isUpdatePrepared() ) {
- $updater->prepareContent( $user, $slots, [], $useCache );
+ $updater->prepareContent( $user, $slots, $useCache );
if ( $revision ) {
$updater->prepareUpdate( $revision );
* @param int &$cascade Set to false if cascading protection isn't allowed.
* @param string $reason
* @param User $user The user updating the restrictions
- * @param string|string[] $tags Change tags to add to the pages and protection log entries
+ * @param string|string[]|null $tags Change tags to add to the pages and protection log entries
* ($user should be able to add the specified tags before this is called)
* @return Status Status object; if action is taken, $status->value is the log_id of the
* protection log entry.
* @param string $reason Delete reason for deletion log
* @param bool $suppress Suppress all revisions and log the deletion in
* the suppression log instead of the deletion log
- * @param int $u1 Unused
- * @param bool $u2 Unused
+ * @param int|null $u1 Unused
+ * @param bool|null $u2 Unused
* @param array|string &$error Array of errors to append to
- * @param User $user The deleting user
+ * @param User|null $user The deleting user
* @return bool True if successful
*/
public function doDeleteArticle(
* @param string $reason Delete reason for deletion log
* @param bool $suppress Suppress all revisions and log the deletion in
* the suppression log instead of the deletion log
- * @param int $u1 Unused
- * @param bool $u2 Unused
+ * @param int|null $u1 Unused
+ * @param bool|null $u2 Unused
* @param array|string &$error Array of errors to append to
- * @param User $deleter The deleting user
+ * @param User|null $deleter The deleting user
* @param array $tags Tags to apply to the deletion action
* @param string $logsubtype
* @return Status Status object; if successful, $status->value is the log_id of the
// 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(
+ $dbw->lockForUpdate(
array_intersect(
- $tablesFlat,
+ $revQuery['tables'],
[ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
),
- '1',
[ 'rev_page' => $id ],
__METHOD__,
- 'FOR UPDATE',
+ [],
$revQuery['joins']
);
- foreach ( $res as $row ) {
- // Fetch all rows in case the DB needs that to properly lock them.
+
+ // If SCHEMA_COMPAT_WRITE_OLD is set, also select all extra fields we still write,
+ // so we can copy it to the archive table.
+ // We know the fields exist, otherwise SCHEMA_COMPAT_WRITE_OLD could not function.
+ if ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
+ $revQuery['fields'][] = 'rev_text_id';
+
+ if ( $wgContentHandlerUseDB ) {
+ $revQuery['fields'][] = 'rev_content_model';
+ $revQuery['fields'][] = 'rev_content_format';
+ }
}
- // Get all of the page revisions
+ // Get all of the page revisions
$res = $dbw->select(
$revQuery['tables'],
$revQuery['fields'],
] + $commentStore->insert( $dbw, 'ar_comment', $comment )
+ $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
- if ( $wgMultiContentRevisionSchemaMigrationStage < MIGRATION_NEW ) {
+ if ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
$rowInsert['ar_text_id'] = $row->rev_text_id;
- }
- if (
- $wgContentHandlerUseDB &&
- $wgMultiContentRevisionSchemaMigrationStage <= MIGRATION_WRITE_BOTH
- ) {
- $rowInsert['ar_content_model'] = $row->rev_content_model;
- $rowInsert['ar_content_format'] = $row->rev_content_format;
+ if ( $wgContentHandlerUseDB ) {
+ $rowInsert['ar_content_model'] = $row->rev_content_model;
+ $rowInsert['ar_content_format'] = $row->rev_content_format;
+ }
}
+
$rowsInsert[] = $rowInsert;
$revids[] = $row->rev_id;
* 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() );
+ // Do not call setUndidRevisionId(), that causes an extra "mw-undo" tag to be added (T190374)
+ $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(