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