Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
4.83% covered (danger)
4.83%
40 / 828
7.41% covered (danger)
7.41%
4 / 54
CRAP
0.00% covered (danger)
0.00%
0 / 1
FlaggablePageView
4.83% covered (danger)
4.83%
40 / 828
7.41% covered (danger)
7.41%
4 / 54
87482.95
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 / 24
0.00% covered (danger)
0.00%
0 / 1
110
 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\Output\OutputPage;
11use MediaWiki\Page\PageIdentity;
12use MediaWiki\Parser\ParserOptions;
13use MediaWiki\Parser\ParserOutput;
14use MediaWiki\Request\WebRequest;
15use MediaWiki\Revision\RevisionRecord;
16use MediaWiki\SpecialPage\SpecialPage;
17use MediaWiki\Title\Title;
18use MediaWiki\Title\TitleValue;
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        $namespaceInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1096        $currentTitle = $this->article->getTitle();
1097
1098        $title = $currentTitle->isTalkPage() ?
1099            $namespaceInfo->getSubjectPage( $currentTitle ) :
1100            $namespaceInfo->getTalkPage( $currentTitle );
1101
1102        // Convert TitleValue to Title if necessary
1103        if ( $title instanceof TitleValue ) {
1104            $title = Title::newFromLinkTarget( $title );
1105        }
1106
1107        if ( !FlaggedRevs::inReviewNamespace( $title ) ) {
1108            return; // Only reviewable pages need these tabs
1109        }
1110
1111        // Check if we should show a stabilization tab
1112        $pm = MediaWikiServices::getInstance()->getPermissionManager();
1113        if (
1114            !$this->article->getTitle()->isTalkPage() &&
1115            !isset( $actions['protect'] ) &&
1116            !isset( $actions['unprotect'] ) &&
1117            $pm->userHasRight( $reqUser, 'stablesettings' ) &&
1118            $title->exists()
1119        ) {
1120            $stableTitle = SpecialPage::getTitleFor( 'Stabilization' );
1121            // Add the tab
1122            $actions['default'] = [
1123                'class' => false,
1124                'text' => $this->msg( 'stabilization-tab' )->text(),
1125                'href' => $stableTitle->getLocalURL( 'page=' . $title->getPrefixedURL() )
1126            ];
1127        }
1128    }
1129
1130    /**
1131     * Modify an array of tab links to include flagged revs UI elements
1132     * @param Skin $skin
1133     * @param array[] &$views
1134     */
1135    public function setViewTabs( Skin $skin, array &$views ): void {
1136        if ( !FlaggedRevs::inReviewNamespace( $this->article ) ) {
1137            // Short-circuit for non-reviewable pages
1138            return;
1139        }
1140        # Hack for bug 16734 (some actions update and view all at once)
1141        if ( $this->pageWriteOpRequested() &&
1142            MediaWikiServices::getInstance()->getDBLoadBalancer()->hasOrMadeRecentPrimaryChanges()
1143        ) {
1144            # Tabs need to reflect the new stable version so users actually
1145            # see the results of their action (i.e. "delete"/"rollback")
1146            $this->article->loadPageData( IDBAccessObject::READ_LATEST );
1147        }
1148        $srev = $this->article->getStableRev();
1149        if ( !$srev ) {
1150            // No stable revision exists
1151            return;
1152        }
1153        $synced = $this->article->stableVersionIsSynced();
1154        $pendingEdits = !$synced && $this->article->isStableShownByDefault();
1155        // Set the edit tab names as needed...
1156        if ( $pendingEdits && $this->isPageView() && $this->showingStable() ) {
1157            // bug 31489; direct user to current
1158            if ( isset( $views['edit'] ) ) {
1159                $views['edit']['href'] = $skin->getTitle()->getFullURL( 'action=edit' );
1160            }
1161            if ( isset( $views['viewsource'] ) ) {
1162                $views['viewsource']['href'] = $skin->getTitle()->getFullURL( 'action=edit' );
1163            }
1164            // Instruct alternative editors like VisualEditor to load the latest ("current")
1165            // revision for editing, rather than the one from 'wgRevisionId'
1166            $skin->getOutput()->addJsConfigVars( 'wgEditLatestRevision', true );
1167        }
1168        # Add "pending changes" tab if the page is not synced
1169        if ( !$synced ) {
1170            $this->addDraftTab( $views, $srev );
1171        }
1172    }
1173
1174    /**
1175     * Add "pending changes" tab and set tab selection CSS
1176     * @param array[] &$views
1177     * @param FlaggedRevision $srev
1178     */
1179    private function addDraftTab( array &$views, FlaggedRevision $srev ): void {
1180        $request = $this->getRequest();
1181        $title = $this->article->getTitle(); // convenience
1182        $tabs = [
1183            'read' => [ // view stable
1184                'text'  => '', // unused
1185                'href'  => $title->getLocalURL( 'stable=1' ),
1186                'class' => ''
1187            ],
1188            'draft' => [ // view draft
1189                'text'  => $this->msg( 'revreview-current' )->text(),
1190                'href'  => $title->getLocalURL( 'stable=0&redirect=no' ),
1191                'class' => 'collapsible'
1192            ],
1193        ];
1194        // Set tab selection CSS
1195        if ( ( $this->isPageView() && $this->showingStable() ) || $request->getVal( 'stableid' ) ) {
1196            // We are looking a the stable version or an old reviewed one
1197            $tabs['read']['class'] = 'selected';
1198        } elseif ( $this->isPageViewOrDiff() ) {
1199            $ts = null;
1200            if ( $this->out->getRevisionId() ) { // @TODO: avoid same query in Skin.php
1201                if ( $this->out->getRevisionId() == $this->article->getLatest() ) {
1202                    $ts = $this->article->getTimestamp(); // skip query
1203                } else {
1204                    $ts = MediaWikiServices::getInstance()
1205                        ->getRevisionLookup()
1206                        ->getTimestampFromId( $this->out->getRevisionId() );
1207                }
1208            }
1209            // Are we looking at a pending revision?
1210            if ( $ts > $srev->getRevTimestamp() ) { // bug 15515
1211                $tabs['draft']['class'] .= ' selected';
1212            // Are there *just* pending template changes.
1213            } elseif ( $this->article->onlyTemplatesPending()
1214                && $this->out->getRevisionId() == $this->article->getStable()
1215            ) {
1216                $tabs['draft']['class'] .= ' selected';
1217            // Otherwise, fallback to regular tab behavior
1218            } else {
1219                $tabs['read']['class'] = 'selected';
1220            }
1221        }
1222        $newViews = [];
1223        // Rebuild tabs array
1224        $previousTab = null;
1225        foreach ( $views as $tabAction => $data ) {
1226            // The 'view' tab. Make it go to the stable version...
1227            if ( $tabAction == 'view' ) {
1228                // 'view' for content page; make it go to the stable version
1229                $newViews[$tabAction]['text'] = $data['text']; // keep tab name
1230                $newViews[$tabAction]['href'] = $tabs['read']['href'];
1231                $newViews[$tabAction]['class'] = $tabs['read']['class'];
1232            // All other tabs...
1233            } else {
1234                if ( $previousTab == 'view' ) {
1235                    $newViews['current'] = $tabs['draft'];
1236                }
1237                $newViews[$tabAction] = $data;
1238            }
1239            $previousTab = $tabAction;
1240        }
1241        // Replaces old tabs with new tabs
1242        $views = $newViews;
1243    }
1244
1245    /**
1246     * Check if a flaggedrevs relevant write op was done this page view
1247     */
1248    private function pageWriteOpRequested(): bool {
1249        $request = $this->getRequest();
1250        # Hack for bug 16734 (some actions update and view all at once)
1251        $action = $request->getVal( 'action' );
1252        return $action === 'rollback' ||
1253            ( $action === 'delete' && $request->wasPosted() );
1254    }
1255
1256    private function getOldIDFromRequest(): int {
1257        $article = Article::newFromWikiPage( $this->article, RequestContext::getMain() );
1258        return $article->getOldIDFromRequest();
1259    }
1260
1261    /**
1262     * Adds a notice saying that this revision is pending review
1263     *
1264     * @param FlaggedRevision $srev The stable version
1265     * @param string $diffToggle either "" or " <diff toggle><diff div>"
1266     */
1267    private function setPendingNotice( FlaggedRevision $srev, string $diffToggle = '' ): void {
1268        $time = $this->getLanguage()->date( $srev->getTimestamp(), true );
1269        $revsSince = $this->article->getPendingRevCount();
1270        $msg = !$revsSince ? 'revreview-newest-basic-i' : 'revreview-newest-basic';
1271        # Add bar msg to the top of the page...
1272        $msgHTML = $this->msg( $msg, $srev->getRevId(), $time )->numParams( $revsSince )->parse();
1273
1274        if ( !$this->useSimpleUI() ) {
1275            $this->reviewNotice .= FlaggedRevsHTML::addMessageBox( 'block', $msgHTML . $diffToggle );
1276        }
1277    }
1278
1279    /**
1280     * When viewing a diff:
1281     * (a) Add the review form to the top of the page
1282     * (b) Mark off which versions are checked or not
1283     * (c) When comparing the stable revision to the current:
1284     *   (i)  Show a tag with some explanation for the diff
1285     */
1286    public function addToDiffView( ?RevisionRecord $oldRevRecord, ?RevisionRecord $newRevRecord ): void {
1287        $pm = MediaWikiServices::getInstance()->getPermissionManager();
1288        $request = $this->getRequest();
1289        $reqUser = $this->getUser();
1290        # Exempt printer-friendly output
1291        if ( $this->out->isPrintable() ) {
1292            return;
1293        # Multi-page diffs are useless and misbehave (bug 19327). Sanity check $newRevRecord.
1294        } elseif ( $this->isMultiPageDiff || !$newRevRecord ) {
1295            return;
1296        # Page must be reviewable.
1297        } elseif ( !$this->article->isReviewable() ) {
1298            return;
1299        }
1300        $srev = $this->article->getStableRev();
1301        if ( $srev && $this->isReviewableDiff ) {
1302            $this->reviewFormRevRecord = $newRevRecord;
1303        }
1304        # Check if this is a diff-to-stable. If so:
1305        # (a) prompt reviewers to review the changes
1306        if ( $srev
1307            && $this->isDiffFromStable
1308            && !$this->article->stableVersionIsSynced() // pending changes
1309        ) {
1310            # If there are pending revs, notify the user...
1311            if ( $this->article->revsArePending() ) {
1312                # If the user can review then prompt them to review them...
1313                if ( $pm->userHasRight( $reqUser, 'review' ) ) {
1314                    // Reviewer just edited...
1315                    if ( $request->getInt( 'shownotice' )
1316                        && $newRevRecord->isCurrent()
1317                        && $newRevRecord->getUser( RevisionRecord::RAW )
1318                            ->equals( $reqUser )
1319                    ) {
1320                        $title = $this->article->getTitle(); // convenience
1321                        // @TODO: make diff class cache this
1322                        $n = MediaWikiServices::getInstance()
1323                            ->getRevisionStore()
1324                            ->countRevisionsBetween(
1325                                $title->getArticleID(),
1326                                $oldRevRecord,
1327                                $newRevRecord
1328                            );
1329                        if ( $n ) {
1330                            $msg = 'revreview-update-edited-prev'; // previous pending edits
1331                        } else {
1332                            $msg = 'revreview-update-edited'; // just couldn't autoreview
1333                        }
1334                    // All other cases...
1335                    } else {
1336                        $msg = 'revreview-update'; // generic "please review" notice...
1337                    }
1338                    // add as part of form
1339                    $this->diffNoticeBox = $this->msg( $msg )->parseAsBlock();
1340                }
1341            }
1342        }
1343        # Add a link to diff from stable to current as needed.
1344        # Show review status of the diff revision(s). Uses a <table>.
1345        $this->out->addHTML(
1346            '<div id="mw-fr-diff-headeritems">' .
1347            self::diffLinkAndMarkers(
1348                $this->article,
1349                $oldRevRecord,
1350                $newRevRecord
1351            ) .
1352            '</div>'
1353        );
1354    }
1355
1356    /**
1357     * get new diff header items for in-place page review
1358     */
1359    public static function buildDiffHeaderItems(): string {
1360        $args = func_get_args(); // <oldid, newid>
1361        if ( count( $args ) >= 2 ) {
1362            $oldid = (int)$args[0];
1363            $newid = (int)$args[1];
1364            $revLookup = MediaWikiServices::getInstance()->getRevisionLookup();
1365            $newRevRecord = $revLookup->getRevisionById( $newid );
1366            if ( $newRevRecord && $newRevRecord->getPageAsLinkTarget() ) {
1367                $oldRevRecord = $revLookup->getRevisionById( $oldid );
1368                $fa = FlaggableWikiPage::getTitleInstance(
1369                    Title::newFromLinkTarget( $newRevRecord->getPageAsLinkTarget() )
1370                );
1371                return self::diffLinkAndMarkers( $fa, $oldRevRecord, $newRevRecord );
1372            }
1373        }
1374        return '';
1375    }
1376
1377    /**
1378     * (a) Add a link to diff from stable to current as needed
1379     * (b) Show review status of the diff revision(s). Uses a <table>.
1380     * Note: used by ajax function to rebuild diff page
1381     */
1382    private static function diffLinkAndMarkers(
1383        FlaggableWikiPage $article,
1384        ?RevisionRecord $oldRevRecord,
1385        ?RevisionRecord $newRevRecord
1386    ): string {
1387        $s = '<form id="mw-fr-diff-dataform">';
1388        $s .= Html::hidden( 'oldid', $oldRevRecord ? $oldRevRecord->getId() : 0 );
1389        $s .= Html::hidden( 'newid', $newRevRecord ? $newRevRecord->getId() : 0 );
1390        $s .= "</form>\n";
1391        if ( $newRevRecord && $oldRevRecord ) { // sanity check
1392            $s .= self::diffToStableLink( $article, $oldRevRecord, $newRevRecord );
1393            $s .= self::diffReviewMarkers( $article, $oldRevRecord, $newRevRecord );
1394        }
1395        return $s;
1396    }
1397
1398    /**
1399     * Add a link to diff-to-stable for reviewable pages
1400     */
1401    private static function diffToStableLink(
1402        FlaggableWikiPage $article,
1403        RevisionRecord $oldRevRecord,
1404        RevisionRecord $newRevRecord
1405    ): string {
1406        $srev = $article->getStableRev();
1407        if ( !$srev ) {
1408            return ''; // nothing to do
1409        }
1410        $review = '';
1411        # Is this already the full diff-to-stable?
1412        $fullStableDiff = $newRevRecord->isCurrent()
1413            && self::isDiffToStable(
1414                $srev,
1415                $oldRevRecord,
1416                $newRevRecord
1417            );
1418        # Make a link to the full diff-to-stable if:
1419        # (a) Actual revs are pending and (b) We are not viewing the full diff-to-stable
1420        if ( $article->revsArePending() && !$fullStableDiff ) {
1421            $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
1422            $reviewLink = $linkRenderer->makeKnownLink(
1423                $article->getTitle(),
1424                wfMessage( 'review-diff2stable' )->text(),
1425                [],
1426                [ 'oldid' => $srev->getRevId(), 'diff' => 'cur' ]
1427            );
1428            $reviewWrapped = wfMessage( 'parentheses' )->rawParams( $reviewLink )->escaped();
1429            $review = "<div class='fr-diff-to-stable' style='text-align: center;'>$reviewWrapped</div>";
1430        }
1431        return $review;
1432    }
1433
1434    /**
1435     * Add [checked version] and such to left and right side of diff
1436     */
1437    private static function diffReviewMarkers(
1438        FlaggableWikiPage $article,
1439        ?RevisionRecord $oldRevRecord,
1440        ?RevisionRecord $newRevRecord
1441    ): string {
1442        $table = '';
1443        $srev = $article->getStableRev();
1444        # Diff between two revisions
1445        if ( $oldRevRecord && $newRevRecord ) {
1446            [ $msg, $class ] = self::getDiffRevMsgAndClass( $oldRevRecord, $srev );
1447            $table .= "<table class='fr-diff-ratings'><tr>";
1448            $table .= "<td style='text-align: center; width: 50%;'>";
1449            // @todo i18n FIXME: Hard coded brackets
1450            $table .= "<span class='$class'>[" .
1451                wfMessage( $msg )->escaped() . "]</span>";
1452
1453            [ $msg, $class ] = self::getDiffRevMsgAndClass( $newRevRecord, $srev );
1454            $table .= "</td><td style='text-align: center; width: 50%;'>";
1455            // @todo i18n FIXME: Hard coded brackets
1456            $table .= "<span class='$class'>[" .
1457                wfMessage( $msg )->escaped() . "]</span>";
1458
1459            $table .= "</td></tr></table>\n";
1460        # New page "diffs" - just one rev
1461        } elseif ( $newRevRecord ) {
1462            [ $msg, $class ] = self::getDiffRevMsgAndClass( $newRevRecord, $srev );
1463            $table .= "<table class='fr-diff-ratings'>";
1464            $table .= "<tr><td style='text-align: center;'><span class='$class'>";
1465            // @todo i18n FIXME: Hard coded brackets
1466            $table .= '[' . wfMessage( $msg )->escaped() . ']';
1467            $table .= "</span></td></tr></table>\n";
1468        }
1469        return $table;
1470    }
1471
1472    /**
1473     * @return string[]
1474     */
1475    private static function getDiffRevMsgAndClass(
1476        RevisionRecord $revRecord, ?FlaggedRevision $srev
1477    ): array {
1478        $checked = FlaggedRevision::revIsFlagged( $revRecord->getId() );
1479        if ( $checked ) {
1480            $msg = 'revreview-hist-basic';
1481        } else {
1482            $msg = ( $srev && $revRecord->getTimestamp() > $srev->getRevTimestamp() ) ? // bug 15515
1483                'revreview-hist-pending' :
1484                'revreview-hist-draft';
1485        }
1486        return [ $msg, $checked ? 'flaggedrevs-color-1' : 'flaggedrevs-color-0' ];
1487    }
1488
1489    /**
1490     * Set $this->isDiffFromStable and $this->isMultiPageDiff fields
1491     */
1492    public function setViewFlags(
1493        DifferenceEngine $diff,
1494        ?RevisionRecord $oldRevRecord,
1495        ?RevisionRecord $newRevRecord
1496    ): void {
1497        // We only want valid diffs that actually make sense...
1498        if ( !( $newRevRecord
1499            && $oldRevRecord
1500            && $newRevRecord->getTimestamp() >= $oldRevRecord->getTimestamp() )
1501        ) {
1502            return;
1503        }
1504
1505        // Is this a diff between two pages?
1506        if ( $newRevRecord->getPageId() != $oldRevRecord->getPageId() ) {
1507            $this->isMultiPageDiff = true;
1508        // Is there a stable version?
1509        } elseif ( $this->article->isReviewable() ) {
1510            $srev = $this->article->getStableRev();
1511            // Is this a diff of a draft rev against the stable rev?
1512            if ( self::isDiffToStable(
1513                $srev,
1514                $oldRevRecord,
1515                $newRevRecord
1516            ) ) {
1517                $this->isDiffFromStable = true;
1518                $this->isReviewableDiff = true;
1519            // Is this a diff of a draft rev against a reviewed rev?
1520            } elseif (
1521                FlaggedRevision::newFromTitle(
1522                    $diff->getTitle(),
1523                    $oldRevRecord->getId()
1524                ) ||
1525                FlaggedRevision::newFromTitle(
1526                    $diff->getTitle(),
1527                    $newRevRecord->getId()
1528                )
1529            ) {
1530                $this->isReviewableDiff = true;
1531            }
1532        }
1533
1534        $this->diffRevRecords = [
1535            'old' => $oldRevRecord,
1536            'new' => $newRevRecord
1537        ];
1538    }
1539
1540    /**
1541     * Is a diff from $oldRev to $newRev a diff-to-stable?
1542     */
1543    private static function isDiffToStable(
1544        ?FlaggedRevision $srev,
1545        ?RevisionRecord $oldRevRecord,
1546        ?RevisionRecord $newRevRecord
1547    ): bool {
1548        return ( $srev
1549            && $oldRevRecord
1550            && $newRevRecord
1551            && $oldRevRecord->getPageId() === $newRevRecord->getPageId() // no multipage diffs
1552            && $oldRevRecord->getId() == $srev->getRevId()
1553            && $newRevRecord->getTimestamp() >= $oldRevRecord->getTimestamp() // no backwards diffs
1554        );
1555    }
1556
1557    /**
1558     * Redirect users out to review the changes to the stable version.
1559     * Only for people who can review and for pages that have a stable version.
1560     */
1561    public function injectPostEditURLParams( string &$sectionAnchor, string &$extraQuery ): void {
1562        $reqUser = $this->getUser();
1563        $this->article->loadPageData( IDBAccessObject::READ_LATEST );
1564        # Get the stable version from the primary DB
1565        $frev = $this->article->getStableRev();
1566        if ( !$frev ) {
1567            // Only for pages with stable versions
1568            return;
1569        }
1570
1571        $params = [];
1572        $pm = MediaWikiServices::getInstance()->getPermissionManager();
1573        // If the edit was not autoreviewed, and the user can actually make a
1574        // new stable version, then go to the diff...
1575        if ( $this->article->revsArePending() && $frev->userCanSetTag( $reqUser ) ) {
1576            $params += [ 'oldid' => $frev->getRevId(), 'diff' => 'cur', 'shownotice' => 1 ];
1577        // ...otherwise, go to the draft revision after completing an edit.
1578        // This allows for users to immediately see their changes. Even if the stable
1579        // and draft page match, we can avoid a parse due to FR_INCLUDES_STABLE.
1580        } else {
1581            $params += [ 'stable' => 0 ];
1582            // Show a notice at the top of the page for non-reviewers...
1583            if ( $this->article->revsArePending()
1584                && $this->article->isStableShownByDefault()
1585                && !$pm->userHasRight( $reqUser, 'review' )
1586            ) {
1587                $params += [ 'shownotice' => 1 ];
1588                if ( $sectionAnchor ) {
1589                    // Pass a section parameter in the URL as needed to add a link to
1590                    // the "your changes are pending" box on the top of the page...
1591                    $params += [ 'fromsection' => substr( $sectionAnchor, 1 ) ]; // strip #
1592                    $sectionAnchor = ''; // go to the top of the page to see notice
1593                }
1594            }
1595        }
1596        if ( $extraQuery !== '' ) {
1597            $extraQuery .= '&';
1598        }
1599        $extraQuery .= wfArrayToCgi( $params ); // note: EditPage will add initial "&"
1600    }
1601
1602    /**
1603     * If submitting the edit will leave it pending, then change the button text
1604     * Note: interacts with 'review pending changes' checkbox
1605     * @param EditPage $editPage
1606     * @param ButtonInputWidget[] $buttons
1607     */
1608    public function changeSaveButton( EditPage $editPage, array $buttons ): void {
1609        if ( !$this->editWillRequireReview( $editPage ) ) {
1610            // Edit will go live or be reviewed on save
1611            return;
1612        }
1613        if ( isset( $buttons['save'] ) ) {
1614            $buttonLabel = $this->msg( 'revreview-submitedit' )->text();
1615            $buttons['save']->setLabel( $buttonLabel );
1616            $buttonTitle = $this->msg( 'revreview-submitedit-title' )->text();
1617            $buttons['save']->setTitle( $buttonTitle );
1618        }
1619    }
1620
1621    /**
1622     * If this edit will not go live on submit (accounting for wpReviewEdit)
1623     */
1624    private function editWillRequireReview( EditPage $editPage ): bool {
1625        $request = $this->getRequest(); // convenience
1626        $title = $this->article->getTitle(); // convenience
1627        if ( !$this->article->editsRequireReview() || $this->editWillBeAutoreviewed( $editPage ) ) {
1628            return false; // edit will go live immediately
1629        } elseif ( $request->getCheck( 'wpReviewEdit' ) &&
1630            MediaWikiServices::getInstance()->getPermissionManager()
1631                ->userCan( 'review', $this->getUser(), $title )
1632        ) {
1633            return false; // edit checked off to be reviewed on save
1634        }
1635        return true; // edit needs review
1636    }
1637
1638    /**
1639     * If this edit will be auto-reviewed on submit
1640     * Note: checking wpReviewEdit does not count as auto-reviewed
1641     */
1642    private function editWillBeAutoreviewed( EditPage $editPage ): bool {
1643        $title = $this->article->getTitle(); // convenience
1644        if ( !$this->article->isReviewable() ) {
1645            return false;
1646        }
1647        if ( MediaWikiServices::getInstance()->getPermissionManager()
1648            ->quickUserCan( 'autoreview', $this->getUser(), $title )
1649        ) {
1650            if ( FlaggedRevs::autoReviewNewPages() && !$this->article->exists() ) {
1651                return true; // edit will be autoreviewed
1652            }
1653
1654            $baseRevId = self::getBaseRevId( $editPage, $this->getRequest() );
1655            $baseRevId2 = self::getAltBaseRevId( $editPage, $this->getRequest() );
1656            $baseFRev = FlaggedRevision::newFromTitle( $title, $baseRevId );
1657            if ( !$baseFRev && $baseRevId2 ) {
1658                $baseFRev = FlaggedRevision::newFromTitle( $title, $baseRevId2 );
1659            }
1660
1661            if ( $baseFRev ) {
1662                return true; // edit will be autoreviewed
1663            }
1664        }
1665        return false; // edit won't be autoreviewed
1666    }
1667
1668    /**
1669     * Add a "review pending changes" checkbox to the edit form iff:
1670     * (a) there are currently any revisions pending (bug 16713)
1671     * (b) this is an unreviewed page (bug 23970)
1672     */
1673    public function addReviewCheck( EditPage $editPage, array &$checkboxes ): void {
1674        $request = $this->getRequest();
1675        $title = $this->article->getTitle(); // convenience
1676        if ( !$this->article->isReviewable() ||
1677            !MediaWikiServices::getInstance()->getPermissionManager()
1678                ->userCan( 'review', $this->getUser(), $title )
1679        ) {
1680            // Not needed
1681            return;
1682        } elseif ( $this->editWillBeAutoreviewed( $editPage ) ) {
1683            // Edit will be auto-reviewed
1684            return;
1685        }
1686        if ( self::getBaseRevId( $editPage, $request ) == $this->article->getLatest() ) {
1687            # For pages with either no stable version, or an outdated one, let
1688            # the user decide if he/she wants it reviewed on the spot. One might
1689            # do this if he/she just saw the diff-to-stable and *then* decided to edit.
1690            # Note: check not shown when editing old revisions, which is confusing.
1691            $name = 'wpReviewEdit';
1692            $options = [
1693                'id' => $name,
1694                'default' => $request->getCheck( $name ),
1695                'legacy-name' => 'reviewed',
1696            ];
1697            // For reviewed pages...
1698            if ( $this->article->getStable() ) {
1699                // For pending changes...
1700                if ( $this->article->revsArePending() ) {
1701                    $n = $this->article->getPendingRevCount();
1702                    $options['title-message'] = 'revreview-check-flag-p-title';
1703                    $options['label-message'] = $this->msg( 'revreview-check-flag-p' )
1704                        ->numParams( $n );
1705                // For just the user's changes...
1706                } else {
1707                    $options['title-message'] = 'revreview-check-flag-y-title';
1708                    $options['label-message'] = 'revreview-check-flag-y';
1709                }
1710            // For unreviewed pages...
1711            } else {
1712                $options['title-message'] = 'revreview-check-flag-u-title';
1713                $options['label-message'] = 'revreview-check-flag-u';
1714            }
1715            $checkboxes[$name] = $options;
1716        }
1717    }
1718
1719    /**
1720     * Guess the rev ID the text of this form is based off
1721     */
1722    private static function getBaseRevId( EditPage $editPage, WebRequest $request ): int {
1723        if ( $editPage->isConflict ) {
1724            return 0; // throw away these values (bug 33481)
1725        }
1726
1727        $article = $editPage->getArticle(); // convenience
1728        $latestId = $article->getPage()->getLatest(); // current rev
1729        # Undoing edits...
1730        if ( $request->getIntOrNull( 'wpUndidRevision' ) ?? $request->getIntOrNull( 'undo' ) ) {
1731            $revId = $latestId; // current rev is the base rev
1732            # Other edits...
1733        } else {
1734            # If we are editing via oldid=X, then use that rev ID.
1735            $revId = $article->getOldID();
1736        }
1737        # Zero oldid => draft revision
1738        return $revId ?: $latestId;
1739    }
1740
1741    /**
1742     * Guess the alternative rev ID the text of this form is based off.
1743     * When undoing the top X edits, the base can be though of as either
1744     * the current or the edit X edits prior to the latest.
1745     */
1746    private static function getAltBaseRevId( EditPage $editPage, WebRequest $request ): int {
1747        if ( $editPage->isConflict ) {
1748            return 0; // throw away these values (bug 33481)
1749        }
1750
1751        $article = $editPage->getArticle(); // convenience
1752        $latestId = $article->getPage()->getLatest(); // current rev
1753        $undo = $request->getIntOrNull( 'wpUndidRevision' ) ?? $request->getIntOrNull( 'undo' );
1754        # Undoing consecutive top edits...
1755        if ( $undo && $undo === $latestId ) {
1756            # Treat this like a revert to a base revision.
1757            # We are undoing all edits *after* some rev ID (undoafter).
1758            # If undoafter is not given, then it is the previous rev ID.
1759            $revisionLookup = MediaWikiServices::getInstance()->getRevisionLookup();
1760            $revision = $revisionLookup->getRevisionById( $latestId );
1761            $previousRevision = $revision ? $revisionLookup->getPreviousRevision( $revision ) : null;
1762            $altBaseRevId = $request->getInt( 'wpUndoAfter', $request->getInt( 'undoafter',
1763                $previousRevision ? $previousRevision->getId() : null
1764            ) );
1765        } else {
1766            $altBaseRevId = 0;
1767        }
1768
1769        return $altBaseRevId;
1770    }
1771}