Merge "FauxRequest: don’t override getValues()"
[lhc/web/wiklou.git] / includes / Revision / RevisionStore.php
index 818a536..3ecef76 100644 (file)
@@ -54,8 +54,10 @@ use Psr\Log\NullLogger;
 use RecentChange;
 use Revision;
 use RuntimeException;
+use StatusValue;
 use stdClass;
 use Title;
+use Traversable;
 use User;
 use WANObjectCache;
 use Wikimedia\Assert\Assert;
@@ -324,10 +326,10 @@ class RevisionStore
 
                $canUseTitleNewFromId = ( $pageId !== null && $pageId > 0 && $this->dbDomain === false );
                list( $dbMode, $dbOptions ) = DBAccessObjectUtils::getDBOptions( $queryFlags );
-               $titleFlags = ( $dbMode == DB_MASTER ? Title::GAID_FOR_UPDATE : 0 );
 
                // Loading by ID is best, but Title::newFromID does not support that for foreign IDs.
                if ( $canUseTitleNewFromId ) {
+                       $titleFlags = ( $dbMode == DB_MASTER ? Title::READ_LATEST : 0 );
                        // TODO: better foreign title handling (introduce TitleFactory)
                        $title = Title::newFromID( $pageId, $titleFlags );
                        if ( $title ) {
@@ -1621,10 +1623,18 @@ class RevisionStore
         * @param object[]|IResultWrapper $slotRows
         * @param int $queryFlags
         * @param Title $title
+        * @param array|null $slotContents a map from blobAddress to slot
+        *      content blob or Content object.
         *
         * @return SlotRecord[]
         */
-       private function constructSlotRecords( $revId, $slotRows, $queryFlags, Title $title ) {
+       private function constructSlotRecords(
+               $revId,
+               $slotRows,
+               $queryFlags,
+               Title $title,
+               $slotContents = null
+       ) {
                $slots = [];
 
                foreach ( $slotRows as $row ) {
@@ -1649,8 +1659,15 @@ class RevisionStore
                                        = $this->emulateContentId( intval( $row->rev_text_id ) );
                        }
 
-                       $contentCallback = function ( SlotRecord $slot ) use ( $queryFlags ) {
-                               return $this->loadSlotContent( $slot, null, null, null, $queryFlags );
+                       $contentCallback = function ( SlotRecord $slot ) use ( $slotContents, $queryFlags ) {
+                               $blob = null;
+                               if ( isset( $slotContents[$slot->getAddress()] ) ) {
+                                       $blob = $slotContents[$slot->getAddress()];
+                                       if ( $blob instanceof Content ) {
+                                               return $blob;
+                                       }
+                               }
+                               return $this->loadSlotContent( $slot, $blob, null, null, $queryFlags );
                        };
 
                        $slots[$row->role_name] = new SlotRecord( $row, $contentCallback );
@@ -1802,8 +1819,10 @@ class RevisionStore
 
        /**
         * @param object $row A database row generated from a query based on getQueryInfo()
-        * @param null|object[] $slotRows Database rows generated from a query based on
-        *        getSlotsQueryInfo with the 'content' flag set.
+        * @param null|object[]|RevisionSlots $slots
+        *      - Database rows generated from a query based on getSlotsQueryInfo
+        *        with the 'content' flag set. Or
+        *  - RevisionSlots instance
         * @param int $queryFlags
         * @param Title|null $title
         * @param bool $fromCache if true, the returned RevisionRecord will ensure that no stale
@@ -1814,11 +1833,10 @@ class RevisionStore
         * @see RevisionFactory::newRevisionFromRow
         *
         * MCR migration note: this replaces Revision::newFromRow
-        *
         */
        public function newRevisionFromRowAndSlots(
                $row,
-               $slotRows,
+               $slots,
                $queryFlags = 0,
                Title $title = null,
                $fromCache = false
@@ -1855,7 +1873,9 @@ class RevisionStore
                // Legacy because $row may have come from self::selectFields()
                $comment = $this->commentStore->getCommentLegacy( $db, 'rev_comment', $row, true );
 
-               $slots = $this->newRevisionSlots( $row->rev_id, $row, $slotRows, $queryFlags, $title );
+               if ( !( $slots instanceof RevisionSlots ) ) {
+                       $slots = $this->newRevisionSlots( $row->rev_id, $row, $slots, $queryFlags, $title );
+               }
 
                // If this is a cached row, instantiate a cache-aware revision class to avoid stale data.
                if ( $fromCache ) {
@@ -1876,6 +1896,149 @@ class RevisionStore
                return $rev;
        }
 
+       /**
+        * Construct a RevisionRecord instance for each row in $rows,
+        * and return them as an associative array indexed by revision ID.
+        * @param Traversable|array $rows the rows to construct revision records from
+        * @param array $options Supports the following options:
+        *               'slots' - whether metadata about revision slots should be
+        *               loaded immediately. Supports falsy or truthy value as well
+        *               as an explicit list of slot role names.
+        *               'content'- whether the actual content of the slots should be
+        *               preloaded.
+        * @param int $queryFlags
+        * @param Title|null $title
+        * @return StatusValue a status with a RevisionRecord[] of successfully fetched revisions
+        *                                         and an array of errors for the revisions failed to fetch.
+        */
+       public function newRevisionsFromBatch(
+               $rows,
+               array $options = [],
+               $queryFlags = 0,
+               Title $title = null
+       ) {
+               $result = new StatusValue();
+
+               $rowsByRevId = [];
+               $pageIds = [];
+               $titlesByPageId = [];
+               foreach ( $rows as $row ) {
+                       if ( isset( $rowsByRevId[$row->rev_id] ) ) {
+                               throw new InvalidArgumentException( "Duplicate rows in newRevisionsFromBatch {$row->rev_id}" );
+                       }
+                       if ( $title && $row->rev_page != $title->getArticleID() ) {
+                               throw new InvalidArgumentException(
+                                       "Revision {$row->rev_id} doesn't belong to page {$title->getArticleID()}"
+                               );
+                       }
+                       $pageIds[] = $row->rev_page;
+                       $rowsByRevId[$row->rev_id] = $row;
+               }
+
+               if ( empty( $rowsByRevId ) ) {
+                       $result->setResult( true, [] );
+                       return $result;
+               }
+
+               // If the title is not supplied, batch-fetch Title objects.
+               if ( $title ) {
+                       $titlesByPageId[$title->getArticleID()] = $title;
+               } else {
+                       $pageIds = array_unique( $pageIds );
+                       foreach ( Title::newFromIDs( $pageIds ) as $t ) {
+                               $titlesByPageId[$t->getArticleID()] = $t;
+                       }
+               }
+
+               if ( !isset( $options['slots'] ) || $this->hasMcrSchemaFlags( SCHEMA_COMPAT_READ_OLD ) ) {
+                       $result->setResult( true,
+                               array_map( function ( $row ) use ( $queryFlags, $titlesByPageId, $result ) {
+                                       try {
+                                               return $this->newRevisionFromRow(
+                                                       $row,
+                                                       $queryFlags,
+                                                       $titlesByPageId[$row->rev_page]
+                                               );
+                                       } catch ( MWException $e ) {
+                                               $result->warning( 'internalerror', $e->getMessage() );
+                                               return null;
+                                       }
+                               }, $rowsByRevId )
+                       );
+                       return $result;
+               }
+
+               $slotQueryConds = [ 'slot_revision_id' => array_keys( $rowsByRevId ) ];
+               if ( is_array( $options['slots'] ) ) {
+                       $slotQueryConds['slot_role_id'] = array_map( function ( $slot_name ) {
+                               return $this->slotRoleStore->getId( $slot_name );
+                       }, $options['slots'] );
+               }
+
+               // We need to set the `content` flag because newRevisionFromRowAndSlots requires content
+               // metadata to be loaded.
+               $slotQueryInfo = self::getSlotsQueryInfo( [ 'content' ] );
+               $db = $this->getDBConnectionRefForQueryFlags( $queryFlags );
+               $slotRows = $db->select(
+                       $slotQueryInfo['tables'],
+                       $slotQueryInfo['fields'],
+                       $slotQueryConds,
+                       __METHOD__,
+                       [],
+                       $slotQueryInfo['joins']
+               );
+
+               $slotRowsByRevId = [];
+               foreach ( $slotRows as $slotRow ) {
+                       $slotRowsByRevId[$slotRow->slot_revision_id][] = $slotRow;
+               }
+
+               $slotContents = null;
+               if ( $options['content'] ?? false ) {
+                       $blobAddresses = [];
+                       foreach ( $slotRows as $slotRow ) {
+                               $blobAddresses[] = $slotRow->content_address;
+                       }
+                       $slotContentFetchStatus = $this->blobStore
+                               ->getBlobBatch( $blobAddresses, $queryFlags );
+                       foreach ( $slotContentFetchStatus->getErrors() as $error ) {
+                               $result->warning( $error['message'], ...$error['params'] );
+                       }
+                       $slotContents = $slotContentFetchStatus->getValue();
+               }
+
+               $result->setResult( true, array_map( function ( $row ) use
+                       ( $slotRowsByRevId, $queryFlags, $titlesByPageId, $slotContents, $result ) {
+                               if ( !isset( $slotRowsByRevId[$row->rev_id] ) ) {
+                                       $result->warning(
+                                               'internalerror',
+                                               "Couldn't find slots for rev {$row->rev_id}"
+                                       );
+                                       return null;
+                               }
+                               try {
+                                       return $this->newRevisionFromRowAndSlots(
+                                               $row,
+                                               new RevisionSlots(
+                                                       $this->constructSlotRecords(
+                                                               $row->rev_id,
+                                                               $slotRowsByRevId[$row->rev_id],
+                                                               $queryFlags,
+                                                               $titlesByPageId[$row->rev_page],
+                                                               $slotContents
+                                                       )
+                                               ),
+                                               $queryFlags,
+                                               $titlesByPageId[$row->rev_page]
+                                       );
+                               } catch ( MWException $e ) {
+                                       $result->warning( 'internalerror', $e->getMessage() );
+                                       return null;
+                               }
+               }, $rowsByRevId ) );
+               return $result;
+       }
+
        /**
         * Constructs a new MutableRevisionRecord based on the given associative array following
         * the MW1.29 convention for the Revision constructor.