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