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