Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
4.87% covered (danger)
4.87%
40 / 821
7.41% covered (danger)
7.41%
4 / 54
CRAP
0.00% covered (danger)
0.00%
0 / 1
FlaggablePageView
4.87% covered (danger)
4.87%
40 / 821
7.41% covered (danger)
7.41%
4 / 54
86276.27
0.00% covered (danger)
0.00%
0 / 1
 getInstanceCache
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 newFromTitle
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 singleton
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 __clone
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 clear
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 diffRevRecordsAreSet
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 showingStable
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
7
 useSimpleUI
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getPageViewStabilityModeForUser
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 isPageViewOrDiff
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isPageView
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 displayTag
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 addStableLink
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 addStatusIndicator
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
132
 setPageContent
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
210
 addTagNoticeIfApplicable
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 determineRequestedRevision
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
110
 setRobotPolicy
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
72
 makeParserOptions
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 showDraftVersion
0.00% covered (danger)
0.00%
0 / 48
0.00% covered (danger)
0.00%
0 / 1
240
 showOldReviewedVersion
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
20
 showStableVersion
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 1
342
 showUnreviewedVersion
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getTopDiffToggle
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
72
 addToHistView
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getEditNotices
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
306
 pendingEditNoticeMessage
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 stabilityLogNotice
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 addToNoSuchSection
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 addToCategoryView
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 addReviewForm
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
182
 addStabilizationLink
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 setActionTabs
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
72
 setViewTabs
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
156
 addDraftTab
0.00% covered (danger)
0.00%
0 / 42
0.00% covered (danger)
0.00%
0 / 1
182
 pageWriteOpRequested
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getOldIDFromRequest
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 setPendingNotice
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 addToDiffView
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
272
 buildDiffHeaderItems
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 diffLinkAndMarkers
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 diffToStableLink
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
30
 diffReviewMarkers
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
20
 getDiffRevMsgAndClass
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 setViewFlags
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
90
 isDiffToStable
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
42
 injectPostEditURLParams
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
90
 changeSaveButton
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 editWillRequireReview
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 editWillBeAutoreviewed
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 addReviewCheck
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
56
 getBaseRevId
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getAltBaseRevId
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3use MediaWiki\Cache\CacheKeyHelper;
4use MediaWiki\Context\ContextSource;
5use MediaWiki\Context\RequestContext;
6use MediaWiki\EditPage\EditPage;
7use MediaWiki\HookContainer\HookRunner;
8use MediaWiki\Html\Html;
9use MediaWiki\MediaWikiServices;
10use MediaWiki\Message\Message;
11use MediaWiki\Output\OutputPage;
12use MediaWiki\Page\PageIdentity;
13use MediaWiki\Parser\ParserOptions;
14use MediaWiki\Parser\ParserOutput;
15use MediaWiki\Request\WebRequest;
16use MediaWiki\Revision\RevisionRecord;
17use MediaWiki\SpecialPage\SpecialPage;
18use MediaWiki\Title\Title;
19use MediaWiki\User\User;
20use MediaWiki\User\UserIdentity;
21use OOUI\ButtonInputWidget;
22use Wikimedia\Rdbms\IDBAccessObject;
23
24/**
25 * Class representing a web view of a MediaWiki page
26 */
27class FlaggablePageView extends ContextSource {
28    private static ?MapCacheLRU $instances = null;
29
30    private OutputPage $out;
31    private FlaggableWikiPage $article;
32    /** @var RevisionRecord[]|null Array of `old` and `new` RevisionsRecords for diffs */
33    private ?array $diffRevRecords = null;
34    private bool $isReviewableDiff = false;
35    private bool $isDiffFromStable = false;
36    private bool $isMultiPageDiff = false;
37    private string $reviewNotice = '';
38    private string $diffNoticeBox = '';
39    private string $diffIncChangeBox = '';
40    private ?RevisionRecord $reviewFormRevRecord = null;
41    /**
42     * The stable revision.
43     */
44    private ?FlaggedRevision $srev = null;
45
46    /**
47     * The flagged revision being viewed.
48     */
49    private ?FlaggedRevision $frev = null;
50
51    /**
52     * @return MapCacheLRU
53     */
54    private static function getInstanceCache(): MapCacheLRU {
55        if ( !self::$instances ) {
56            self::$instances = new MapCacheLRU( 10 );
57        }
58        return self::$instances;
59    }
60
61    /**
62     * Get a FlaggableWikiPage for a given title
63     *
64     * @param PageIdentity $title
65     *
66     * @return self
67     */
68    public static function newFromTitle( PageIdentity $title ): FlaggablePageView {
69        $cache = self::getInstanceCache();
70        $key = CacheKeyHelper::getKeyForPage( $title );
71        $view = $cache->get( $key );
72        if ( !$view ) {
73            $view = new self( $title );
74            $cache->set( $key, $view );
75        }
76        return $view;
77    }
78
79    /**
80     * Get the FlaggablePageView for this request
81     *
82     * @deprecated Use ::newFromTitle() instead
83     * @return self
84     */
85    public static function singleton(): FlaggablePageView {
86        return self::newFromTitle( RequestContext::getMain()->getTitle() );
87    }
88
89    /**
90     * @param Title|PageIdentity $title
91     */
92    private function __construct( PageIdentity $title ) {
93        // Title is injected (a step up from everything being global), but
94        // the rest is still implicitly uses RequestContext::getMain()
95        // via parent class ContextSource::getContext().
96        // TODO: Inject $context and call setContext() here.
97
98        if ( !$title->canExist() ) {
99            throw new InvalidArgumentException( 'FlaggablePageView needs a proper page' );
100        }
101        $this->article = FlaggableWikiPage::getTitleInstance( $title );
102        $this->out = $this->getOutput(); // convenience
103    }
104
105    private function __clone() {
106    }
107
108    /**
109     * Clear the FlaggablePageView for this request.
110     * Only needed when page redirection changes the environment.
111     */
112    public function clear(): void {
113        self::$instances = null;
114    }
115
116    /**
117     * Check if the old and new diff revs are set for this page view
118     */
119    public function diffRevRecordsAreSet(): bool {
120        return (bool)$this->diffRevRecords;
121    }
122
123    /**
124     * Assuming that the current request is a page view (see isPageView()),
125     * check if a stable version exists and should be displayed.
126     */
127    public function showingStable(): bool {
128        $request = $this->getRequest();
129
130        $canShowStable = (
131            // Page is reviewable and has a stable version
132            $this->article->getStableRev() &&
133            // No parameters requesting a different version of the page
134            !$request->getCheck( 'oldid' ) && !$request->getCheck( 'stableid' )
135        );
136        if ( !$canShowStable ) {
137            return false;
138        }
139
140        // Check if a stable or unstable version is explicitly requested (?stable=1 or ?stable=0).
141        $stableQuery = $request->getIntOrNull( 'stable' );
142        if ( $stableQuery !== null ) {
143            return $stableQuery === 1;
144        }
145
146        // Otherwise follow site/page config and user preferences.
147        $reqUser = $this->getUser();
148        $defaultForUser = $this->getPageViewStabilityModeForUser( $reqUser );
149        return (
150            // User is not configured to prefer current versions
151            $defaultForUser !== FR_SHOW_STABLE_NEVER &&
152            // User explicitly prefers stable versions of pages
153            (
154                $defaultForUser === FR_SHOW_STABLE_ALWAYS ||
155                // Check if the stable version overrides the draft
156                $this->article->getStabilitySettings()['override']
157            )
158        );
159    }
160
161    /**
162     * Should this be using a simple icon-based UI?
163     * Check the user's preferences first, using the site settings as the default.
164     */
165    private function useSimpleUI(): bool {
166        $default = (int)$this->getConfig()->get( 'SimpleFlaggedRevsUI' );
167        $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup();
168        return (bool)$userOptionsLookup->getOption(
169            $this->getUser(),
170            'flaggedrevssimpleui',
171            $default
172        );
173    }
174
175    /**
176     * What version of pages should this user see by default?
177     *
178     * @param UserIdentity $user The user to get the stability mode for.
179     * @return int One of the FR_SHOW_STABLE_* constants
180     */
181    private function getPageViewStabilityModeForUser( UserIdentity $user ): int {
182        $services = MediaWikiServices::getInstance();
183
184        # Check user preferences (e.g. "show stable version by default?")
185        $userOptionsLookup = $services->getUserOptionsLookup();
186        $preference = (int)$userOptionsLookup->getOption( $user, 'flaggedrevsstable' );
187        if ( $preference === FR_SHOW_STABLE_ALWAYS || $preference === FR_SHOW_STABLE_NEVER ) {
188            return $preference;
189        }
190
191        $userIdentityUtils = $services->getUserIdentityUtils();
192
193        return $userIdentityUtils->isNamed( $user ) ? FR_SHOW_STABLE_NEVER : FR_SHOW_STABLE_DEFAULT;
194    }
195
196    /**
197     * Is this a view page action (including diffs)?
198     */
199    private function isPageViewOrDiff(): bool {
200        $action = $this->getActionName();
201        return $action === 'view' || $action === 'render';
202    }
203
204    /**
205     * Is this a view page action (not including diffs)?
206     */
207    private function isPageView(): bool {
208        $request = $this->getRequest();
209        return $this->isPageViewOrDiff()
210            && $request->getVal( 'diff' ) === null;
211    }
212
213    /**
214     * Output review notice
215     */
216    public function displayTag(): void {
217        // Sanity check that this is a reviewable page
218        if ( $this->article->isReviewable() && $this->reviewNotice ) {
219            $this->out->addSubtitle( $this->reviewNotice );
220        }
221    }
222
223    /**
224     * Add a stable link when viewing old versions of an article that
225     * have been reviewed. (e.g. for &oldid=x urls)
226     */
227    public function addStableLink(): void {
228        $request = $this->getRequest();
229        if ( !$this->article->isReviewable() ||
230            !$request->getVal( 'oldid' ) ||
231            $this->out->isPrintable()
232        ) {
233            return;
234        }
235
236        # We may have nav links like "direction=prev&oldid=x"
237        $revID = $this->getOldIDFromRequest();
238        $frev = FlaggedRevision::newFromTitle( $this->article->getTitle(), $revID );
239        if ( !$frev ) {
240            return;
241        }
242
243        # Give a notice if this rev ID corresponds to a reviewed version...
244        $time = $this->getLanguage()->date( $frev->getTimestamp(), true );
245        $message = $this->msg( 'revreview-basic-source', $frev->getRevId(), $time )->parse();
246        $html = FlaggedRevsHTML::addMessageBox( 'block', $message, [
247            'id' => 'mw-fr-revision-tag-old',
248            'class' => 'flaggedrevs_notice plainlinks noprint'
249        ] );
250        $this->out->addHTML( $html );
251    }
252
253    /**
254     * Adds a visual indicator to the page view based on the review status of the current page revision.
255     *
256     * This indicator helps users quickly identify whether the current revision is stable, a draft,
257     * or unchecked. The indicator is displayed using an appropriate icon and message at the top
258     * of the page, near the title.
259     *
260     * @return void
261     */
262    public function addStatusIndicator(): void {
263        if ( $this->getSkin()->getSkinName() === 'minerva' ) {
264            return;
265        }
266
267        if ( !$this->article->isReviewable() ) {
268            return;
269        }
270
271        // Determine the requested revision type
272        $requestedRevision = $this->determineRequestedRevision();
273
274        // Determine the message key, icon class, and indicator ID based on the requested revision type
275        $statusMessageKey = '';
276        $iconClass = '';
277        $indicatorId = 'mw-fr-revision-toggle'; // Default ID for the indicator
278
279        switch ( $requestedRevision ) {
280            case 'stable':
281                $statusMessageKey = 'revreview-quick-basic-same-title';
282                $iconClass = 'cdx-fr-css-icon-review--status--stable';
283                break;
284            case 'draft':
285                $statusMessageKey = 'revreview-draft-indicator-title';
286                $iconClass = 'cdx-fr-css-icon-review--status--pending';
287                break;
288            case 'unreviewed':
289                if ( !$this->out->isPrintable() ) {
290                    $statusMessageKey = $this->useSimpleUI() ? 'revreview-quick-none' : 'revreview-noflagged';
291                    $iconClass = 'cdx-fr-css-icon-review--status--unchecked';
292                }
293                break;
294            case 'invalid':
295            case 'old':
296                break;
297        }
298
299        // Only proceed if a valid status message key was determined
300        if ( $statusMessageKey ) {
301            // Prepare the attributes for the indicator element
302            $attributes = [
303                'name' => 'fr-review-status',
304                'class' => 'mw-fr-review-status-indicator',
305                'id' => $indicatorId,
306            ];
307
308            // Generate the HTML for the indicator
309            $indicatorHtml = Html::rawElement(
310                'indicator',
311                $attributes,
312                Html::element( 'span', [ 'class' => $iconClass ] ) . $this->msg( $statusMessageKey )->parse()
313            );
314
315            // Add the indicator to the output page
316            $this->out->setIndicators( [ 'indicator-fr-review-status' => $indicatorHtml ] );
317        }
318    }
319
320    /**
321     * Handles the display of the page content, prioritizing the last stable version if possible.
322     *
323     * This method replaces the current page view with the last stable version if conditions allow.
324     * It determines the type of revision requested by the user (e.g., 'stable', 'draft', 'unreviewed'),
325     * and adjusts the page content accordingly. Depending on the revision type, it may display tags,
326     * notices, and a review form.
327     *
328     * The method also controls whether the parser cache should be used and whether the parser output
329     * is completed.
330     *
331     * @param bool|ParserOutput|null &$outputDone Indicates whether the parser output is completed.
332     * @param bool &$useParserCache Controls whether the parser cache should be used for this page view.
333     *
334     * @return void
335     */
336    public function setPageContent( &$outputDone, &$useParserCache ): void {
337        $request = $this->getRequest();
338
339        // Only proceed if this is a page view without an oldid parameter, and the page exists and is reviewable
340        if ( !$this->isPageView() || $request->getVal( 'oldid' ) || !$this->article->exists() ||
341            !$this->article->isReviewable() ) {
342            return;
343        }
344
345        // Initialize $tag as an empty string
346        $tag = '';
347
348        // Determine the requested revision type
349        $requestedRevision = $this->determineRequestedRevision();
350
351        switch ( $requestedRevision ) {
352            case 'invalid':
353                $this->out->addWikiMsg( 'revreview-invalid' );
354                $this->out->returnToMain( false, $this->article->getTitle() );
355                $outputDone = true;
356                $useParserCache = false;
357                return;
358
359            case 'old':
360                $outputDone = $this->showOldReviewedVersion( $this->frev, $tag );
361                $tagTypeClass = 'mw-fr-old-stable';
362                $useParserCache = false;
363                break;
364
365            case 'stable':
366                $outputDone = $this->showStableVersion( $this->srev, $tag );
367                $tagTypeClass = $this->article->stableVersionIsSynced() ? 'mw-fr-stable-synced' :
368                    'mw-fr-stable-not-synced';
369                $useParserCache = false;
370                break;
371
372            case 'draft':
373                $this->showDraftVersion( $this->srev, $tag );
374                $tagTypeClass = $this->article->stableVersionIsSynced() ? 'mw-fr-draft-synced' :
375                    'mw-fr-draft-not-synced';
376                break;
377
378            case 'unreviewed':
379            default:
380                $outputDone = $this->showUnreviewedVersion( $tag );
381                $tagTypeClass = $this->article->stableVersionIsSynced() ? 'mw-fr-stable-unreviewed' :
382                    'mw-fr-stable-not-unreviewed';
383                break;
384        }
385
386        $this->addTagNoticeIfApplicable( $tag, $tagTypeClass );
387    }
388
389    /**
390     * Add the tag notice if applicable.
391     *
392     * @param string $tag The tag message.
393     * @param string $tagTypeClass The CSS class for the tag type.
394     */
395    private function addTagNoticeIfApplicable( string $tag, string $tagTypeClass ): void {
396        if ( $tag !== '' ) {
397            $notice = Html::openElement( 'div', [ 'id' => 'mw-fr-revision-messages' ] );
398            if ( $this->useSimpleUI() ) {
399                $this->addStatusIndicator();
400                $notice .= $tag;
401            } else {
402                $cssClasses = "mw-fr-basic $tagTypeClass plainlinks noprint";
403                $notice .= FlaggedRevsHTML::addMessageBox( 'block', $tag, [
404                    'class' => $cssClasses,
405                ] );
406            }
407            $notice .= Html::closeElement( 'div' );
408            $this->reviewNotice .= $notice;
409        }
410    }
411
412    /**
413     * Determines the type of revision requested based on the current request.
414     *
415     * This method examines the request parameters to identify whether the user has requested a specific
416     * revision (e.g., via 'stableid'). It determines whether to show a stable, old reviewed, draft,
417     * or unreviewed version of the page. If no specific revision is requested, it falls back on the
418     * user's preferences and site configuration to decide which version to show.
419     *
420     * The method updates the stable and flagged revision properties (`$srev` and `$frev`) accordingly.
421     *
422     * @return string The type of revision requested: 'invalid', 'old', 'stable', 'draft', or 'unreviewed'.
423     */
424    private function determineRequestedRevision(): string {
425        $request = $this->getRequest();
426        $this->srev = $this->article->getStableRev();
427        $this->frev = $this->srev;
428        $stableId = $this->srev ? $this->srev->getRevId() : 0;
429
430        $reqId = $request->getVal( 'stableid' );
431        if ( $reqId === "best" ) {
432            $reqId = $this->article->getBestFlaggedRevId();
433        }
434        if ( $reqId ) {
435            if ( !$stableId ) {
436                return 'invalid'; // Invalid ID
437            } elseif ( $stableId == $reqId ) {
438                return 'stable'; // Stable version requested
439            } else {
440                $this->frev = FlaggedRevision::newFromTitle( $this->article->getTitle(), $reqId );
441                return $this->frev ? 'old' : 'invalid'; // Old reviewed version requested, or invalid ID
442            }
443        }
444
445        // If no specific revision is requested, determine whether to show the draft or unreviewed version
446        if ( $this->frev ) {
447            if ( $this->showingStable() || $this->article->stableVersionIsSynced() ) {
448                return 'stable';
449            }
450            return 'draft';
451        } else {
452            return 'unreviewed';
453        }
454    }
455
456    /**
457     * If the page has a stable version and it shows by default,
458     * tell search crawlers to index only that version of the page.
459     * Also index the draft as well if they are synced (bug 27173).
460     * However, any URL with ?stableid=x should not be indexed (as with ?oldid=x).
461     */
462    public function setRobotPolicy(): void {
463        $request = $this->getRequest();
464        if ( $this->article->getStableRev() && $this->article->isStableShownByDefault() ) {
465            if ( $this->isPageView() && $this->showingStable() ) {
466                return; // stable version - index this
467            } elseif ( !$request->getVal( 'stableid' )
468                && $this->out->getRevisionId() == $this->article->getStable()
469                && $this->article->stableVersionIsSynced()
470            ) {
471                return; // draft that is synced with the stable version - index this
472            }
473            $this->out->setRobotPolicy( 'noindex,nofollow' ); // don't index this version
474        }
475    }
476
477    /**
478     * @param User $reqUser
479     * @return ParserOptions
480     */
481    private function makeParserOptions( User $reqUser ): ParserOptions {
482        $parserOptions = $this->article->makeParserOptions( $reqUser );
483        # T349037: The ArticleParserOptions hook should be broadened to take
484        # a WikiPage (aka $this->article) instead of an Article.  But for now
485        # fake the Article.
486        $article = Article::newFromWikiPage( $this->article, RequestContext::getMain() );
487        # Allow extensions to vary parser options used for article rendering,
488        # in the same way Article does
489        ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
490            ->onArticleParserOptions( $article, $parserOptions );
491
492        return $parserOptions;
493    }
494
495    /**
496     * Displays the draft version of a page.
497     *
498     * This method outputs the draft version of a page, which may differ from the stable version.
499     * It adds a notice indicating that the page is pending review and optionally displays a diff
500     * between the stable and draft versions. If the stable and draft versions are synchronized,
501     * the method does not display a diff. It also adds a "your edit will pending" notice for users
502     * who have made unreviewed edits, especially if the user lacks review rights.
503     *
504     * The method also adjusts the tag that is used for the review box/bar info.
505     *
506     * @param FlaggedRevision $srev The stable revision.
507     * @param string &$tag Reference to the variable holding the review box/bar info.
508     *
509     * @return void
510     */
511    private function showDraftVersion( FlaggedRevision $srev, string &$tag ): void {
512        $request = $this->getRequest();
513        $reqUser = $this->getUser();
514        if ( $this->out->isPrintable() ) {
515            return; // all this function does is add notices; don't show them
516        }
517        $time = $this->getLanguage()->date( $srev->getTimestamp(), true );
518        # Get stable version sync status
519        $synced = $this->article->stableVersionIsSynced();
520        if ( $synced ) { // draft == stable
521            $diffToggle = ''; // no diff to show
522        } else { // draft != stable
523            # The user may want the diff (via prefs)
524            $diffToggle = $this->getTopDiffToggle( $srev );
525            if ( $diffToggle != '' ) {
526                $diffToggle = " $diffToggle";
527            }
528            # Make sure there is always a notice bar when viewing the draft.
529            if ( $this->useSimpleUI() ) { // we already one for detailed UI
530                $this->setPendingNotice( $srev, $diffToggle );
531            }
532        }
533        # Give a "your edit is pending" notice to newer users if
534        # an unreviewed edit was completed...
535        $pm = MediaWikiServices::getInstance()->getPermissionManager();
536        if ( $request->getVal( 'shownotice' )
537            && $this->article->getUserText( RevisionRecord::RAW ) == $reqUser->getName()
538            && $this->article->revsArePending()
539            && !$pm->userHasRight( $reqUser, 'review' )
540        ) {
541            $revsSince = $this->article->getPendingRevCount();
542            $pending = $this->msg( 'revreview-edited', $srev->getRevId() )
543                ->numParams( $revsSince )->parse();
544            $anchor = $request->getVal( 'fromsection' );
545            if ( $anchor != null ) {
546                // Hack: reverse some of the Sanitizer::escapeId() encoding
547                $section = urldecode( str_replace( // bug 35661
548                    [ ':', '.' ], [ '%3A', '%' ], $anchor
549                ) );
550                $section = str_replace( '_', ' ', $section ); // prettify
551                $pending .= $this->msg( 'revreview-edited-section', $anchor, $section )
552                    ->parseAsBlock();
553            }
554        # Notice should always use subtitle
555            $this->reviewNotice = Html::openElement( 'div', [
556                    'id' => 'mw-fr-reviewnotice',
557                    'class' => 'cdx-message cdx-message--block cdx-message--notice
558                    flaggedrevs_preview plainlinks noprint',
559                ] )
560                . Html::element( 'span', [ 'class' => 'cdx-message__icon' ] )
561                . Html::rawElement( 'div', [ 'class' => 'cdx-message__content' ], $pending )
562                . Html::closeElement( 'div' );
563        # Otherwise, construct some tagging info for non-printable outputs.
564        # Also, if low profile UI is enabled and the page is synced, skip the tag.
565        # Note: the "your edit is pending" notice has all this info, so we never add both.
566        } elseif ( !( $this->article->lowProfileUI() && $synced ) ) {
567            $revsSince = $this->article->getPendingRevCount();
568            // Simple icon-based UI
569            if ( $this->useSimpleUI() ) {
570                $revisionId = $srev->getRevId();
571                $tag .= FlaggedRevsHTML::reviewDialog( $srev, $revisionId, $revsSince, 'draft', $synced );
572            // Standard UI
573            } else {
574                if ( $synced ) {
575                    $msg = 'revreview-basic-same';
576                } else {
577                    $msg = !$revsSince ? 'revreview-newest-basic-i' : 'revreview-newest-basic';
578                }
579                $msgHTML = $this->msg( $msg, $srev->getRevId(), $time )
580                    ->numParams( $revsSince )->parse();
581                $tag .= $msgHTML . $diffToggle;
582            }
583        }
584    }
585
586    /**
587     * Displays an old reviewed version of a page.
588     *
589     * This method outputs an older reviewed version of the page. It sets the revision ID for display
590     * and generates the appropriate tags and notices, including handling the case where the page has
591     * pending revisions. It creates a `ParserOutput` for this version and adds it to the output page.
592     *
593     * The method is primarily used when viewing a specific old version of a page.
594     *
595     * @param FlaggedRevision $frev The selected flagged revision.
596     * @param string &$tag Reference to the variable holding the review box/bar info.
597     *
598     * @return ?ParserOutput The generated ParserOutput for the old reviewed version, or null if generation fails.
599     */
600    private function showOldReviewedVersion( FlaggedRevision $frev, string &$tag ): ?ParserOutput {
601        $reqUser = $this->getUser();
602        $time = $this->getLanguage()->date( $frev->getTimestamp(), true );
603        # Set display revision ID
604        $this->out->setRevisionId( $frev->getRevId() );
605
606        # Construct some tagging for non-printable outputs. Note that the pending
607        # notice has all this info already, so don't do this if we added that already.
608        if ( !$this->out->isPrintable() ) {
609            // Simple icon-based UI
610            if ( $this->useSimpleUI() ) {
611                # For protection based configs, show lock only if it's not redundant.
612                $revsSince = $this->article->getPendingRevCount();
613                $srev = $this->article->getStableRev();
614                $revisionId = $srev->getRevId();
615                $tag = FlaggedRevsHTML::reviewDialog( $frev, $revisionId, $revsSince );
616                // Standard UI
617            } else {
618                $msg = 'revreview-basic-old';
619                $tag = $this->msg( $msg, $frev->getRevId(), $time )->parse();
620            }
621        }
622
623        # Generate the uncached parser output for this old reviewed version
624        $parserOptions = $this->makeParserOptions( $reqUser );
625        $parserOut = FlaggedRevs::parseStableRevision( $frev, $parserOptions );
626        if ( !$parserOut ) {
627            return null;
628        }
629
630        # Add the parser output to the page view
631        $this->out->addParserOutput(
632            $parserOut,
633            [ 'enableSectionEditLinks' => false, ]
634        );
635
636        return $parserOut;
637    }
638
639    /**
640     * Displays the stable version of a page.
641     *
642     * This method outputs the stable version of a page, which is the version that has been flagged as reviewed.
643     * It generates and caches the `ParserOutput` for the stable version, if not already cached. If the stable
644     * version is synchronized with the current draft, it skips the diff display. The method also adds relevant
645     * tags and notices based on the stability and synchronization status.
646     *
647     * The method ensures that only the stable version or a synced draft is indexed by search engines.
648     *
649     * @param FlaggedRevision $srev The stable revision.
650     * @param string &$tag Reference to the variable holding the review box/bar info.
651     *
652     * @return ?ParserOutput The generated ParserOutput for the stable version, or null if generation fails.
653     */
654    private function showStableVersion( FlaggedRevision $srev, string &$tag ): ?ParserOutput {
655        $reqUser = $this->getUser();
656        $time = $this->getLanguage()->date( $srev->getTimestamp(), true );
657        # Set display revision ID
658        $this->out->setRevisionId( $srev->getRevId() );
659        $synced = $this->article->stableVersionIsSynced();
660        # Construct some tagging
661        if (
662            !$this->out->isPrintable() &&
663            !( $this->article->lowProfileUI() && $synced )
664        ) {
665            $revsSince = $this->article->getPendingRevCount();
666            // Simple icon-based UI
667            if ( $this->useSimpleUI() ) {
668                # For protection based configs, show lock only if it's not redundant.
669                $revisionId = $srev->getRevId();
670                $tag = FlaggedRevsHTML::reviewDialog(
671                    $srev,
672                    $revisionId,
673                    $revsSince,
674                    'stable',
675                    $synced
676                );
677            // Standard UI
678            } else {
679                if ( $synced ) {
680                    $msg = 'revreview-basic-same';
681                } else {
682                    $msg = !$revsSince ? 'revreview-basic-i' : 'revreview-basic';
683                }
684                $tag = $this->msg( $msg, $srev->getRevId(), $time )
685                    ->numParams( $revsSince )->parse();
686            }
687        }
688
689        // TODO: Rewrite to use ParserOutputAccess
690        $parserOptions = $this->makeParserOptions( $reqUser );
691        $stableParserCache = FlaggedRevs::getParserCacheInstance( $parserOptions );
692        // Check the stable version cache for the parser output
693        $parserOut = $stableParserCache->get( $this->article, $parserOptions );
694
695        if ( !$parserOut ) {
696            if ( FlaggedRevs::inclusionSetting() == FR_INCLUDES_CURRENT && $synced ) {
697                # Stable and draft version are identical; check the draft version cache
698                $draftParserCache = MediaWikiServices::getInstance()->getParserCache();
699                $parserOut = $draftParserCache->get( $this->article, $parserOptions );
700            }
701
702            if ( !$parserOut ) {
703                # Regenerate the parser output, debouncing parse requests via PoolCounter
704                $status = FlaggedRevs::parseStableRevisionPooled( $srev, $parserOptions );
705                if ( !$status->isGood() ) {
706                    $this->out->disableClientCache();
707                    $this->out->setRobotPolicy( 'noindex,nofollow' );
708                    $statusFormatter = MediaWikiServices::getInstance()->getFormatterFactory()->getStatusFormatter(
709                        $this->getContext() );
710                    $errorText = $statusFormatter->getMessage( $status );
711                    $this->out->addHTML(
712                        Html::errorBox( $this->out->parseAsContent( $errorText ) )
713                    );
714                    return null;
715                }
716                $parserOut = $status->getValue();
717            }
718
719            if ( $parserOut instanceof ParserOutput ) {
720                # Update the stable version cache
721                $stableParserCache->save( $parserOut, $this->article, $parserOptions );
722
723                # Enqueue a job to update the "stable version only" dependencies
724                if ( !MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) {
725                    $update = new FRDependencyUpdate( $this->article->getTitle(), $parserOut );
726                    $update->doUpdate( FRDependencyUpdate::DEFERRED );
727                }
728            }
729        }
730
731        if ( !$parserOut ) {
732            $this->out->disableClientCache();
733            $this->out->setRobotPolicy( 'noindex,nofollow' );
734
735            $this->out->addWikiMsg(
736                'missing-article',
737                $this->article->getTitle()->getPrefixedText(),
738                $this->msg( 'missingarticle-rev', $srev->getRevId() )->plain()
739            );
740            return null;
741        }
742
743        # Add the parser output to the page view
744        $pm = MediaWikiServices::getInstance()->getPermissionManager();
745        $poOptions = [];
746        if ( $this->out->isPrintable() ||
747            !$pm->quickUserCan( 'edit', $reqUser, $this->article->getTitle() )
748        ) {
749            $poOptions['enableSectionEditLinks'] = false;
750        }
751        $this->out->addParserOutput( $parserOut, $poOptions );
752
753        # Update page sync status for tracking purposes.
754        # NOTE: avoids primary hits and doesn't have to be perfect for what it does
755        if ( $this->article->syncedInTracking() != $synced ) {
756            $this->article->lazyUpdateSyncStatus();
757        }
758
759        return $parserOut;
760    }
761
762    /**
763     * Displays an unreviewed version of a page.
764     *
765     * This method handles the display of an unreviewed version of the page, showing the appropriate
766     * UI elements based on user preferences (simple or detailed). It sets the appropriate tag and
767     * tag type class to indicate the page's unreviewed status. The method generates a `ParserOutput`
768     * for the unreviewed version and adds it to the output page.
769     *
770     * The method is used when there is no stable version or when a draft needs to be displayed.
771     *
772     * @param string &$tag Reference to the variable holding the review box/bar info.
773     *
774     * @return ParserOutput|bool The generated ParserOutput for the unreviewed version, or null if none is available.
775     */
776    private function showUnreviewedVersion( string &$tag ) {
777        $reqUser = $this->getUser();
778
779        if ( $this->useSimpleUI() ) {
780            $this->addStatusIndicator();
781            $this->out->addHTML( FlaggedRevsHTML::reviewDialog( null, 0, 0, 'unreviewed' ) );
782        } else {
783            $tag = $this->msg( 'revreview-noflagged' )->parse();
784        }
785
786        // Generate the ParserOutput for the unreviewed version
787        $parserOptions = $this->makeParserOptions( $reqUser );
788        $parserOut = $this->article->getParserOutput( $parserOptions );
789
790        // Add the ParserOutput to the output page
791        if ( $parserOut ) {
792            $this->out->addParserOutput( $parserOut );
793        }
794
795        return $parserOut;
796    }
797
798    /**
799     * Get a toggle for a collapsible diff-to-stable to add to the review notice as needed
800     * @param FlaggedRevision $srev stable version
801     * @return string|false the html line (either "" or "<diff toggle>")
802     */
803    private function getTopDiffToggle( FlaggedRevision $srev ) {
804        $reqUser = $this->getUser();
805        if ( !MediaWikiServices::getInstance()->getUserOptionsLookup()
806            ->getBoolOption( $reqUser, 'flaggedrevsviewdiffs' )
807        ) {
808            return false; // nothing to do here
809        }
810        # Diff should only show for the draft
811        $oldid = $this->getOldIDFromRequest();
812        $latest = $this->article->getLatest();
813        if ( $oldid && $oldid != $latest ) {
814            return false; // not viewing the draft
815        }
816        $revsSince = $this->article->getPendingRevCount();
817        if ( !$revsSince ) {
818            return false; // no pending changes
819        }
820
821        $title = $this->article->getTitle(); // convenience
822        if ( $srev->getRevId() !== $latest ) {
823            $nEdits = $revsSince - 1; // full diff-to-stable, no need for query
824            if ( $nEdits ) {
825                $limit = 100;
826                try {
827                    $latestRevObj = MediaWikiServices::getInstance()
828                        ->getRevisionLookup()
829                        ->getRevisionById( $latest );
830                    $users = MediaWikiServices::getInstance()
831                        ->getRevisionStore()
832                        ->getAuthorsBetween(
833                            $title->getArticleID(),
834                            $srev->getRevisionRecord(),
835                            $latestRevObj,
836                            null,
837                            $limit
838                        );
839                    $nUsers = count( $users );
840                } catch ( InvalidArgumentException $e ) {
841                    $nUsers = 0;
842                }
843                $multiNotice = DifferenceEngine::intermediateEditsMsg( $nEdits, $nUsers, $limit );
844            } else {
845                $multiNotice = '';
846            }
847            $this->isDiffFromStable = true; // alter default review form tags
848            // @phan-suppress-next-line SecurityCheck-DoubleEscaped multiNotice is used in a-tag
849            return FlaggedRevsHTML::diffToggle( $title, $srev->getRevId(), $latest, $multiNotice );
850        }
851
852        return '';
853    }
854
855    /**
856     * Adds stable version tags to page when viewing history
857     */
858    public function addToHistView(): void {
859        # Add a notice if there are pending edits...
860        $srev = $this->article->getStableRev();
861        if ( $srev && $this->article->revsArePending() ) {
862            $revsSince = $this->article->getPendingRevCount();
863            $noticeContent = $this->pendingEditNoticeMessage( $srev, $revsSince )->parse();
864            $tag = FlaggedRevsHTML::addMessageBox( 'block', $noticeContent, [
865                'id' => 'mw-fr-revision-tag-edit',
866                'class' => 'flaggedrevs_notice plainlinks'
867            ] );
868            $this->out->addHTML( $tag );
869        }
870    }
871
872    /**
873     * @param Title $title
874     * @param int $oldid
875     * @param string[] &$notices
876     */
877    public function getEditNotices( Title $title, int $oldid, array &$notices ): void {
878        if ( !$this->article->isReviewable() ) {
879            return;
880        }
881        // HACK fake EditPage
882        $editPage = new EditPage( new Article( $title, $oldid ) );
883        $editPage->oldid = $oldid;
884        $reqUser = $this->getUser();
885
886        $lines = [];
887
888        $log = $this->stabilityLogNotice();
889        if ( $log ) {
890            $lines[] = $log;
891        } elseif ( $this->editWillRequireReview( $editPage ) ) {
892            $lines[] = $this->msg( 'revreview-editnotice' )->parseAsBlock();
893        }
894        $frev = $this->article->getStableRev();
895        if ( $frev && $this->article->revsArePending() ) {
896            $revsSince = $this->article->getPendingRevCount();
897            $pendingMsg = $this->pendingEditNoticeMessage( $frev, $revsSince );
898            $lines[] = $pendingMsg->parseAsBlock();
899        }
900        $latestId = $this->article->getLatest();
901        $revId  = $oldid ?: $latestId;
902        if ( $frev && $frev->getRevId() < $latestId // changes were made
903            && MediaWikiServices::getInstance()->getUserOptionsLookup()
904                ->getBoolOption( $reqUser, 'flaggedrevseditdiffs' ) // not disabled via prefs
905            && $revId === $latestId // only for current rev
906        ) {
907            $lines[] = '<p>' . $this->msg( 'review-edit-diff' )->parse() . ' ' .
908                FlaggedRevsHTML::diffToggle( $this->article->getTitle(), $frev->getRevId(), $revId ) . '</p>';
909        }
910
911        $srev = $this->article->getStableRev();
912        $revsSince = $this->article->getPendingRevCount();
913
914        if ( $frev && $this->article->onlyTemplatesPending() && $revsSince === 0 && $srev ) {
915            $time = $this->getLanguage()->userTimeAndDate( $srev->getTimestamp(), $this->getUser() );
916            $lines[] = '<p>' . $this->msg( 'revreview-newest-basic-i',
917                    $srev->getRevId(), $time )->numParams( $revsSince )->parse() . '</p>';
918        }
919
920        if ( $lines ) {
921            $lineMessages = '';
922            foreach ( $lines as $line ) {
923                $lineMessages .= FlaggedRevsHTML::addMessageBox( 'inline', $line );
924            }
925
926            $notices['flaggedrevs_editnotice'] = Html::rawElement( 'div', [
927                'class' => 'mw-fr-edit-messages',
928            ], $lineMessages );
929        }
930    }
931
932    /**
933     * Creates "stable rev reviewed on"/"x pending edits" message
934     */
935    private function pendingEditNoticeMessage( FlaggedRevision $frev, int $revsSince ): Message {
936        $time = $this->getLanguage()->date( $frev->getTimestamp(), true );
937        # Add message text for pending edits
938        return $this->msg( 'revreview-pending-basic', $frev->getRevId(), $time )->numParams( $revsSince );
939    }
940
941    private function stabilityLogNotice(): string {
942        if ( $this->article->isPageLocked() ) {
943            $msg = 'revreview-locked';
944        } elseif ( $this->article->isPageUnlocked() ) {
945            $msg = 'revreview-unlocked';
946        } else {
947            return '';
948        }
949        $s = $this->msg( $msg )->parseAsBlock();
950        return $s . FlaggedRevsHTML::stabilityLogExcerpt( $this->article->getTitle() );
951    }
952
953    public function addToNoSuchSection( string &$s ): void {
954        $srev = $this->article->getStableRev();
955        # Add notice for users that may have clicked "edit" for a
956        # section in the stable version that isn't in the draft.
957        if ( $srev && $this->article->revsArePending() ) {
958            $revsSince = $this->article->getPendingRevCount();
959            if ( $revsSince ) {
960                $s .= "<div class='flaggedrevs_editnotice plainlinks'>" .
961                    $this->msg( 'revreview-pending-nosection',
962                        $srev->getRevId() )->numParams( $revsSince )->parse() . "</div>";
963            }
964        }
965    }
966
967    /**
968     * Add unreviewed pages links
969     */
970    public function addToCategoryView(): void {
971        $reqUser = $this->getUser();
972        $pm = MediaWikiServices::getInstance()->getPermissionManager();
973        if ( !$pm->userHasRight( $reqUser, 'review' ) ) {
974            return;
975        }
976
977        if ( !FlaggedRevs::useOnlyIfProtected() ) {
978            # Add links to lists of unreviewed pages and pending changes in this category
979            $category = $this->article->getTitle()->getText();
980            $this->out->addSubtitle(
981                Html::rawElement(
982                    'span',
983                    [ 'class' => 'plainlinks', 'id' => 'mw-fr-category-oldreviewed' ],
984                    $this->msg( 'flaggedrevs-categoryview', urlencode( $category ) )->parse()
985                )
986            );
987        }
988    }
989
990    /**
991     * Add review form to pages when necessary on a regular page view (action=view).
992     * If $output is an OutputPage then this prepends the form onto it.
993     * If $output is a string then this appends the review form to it.
994     * @param string|OutputPage &$output
995     */
996    public function addReviewForm( &$output ): void {
997        if ( $this->out->isPrintable() ) {
998            // Must be on non-printable output
999            return;
1000        }
1001
1002        # User must have review rights
1003        $reqUser = $this->getUser();
1004        if ( !MediaWikiServices::getInstance()->getPermissionManager()
1005            ->userHasRight( $reqUser, 'review' )
1006        ) {
1007            return;
1008        }
1009        # Page must exist and be reviewable
1010        if ( !$this->article->exists() || !$this->article->isReviewable() ) {
1011            return;
1012        }
1013        # Must be a page view action...
1014        if ( !$this->isPageViewOrDiff() ) {
1015            return;
1016        }
1017        // Determine the revision to be reviewed, either from the current output or fallback to
1018        // the latest revision for unchecked pages
1019        $revisionId = $this->out->getRevisionId() ?: $this->article->getLatest();
1020        $revRecord = $this->reviewFormRevRecord ?: MediaWikiServices::getInstance()
1021            ->getRevisionLookup()
1022            ->getRevisionById( $revisionId );
1023
1024        # Build the review form as needed
1025        if ( $revRecord && ( !$this->diffRevRecords || $this->isReviewableDiff ) ) {
1026            $form = new RevisionReviewFormUI(
1027                $this->getContext(),
1028                $this->article,
1029                $revRecord
1030            );
1031            # Default tags and existence of "reject" button depend on context
1032            if ( $this->diffRevRecords ) {
1033                $oldRevRecord = $this->diffRevRecords['old'];
1034                $form->setDiffPriorRevRecord( $oldRevRecord );
1035            }
1036            # Review notice box goes in top of form
1037            $form->setTopNotice( $this->diffNoticeBox );
1038            $form->setBottomNotice( $this->diffIncChangeBox );
1039
1040            [ $html, ] = $form->getHtml();
1041            # Diff action: place the form at the top of the page
1042            if ( $output instanceof OutputPage ) {
1043                $output->prependHTML( $html );
1044            # View action: place the form at the bottom of the page
1045            } else {
1046                $output .= $html;
1047            }
1048        }
1049    }
1050
1051    /**
1052     * Add link to stable version setting to protection form
1053     */
1054    public function addStabilizationLink(): void {
1055        $request = $this->getRequest();
1056        if ( FlaggedRevs::useOnlyIfProtected() ) {
1057            // Simple custom levels set for action=protect
1058            return;
1059        }
1060        # Check only if the title is reviewable
1061        if ( !FlaggedRevs::inReviewNamespace( $this->article ) ) {
1062            return;
1063        }
1064        $action = $request->getVal( 'action', 'view' );
1065        if ( $action == 'protect' || $action == 'unprotect' ) {
1066            $title = SpecialPage::getTitleFor( 'Stabilization' );
1067            # Give a link to the page to configure the stable version
1068            $frev = $this->article->getStableRev();
1069            if ( !$frev ) {
1070                $msg = 'revreview-visibility-nostable';
1071            } elseif ( $frev->getRevId() == $this->article->getLatest() ) {
1072                $msg = 'revreview-visibility-synced';
1073            } else {
1074                $msg = 'revreview-visibility-outdated';
1075            }
1076            $this->out->prependHTML( "<span class='revreview-visibility $msg plainlinks'>" .
1077                $this->msg( $msg, $title->getPrefixedText() )->parse() . '</span>' );
1078        }
1079    }
1080
1081    /**
1082     * Modify an array of action links, as used by SkinTemplateNavigation and
1083     * SkinTemplateTabs, to include flagged revs UI elements
1084     *
1085     * @param array &$actions
1086     * @throws MWException
1087     */
1088    public function setActionTabs( array &$actions ): void {
1089        $reqUser = $this->getUser();
1090
1091        if ( FlaggedRevs::useOnlyIfProtected() ) {
1092            return; // simple custom levels set for action=protect
1093        }
1094
1095        if ( !FlaggedRevs::inReviewNamespace( $this->article ) ) {
1096            return; // Only reviewable pages need these tabs
1097        }
1098
1099        // Check if we should show a stabilization tab
1100        $pm = MediaWikiServices::getInstance()->getPermissionManager();
1101        if (
1102            !$this->article->getTitle()->isTalkPage() &&
1103            !isset( $actions['protect'] ) &&
1104            !isset( $actions['unprotect'] ) &&
1105            $pm->userHasRight( $reqUser, 'stablesettings' ) &&
1106            $this->article->exists()
1107        ) {
1108            $stableTitle = SpecialPage::getTitleFor( 'Stabilization' );
1109            // Add the tab
1110            $actions['default'] = [
1111                'class' => false,
1112                'text' => $this->msg( 'stabilization-tab' )->text(),
1113                'href' => $stableTitle->getLocalURL( 'page=' . $this->article->getTitle()->getPrefixedURL() )
1114            ];
1115        }
1116    }
1117
1118    /**
1119     * Modify an array of tab links to include flagged revs UI elements
1120     * @param Skin $skin
1121     * @param array[] &$views
1122     */
1123    public function setViewTabs( Skin $skin, array &$views ): void {
1124        if ( !FlaggedRevs::inReviewNamespace( $this->article ) ) {
1125            // Short-circuit for non-reviewable pages
1126            return;
1127        }
1128        # Hack for bug 16734 (some actions update and view all at once)
1129        if ( $this->pageWriteOpRequested() &&
1130            MediaWikiServices::getInstance()->getDBLoadBalancer()->hasOrMadeRecentPrimaryChanges()
1131        ) {
1132            # Tabs need to reflect the new stable version so users actually
1133            # see the results of their action (i.e. "delete"/"rollback")
1134            $this->article->loadPageData( IDBAccessObject::READ_LATEST );
1135        }
1136        $srev = $this->article->getStableRev();
1137        if ( !$srev ) {
1138            // No stable revision exists
1139            return;
1140        }
1141        $synced = $this->article->stableVersionIsSynced();
1142        $pendingEdits = !$synced && $this->article->isStableShownByDefault();
1143        // Set the edit tab names as needed...
1144        if ( $pendingEdits && $this->isPageView() && $this->showingStable() ) {
1145            // bug 31489; direct user to current
1146            if ( isset( $views['edit'] ) ) {
1147                $views['edit']['href'] = $skin->getTitle()->getFullURL( 'action=edit' );
1148            }
1149            if ( isset( $views['viewsource'] ) ) {
1150                $views['viewsource']['href'] = $skin->getTitle()->getFullURL( 'action=edit' );
1151            }
1152            // Instruct alternative editors like VisualEditor to load the latest ("current")
1153            // revision for editing, rather than the one from 'wgRevisionId'
1154            $skin->getOutput()->addJsConfigVars( 'wgEditLatestRevision', true );
1155        }
1156        # Add "pending changes" tab if the page is not synced
1157        if ( !$synced ) {
1158            $this->addDraftTab( $views, $srev );
1159        }
1160    }
1161
1162    /**
1163     * Add "pending changes" tab and set tab selection CSS
1164     * @param array[] &$views
1165     * @param FlaggedRevision $srev
1166     */
1167    private function addDraftTab( array &$views, FlaggedRevision $srev ): void {
1168        $request = $this->getRequest();
1169        $title = $this->article->getTitle(); // convenience
1170        $tabs = [
1171            'read' => [ // view stable
1172                'text'  => '', // unused
1173                'href'  => $title->getLocalURL( 'stable=1' ),
1174                'class' => ''
1175            ],
1176            'draft' => [ // view draft
1177                'text'  => $this->msg( 'revreview-current' )->text(),
1178                'href'  => $title->getLocalURL( 'stable=0&redirect=no' ),
1179                'class' => 'collapsible'
1180            ],
1181        ];
1182        // Set tab selection CSS
1183        if ( ( $this->isPageView() && $this->showingStable() ) || $request->getVal( 'stableid' ) ) {
1184            // We are looking a the stable version or an old reviewed one
1185            $tabs['read']['class'] = 'selected';
1186        } elseif ( $this->isPageViewOrDiff() ) {
1187            $ts = null;
1188            if ( $this->out->getRevisionId() ) { // @TODO: avoid same query in Skin.php
1189                if ( $this->out->getRevisionId() == $this->article->getLatest() ) {
1190                    $ts = $this->article->getTimestamp(); // skip query
1191                } else {
1192                    $ts = MediaWikiServices::getInstance()
1193                        ->getRevisionLookup()
1194                        ->getTimestampFromId( $this->out->getRevisionId() );
1195                }
1196            }
1197            // Are we looking at a pending revision?
1198            if ( $ts > $srev->getRevTimestamp() ) { // bug 15515
1199                $tabs['draft']['class'] .= ' selected';
1200            // Are there *just* pending template changes.
1201            } elseif ( $this->article->onlyTemplatesPending()
1202                && $this->out->getRevisionId() == $this->article->getStable()
1203            ) {
1204                $tabs['draft']['class'] .= ' selected';
1205            // Otherwise, fallback to regular tab behavior
1206            } else {
1207                $tabs['read']['class'] = 'selected';
1208            }
1209        }
1210        $newViews = [];
1211        // Rebuild tabs array
1212        $previousTab = null;
1213        foreach ( $views as $tabAction => $data ) {
1214            // The 'view' tab. Make it go to the stable version...
1215            if ( $tabAction == 'view' ) {
1216                // 'view' for content page; make it go to the stable version
1217                $newViews[$tabAction]['text'] = $data['text']; // keep tab name
1218                $newViews[$tabAction]['href'] = $tabs['read']['href'];
1219                $newViews[$tabAction]['class'] = $tabs['read']['class'];
1220            // All other tabs...
1221            } else {
1222                if ( $previousTab == 'view' ) {
1223                    $newViews['current'] = $tabs['draft'];
1224                }
1225                $newViews[$tabAction] = $data;
1226            }
1227            $previousTab = $tabAction;
1228        }
1229        // Replaces old tabs with new tabs
1230        $views = $newViews;
1231    }
1232
1233    /**
1234     * Check if a flaggedrevs relevant write op was done this page view
1235     */
1236    private function pageWriteOpRequested(): bool {
1237        $request = $this->getRequest();
1238        # Hack for bug 16734 (some actions update and view all at once)
1239        $action = $request->getVal( 'action' );
1240        return $action === 'rollback' ||
1241            ( $action === 'delete' && $request->wasPosted() );
1242    }
1243
1244    private function getOldIDFromRequest(): int {
1245        $article = Article::newFromWikiPage( $this->article, RequestContext::getMain() );
1246        return $article->getOldIDFromRequest();
1247    }
1248
1249    /**
1250     * Adds a notice saying that this revision is pending review
1251     *
1252     * @param FlaggedRevision $srev The stable version
1253     * @param string $diffToggle either "" or " <diff toggle><diff div>"
1254     */
1255    private function setPendingNotice( FlaggedRevision $srev, string $diffToggle = '' ): void {
1256        $time = $this->getLanguage()->date( $srev->getTimestamp(), true );
1257        $revsSince = $this->article->getPendingRevCount();
1258        $msg = !$revsSince ? 'revreview-newest-basic-i' : 'revreview-newest-basic';
1259        # Add bar msg to the top of the page...
1260        $msgHTML = $this->msg( $msg, $srev->getRevId(), $time )->numParams( $revsSince )->parse();
1261
1262        if ( !$this->useSimpleUI() ) {
1263            $this->reviewNotice .= FlaggedRevsHTML::addMessageBox( 'block', $msgHTML . $diffToggle );
1264        }
1265    }
1266
1267    /**
1268     * When viewing a diff:
1269     * (a) Add the review form to the top of the page
1270     * (b) Mark off which versions are checked or not
1271     * (c) When comparing the stable revision to the current:
1272     *   (i)  Show a tag with some explanation for the diff
1273     */
1274    public function addToDiffView( ?RevisionRecord $oldRevRecord, ?RevisionRecord $newRevRecord ): void {
1275        $pm = MediaWikiServices::getInstance()->getPermissionManager();
1276        $request = $this->getRequest();
1277        $reqUser = $this->getUser();
1278        # Exempt printer-friendly output
1279        if ( $this->out->isPrintable() ) {
1280            return;
1281        # Multi-page diffs are useless and misbehave (bug 19327). Sanity check $newRevRecord.
1282        } elseif ( $this->isMultiPageDiff || !$newRevRecord ) {
1283            return;
1284        # Page must be reviewable.
1285        } elseif ( !$this->article->isReviewable() ) {
1286            return;
1287        }
1288        $srev = $this->article->getStableRev();
1289        if ( $srev && $this->isReviewableDiff ) {
1290            $this->reviewFormRevRecord = $newRevRecord;
1291        }
1292        # Check if this is a diff-to-stable. If so:
1293        # (a) prompt reviewers to review the changes
1294        if ( $srev
1295            && $this->isDiffFromStable
1296            && !$this->article->stableVersionIsSynced() // pending changes
1297        ) {
1298            # If there are pending revs, notify the user...
1299            if ( $this->article->revsArePending() ) {
1300                # If the user can review then prompt them to review them...
1301                if ( $pm->userHasRight( $reqUser, 'review' ) ) {
1302                    // Reviewer just edited...
1303                    if ( $request->getInt( 'shownotice' )
1304                        && $newRevRecord->isCurrent()
1305                        && $newRevRecord->getUser( RevisionRecord::RAW )
1306                            ->equals( $reqUser )
1307                    ) {
1308                        $title = $this->article->getTitle(); // convenience
1309                        // @TODO: make diff class cache this
1310                        $n = MediaWikiServices::getInstance()
1311                            ->getRevisionStore()
1312                            ->countRevisionsBetween(
1313                                $title->getArticleID(),
1314                                $oldRevRecord,
1315                                $newRevRecord
1316                            );
1317                        if ( $n ) {
1318                            $msg = 'revreview-update-edited-prev'; // previous pending edits
1319                        } else {
1320                            $msg = 'revreview-update-edited'; // just couldn't autoreview
1321                        }
1322                    // All other cases...
1323                    } else {
1324                        $msg = 'revreview-update'; // generic "please review" notice...
1325                    }
1326                    // add as part of form
1327                    $this->diffNoticeBox = $this->msg( $msg )->parseAsBlock();
1328                }
1329            }
1330        }
1331        # Add a link to diff from stable to current as needed.
1332        # Show review status of the diff revision(s). Uses a <table>.
1333        $this->out->addHTML(
1334            '<div id="mw-fr-diff-headeritems">' .
1335            self::diffLinkAndMarkers(
1336                $this->article,
1337                $oldRevRecord,
1338                $newRevRecord
1339            ) .
1340            '</div>'
1341        );
1342    }
1343
1344    /**
1345     * get new diff header items for in-place page review
1346     */
1347    public static function buildDiffHeaderItems(): string {
1348        $args = func_get_args(); // <oldid, newid>
1349        if ( count( $args ) >= 2 ) {
1350            $oldid = (int)$args[0];
1351            $newid = (int)$args[1];
1352            $revLookup = MediaWikiServices::getInstance()->getRevisionLookup();
1353            $newRevRecord = $revLookup->getRevisionById( $newid );
1354            if ( $newRevRecord && $newRevRecord->getPageAsLinkTarget() ) {
1355                $oldRevRecord = $revLookup->getRevisionById( $oldid );
1356                $fa = FlaggableWikiPage::getTitleInstance(
1357                    Title::newFromLinkTarget( $newRevRecord->getPageAsLinkTarget() )
1358                );
1359                return self::diffLinkAndMarkers( $fa, $oldRevRecord, $newRevRecord );
1360            }
1361        }
1362        return '';
1363    }
1364
1365    /**
1366     * (a) Add a link to diff from stable to current as needed
1367     * (b) Show review status of the diff revision(s). Uses a <table>.
1368     * Note: used by ajax function to rebuild diff page
1369     */
1370    private static function diffLinkAndMarkers(
1371        FlaggableWikiPage $article,
1372        ?RevisionRecord $oldRevRecord,
1373        ?RevisionRecord $newRevRecord
1374    ): string {
1375        $s = '<form id="mw-fr-diff-dataform">';
1376        $s .= Html::hidden( 'oldid', $oldRevRecord ? $oldRevRecord->getId() : 0 );
1377        $s .= Html::hidden( 'newid', $newRevRecord ? $newRevRecord->getId() : 0 );
1378        $s .= "</form>\n";
1379        if ( $newRevRecord && $oldRevRecord ) { // sanity check
1380            $s .= self::diffToStableLink( $article, $oldRevRecord, $newRevRecord );
1381            $s .= self::diffReviewMarkers( $article, $oldRevRecord, $newRevRecord );
1382        }
1383        return $s;
1384    }
1385
1386    /**
1387     * Add a link to diff-to-stable for reviewable pages
1388     */
1389    private static function diffToStableLink(
1390        FlaggableWikiPage $article,
1391        RevisionRecord $oldRevRecord,
1392        RevisionRecord $newRevRecord
1393    ): string {
1394        $srev = $article->getStableRev();
1395        if ( !$srev ) {
1396            return ''; // nothing to do
1397        }
1398        $review = '';
1399        # Is this already the full diff-to-stable?
1400        $fullStableDiff = $newRevRecord->isCurrent()
1401            && self::isDiffToStable(
1402                $srev,
1403                $oldRevRecord,
1404                $newRevRecord
1405            );
1406        # Make a link to the full diff-to-stable if:
1407        # (a) Actual revs are pending and (b) We are not viewing the full diff-to-stable
1408        if ( $article->revsArePending() && !$fullStableDiff ) {
1409            $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1410            $reviewLink = $linkRenderer->makeKnownLink(
1411                $article->getTitle(),
1412                wfMessage( 'review-diff2stable' )->text(),
1413                [],
1414                [ 'oldid' => $srev->getRevId(), 'diff' => 'cur' ]
1415            );
1416            $reviewWrapped = wfMessage( 'parentheses' )->rawParams( $reviewLink )->escaped();
1417            $review = "<div class='fr-diff-to-stable' style='text-align: center;'>$reviewWrapped</div>";
1418        }
1419        return $review;
1420    }
1421
1422    /**
1423     * Add [checked version] and such to left and right side of diff
1424     */
1425    private static function diffReviewMarkers(
1426        FlaggableWikiPage $article,
1427        ?RevisionRecord $oldRevRecord,
1428        ?RevisionRecord $newRevRecord
1429    ): string {
1430        $table = '';
1431        $srev = $article->getStableRev();
1432        # Diff between two revisions
1433        if ( $oldRevRecord && $newRevRecord ) {
1434            [ $msg, $class ] = self::getDiffRevMsgAndClass( $oldRevRecord, $srev );
1435            $table .= "<table class='fr-diff-ratings'><tr>";
1436            $table .= "<td style='text-align: center; width: 50%;'>";
1437            // @todo i18n FIXME: Hard coded brackets
1438            $table .= "<span class='$class'>[" .
1439                wfMessage( $msg )->escaped() . "]</span>";
1440
1441            [ $msg, $class ] = self::getDiffRevMsgAndClass( $newRevRecord, $srev );
1442            $table .= "</td><td style='text-align: center; width: 50%;'>";
1443            // @todo i18n FIXME: Hard coded brackets
1444            $table .= "<span class='$class'>[" .
1445                wfMessage( $msg )->escaped() . "]</span>";
1446
1447            $table .= "</td></tr></table>\n";
1448        # New page "diffs" - just one rev
1449        } elseif ( $newRevRecord ) {
1450            [ $msg, $class ] = self::getDiffRevMsgAndClass( $newRevRecord, $srev );
1451            $table .= "<table class='fr-diff-ratings'>";
1452            $table .= "<tr><td style='text-align: center;'><span class='$class'>";
1453            // @todo i18n FIXME: Hard coded brackets
1454            $table .= '[' . wfMessage( $msg )->escaped() . ']';
1455            $table .= "</span></td></tr></table>\n";
1456        }
1457        return $table;
1458    }
1459
1460    /**
1461     * @return string[]
1462     */
1463    private static function getDiffRevMsgAndClass(
1464        RevisionRecord $revRecord, ?FlaggedRevision $srev
1465    ): array {
1466        $checked = FlaggedRevision::revIsFlagged( $revRecord->getId() );
1467        if ( $checked ) {
1468            $msg = 'revreview-hist-basic';
1469        } else {
1470            $msg = ( $srev && $revRecord->getTimestamp() > $srev->getRevTimestamp() ) ? // bug 15515
1471                'revreview-hist-pending' :
1472                'revreview-hist-draft';
1473        }
1474        return [ $msg, $checked ? 'flaggedrevs-color-1' : 'flaggedrevs-color-0' ];
1475    }
1476
1477    /**
1478     * Set $this->isDiffFromStable and $this->isMultiPageDiff fields
1479     */
1480    public function setViewFlags(
1481        DifferenceEngine $diff,
1482        ?RevisionRecord $oldRevRecord,
1483        ?RevisionRecord $newRevRecord
1484    ): void {
1485        // We only want valid diffs that actually make sense...
1486        if ( !( $newRevRecord
1487            && $oldRevRecord
1488            && $newRevRecord->getTimestamp() >= $oldRevRecord->getTimestamp() )
1489        ) {
1490            return;
1491        }
1492
1493        // Is this a diff between two pages?
1494        if ( $newRevRecord->getPageId() != $oldRevRecord->getPageId() ) {
1495            $this->isMultiPageDiff = true;
1496        // Is there a stable version?
1497        } elseif ( $this->article->isReviewable() ) {
1498            $srev = $this->article->getStableRev();
1499            // Is this a diff of a draft rev against the stable rev?
1500            if ( self::isDiffToStable(
1501                $srev,
1502                $oldRevRecord,
1503                $newRevRecord
1504            ) ) {
1505                $this->isDiffFromStable = true;
1506                $this->isReviewableDiff = true;
1507            // Is this a diff of a draft rev against a reviewed rev?
1508            } elseif (
1509                FlaggedRevision::newFromTitle(
1510                    $diff->getTitle(),
1511                    $oldRevRecord->getId()
1512                ) ||
1513                FlaggedRevision::newFromTitle(
1514                    $diff->getTitle(),
1515                    $newRevRecord->getId()
1516                )
1517            ) {
1518                $this->isReviewableDiff = true;
1519            }
1520        }
1521
1522        $this->diffRevRecords = [
1523            'old' => $oldRevRecord,
1524            'new' => $newRevRecord
1525        ];
1526    }
1527
1528    /**
1529     * Is a diff from $oldRev to $newRev a diff-to-stable?
1530     */
1531    private static function isDiffToStable(
1532        ?FlaggedRevision $srev,
1533        ?RevisionRecord $oldRevRecord,
1534        ?RevisionRecord $newRevRecord
1535    ): bool {
1536        return ( $srev
1537            && $oldRevRecord
1538            && $newRevRecord
1539            && $oldRevRecord->getPageId() === $newRevRecord->getPageId() // no multipage diffs
1540            && $oldRevRecord->getId() == $srev->getRevId()
1541            && $newRevRecord->getTimestamp() >= $oldRevRecord->getTimestamp() // no backwards diffs
1542        );
1543    }
1544
1545    /**
1546     * Redirect users out to review the changes to the stable version.
1547     * Only for people who can review and for pages that have a stable version.
1548     */
1549    public function injectPostEditURLParams( string &$sectionAnchor, string &$extraQuery ): void {
1550        $reqUser = $this->getUser();
1551        $this->article->loadPageData( IDBAccessObject::READ_LATEST );
1552        # Get the stable version from the primary DB
1553        $frev = $this->article->getStableRev();
1554        if ( !$frev ) {
1555            // Only for pages with stable versions
1556            return;
1557        }
1558
1559        $params = [];
1560        $pm = MediaWikiServices::getInstance()->getPermissionManager();
1561        // If the edit was not autoreviewed, and the user can actually make a
1562        // new stable version, then go to the diff...
1563        if ( $this->article->revsArePending() && $frev->userCanSetTag( $reqUser ) ) {
1564            $params += [ 'oldid' => $frev->getRevId(), 'diff' => 'cur', 'shownotice' => 1 ];
1565        // ...otherwise, go to the draft revision after completing an edit.
1566        // This allows for users to immediately see their changes. Even if the stable
1567        // and draft page match, we can avoid a parse due to FR_INCLUDES_STABLE.
1568        } else {
1569            $params += [ 'stable' => 0 ];
1570            // Show a notice at the top of the page for non-reviewers...
1571            if ( $this->article->revsArePending()
1572                && $this->article->isStableShownByDefault()
1573                && !$pm->userHasRight( $reqUser, 'review' )
1574            ) {
1575                $params += [ 'shownotice' => 1 ];
1576                if ( $sectionAnchor ) {
1577                    // Pass a section parameter in the URL as needed to add a link to
1578                    // the "your changes are pending" box on the top of the page...
1579                    $params += [ 'fromsection' => substr( $sectionAnchor, 1 ) ]; // strip #
1580                    $sectionAnchor = ''; // go to the top of the page to see notice
1581                }
1582            }
1583        }
1584        if ( $extraQuery !== '' ) {
1585            $extraQuery .= '&';
1586        }
1587        $extraQuery .= wfArrayToCgi( $params ); // note: EditPage will add initial "&"
1588    }
1589
1590    /**
1591     * If submitting the edit will leave it pending, then change the button text
1592     * Note: interacts with 'review pending changes' checkbox
1593     * @param EditPage $editPage
1594     * @param ButtonInputWidget[] $buttons
1595     */
1596    public function changeSaveButton( EditPage $editPage, array $buttons ): void {
1597        if ( !$this->editWillRequireReview( $editPage ) ) {
1598            // Edit will go live or be reviewed on save
1599            return;
1600        }
1601        if ( isset( $buttons['save'] ) ) {
1602            $buttonLabel = $this->msg( 'revreview-submitedit' )->text();
1603            $buttons['save']->setLabel( $buttonLabel );
1604            $buttonTitle = $this->msg( 'revreview-submitedit-title' )->text();
1605            $buttons['save']->setTitle( $buttonTitle );
1606        }
1607    }
1608
1609    /**
1610     * If this edit will not go live on submit (accounting for wpReviewEdit)
1611     */
1612    private function editWillRequireReview( EditPage $editPage ): bool {
1613        $request = $this->getRequest(); // convenience
1614        $title = $this->article->getTitle(); // convenience
1615        if ( !$this->article->editsRequireReview() || $this->editWillBeAutoreviewed( $editPage ) ) {
1616            return false; // edit will go live immediately
1617        } elseif ( $request->getCheck( 'wpReviewEdit' ) &&
1618            MediaWikiServices::getInstance()->getPermissionManager()
1619                ->userCan( 'review', $this->getUser(), $title )
1620        ) {
1621            return false; // edit checked off to be reviewed on save
1622        }
1623        return true; // edit needs review
1624    }
1625
1626    /**
1627     * If this edit will be auto-reviewed on submit
1628     * Note: checking wpReviewEdit does not count as auto-reviewed
1629     */
1630    private function editWillBeAutoreviewed( EditPage $editPage ): bool {
1631        $title = $this->article->getTitle(); // convenience
1632        if ( !$this->article->isReviewable() ) {
1633            return false;
1634        }
1635        if ( MediaWikiServices::getInstance()->getPermissionManager()
1636            ->quickUserCan( 'autoreview', $this->getUser(), $title )
1637        ) {
1638            if ( FlaggedRevs::autoReviewNewPages() && !$this->article->exists() ) {
1639                return true; // edit will be autoreviewed
1640            }
1641
1642            $baseRevId = self::getBaseRevId( $editPage, $this->getRequest() );
1643            $baseRevId2 = self::getAltBaseRevId( $editPage, $this->getRequest() );
1644            $baseFRev = FlaggedRevision::newFromTitle( $title, $baseRevId );
1645            if ( !$baseFRev && $baseRevId2 ) {
1646                $baseFRev = FlaggedRevision::newFromTitle( $title, $baseRevId2 );
1647            }
1648
1649            if ( $baseFRev ) {
1650                return true; // edit will be autoreviewed
1651            }
1652        }
1653        return false; // edit won't be autoreviewed
1654    }
1655
1656    /**
1657     * Add a "review pending changes" checkbox to the edit form iff:
1658     * (a) there are currently any revisions pending (bug 16713)
1659     * (b) this is an unreviewed page (bug 23970)
1660     */
1661    public function addReviewCheck( EditPage $editPage, array &$checkboxes ): void {
1662        $request = $this->getRequest();
1663        $title = $this->article->getTitle(); // convenience
1664        if ( !$this->article->isReviewable() ||
1665            !MediaWikiServices::getInstance()->getPermissionManager()
1666                ->userCan( 'review', $this->getUser(), $title )
1667        ) {
1668            // Not needed
1669            return;
1670        } elseif ( $this->editWillBeAutoreviewed( $editPage ) ) {
1671            // Edit will be auto-reviewed
1672            return;
1673        }
1674        if ( self::getBaseRevId( $editPage, $request ) == $this->article->getLatest() ) {
1675            # For pages with either no stable version, or an outdated one, let
1676            # the user decide if he/she wants it reviewed on the spot. One might
1677            # do this if he/she just saw the diff-to-stable and *then* decided to edit.
1678            # Note: check not shown when editing old revisions, which is confusing.
1679            $name = 'wpReviewEdit';
1680            $options = [
1681                'id' => $name,
1682                'default' => $request->getCheck( $name ),
1683                'legacy-name' => 'reviewed',
1684            ];
1685            // For reviewed pages...
1686            if ( $this->article->getStable() ) {
1687                // For pending changes...
1688                if ( $this->article->revsArePending() ) {
1689                    $n = $this->article->getPendingRevCount();
1690                    $options['title-message'] = 'revreview-check-flag-p-title';
1691                    $options['label-message'] = $this->msg( 'revreview-check-flag-p' )
1692                        ->numParams( $n );
1693                // For just the user's changes...
1694                } else {
1695                    $options['title-message'] = 'revreview-check-flag-y-title';
1696                    $options['label-message'] = 'revreview-check-flag-y';
1697                }
1698            // For unreviewed pages...
1699            } else {
1700                $options['title-message'] = 'revreview-check-flag-u-title';
1701                $options['label-message'] = 'revreview-check-flag-u';
1702            }
1703            $checkboxes[$name] = $options;
1704        }
1705    }
1706
1707    /**
1708     * Guess the rev ID the text of this form is based off
1709     */
1710    private static function getBaseRevId( EditPage $editPage, WebRequest $request ): int {
1711        if ( $editPage->isConflict ) {
1712            return 0; // throw away these values (bug 33481)
1713        }
1714
1715        $article = $editPage->getArticle(); // convenience
1716        $latestId = $article->getPage()->getLatest(); // current rev
1717        # Undoing edits...
1718        if ( $request->getIntOrNull( 'wpUndidRevision' ) ?? $request->getIntOrNull( 'undo' ) ) {
1719            $revId = $latestId; // current rev is the base rev
1720            # Other edits...
1721        } else {
1722            # If we are editing via oldid=X, then use that rev ID.
1723            $revId = $article->getOldID();
1724        }
1725        # Zero oldid => draft revision
1726        return $revId ?: $latestId;
1727    }
1728
1729    /**
1730     * Guess the alternative rev ID the text of this form is based off.
1731     * When undoing the top X edits, the base can be though of as either
1732     * the current or the edit X edits prior to the latest.
1733     */
1734    private static function getAltBaseRevId( EditPage $editPage, WebRequest $request ): int {
1735        if ( $editPage->isConflict ) {
1736            return 0; // throw away these values (bug 33481)
1737        }
1738
1739        $article = $editPage->getArticle(); // convenience
1740        $latestId = $article->getPage()->getLatest(); // current rev
1741        $undo = $request->getIntOrNull( 'wpUndidRevision' ) ?? $request->getIntOrNull( 'undo' );
1742        # Undoing consecutive top edits...
1743        if ( $undo && $undo === $latestId ) {
1744            # Treat this like a revert to a base revision.
1745            # We are undoing all edits *after* some rev ID (undoafter).
1746            # If undoafter is not given, then it is the previous rev ID.
1747            $revisionLookup = MediaWikiServices::getInstance()->getRevisionLookup();
1748            $revision = $revisionLookup->getRevisionById( $latestId );
1749            $previousRevision = $revision ? $revisionLookup->getPreviousRevision( $revision ) : null;
1750            $altBaseRevId = $request->getInt( 'wpUndoAfter', $request->getInt( 'undoafter',
1751                $previousRevision ? $previousRevision->getId() : null
1752            ) );
1753        } else {
1754            $altBaseRevId = 0;
1755        }
1756
1757        return $altBaseRevId;
1758    }
1759}