Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
18.82% covered (danger)
18.82%
51 / 271
48.57% covered (danger)
48.57%
17 / 35
CRAP
0.00% covered (danger)
0.00%
0 / 1
FlaggedRevs
18.82% covered (danger)
18.82%
51 / 271
48.57% covered (danger)
48.57%
17 / 35
7567.45
0.00% covered (danger)
0.00%
0 / 1
 binaryFlagging
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getTagName
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 autoReviewEdits
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 autoReviewNewPages
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 autoReviewEnabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 maxAutoReviewLevel
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 isStableShownByDefault
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 useOnlyIfProtected
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 inclusionSetting
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 useProtectionLevels
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getRestrictionLevels
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getMaxLevel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 valueIsValid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 tagIsValid
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 userCanSetValue
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
72
 userCanSetTag
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
56
 userCanSetAutoreviewLevel
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
72
 getParserCacheInstance
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 parseStableRevisionPooled
83.33% covered (warning)
83.33%
20 / 24
0.00% covered (danger)
0.00%
0 / 1
3.04
 parseStableRevision
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
272
 stableVersionUpdates
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
156
 clearTrackingRows
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 clearStableOnlyDeps
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 purgeMediaWikiHtmlCdn
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 updateHtmlCaches
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 extraHTMLCacheUpdate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 markRevisionPatrolled
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 quickTags
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 quickTag
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getAutoReviewTags
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 getReviewNamespaces
37.50% covered (danger)
37.50%
3 / 8
0.00% covered (danger)
0.00%
0 / 1
11.10
 getFirstReviewNamespace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isReviewNamespace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 inReviewNamespace
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 autoReviewEdit
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
182
1<?php
2
3use MediaWiki\Config\ConfigException;
4use MediaWiki\Deferred\DeferredUpdates;
5use MediaWiki\MediaWikiServices;
6use MediaWiki\Page\PageIdentity;
7use MediaWiki\Page\PageReference;
8use MediaWiki\Parser\ParserOutput;
9use MediaWiki\PoolCounter\PoolCounterWorkViaCallback;
10use MediaWiki\Revision\RenderedRevision;
11use MediaWiki\Revision\RevisionRecord;
12use MediaWiki\Revision\SlotRecord;
13use MediaWiki\Status\Status;
14use MediaWiki\Title\Title;
15use MediaWiki\User\User;
16use MediaWiki\User\UserIdentity;
17use Wikimedia\Assert\PreconditionException;
18
19/**
20 * Class containing utility functions for a FlaggedRevs environment
21 *
22 * Class is lazily-initialized, calling load() as needed
23 */
24class FlaggedRevs {
25    /**
26     * The name of the ParserCache to use for stable revisions caching.
27     *
28     * @note This name is used as a part of the ParserCache key, so
29     * changing it will invalidate the parser cache for stable revisions.
30     *
31     * TODO: Extract constant to FlaggedRevsParserCache
32     *
33     * @deprecated 1.39
34     */
35    public const PARSER_CACHE_NAME = 'stable-pcache';
36    public const PARSOID_PARSER_CACHE_NAME = 'stable-parsoid-pcache';
37
38    # ################ Basic config accessors #################
39
40    /**
41     * Is there only one tag and it has only one level?
42     * @return bool
43     */
44    public static function binaryFlagging() {
45        return self::useOnlyIfProtected() || self::getMaxLevel() <= 1;
46    }
47
48    /**
49     * Get the supported dimension name.
50     * @return string
51     */
52    public static function getTagName(): string {
53        global $wgFlaggedRevsTags;
54        if ( count( $wgFlaggedRevsTags ) !== 1 ) {
55            throw new ConfigException( 'FlaggedRevs given invalid tag name! We only support one dimension now.' );
56        }
57        return array_keys( $wgFlaggedRevsTags )[0];
58    }
59
60    /**
61     * Allow auto-review edits directly to the stable version by reviewers?
62     * @return bool
63     */
64    public static function autoReviewEdits() {
65        global $wgFlaggedRevsAutoReview;
66        return (bool)( $wgFlaggedRevsAutoReview & FR_AUTOREVIEW_CHANGES );
67    }
68
69    /**
70     * Auto-review new pages with the minimal level?
71     * @return bool
72     */
73    public static function autoReviewNewPages() {
74        global $wgFlaggedRevsAutoReview;
75        return (bool)( $wgFlaggedRevsAutoReview & FR_AUTOREVIEW_CREATION );
76    }
77
78    /**
79     * Auto-review of new pages or edits to pages enabled?
80     * @return bool
81     */
82    public static function autoReviewEnabled() {
83        return self::autoReviewEdits() || self::autoReviewNewPages();
84    }
85
86    /**
87     * Get the maximum level that can be autoreviewed
88     * @return int
89     */
90    private static function maxAutoReviewLevel() {
91        global $wgFlaggedRevsTagsAuto;
92        if ( !self::autoReviewEnabled() ) {
93            return 0; // shouldn't happen
94        }
95        // B/C (before $wgFlaggedRevsTagsAuto)
96        return (int)( $wgFlaggedRevsTagsAuto[self::getTagName()] ?? 1 );
97    }
98
99    /**
100     * Is a "stable version" used as the default display
101     * version for all pages in reviewable namespaces?
102     * @return bool
103     */
104    public static function isStableShownByDefault() {
105        global $wgFlaggedRevsOverride;
106        if ( self::useOnlyIfProtected() ) {
107            return false; // must be configured per-page
108        }
109        return (bool)$wgFlaggedRevsOverride;
110    }
111
112    /**
113     * Are pages reviewable only if they have been manually
114     * configured by an admin to use a "stable version" as the default?
115     * @return bool
116     */
117    public static function useOnlyIfProtected() {
118        global $wgFlaggedRevsProtection;
119        return (bool)$wgFlaggedRevsProtection;
120    }
121
122    /**
123     * @return int
124     */
125    public static function inclusionSetting() {
126        global $wgFlaggedRevsHandleIncludes;
127        return $wgFlaggedRevsHandleIncludes;
128    }
129
130    /**
131     * Are there site defined protection levels for review
132     * @return bool
133     */
134    public static function useProtectionLevels(): bool {
135        return self::useOnlyIfProtected() && self::getRestrictionLevels();
136    }
137
138    /**
139     * Get the autoreview restriction levels available
140     * @return string[] Value from $wgFlaggedRevsRestrictionLevels
141     */
142    public static function getRestrictionLevels(): array {
143        global $wgFlaggedRevsRestrictionLevels;
144        if ( in_array( '', $wgFlaggedRevsRestrictionLevels ) ) {
145            throw new ConfigException( 'Invalid empty value in $wgFlaggedRevsRestrictionLevels' );
146        }
147        return $wgFlaggedRevsRestrictionLevels;
148    }
149
150    /**
151     * @return int Number of levels, excluding "0" level
152     */
153    public static function getMaxLevel() {
154        global $wgFlaggedRevsTags;
155        return reset( $wgFlaggedRevsTags )['levels'];
156    }
157
158    # ################ Permission functions #################
159
160    /** Check if the tag has a valid value */
161    private static function valueIsValid( int $value ): bool {
162        return $value >= 0 && $value <= self::getMaxLevel();
163    }
164
165    /**
166     * Check if we’re in protection mode or the tag has a valid value
167     */
168    public static function tagIsValid( ?int $tag ): bool {
169        return self::useOnlyIfProtected() || ( $tag !== null && self::valueIsValid( $tag ) );
170    }
171
172    /**
173     * Returns true if a user can set $value
174     */
175    public static function userCanSetValue( UserIdentity $user, int $value ): bool {
176        global $wgFlaggedRevsTagsRestrictions;
177
178        $pm = MediaWikiServices::getInstance()->getPermissionManager();
179        # Sanity check tag and value
180        if ( !self::valueIsValid( $value ) ) {
181            return false; // flag range is invalid
182        }
183        $restrictions = $wgFlaggedRevsTagsRestrictions[self::getTagName()] ?? [];
184        # No restrictions -> full access
185        # Validators always have full access
186        if ( !$restrictions || $pm->userHasRight( $user, 'validate' ) ) {
187            return true;
188        }
189        # Check if this user has any right that lets him/her set
190        # up to this particular value
191        foreach ( $restrictions as $right => $level ) {
192            if ( $value <= $level && $level > 0 && $pm->userHasRight( $user, $right ) ) {
193                return true;
194            }
195        }
196        return false;
197    }
198
199    /**
200     * Returns true if a user can set $tag for a revision via review.
201     * Requires the same for $oldTag if given.
202     * @param UserIdentity $user
203     * @param int|null $tag suggested tag
204     * @param int|null $oldTag pre-existing tag
205     */
206    public static function userCanSetTag( UserIdentity $user, ?int $tag, ?int $oldTag = null ): bool {
207        if ( !MediaWikiServices::getInstance()->getPermissionManager()
208            ->userHasRight( $user, 'review' )
209        ) {
210            return false; // User is not able to review pages
211        }
212        if ( self::useOnlyIfProtected() ) {
213            return true;
214        }
215
216        if ( $tag === null ) {
217            return false; // unspecified
218        } elseif ( !self::userCanSetValue( $user, $tag ) ) {
219            return false; // user cannot set proposed flag
220        } elseif ( $oldTag !== null && !self::userCanSetValue( $user, $oldTag ) ) {
221            return false; // user cannot change old flag
222        }
223        return true;
224    }
225
226    /**
227     * Check if a user can set the autoreview restiction level to $right
228     * @param User $user
229     * @param string $right the level
230     * @return bool
231     */
232    public static function userCanSetAutoreviewLevel( $user, $right ) {
233        if ( $right == '' ) {
234            return true; // no restrictions (none)
235        }
236        if ( !in_array( $right, self::getRestrictionLevels() ) ) {
237            return false; // invalid restriction level
238        }
239        $pm = MediaWikiServices::getInstance()->getPermissionManager();
240        # Don't let them choose levels above their own rights
241        if ( $right == 'sysop' ) {
242            // special case, rewrite sysop to editprotected
243            if ( !$pm->userHasRight( $user, 'editprotected' ) ) {
244                return false;
245            }
246        } elseif ( $right == 'autoconfirmed' ) {
247            // special case, rewrite autoconfirmed to editsemiprotected
248            if ( !$pm->userHasRight( $user, 'editsemiprotected' ) ) {
249                return false;
250            }
251        } elseif ( !$pm->userHasRight( $user, $right ) ) {
252            return false;
253        }
254        return true;
255    }
256
257    # ################ Parsing functions #################
258
259    /**
260     * @param ParserOptions $pOpts
261     * @return FlaggedRevsParserCache
262     */
263    public static function getParserCacheInstance( ParserOptions $pOpts ): FlaggedRevsParserCache {
264        $cacheName = $pOpts->getUseParsoid() ? 'FlaggedRevsParsoidParserCache' : 'FlaggedRevsParserCache';
265        /** @var FlaggedRevsParserCache $cache */
266        $cache = MediaWikiServices::getInstance()->getService( $cacheName );
267        return $cache;
268    }
269
270    /**
271     * Get the HTML output of a revision, using PoolCounter in the process
272     *
273     * @param FlaggedRevision $frev
274     * @param ParserOptions $pOpts
275     * @return Status Fatal if the pool is full. Otherwise good with an optional ParserOutput, or
276     *  null if the revision is missing.
277     */
278    public static function parseStableRevisionPooled(
279        FlaggedRevision $frev, ParserOptions $pOpts
280    ) {
281        $services = MediaWikiServices::getInstance();
282        $page = $services->getWikiPageFactory()->newFromTitle( $frev->getTitle() );
283        $stableParserCache = self::getParserCacheInstance( $pOpts );
284        $keyPrefix = $stableParserCache->makeKey( $page, $pOpts );
285
286        $work = new PoolCounterWorkViaCallback(
287            'ArticleView', // use standard parse PoolCounter config
288            $keyPrefix . ':revid:' . $frev->getRevId(),
289            [
290                'doWork' => function () use ( $frev, $pOpts ) {
291                    return Status::newGood( self::parseStableRevision( $frev, $pOpts ) );
292                },
293                'doCachedWork' => static function () use ( $page, $pOpts, $stableParserCache ) {
294                    // Use new cache value from other thread
295                    return Status::newGood( $stableParserCache->get( $page, $pOpts ) ?: null );
296                },
297                'fallback' => static function () use ( $page, $pOpts, $stableParserCache ) {
298                    // Use stale cache if possible
299                    $parserOutput = $stableParserCache->getDirty( $page, $pOpts );
300                    // The fallback wasn't able to prevent the error situation, return false to
301                    // continue the original error handling
302                    return $parserOutput ? Status::newGood( $parserOutput ) : false;
303                },
304                'error' => static function ( Status $status ) {
305                    return $status;
306                },
307            ]
308        );
309
310        return $work->execute();
311    }
312
313    /**
314     * Get the HTML output of a revision.
315     * @param FlaggedRevision $frev
316     * @param ParserOptions $pOpts
317     * @return ParserOutput|null
318     */
319    public static function parseStableRevision( FlaggedRevision $frev, ParserOptions $pOpts ) {
320        # Notify Parser if includes should be stabilized
321        $resetManager = false;
322        $incManager = FRInclusionManager::singleton();
323        if ( $frev->getRevId() && self::inclusionSetting() != FR_INCLUDES_CURRENT ) {
324            # Use FRInclusionManager to do the template version query
325            # up front unless the versions are already specified there...
326            if ( !$incManager->parserOutputIsStabilized() ) {
327                $incManager->stabilizeParserOutput( $frev );
328                $resetManager = true; // need to reset when done
329            }
330        }
331        # Parse the new body
332        $content = $frev->getRevisionRecord()->getContent( SlotRecord::MAIN );
333        if ( $content === null ) {
334            return null; // missing revision
335        }
336
337        // Make this parse use reviewed/stable versions of templates
338        $oldCurrentRevisionRecordCallback = $pOpts->setCurrentRevisionRecordCallback(
339            function ( $title, $parser = null ) use ( &$oldCurrentRevisionRecordCallback, $incManager ) {
340                if ( !( $parser instanceof Parser ) ) {
341                    // nothing to do
342                    return call_user_func( $oldCurrentRevisionRecordCallback, $title, $parser );
343                }
344                if ( $title->getNamespace() < 0 || $title->getNamespace() === NS_MEDIAWIKI ) {
345                    // nothing to do (bug 29579 for NS_MEDIAWIKI)
346                    return call_user_func( $oldCurrentRevisionRecordCallback, $title, $parser );
347                }
348                if ( !$incManager->parserOutputIsStabilized() ) {
349                    // nothing to do
350                    return call_user_func( $oldCurrentRevisionRecordCallback, $title, $parser );
351                }
352                $id = false; // current version
353                # Check for the version of this template used when reviewed...
354                $maybeId = $incManager->getReviewedTemplateVersion( $title );
355                if ( $maybeId !== null ) {
356                    $id = (int)$maybeId; // use if specified (even 0)
357                }
358                # Check for stable version of template if this feature is enabled...
359                if ( self::inclusionSetting() == FR_INCLUDES_STABLE ) {
360                    $maybeId = $incManager->getStableTemplateVersion( $title );
361                    # Take the newest of these two...
362                    if ( $maybeId && $maybeId > $id ) {
363                        $id = (int)$maybeId;
364                    }
365                }
366                # Found a reviewed/stable revision
367                if ( $id !== false ) {
368                    # If $id is zero, don't bother loading it (page does not exist)
369                    if ( $id === 0 ) {
370                        return null;
371                    }
372                    return MediaWikiServices::getInstance()
373                        ->getRevisionLookup()
374                        ->getRevisionById( $id );
375                }
376                # Otherwise, fall back to default behavior (load latest revision)
377                return call_user_func( $oldCurrentRevisionRecordCallback, $title, $parser );
378            }
379        );
380        $contentRenderer = MediaWikiServices::getInstance()->getContentRenderer();
381        $parserOut = $contentRenderer->getParserOutput(
382            $content, $frev->getTitle(), $frev->getRevisionRecord(), $pOpts );
383        # Stable parse done!
384        if ( $resetManager ) {
385            $incManager->clear(); // reset the FRInclusionManager as needed
386        }
387        $pOpts->setCurrentRevisionRecordCallback( $oldCurrentRevisionRecordCallback );
388        return $parserOut;
389    }
390
391    # ################ Tracking/cache update update functions #################
392
393    /**
394     * Update the page tables with a new stable version.
395     * @param FlaggableWikiPage|PageIdentity $page
396     * @param FlaggedRevision|null $sv the new stable version (optional)
397     * @param FlaggedRevision|null $oldSv the old stable version (optional)
398     * @param RenderedRevision|null $renderedRevision (optional)
399     * @return bool stable version text changed and FR_INCLUDES_STABLE
400     */
401    public static function stableVersionUpdates(
402        object $page, $sv = null, $oldSv = null, $renderedRevision = null
403    ) {
404        if ( $page instanceof FlaggableWikiPage ) {
405            $article = $page;
406        } elseif ( $page instanceof PageIdentity ) {
407            $article = FlaggableWikiPage::getTitleInstance( $page );
408        } else {
409            throw new InvalidArgumentException( "First argument should be a PageIdentity." );
410        }
411        if ( !$article->isReviewable() ) {
412            return false;
413        }
414        $title = $article->getTitle();
415
416        $changed = false;
417        if ( $oldSv === null ) { // optional
418            $oldSv = FlaggedRevision::newFromStable( $title, IDBAccessObject::READ_LATEST );
419        }
420        if ( $sv === null ) { // optional
421            $sv = FlaggedRevision::determineStable( $title );
422        }
423
424        if ( !$sv ) {
425            # Empty flaggedrevs data for this page if there is no stable version
426            $article->clearStableVersion();
427            # Check if pages using this need to be refreshed...
428            if ( self::inclusionSetting() == FR_INCLUDES_STABLE ) {
429                $changed = (bool)$oldSv;
430            }
431        } else {
432            if ( $renderedRevision ) {
433                $renderedId = $renderedRevision->getRevision()->getId();
434            } else {
435                $renderedId = null;
436            }
437
438            # Update flagged page related fields
439            $article->updateStableVersion( $sv, $renderedId );
440            # Check if pages using this need to be invalidated/purged...
441            if ( self::inclusionSetting() == FR_INCLUDES_STABLE ) {
442                $changed = (
443                    !$oldSv ||
444                    $sv->getRevId() != $oldSv->getRevId()
445                );
446            }
447        }
448        # Lazily rebuild dependencies on next parse (we invalidate below)
449        self::clearStableOnlyDeps( $title->getArticleID() );
450        # Clear page cache unless this is hooked via RevisionDataUpdates, in
451        # which case these updates will happen already with tuned timestamps
452        if ( !$renderedRevision ) {
453            $title->invalidateCache();
454            self::purgeMediaWikiHtmlCdn( $title );
455        }
456
457        return $changed;
458    }
459
460    /**
461     * Clear FlaggedRevs tracking tables for this page
462     * @param int|int[] $pageId (int or array)
463     */
464    public static function clearTrackingRows( $pageId ) {
465        $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
466
467        $dbw->newDeleteQueryBuilder()
468            ->deleteFrom( 'flaggedpages' )
469            ->where( [ 'fp_page_id' => $pageId ] )
470            ->caller( __METHOD__ )
471            ->execute();
472        $dbw->newDeleteQueryBuilder()
473            ->deleteFrom( 'flaggedrevs_tracking' )
474            ->where( [ 'ftr_from' => $pageId ] )
475            ->caller( __METHOD__ )
476            ->execute();
477        $dbw->newDeleteQueryBuilder()
478            ->deleteFrom( 'flaggedpage_pending' )
479            ->where( [ 'fpp_page_id' => $pageId ] )
480            ->caller( __METHOD__ )
481            ->execute();
482    }
483
484    /**
485     * Clear tracking table of stable-only links for this page
486     * @param int|int[] $pageId (int or array)
487     */
488    public static function clearStableOnlyDeps( $pageId ) {
489        $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
490
491        $dbw->newDeleteQueryBuilder()
492            ->deleteFrom( 'flaggedrevs_tracking' )
493            ->where( [ 'ftr_from' => $pageId ] )
494            ->caller( __METHOD__ )
495            ->execute();
496    }
497
498    /**
499     * Updates MediaWiki's HTML cache for a Title. Defers till after main commit().
500     *
501     * @param Title $title
502     */
503    public static function purgeMediaWikiHtmlCdn( Title $title ) {
504        DeferredUpdates::addCallableUpdate( static function () use ( $title ) {
505            $htmlCache = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
506            $htmlCache->purgeTitleUrls( $title, $htmlCache::PURGE_INTENT_TXROUND_REFLECTED );
507        } );
508    }
509
510    /**
511     * Do cache updates for when the stable version of a page changed.
512     * Invalidates/purges pages that include the given page.
513     * @param Title $title
514     */
515    public static function updateHtmlCaches( Title $title ) {
516        $jobs = [];
517        $jobs[] = HTMLCacheUpdateJob::newForBacklinks( $title, 'templatelinks' );
518        MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $jobs );
519
520        DeferredUpdates::addUpdate( new FRExtraCacheUpdate( $title ) );
521    }
522
523    /**
524     * Invalidates/purges pages where only stable version includes this page.
525     * @param Title $title
526     */
527    public static function extraHTMLCacheUpdate( Title $title ) {
528        DeferredUpdates::addUpdate( new FRExtraCacheUpdate( $title ) );
529    }
530
531    # ################ Revision functions #################
532
533    /**
534     * Mark a revision as patrolled if needed
535     * @param RevisionRecord $revRecord
536     */
537    public static function markRevisionPatrolled( RevisionRecord $revRecord ) {
538        $rcid = MediaWikiServices::getInstance()
539            ->getRevisionStore()
540            ->getRcIdIfUnpatrolled( $revRecord );
541        # Make sure it is now marked patrolled...
542        if ( $rcid ) {
543            $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase();
544
545            $dbw->newUpdateQueryBuilder()
546                ->update( 'recentchanges' )
547                ->set( [ 'rc_patrolled' => 1 ] )
548                ->where( [ 'rc_id' => $rcid ] )
549                ->caller( __METHOD__ )
550                ->execute();
551        }
552    }
553
554    # ################ Other utility functions #################
555
556    /**
557     * Get minimum level tags for a tier
558     * @deprecated Use quickTag() instead.
559     * @return array<string,int>
560     */
561    public static function quickTags() {
562        return self::useOnlyIfProtected() ?
563            [] :
564            [ self::getTagName() => 1 ];
565    }
566
567    /**
568     * Get minimum level tag for the default tier,
569     * or `null` if FlaggedRevs is used in protection mode
570     */
571    public static function quickTag(): ?int {
572        return self::useOnlyIfProtected() ? null : 1;
573    }
574
575    /**
576     * Get minimum tags that are closest to $oldFlags
577     * given the site, page, and user rights limitations.
578     * @param User $user
579     * @param array<string,int> $oldFlags previous stable rev flags
580     * @return array<string,int>|null
581     */
582    private static function getAutoReviewTags( $user, array $oldFlags ) {
583        if ( !self::autoReviewEdits() ) {
584            return null; // shouldn't happen
585        }
586        if ( self::useOnlyIfProtected() ) {
587            return [];
588        }
589        $tag = self::getTagName();
590        # Try to keep this tag val the same as the stable rev's
591        $val = $oldFlags[$tag] ?? 1;
592        $val = min( $val, self::maxAutoReviewLevel() );
593        # Dial down the level to one the user has permission to set
594        while ( !self::userCanSetValue( $user, $val ) ) {
595            $val--;
596            if ( $val <= 0 ) {
597                return null; // all tags vals must be > 0
598            }
599        }
600        return [ $tag => $val ];
601    }
602
603    /**
604     * Get the list of reviewable namespaces
605     * @return int[] Value from $wgFlaggedRevsNamespaces
606     */
607    public static function getReviewNamespaces(): array {
608        global $wgFlaggedRevsNamespaces;
609        static $validated = false;
610        if ( !$validated ) {
611            $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
612            foreach ( $wgFlaggedRevsNamespaces as $ns ) {
613                if ( $ns === NS_MEDIAWIKI || $namespaceInfo->isTalk( $ns ) ) {
614                    throw new ConfigException( 'Invalid talk or project namespace in $wgFlaggedRevsNamespaces' );
615                }
616            }
617            $validated = true;
618        }
619        return $wgFlaggedRevsNamespaces;
620    }
621
622    public static function getFirstReviewNamespace(): int {
623        return self::getReviewNamespaces()[0] ?? NS_MAIN;
624    }
625
626    public static function isReviewNamespace( int $ns ): bool {
627        return in_array( $ns, self::getReviewNamespaces() );
628    }
629
630    public static function inReviewNamespace( PageReference $page ): bool {
631        $ns = $page->getNamespace();
632        if ( $ns === NS_MEDIA ) {
633            $ns = NS_FILE;
634        }
635        return self::isReviewNamespace( $ns );
636    }
637
638    # ################ Auto-review function #################
639
640    /**
641     * Automatically review an revision and add a log entry in the review log.
642     *
643     * This is called during edit operations after the new revision is added
644     * and the page tables updated, but before LinksUpdate is called.
645     *
646     * $auto is here for revisions checked off to be reviewed. Auto-review
647     * triggers on edit, but we don't want those to count as just automatic.
648     * This also makes it so the user's name shows up in the page history.
649     *
650     * If $flags is given, then they will be the review tags. If not, the one
651     * from the stable version will be used or minimal tags if that's not possible.
652     * If no appropriate tags can be found, then the review will abort.
653     * @param WikiPage $article
654     * @param User $user
655     * @param RevisionRecord $revRecord
656     * @param int[]|null $flags
657     * @param bool $auto
658     * @param bool $approveRevertedTagUpdate Whether to notify the reverted tag
659     *  subsystem that the edit was reviewed. Should be false when autoreviewing
660     *  during page creation, true otherwise. Default is false.
661     * @return bool
662     */
663    public static function autoReviewEdit(
664        WikiPage $article,
665        $user,
666        RevisionRecord $revRecord,
667        array $flags = null,
668        $auto = true,
669        $approveRevertedTagUpdate = false
670    ) {
671        $title = $article->getTitle(); // convenience
672        # Get current stable version ID (for logging)
673        $oldSv = FlaggedRevision::newFromStable( $title, IDBAccessObject::READ_LATEST );
674        $oldSvId = $oldSv ? $oldSv->getRevId() : 0;
675
676        if ( self::useOnlyIfProtected() ) {
677            $flags = [];
678        } else {
679            # Set the auto-review tags from the prior stable version.
680            # Normally, this should already be done and given here...
681            if ( !is_array( $flags ) ) {
682                if ( $oldSv ) {
683                    # Use the last stable version if $flags not given
684                    if ( MediaWikiServices::getInstance()->getPermissionManager()
685                        ->userHasRight( $user, 'bot' )
686                    ) {
687                        $flags = $oldSv->getTags(); // no change for bot edits
688                    } else {
689                        # Account for perms/tags...
690                        $flags = self::getAutoReviewTags( $user, $oldSv->getTags() );
691                    }
692                } else { // new page?
693                    $flags = self::quickTags();
694                }
695                if ( !is_array( $flags ) ) {
696                    return false; // can't auto-review this revision
697                }
698            }
699        }
700
701        try {
702            $updater = $article->getCurrentUpdate();
703            $poutput = $updater->getParserOutputForMetaData();
704        } catch ( PreconditionException | LogicException $exception ) {
705            // If there is no ongoing edit, we still need to get the
706            // parsed page somehow. This happens when the RevisionFromEditComplete hook
707            // is triggered during page moves, imports, null revisions for page protection,
708            // etc.
709            // It would be nice if we could get a PreparedUpdate in these situations as
710            // well. See FlaggableWikiPage::preloadPreparedEdit() and its callers.
711            $services = MediaWikiServices::getInstance();
712            $poutputAccess = $services->getParserOutputAccess();
713            $poutput = $poutputAccess->getParserOutput(
714                $article,
715                ParserOptions::newFromContext( RequestContext::getMain() )
716            )->getValue();
717        }
718
719        if ( !$poutput ) {
720            return false;
721        }
722
723        # Our review entry
724        $flaggedRevision = new FlaggedRevision( [
725            'revrecord'            => $revRecord,
726            'user_id'               => $user->getId(),
727            'timestamp'         => $revRecord->getTimestamp(), // same as edit time
728            'tags'                   => $flags,
729            'flags'             => $auto ? 'auto' : '',
730        ] );
731
732        // Insert the flagged revision
733        $success = $flaggedRevision->insert();
734        if ( $success !== true ) {
735            return false;
736        }
737
738        if ( $approveRevertedTagUpdate ) {
739            $flaggedRevision->approveRevertedTagUpdate();
740        }
741
742        # Update the article review log
743        if ( !$auto ) {
744            FlaggedRevsLog::updateReviewLog( $title,
745                $flags, '', $revRecord->getId(), $oldSvId, true, $user );
746        }
747
748        # Update page and tracking tables and clear cache
749        self::stableVersionUpdates( $article );
750
751        return true;
752    }
753
754}