Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.50% covered (warning)
80.50%
1135 / 1410
58.90% covered (warning)
58.90%
43 / 73
CRAP
0.00% covered (danger)
0.00%
0 / 1
RevisionStore
80.50% covered (warning)
80.50%
1135 / 1410
58.90% covered (warning)
58.90%
43 / 73
1022.96
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 setLogger
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isReadOnly
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getWikiId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDBConnectionRefForQueryFlags
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getReplicaConnection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPrimaryConnection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTitle
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getPage
93.55% covered (success)
93.55%
29 / 31
0.00% covered (danger)
0.00%
0 / 1
12.04
 wrapPage
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 failOnNull
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 failOnEmpty
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 insertRevisionOn
98.57% covered (success)
98.57%
69 / 70
0.00% covered (danger)
0.00%
0 / 1
5
 updateSlotsOn
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
20
 updateSlotsInternal
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 insertRevisionInternal
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
4
 insertSlotOn
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 insertIpChangesRow
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 insertRevisionRowOn
20.00% covered (danger)
20.00%
14 / 70
0.00% covered (danger)
0.00%
0 / 1
40.77
 getBaseRevisionRow
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
 storeContentBlob
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 insertSlotRowOn
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 insertContentRowOn
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 checkContent
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 newNullRevision
83.33% covered (warning)
83.33%
30 / 36
0.00% covered (danger)
0.00%
0 / 1
3.04
 getRcIdIfUnpatrolled
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getRecentChange
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
2.00
 loadSlotContent
65.91% covered (warning)
65.91%
29 / 44
0.00% covered (danger)
0.00%
0 / 1
12.21
 getRevisionById
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRevisionByTitle
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
4.07
 getRevisionByPageId
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getRevisionByTimestamp
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 loadSlotRecords
8.57% covered (danger)
8.57%
3 / 35
0.00% covered (danger)
0.00%
0 / 1
16.23
 loadSlotRecordsFromDb
42.86% covered (danger)
42.86%
9 / 21
0.00% covered (danger)
0.00%
0 / 1
4.68
 constructSlotRecords
57.58% covered (warning)
57.58%
19 / 33
0.00% covered (danger)
0.00%
0 / 1
15.18
 newRevisionSlots
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 newRevisionFromArchiveRow
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newRevisionFromRow
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newRevisionFromArchiveRowAndSlots
87.50% covered (warning)
87.50%
28 / 32
0.00% covered (danger)
0.00%
0 / 1
10.20
 newRevisionFromRowAndSlots
78.38% covered (warning)
78.38%
58 / 74
0.00% covered (danger)
0.00%
0 / 1
13.46
 ensureRevisionRowMatchesPage
95.24% covered (success)
95.24%
40 / 42
0.00% covered (danger)
0.00%
0 / 1
7
 newRevisionsFromBatch
86.47% covered (warning)
86.47%
115 / 133
0.00% covered (danger)
0.00%
0 / 1
33.38
 getSlotRowsForBatch
100.00% covered (success)
100.00%
55 / 55
100.00% covered (success)
100.00%
1 / 1
20
 getContentBlobsForBatch
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
6.07
 newRevisionFromConds
70.00% covered (warning)
70.00%
7 / 10
0.00% covered (danger)
0.00%
0 / 1
5.68
 loadRevisionFromConds
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 checkDatabaseDomain
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 fetchRevisionRowFromConds
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
2.00
 getQueryInfo
94.23% covered (success)
94.23%
49 / 52
0.00% covered (danger)
0.00%
0 / 1
4.00
 newSelectQueryBuilder
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newArchiveSelectQueryBuilder
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSlotsQueryInfo
100.00% covered (success)
100.00%
34 / 34
100.00% covered (success)
100.00%
1 / 1
4
 isRevisionRow
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 getArchiveQueryInfo
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
1
 getRevisionSizes
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 getRelativeRevision
79.41% covered (warning)
79.41%
27 / 34
0.00% covered (danger)
0.00%
0 / 1
9.71
 getPreviousRevision
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNextRevision
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPreviousRevisionId
52.94% covered (warning)
52.94%
9 / 17
0.00% covered (danger)
0.00%
0 / 1
3.94
 getTimestampFromId
76.92% covered (warning)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
6.44
 countRevisionsByPageId
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 countRevisionsByTitle
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 userWasLastToEdit
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
4.00
 getKnownCurrentRevision
76.19% covered (warning)
76.19%
32 / 42
0.00% covered (danger)
0.00%
0 / 1
10.09
 getFirstRevision
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 getRevisionRowCacheKey
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 assertRevisionParameter
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getRevisionLimitConditions
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
7
 getRevisionIdsBetween
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
10
 getAuthorsBetween
89.19% covered (warning)
89.19%
33 / 37
0.00% covered (danger)
0.00%
0 / 1
7.06
 countAuthorsBetween
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 countRevisionsBetween
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
5
 findIdenticalRevision
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * Attribution notice: when this file was created, much of its content was taken
19 * from the Revision.php file as present in release 1.30. Refer to the history
20 * of that file for original authorship (that file was removed entirely in 1.37,
21 * but its history can still be found in prior versions of MediaWiki).
22 *
23 * @file
24 */
25
26namespace MediaWiki\Revision;
27
28use InvalidArgumentException;
29use LogicException;
30use MediaWiki\CommentStore\CommentStore;
31use MediaWiki\CommentStore\CommentStoreComment;
32use MediaWiki\Content\Content;
33use MediaWiki\Content\FallbackContent;
34use MediaWiki\Content\IContentHandlerFactory;
35use MediaWiki\DAO\WikiAwareEntity;
36use MediaWiki\HookContainer\HookContainer;
37use MediaWiki\HookContainer\HookRunner;
38use MediaWiki\Linker\LinkTarget;
39use MediaWiki\MainConfigNames;
40use MediaWiki\MediaWikiServices;
41use MediaWiki\Page\LegacyArticleIdAccess;
42use MediaWiki\Page\PageIdentity;
43use MediaWiki\Page\PageIdentityValue;
44use MediaWiki\Page\PageStore;
45use MediaWiki\Permissions\Authority;
46use MediaWiki\Storage\BadBlobException;
47use MediaWiki\Storage\BlobAccessException;
48use MediaWiki\Storage\BlobStore;
49use MediaWiki\Storage\NameTableAccessException;
50use MediaWiki\Storage\NameTableStore;
51use MediaWiki\Storage\RevisionSlotsUpdate;
52use MediaWiki\Storage\SqlBlobStore;
53use MediaWiki\Title\Title;
54use MediaWiki\Title\TitleFactory;
55use MediaWiki\User\ActorStore;
56use MediaWiki\User\UserIdentity;
57use MediaWiki\Utils\MWTimestamp;
58use MWException;
59use MWUnknownContentModelException;
60use Psr\Log\LoggerAwareInterface;
61use Psr\Log\LoggerInterface;
62use Psr\Log\NullLogger;
63use RecentChange;
64use RuntimeException;
65use StatusValue;
66use stdClass;
67use Traversable;
68use Wikimedia\Assert\Assert;
69use Wikimedia\IPUtils;
70use Wikimedia\ObjectCache\BagOStuff;
71use Wikimedia\ObjectCache\WANObjectCache;
72use Wikimedia\Rdbms\Database;
73use Wikimedia\Rdbms\DBAccessObjectUtils;
74use Wikimedia\Rdbms\IDatabase;
75use Wikimedia\Rdbms\IDBAccessObject;
76use Wikimedia\Rdbms\ILoadBalancer;
77use Wikimedia\Rdbms\IReadableDatabase;
78use Wikimedia\Rdbms\IResultWrapper;
79use Wikimedia\Rdbms\Platform\ISQLPlatform;
80use Wikimedia\Rdbms\SelectQueryBuilder;
81
82/**
83 * Service for looking up page revisions.
84 *
85 * @since 1.31
86 * @since 1.32 Renamed from MediaWiki\Storage\RevisionStore
87 *
88 * @note This was written to act as a drop-in replacement for the corresponding
89 *       static methods in the old Revision class (which was later removed in 1.37).
90 */
91class RevisionStore implements RevisionFactory, RevisionLookup, LoggerAwareInterface {
92
93    use LegacyArticleIdAccess;
94
95    public const ROW_CACHE_KEY = 'revision-row-1.29';
96
97    public const ORDER_OLDEST_TO_NEWEST = 'ASC';
98    public const ORDER_NEWEST_TO_OLDEST = 'DESC';
99
100    // Constants for get(...)Between methods
101    public const INCLUDE_OLD = 'include_old';
102    public const INCLUDE_NEW = 'include_new';
103    public const INCLUDE_BOTH = 'include_both';
104
105    /**
106     * @var SqlBlobStore
107     */
108    private $blobStore;
109
110    /**
111     * @var false|string
112     */
113    private $wikiId;
114
115    /**
116     * @var ILoadBalancer
117     */
118    private $loadBalancer;
119
120    /**
121     * @var WANObjectCache
122     */
123    private $cache;
124
125    /**
126     * @var BagOStuff
127     */
128    private $localCache;
129
130    /**
131     * @var CommentStore
132     */
133    private $commentStore;
134
135    /** @var ActorStore */
136    private $actorStore;
137
138    /**
139     * @var LoggerInterface
140     */
141    private $logger;
142
143    /**
144     * @var NameTableStore
145     */
146    private $contentModelStore;
147
148    /**
149     * @var NameTableStore
150     */
151    private $slotRoleStore;
152
153    /** @var SlotRoleRegistry */
154    private $slotRoleRegistry;
155
156    /** @var IContentHandlerFactory */
157    private $contentHandlerFactory;
158
159    /** @var HookRunner */
160    private $hookRunner;
161
162    /** @var PageStore */
163    private $pageStore;
164
165    /** @var TitleFactory */
166    private $titleFactory;
167
168    /**
169     * @param ILoadBalancer $loadBalancer
170     * @param SqlBlobStore $blobStore
171     * @param WANObjectCache $cache A cache for caching revision rows. This can be the local
172     *        wiki's default instance even if $wikiId refers to a different wiki, since
173     *        makeGlobalKey() is used to constructed a key that allows cached revision rows from
174     *        the same database to be re-used between wikis. For example, enwiki and frwiki will
175     *        use the same cache keys for revision rows from the wikidatawiki database, regardless
176     *        of the cache's default key space.
177     * @param BagOStuff $localCache Another layer of cache, best to use APCu here.
178     * @param CommentStore $commentStore
179     * @param NameTableStore $contentModelStore
180     * @param NameTableStore $slotRoleStore
181     * @param SlotRoleRegistry $slotRoleRegistry
182     * @param ActorStore $actorStore
183     * @param IContentHandlerFactory $contentHandlerFactory
184     * @param PageStore $pageStore
185     * @param TitleFactory $titleFactory
186     * @param HookContainer $hookContainer
187     * @param false|string $wikiId Relevant wiki id or WikiAwareEntity::LOCAL for the current one
188     *
189     * @todo $blobStore should be allowed to be any BlobStore!
190     *
191     */
192    public function __construct(
193        ILoadBalancer $loadBalancer,
194        SqlBlobStore $blobStore,
195        WANObjectCache $cache,
196        BagOStuff $localCache,
197        CommentStore $commentStore,
198        NameTableStore $contentModelStore,
199        NameTableStore $slotRoleStore,
200        SlotRoleRegistry $slotRoleRegistry,
201        ActorStore $actorStore,
202        IContentHandlerFactory $contentHandlerFactory,
203        PageStore $pageStore,
204        TitleFactory $titleFactory,
205        HookContainer $hookContainer,
206        $wikiId = WikiAwareEntity::LOCAL
207    ) {
208        Assert::parameterType( [ 'string', 'false' ], $wikiId, '$wikiId' );
209
210        $this->loadBalancer = $loadBalancer;
211        $this->blobStore = $blobStore;
212        $this->cache = $cache;
213        $this->localCache = $localCache;
214        $this->commentStore = $commentStore;
215        $this->contentModelStore = $contentModelStore;
216        $this->slotRoleStore = $slotRoleStore;
217        $this->slotRoleRegistry = $slotRoleRegistry;
218        $this->actorStore = $actorStore;
219        $this->wikiId = $wikiId;
220        $this->logger = new NullLogger();
221        $this->contentHandlerFactory = $contentHandlerFactory;
222        $this->pageStore = $pageStore;
223        $this->titleFactory = $titleFactory;
224        $this->hookRunner = new HookRunner( $hookContainer );
225    }
226
227    public function setLogger( LoggerInterface $logger ) {
228        $this->logger = $logger;
229    }
230
231    /**
232     * @return bool Whether the store is read-only
233     */
234    public function isReadOnly() {
235        return $this->blobStore->isReadOnly();
236    }
237
238    /**
239     * Get the ID of the wiki this revision belongs to.
240     *
241     * @return string|false The wiki's logical name, of false to indicate the local wiki.
242     */
243    public function getWikiId() {
244        return $this->wikiId;
245    }
246
247    /**
248     * @param int $queryFlags a bit field composed of READ_XXX flags
249     *
250     * @return IReadableDatabase
251     */
252    private function getDBConnectionRefForQueryFlags( $queryFlags ) {
253        if ( ( $queryFlags & IDBAccessObject::READ_LATEST ) == IDBAccessObject::READ_LATEST ) {
254            return $this->getPrimaryConnection();
255        } else {
256            return $this->getReplicaConnection();
257        }
258    }
259
260    /**
261     * @param string|array $groups
262     * @return IReadableDatabase
263     */
264    private function getReplicaConnection( $groups = [] ) {
265        // TODO: Replace with ICP
266        return $this->loadBalancer->getConnection( DB_REPLICA, $groups, $this->wikiId );
267    }
268
269    private function getPrimaryConnection(): IDatabase {
270        // TODO: Replace with ICP
271        return $this->loadBalancer->getConnection( DB_PRIMARY, [], $this->wikiId );
272    }
273
274    /**
275     * Determines the page Title based on the available information.
276     *
277     * MCR migration note: this corresponded to Revision::getTitle
278     *
279     * @deprecated since 1.36, Use RevisionRecord::getPage() instead.
280     * @note The resulting Title object will be misleading if the RevisionStore is not
281     *        for the local wiki.
282     *
283     * @param int|null $pageId
284     * @param int|null $revId
285     * @param int $queryFlags
286     *
287     * @return Title
288     * @throws RevisionAccessException
289     */
290    public function getTitle( $pageId, $revId, $queryFlags = IDBAccessObject::READ_NORMAL ) {
291        // TODO: Hard-deprecate this once getPage() returns a PageRecord. T195069
292        if ( $this->wikiId !== WikiAwareEntity::LOCAL ) {
293            wfDeprecatedMsg( 'Using a Title object to refer to a page on another site.', '1.36' );
294        }
295
296        $page = $this->getPage( $pageId, $revId, $queryFlags );
297        return $this->titleFactory->newFromPageIdentity( $page );
298    }
299
300    /**
301     * Determines the page based on the available information.
302     *
303     * @param int|null $pageId
304     * @param int|null $revId
305     * @param int $queryFlags
306     *
307     * @return PageIdentity
308     * @throws RevisionAccessException
309     */
310    private function getPage( ?int $pageId, ?int $revId, int $queryFlags = IDBAccessObject::READ_NORMAL ) {
311        if ( !$pageId && !$revId ) {
312            throw new InvalidArgumentException( '$pageId and $revId cannot both be 0 or null' );
313        }
314
315        // This method recalls itself with READ_LATEST if READ_NORMAL doesn't get us a Title
316        // So ignore READ_LATEST_IMMUTABLE flags and handle the fallback logic in this method
317        if ( DBAccessObjectUtils::hasFlags( $queryFlags, IDBAccessObject::READ_LATEST_IMMUTABLE ) ) {
318            $queryFlags = IDBAccessObject::READ_NORMAL;
319        }
320
321        // Loading by ID is best
322        if ( $pageId !== null && $pageId > 0 ) {
323            $page = $this->pageStore->getPageById( $pageId, $queryFlags );
324            if ( $page ) {
325                return $this->wrapPage( $page );
326            }
327        }
328
329        // rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
330        if ( $revId !== null && $revId > 0 ) {
331            $pageQuery = $this->pageStore->newSelectQueryBuilder( $queryFlags )
332                ->join( 'revision', null, 'page_id=rev_page' )
333                ->conds( [ 'rev_id' => $revId ] )
334                ->caller( __METHOD__ );
335
336            $page = $pageQuery->fetchPageRecord();
337            if ( $page ) {
338                return $this->wrapPage( $page );
339            }
340        }
341
342        // If we still don't have a title, fallback to primary DB if that wasn't already happening.
343        if ( $queryFlags === IDBAccessObject::READ_NORMAL ) {
344            $title = $this->getPage( $pageId, $revId, IDBAccessObject::READ_LATEST );
345            if ( $title ) {
346                $this->logger->info(
347                    __METHOD__ . ' fell back to READ_LATEST and got a Title.',
348                    [ 'exception' => new RuntimeException() ]
349                );
350                return $title;
351            }
352        }
353
354        throw new RevisionAccessException(
355            'Could not determine title for page ID {page_id} and revision ID {rev_id}',
356            [
357                'page_id' => $pageId,
358                'rev_id' => $revId,
359            ]
360        );
361    }
362
363    /**
364     * @param PageIdentity $page
365     *
366     * @return PageIdentity
367     */
368    private function wrapPage( PageIdentity $page ): PageIdentity {
369        if ( $this->wikiId === WikiAwareEntity::LOCAL ) {
370            // NOTE: since there is still a lot of code that needs a full Title,
371            //       and uses Title::castFromPageIdentity() to get one, it's beneficial
372            //       to create a Title right away if we can, so we don't have to convert
373            //       over and over later on.
374            //       When there is less need to convert to Title, this special case can
375            //       be removed.
376            return $this->titleFactory->newFromPageIdentity( $page );
377        } else {
378            return $page;
379        }
380    }
381
382    /**
383     * @param mixed $value
384     * @param string $name
385     *
386     * @throws IncompleteRevisionException if $value is null
387     * @return mixed $value, if $value is not null
388     */
389    private function failOnNull( $value, $name ) {
390        if ( $value === null ) {
391            throw new IncompleteRevisionException(
392                "$name must not be " . var_export( $value, true ) . "!"
393            );
394        }
395
396        return $value;
397    }
398
399    /**
400     * @param mixed $value
401     * @param string $name
402     *
403     * @throws IncompleteRevisionException if $value is empty
404     * @return mixed $value, if $value is not null
405     */
406    private function failOnEmpty( $value, $name ) {
407        if ( $value === null || $value === 0 || $value === '' ) {
408            throw new IncompleteRevisionException(
409                "$name must not be " . var_export( $value, true ) . "!"
410            );
411        }
412
413        return $value;
414    }
415
416    /**
417     * Insert a new revision into the database, returning the new revision record
418     * on success and dies horribly on failure.
419     *
420     * This should be followed up by a WikiPage::updateRevisionOn on call to update
421     * page_latest on the page the revision is added to.
422     *
423     * MCR migration note: this replaced Revision::insertOn
424     *
425     * @param RevisionRecord $rev
426     * @param IDatabase $dbw (primary connection)
427     *
428     * @return RevisionRecord the new revision record.
429     */
430    public function insertRevisionOn( RevisionRecord $rev, IDatabase $dbw ) {
431        // TODO: pass in a DBTransactionContext instead of a database connection.
432        $this->checkDatabaseDomain( $dbw );
433
434        $slotRoles = $rev->getSlotRoles();
435
436        // Make sure the main slot is always provided throughout migration
437        if ( !in_array( SlotRecord::MAIN, $slotRoles ) ) {
438            throw new IncompleteRevisionException(
439                'main slot must be provided'
440            );
441        }
442
443        // Checks
444        $this->failOnNull( $rev->getSize(), 'size field' );
445        $this->failOnEmpty( $rev->getSha1(), 'sha1 field' );
446        $this->failOnEmpty( $rev->getTimestamp(), 'timestamp field' );
447        $comment = $this->failOnNull( $rev->getComment( RevisionRecord::RAW ), 'comment' );
448        $user = $this->failOnNull( $rev->getUser( RevisionRecord::RAW ), 'user' );
449        $this->failOnNull( $user->getId(), 'user field' );
450        $this->failOnEmpty( $user->getName(), 'user_text field' );
451
452        if ( !$rev->isReadyForInsertion() ) {
453            // This is here for future-proofing. At the time this check being added, it
454            // was redundant to the individual checks above.
455            throw new IncompleteRevisionException( 'Revision is incomplete' );
456        }
457
458        if ( $slotRoles == [ SlotRecord::MAIN ] ) {
459            // T239717: If the main slot is the only slot, make sure the revision's nominal size
460            // and hash match the main slot's nominal size and hash.
461            $mainSlot = $rev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );