Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
24.49% covered (danger)
24.49%
72 / 294
13.79% covered (danger)
13.79%
4 / 29
CRAP
0.00% covered (danger)
0.00%
0 / 1
FlaggableWikiPage
24.49% covered (danger)
24.49%
72 / 294
13.79% covered (danger)
13.79%
4 / 29
2976.97
0.00% covered (danger)
0.00%
0 / 1
 getInstanceCache
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getTitleInstance
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 newInstance
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 preloadPreparedEdit
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getCurrentUpdate
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 clear
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 isStableShownByDefault
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 editsRequireReview
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 isDataLoaded
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 revsArePending
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getPendingRevCount
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
20
 stableVersionIsSynced
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 onlyTemplatesPending
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 isPageLocked
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 isPageUnlocked
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 lowProfileUI
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isReviewable
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
4.12
 getStable
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getStableRev
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
6.56
 getStabilitySettings
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 syncedInTracking
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getBestFlaggedRevId
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 lazyUpdateSyncStatus
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 pageData
75.00% covered (warning)
75.00%
18 / 24
0.00% covered (danger)
0.00%
0 / 1
3.14
 loadPageData
80.95% covered (warning)
80.95%
17 / 21
0.00% covered (danger)
0.00%
0 / 1
8.44
 updateStableVersion
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
72
 clearStableVersion
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
2.00
 updatePendingList
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3use MediaWiki\Cache\CacheKeyHelper;
4use MediaWiki\MediaWikiServices;
5use MediaWiki\Page\PageIdentity;
6use MediaWiki\Revision\RevisionRecord;
7use MediaWiki\Storage\PreparedUpdate;
8use Wikimedia\Assert\PreconditionException;
9use Wikimedia\Rdbms\Database;
10use Wikimedia\Rdbms\IDatabase;
11use Wikimedia\Rdbms\IReadableDatabase;
12use Wikimedia\Rdbms\SelectQueryBuilder;
13
14/**
15 * Class representing a MediaWiki article and history
16 *
17 * FlaggableWikiPage::getTitleInstance() is preferred over constructor calls
18 */
19class FlaggableWikiPage extends WikiPage {
20    /** @var int */
21    private $stable = 0;
22    /** @var FlaggedRevision|false|null */
23    private $stableRev = null;
24    /** @var bool|null */
25    private $revsArePending = null;
26    /** @var int|null */
27    private $pendingRevCount = null;
28    /** @var array|null */
29    private $pageConfig = null;
30    /** @var bool|null */
31    private $syncedInTracking = null;
32    /** @var PreparedUpdate|null */
33    private $preparedUpdate = null;
34    /** @var MapCacheLRU|null */
35    private static $instances = null;
36
37    /**
38     * @return MapCacheLRU
39     */
40    private static function getInstanceCache(): MapCacheLRU {
41        if ( !self::$instances ) {
42            self::$instances = new MapCacheLRU( 10 );
43        }
44        return self::$instances;
45    }
46
47    /**
48     * Get a FlaggableWikiPage for a given title
49     *
50     * @param PageIdentity $title
51     * @return self
52     */
53    public static function getTitleInstance( PageIdentity $title ) {
54        $cache = self::getInstanceCache();
55        $key = CacheKeyHelper::getKeyForPage( $title );
56        $fwp = $cache->get( $key );
57        if ( !$fwp ) {
58            $fwp = self::newInstance( $title );
59            $cache->set( $key, $fwp );
60        }
61        return $fwp;
62    }
63
64    /**
65     * @param PageIdentity $page
66     * @return self
67     */
68    public static function newInstance( PageIdentity $page ) {
69        return $page instanceof self ? $page : new self( $page );
70    }
71
72    /**
73     * @deprecated Please use {@see newInstance} instead
74     * @param PageIdentity $pageIdentity
75     */
76    public function __construct( PageIdentity $pageIdentity ) {
77        parent::__construct( $pageIdentity );
78    }
79
80    /**
81     * Transfer the prepared edit cache from a WikiPage object.
82     * Also make available the current prepared update to later
83     * calls to getCurrentUpdate().
84     *
85     * @note This will throw unless called during an ongoing edit!
86     *
87     * @param WikiPage $page
88     * @return void
89     */
90    public function preloadPreparedEdit( WikiPage $page ) {
91        $this->mPreparedEdit = $page->mPreparedEdit;
92
93        try {
94            $this->preparedUpdate = $page->getCurrentUpdate();
95        } catch ( PreconditionException | LogicException $ex ) {
96            // Ignore. getCurrentUpdate() will throw.
97        }
98    }
99
100    /**
101     * @inheritDoc
102     * @return PreparedUpdate
103     */
104    public function getCurrentUpdate(): PreparedUpdate {
105        return $this->preparedUpdate ?? parent::getCurrentUpdate();
106    }
107
108    /**
109     * Clear object process cache values
110     * @return void
111     */
112    public function clear() {
113        $this->stable = 0;
114        $this->stableRev = null;
115        $this->revsArePending = null;
116        $this->pendingRevCount = null;
117        $this->pageConfig = null;
118        $this->syncedInTracking = null;
119        parent::clear(); // call super!
120    }
121
122    /**
123     * Is the stable version shown by default for this page?
124     * @return bool
125     */
126    public function isStableShownByDefault() {
127        if ( !$this->isReviewable() ) {
128            return false; // no stable versions can exist
129        }
130        $config = $this->getStabilitySettings(); // page configuration
131        return (bool)$config['override'];
132    }
133
134    /**
135     * Do edits have to be reviewed before being shown by default (going live)?
136     * @return bool
137     */
138    public function editsRequireReview() {
139        return (
140            $this->isReviewable() && // reviewable page
141            $this->isStableShownByDefault() && // and stable versions override
142            $this->getStableRev() // and there is a stable version
143        );
144    }
145
146    /**
147     * Has data for this page been loaded?
148     * @return bool
149     */
150    public function isDataLoaded() {
151        return $this->mDataLoaded;
152    }
153
154    /**
155     * Are edits to this page currently pending?
156     * @return bool
157     */
158    public function revsArePending() {
159        if ( !$this->mDataLoaded ) {
160            $this->loadPageData();
161        }
162        return $this->revsArePending;
163    }
164
165    /**
166     * Get number of revs since the stable revision
167     * Note: slower than revsArePending()
168     * @return int
169     */
170    public function getPendingRevCount() {
171        global $wgParserCacheExpireTime;
172
173        if ( !$this->mDataLoaded ) {
174            $this->loadPageData();
175        }
176        # Pending count deferred even after page data load
177        if ( $this->pendingRevCount !== null ) {
178            return $this->pendingRevCount; // use process cache
179        }
180        $srev = $this->getStableRev();
181        if ( !$srev ) {
182            return 0; // none
183        }
184        $sRevId = $srev->getRevId();
185
186        $fname = __METHOD__;
187        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
188        $this->pendingRevCount = (int)$cache->getWithSetCallback(
189            # Confirm that cache value was made against the same current and
190            # stable revision. This avoids lengthy cache pollution if either
191            # of them is outdated, or if the page is edited twice within the
192            # cache expiry time (which is three weeks in Wikimedia production!).
193            $cache->makeKey( 'flaggedrevs-pending-count', $this->getLatest(), $sRevId ),
194            $wgParserCacheExpireTime,
195            function (
196                $oldValue = null, &$ttl = null, array &$setOpts = []
197            ) use ( $srev, $fname ) {
198                $db = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
199                $setOpts += Database::getCacheSetOptions( $db );
200
201                return (int)$db->newSelectQueryBuilder()
202                    ->select( 'COUNT(*)' )
203                    ->from( 'revision' )
204                    ->where( [
205                        'rev_page' => $this->getId(),
206                        // T17515
207                        $db->expr( 'rev_timestamp', '>',
208                            $db->timestamp( $srev->getRevTimestamp() ) ),
209                    ] )
210                    ->caller( $fname )
211                    ->fetchField();
212            },
213            [
214                'touchedCallback' => function () {
215                    return wfTimestampOrNull( TS_UNIX, $this->getTouched() );
216                }
217            ]
218        );
219
220        return $this->pendingRevCount;
221    }
222
223    /**
224     * Checks if the stable version is synced with the current revision
225     * Note: slower than getPendingRevCount()
226     * @return bool
227     */
228    public function stableVersionIsSynced() {
229        global $wgParserCacheExpireTime;
230
231        $srev = $this->getStableRev();
232        if ( !$srev ) {
233            return true;
234        }
235        # Stable text revision must be the same as the current
236        if ( $this->revsArePending() ) {
237            return false;
238        }
239        # If using the current version of includes, there is nothing else to check.
240        if ( FlaggedRevs::inclusionSetting() == FR_INCLUDES_CURRENT ) {
241            return true; // short-circuit
242        }
243
244        $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
245
246        return (bool)$cache->getWithSetCallback(
247            $cache->makeKey( 'flaggedrevs-includes-synced', $this->getId() ),
248            $wgParserCacheExpireTime,
249            static function () use ( $srev ) {
250                # Since the stable and current revisions have the same text and only outputs, the
251                # only other things to check for are template differences in the output.
252                # (a) Check if the current output has a newer template used
253                # (b) Check if the stable version has a template that was deleted
254                return ( !$srev->findPendingTemplateChanges() ) ? 1 : 0;
255            },
256            [
257                'touchedCallback' => function () {
258                    return $this->getTouched();
259                }
260            ]
261        );
262    }
263
264    /**
265     * Are template changes and ONLY template changes pending?
266     * @return bool
267     */
268    public function onlyTemplatesPending() {
269        return ( !$this->revsArePending() && !$this->stableVersionIsSynced() );
270    }
271
272    /**
273     * Is this page less open than the site defaults?
274     * @return bool
275     */
276    public function isPageLocked() {
277        return ( !FlaggedRevs::isStableShownByDefault() && $this->isStableShownByDefault() );
278    }
279
280    /**
281     * Is this page more open than the site defaults?
282     * @return bool
283     */
284    public function isPageUnlocked() {
285        return ( FlaggedRevs::isStableShownByDefault() && !$this->isStableShownByDefault() );
286    }
287
288    /**
289     * Tags are only shown for unreviewed content and this page is not locked/unlocked?
290     * @return bool
291     */
292    public function lowProfileUI() {
293        global $wgFlaggedRevsLowProfile;
294        return $wgFlaggedRevsLowProfile &&
295            FlaggedRevs::isStableShownByDefault() == $this->isStableShownByDefault();
296    }
297
298    /**
299     * Is this article reviewable?
300     * @return bool
301     */
302    public function isReviewable() {
303        if ( !FlaggedRevs::inReviewNamespace( $this ) ) {
304            return false;
305        }
306        # Check if flagging is disabled for this page via config
307        if ( FlaggedRevs::useOnlyIfProtected() ) {
308            $config = $this->getStabilitySettings(); // page configuration
309            return (bool)$config['override']; // stable is default or flagging disabled
310        }
311        return true;
312    }
313
314    /**
315     * Get the stable revision ID
316     * @return int
317     */
318    public function getStable() {
319        if ( !FlaggedRevs::inReviewNamespace( $this ) ) {
320            return 0; // short-circuit
321        }
322        if ( !$this->mDataLoaded ) {
323            $this->loadPageData();
324        }
325        return (int)$this->stable;
326    }
327
328    /**
329     * Get the stable revision
330     * @return FlaggedRevision|null
331     */
332    public function getStableRev() {
333        if ( !FlaggedRevs::inReviewNamespace( $this ) ) {
334            return null; // short-circuit
335        }
336        if ( !$this->mDataLoaded ) {
337            $this->loadPageData();
338        }
339        # Stable rev deferred even after page data load
340        if ( $this->stableRev === null ) {
341            $srev = FlaggedRevision::newFromTitle( $this->mTitle, $this->stable );
342            $this->stableRev = $srev ?: false; // cache negative hits too
343        }
344        return $this->stableRev ?: null; // false => null
345    }
346
347    /**
348     * Get visibility restrictions on page
349     * @return array [ 'override' => int, 'autoreview' => string, 'expiry' => string ]
350     */
351    public function getStabilitySettings() {
352        if ( !$this->mDataLoaded ) {
353            $this->loadPageData();
354        }
355        return $this->pageConfig;
356    }
357
358    /**
359     * Get the fp_reviewed value for this page
360     * @return bool
361     */
362    public function syncedInTracking() {
363        if ( !$this->mDataLoaded ) {
364            $this->loadPageData();
365        }
366        return $this->syncedInTracking;
367    }
368
369    /**
370     * Get the newest of the highest rated flagged revisions of this page
371     * Note: will not return deleted revisions
372     * @return int
373     */
374    public function getBestFlaggedRevId() {
375        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
376
377        # Get the highest quality revision (not necessarily this one).
378        $oldid = $dbr->newSelectQueryBuilder()
379            ->select( 'fr_rev_id' )
380            ->from( 'flaggedrevs' )
381            ->join( 'revision', null, 'rev_id = fr_rev_id' )
382            ->where( [
383                'fr_page_id' => $this->getId(),
384                'rev_page = fr_page_id', // sanity
385                $dbr->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . ' = 0'
386            ] )
387            ->orderBy( [ 'fr_rev_timestamp', 'fr_rev_id' ], SelectQueryBuilder::SORT_DESC )
388            ->caller( __METHOD__ )
389            ->fetchField();
390        return (int)$oldid;
391    }
392
393    /**
394     * Updates the fp_reviewed field for this article
395     */
396    public function lazyUpdateSyncStatus() {
397        $services = MediaWikiServices::getInstance();
398        if ( $services->getReadOnlyMode()->isReadOnly() ) {
399            return;
400        }
401
402        $services->getJobQueueGroup()->push(
403            new FRExtraCacheUpdateJob(
404                $this->getTitle(),
405                [ 'type' => 'updatesyncstate' ]
406            )
407        );
408    }
409
410    /**
411     * Fetch a page record with the given conditions
412     * @param IReadableDatabase $dbr
413     * @param array $conditions
414     * @param array $options
415     * @return stdClass|false
416     */
417    protected function pageData( $dbr, $conditions, $options = [] ) {
418        $fname = __METHOD__;
419        $selectCallback = static function () use ( $dbr, $conditions, $options, $fname ) {
420            $pageQuery = WikiPage::getQueryInfo();
421
422            return $dbr->newSelectQueryBuilder()
423                ->queryInfo( $pageQuery )
424                ->select( [
425                    'fpc_override', 'fpc_level', 'fpc_expiry',
426                    'fp_pending_since', 'fp_stable', 'fp_reviewed',
427                ] )
428                ->leftJoin( 'flaggedpages', null, 'fp_page_id = page_id' )
429                ->leftJoin( 'flaggedpage_config', null, 'fpc_page_id = page_id' )
430                ->where( $conditions )
431                ->options( $options )
432                ->caller( $fname )
433                ->fetchRow();
434        };
435
436        if ( $dbr instanceof IDatabase && !$dbr->isReadOnly() ) {
437            // load data directly without cache
438            return $selectCallback();
439        } else {
440            $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
441
442            return $cache->getWithSetCallback(
443                $cache->makeKey( 'flaggedrevs-pageData', $this->getNamespace(), $this->getDBkey() ),
444                $cache::TTL_MINUTE,
445                $selectCallback
446            );
447        }
448    }
449
450    /**
451     * Set the page field data loaded from some source
452     * @param stdClass|string|int $data Database row object or "fromdb" or "fromdbmaster"
453     * @return void
454     */
455    public function loadPageData( $data = IDBAccessObject::READ_NORMAL ) {
456        $this->mDataLoaded = true; // sanity
457
458        // Initialize defaults before trying to access the database
459        $this->stable = 0; // 0 => "found nothing"
460        $this->stableRev = null; // defer this one...
461        $this->revsArePending = false; // false => "found nothing" or "none pending"
462        $this->pendingRevCount = null; // defer this one...
463        $this->pageConfig = FRPageConfig::getDefaultVisibilitySettings(); // default
464        $this->syncedInTracking = true; // false => "unreviewed" or "synced"
465
466        # Fetch data from DB as needed...
467        $from = WikiPage::convertSelectType( $data );
468        if ( $from === IDBAccessObject::READ_NORMAL || $from === IDBAccessObject::READ_LATEST ) {
469            if ( $from === IDBAccessObject::READ_LATEST ) {
470                $db = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
471            } else {
472                $db = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
473            }
474            $data = $this->pageDataFromTitle( $db, $this->mTitle );
475        }
476        # Load in primary page data...
477        parent::loadPageData( $data /* Row obj */ );
478        # Load in flaggedrevs Row data if the page exists...(sanity check NS)
479        if ( $data && FlaggedRevs::inReviewNamespace( $this ) ) {
480            if ( $data->fpc_override !== null ) { // page config row found
481                $this->pageConfig = FRPageConfig::getVisibilitySettingsFromRow( $data );
482            }
483            if ( $data->fp_stable !== null ) { // stable rev found
484                $this->stable = (int)$data->fp_stable;
485                $this->revsArePending = ( $data->fp_pending_since !== null ); // revs await review
486                $this->syncedInTracking = (bool)$data->fp_reviewed;
487            }
488        }
489    }
490
491    /**
492     * Updates the flagging tracking tables for this page
493     * @param FlaggedRevision $srev The new stable version
494     * @param int|null $latest The latest rev ID (optional)
495     */
496    public function updateStableVersion( FlaggedRevision $srev, $latest = null ) {
497        if ( !$this->exists() ) {
498            // No bogus entries
499            return;
500        }
501
502        $revRecord = $srev->getRevisionRecord();
503        if ( !$revRecord ) {
504            // No bogus entries
505            return;
506        }
507
508        # Get the latest revision ID if not set
509        if ( !$latest ) {
510            $latest = $this->mTitle->getLatestRevID( IDBAccessObject::READ_LATEST );
511        }
512        $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
513
514        # Get the timestamp of the first edit after the stable version (if any)...
515        $nextTimestamp = null;
516        if ( $revRecord->getId() != $latest ) {
517            $timestamp = $dbw->timestamp( $revRecord->getTimestamp() );
518            $nextEditTS = $dbw->newSelectQueryBuilder()
519                ->select( 'rev_timestamp' )
520                ->from( 'revision' )
521                ->where( [
522                    'rev_page' => $this->getId(),
523                    $dbw->expr( 'rev_timestamp', '>', $timestamp ),
524                ] )
525                ->orderBy( 'rev_timestamp' )
526                ->caller( __METHOD__ )
527                ->fetchField();
528            if ( $nextEditTS ) { // sanity check
529                $nextTimestamp = $nextEditTS;
530            }
531        }
532        # Get the new page sync status...
533        $synced = !(
534            $nextTimestamp !== null || // edits pending
535            $srev->findPendingTemplateChanges() // template changes pending
536        );
537        # Alter table metadata
538        $dbw->newReplaceQueryBuilder()
539            ->replaceInto( 'flaggedpages' )
540            ->uniqueIndexFields( 'fp_page_id' )
541            ->row( [
542                'fp_page_id'       => $revRecord->getPageId(), // Don't use $this->getId(), T246720
543                'fp_stable'        => $revRecord->getId(),
544                'fp_reviewed'      => $synced ? 1 : 0,
545                'fp_quality'       => FR_CHECKED,
546                'fp_pending_since' => $dbw->timestampOrNull( $nextTimestamp )
547            ] )
548            ->caller( __METHOD__ )
549            ->execute();
550        # Update pending edit tracking table
551        $this->updatePendingList( $this->getId(), $latest );
552    }
553
554    /**
555     * Updates the flagging tracking tables for this page
556     */
557    public function clearStableVersion() {
558        if ( !$this->exists() ) {
559            return; // nothing to do
560        }
561        $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
562
563        $dbw->newDeleteQueryBuilder()
564            ->deleteFrom( 'flaggedpages' )
565            ->where( [ 'fp_page_id' => $this->getId() ] )
566            ->caller( __METHOD__ )
567            ->execute();
568        $dbw->newDeleteQueryBuilder()
569            ->deleteFrom( 'flaggedpage_pending' )
570            ->where( [ 'fpp_page_id' => $this->getId() ] )
571            ->caller( __METHOD__ )
572            ->execute();
573    }
574
575    /**
576     * Updates the flaggedpage_pending table
577     * @param int $pageId
578     * @param int $latest Latest revision
579     */
580    private function updatePendingList( $pageId, $latest ) {
581        $data = [];
582
583        $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
584
585        # Get the latest revision of FR_CHECKED
586        $row = $dbw->newSelectQueryBuilder()
587            ->select( [ 'fr_rev_id', 'rev_timestamp' ] )
588            ->from( 'flaggedrevs' )
589            ->join( 'revision', null, 'rev_id = fr_rev_id' ) // rev exists
590            ->where( [
591                'fr_page_id' => $pageId,
592                'rev_page = fr_page_id', // sanity
593                $dbw->bitAnd( 'rev_deleted', RevisionRecord::DELETED_TEXT ) . ' = 0'
594            ] )
595            ->orderBy( [ 'fr_rev_timestamp', 'fr_rev_id' ], SelectQueryBuilder::SORT_DESC )
596            ->caller( __METHOD__ )
597            ->fetchRow();
598        # If there is a revision of this level, track it...
599        # Revisions accepted to one tier count as accepted
600        # at the lower tiers (i.e. quality -> checked).
601        if ( $row ) {
602            $id = (int)$row->fr_rev_id;
603            $ts = $row->rev_timestamp;
604        } else { // use previous rev of higher tier (if any)
605            $id = 0;
606            $ts = '';
607        }
608        # Get edits that actually are pending...
609        if ( $id && $latest > $id ) {
610            # Get the timestamp of the edit after this version (if any)
611            $nextTimestamp = $dbw->newSelectQueryBuilder()
612                ->select( 'rev_timestamp' )
613                ->from( 'revision' )
614                ->where( [
615                    'rev_page' => $pageId,
616                    $dbw->buildComparison( '>', [
617                        'rev_timestamp' => $ts,
618                        'rev_id' => $id,
619                    ] ),
620                ] )
621                ->orderBy( [ 'rev_timestamp', 'rev_id' ] )
622                ->caller( __METHOD__ )
623                ->fetchField();
624            // No newer revision found
625            if ( $nextTimestamp !== false ) {
626                $data[] = [
627                    'fpp_page_id'       => $pageId,
628                    'fpp_quality'       => FR_CHECKED,
629                    'fpp_rev_id'        => $id,
630                    'fpp_pending_since' => $nextTimestamp
631                ];
632            }
633        }
634        # Clear any old junk, and insert new rows
635        $dbw->newDeleteQueryBuilder()
636            ->deleteFrom( 'flaggedpage_pending' )
637            ->where( [ 'fpp_page_id' => $pageId ] )
638            ->caller( __METHOD__ )
639            ->execute();
640        if ( $data !== [] ) {
641            $dbw->newInsertQueryBuilder()
642                ->insertInto( 'flaggedpage_pending' )
643                ->rows( $data )
644                ->caller( __METHOD__ )
645                ->execute();
646        }
647    }
648}