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