rdbms: add IDatabase::lockForUpdate() convenience method
[lhc/web/wiklou.git] / includes / page / WikiPage.php
index 5bbdb6c..5608791 100644 (file)
@@ -202,7 +202,7 @@ class WikiPage implements Page, IDBAccessObject {
         * @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;
@@ -1453,17 +1453,45 @@ class WikiPage implements Page, IDBAccessObject {
                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 );
        }
@@ -1742,9 +1770,10 @@ class WikiPage implements Page, IDBAccessObject {
         * 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
@@ -1771,7 +1800,7 @@ class WikiPage implements Page, IDBAccessObject {
         * @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;
@@ -1796,7 +1825,7 @@ class WikiPage implements Page, IDBAccessObject {
                // 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() );
@@ -1906,7 +1935,7 @@ class WikiPage implements Page, IDBAccessObject {
                $updater = $this->getDerivedDataUpdater( $user, $revision, $slots );
 
                if ( !$updater->isUpdatePrepared() ) {
-                       $updater->prepareContent( $user, $slots, [], $useCache );
+                       $updater->prepareContent( $user, $slots, $useCache );
 
                        if ( $revision ) {
                                $updater->prepareUpdate( $revision );
@@ -2498,28 +2527,16 @@ class WikiPage implements Page, IDBAccessObject {
                // 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.
-               }
 
                // Get all of the page revisions
                $res = $dbw->select(
@@ -2799,7 +2816,7 @@ class WikiPage implements Page, IDBAccessObject {
         * 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
@@ -2812,43 +2829,44 @@ class WikiPage implements Page, IDBAccessObject {
                        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__,
@@ -2861,28 +2879,36 @@ class WikiPage implements Page, IDBAccessObject {
                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();
@@ -2904,22 +2930,45 @@ class WikiPage implements Page, IDBAccessObject {
                        $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.
@@ -2936,10 +2985,15 @@ class WikiPage implements Page, IDBAccessObject {
                }
 
                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
                                ],
@@ -2947,18 +3001,17 @@ class WikiPage implements Page, IDBAccessObject {
                        );
                }
 
-               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() : '' )
                        ] ];
                }
 
@@ -2970,7 +3023,7 @@ class WikiPage implements Page, IDBAccessObject {
                        $log->setTarget( $this->mTitle );
                        $log->setComment( $summary );
                        $log->setParameters( [
-                               '4::oldmodel' => $current->getContentModel(),
+                               '4::oldmodel' => $currentContent->getModel(),
                                '5::newmodel' => $targetContent->getModel(),
                        ] );
 
@@ -2978,18 +3031,19 @@ class WikiPage implements Page, IDBAccessObject {
                        $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 [];
        }
 
@@ -3200,16 +3254,13 @@ class WikiPage implements Page, IDBAccessObject {
         */
        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 );
@@ -3241,8 +3292,8 @@ class WikiPage implements Page, IDBAccessObject {
                                        $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(