Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 558
0.00% covered (danger)
0.00%
0 / 38
CRAP
0.00% covered (danger)
0.00%
0 / 1
FlaggedRevsUIHooks
0.00% covered (danger)
0.00%
0 / 558
0.00% covered (danger)
0.00%
0 / 38
28392
0.00% covered (danger)
0.00%
0 / 1
 injectStyleAndJS
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 onMakeGlobalVariablesScript
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 injectStyleForSpecial
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 onBeforePageDisplay
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 onGetPreferences
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
6
 onSkinTemplateNavigation__Universal
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 onArticleViewHeader
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 onInitializeArticleMaybeRedirect
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
90
 onTitleGetEditNotices
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 onEditPageBeforeEditButtons
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 onEditPageNoSuchSection
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 onPageHistoryBeforeList
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 onCategoryPageView
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 onSkinAfterContent
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 onSpecialNewPagesFilters
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 onChangesListSpecialPageStructuredFilters
0.00% covered (danger)
0.00%
0 / 103
0.00% covered (danger)
0.00%
0 / 1
272
 onPageHistoryPager__getQueryInfo
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 onContribsPager__getQueryInfo
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 onSpecialNewpagesConditions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onChangesListSpecialPageQuery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 makeAllQueryChanges
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 addMetadataQueryJoins
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 hideReviewedChangesIfNeeded
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 hideReviewedChangesUnconditionally
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onPageHistoryLineEnding
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
210
 markHistoryRow
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 onContributionsLineEnding
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
72
 onChangesListInsertArticleLink
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
132
 onArticleUpdateBeforeRedirect
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 onNewDifferenceEngine
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 onDifferenceEngineViewHeader
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 onEditPageGetCheckboxesDefinition
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 maybeAddBacklogNotice
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
30
 onProtectionFormAddFormFields
0.00% covered (danger)
0.00%
0 / 70
0.00% covered (danger)
0.00%
0 / 1
182
 onProtectionForm__showLogExtract
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 onProtectionForm__save
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
110
 onSpecialPage_initList
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 onInfoAction
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2// phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
3// phpcs:disable MediaWiki.Commenting.FunctionComment.MissingDocumentationPublic
4
5use MediaWiki\Context\RequestContext;
6use MediaWiki\Diff\Hook\DifferenceEngineViewHeaderHook;
7use MediaWiki\Diff\Hook\NewDifferenceEngineHook;
8use MediaWiki\Hook\ArticleUpdateBeforeRedirectHook;
9use MediaWiki\Hook\BeforePageDisplayHook;
10use MediaWiki\Hook\ChangesListInsertArticleLinkHook;
11use MediaWiki\Hook\ContribsPager__getQueryInfoHook;
12use MediaWiki\Hook\ContributionsLineEndingHook;
13use MediaWiki\Hook\EditPageBeforeEditButtonsHook;
14use MediaWiki\Hook\EditPageGetCheckboxesDefinitionHook;
15use MediaWiki\Hook\EditPageNoSuchSectionHook;
16use MediaWiki\Hook\InitializeArticleMaybeRedirectHook;
17use MediaWiki\Hook\MakeGlobalVariablesScriptHook;
18use MediaWiki\Hook\PageHistoryBeforeListHook;
19use MediaWiki\Hook\PageHistoryLineEndingHook;
20use MediaWiki\Hook\PageHistoryPager__getQueryInfoHook;
21use MediaWiki\Hook\ProtectionForm__saveHook;
22use MediaWiki\Hook\ProtectionForm__showLogExtractHook;
23use MediaWiki\Hook\ProtectionFormAddFormFieldsHook;
24use MediaWiki\Hook\SkinAfterContentHook;
25use MediaWiki\Hook\SkinTemplateNavigation__UniversalHook;
26use MediaWiki\Hook\SpecialNewpagesConditionsHook;
27use MediaWiki\Hook\SpecialNewPagesFiltersHook;
28use MediaWiki\Hook\TitleGetEditNoticesHook;
29use MediaWiki\MediaWikiServices;
30use MediaWiki\Output\OutputPage;
31use MediaWiki\Page\Hook\ArticleViewHeaderHook;
32use MediaWiki\Page\Hook\CategoryPageViewHook;
33use MediaWiki\Preferences\Hook\GetPreferencesHook;
34use MediaWiki\ResourceLoader\ResourceLoader;
35use MediaWiki\Revision\RevisionRecord;
36use MediaWiki\Revision\SlotRecord;
37use MediaWiki\SpecialPage\Hook\ChangesListSpecialPageQueryHook;
38use MediaWiki\SpecialPage\Hook\ChangesListSpecialPageStructuredFiltersHook;
39use MediaWiki\SpecialPage\Hook\SpecialPage_initListHook;
40use MediaWiki\SpecialPage\SpecialPage;
41use MediaWiki\Title\Title;
42
43/**
44 * Class containing hooked functions for a FlaggedRevs environment
45 */
46class FlaggedRevsUIHooks implements
47    ArticleUpdateBeforeRedirectHook,
48    ArticleViewHeaderHook,
49    BeforePageDisplayHook,
50    CategoryPageViewHook,
51    ChangesListInsertArticleLinkHook,
52    ChangesListSpecialPageQueryHook,
53    ChangesListSpecialPageStructuredFiltersHook,
54    ContribsPager__getQueryInfoHook,
55    ContributionsLineEndingHook,
56    DifferenceEngineViewHeaderHook,
57    EditPageBeforeEditButtonsHook,
58    EditPageGetCheckboxesDefinitionHook,
59    EditPageNoSuchSectionHook,
60    GetPreferencesHook,
61    InitializeArticleMaybeRedirectHook,
62    MakeGlobalVariablesScriptHook,
63    NewDifferenceEngineHook,
64    PageHistoryBeforeListHook,
65    PageHistoryLineEndingHook,
66    PageHistoryPager__getQueryInfoHook,
67    ProtectionFormAddFormFieldsHook,
68    ProtectionForm__saveHook,
69    ProtectionForm__showLogExtractHook,
70    SkinAfterContentHook,
71    SkinTemplateNavigation__UniversalHook,
72    SpecialNewpagesConditionsHook,
73    SpecialNewPagesFiltersHook,
74    SpecialPage_initListHook,
75    TitleGetEditNoticesHook
76{
77    /**
78     * Add FlaggedRevs css/js.
79     *
80     * @param OutputPage $out
81     */
82    private static function injectStyleAndJS( OutputPage $out ) {
83        if ( !$out->getTitle()->canExist() ) {
84            return;
85        }
86        $fa = FlaggableWikiPage::getTitleInstance( $out->getTitle() );
87        // Try to only add to relevant pages
88        if ( !$fa || !$fa->isReviewable() ) {
89            return;
90        }
91        // Add main CSS & JS files
92        $out->addModuleStyles( 'ext.flaggedRevs.basic' );
93        $out->addModules( 'ext.flaggedRevs.advanced' );
94        // Add review form and edit page CSS and JS for reviewers
95        if ( MediaWikiServices::getInstance()->getPermissionManager()
96            ->userHasRight( $out->getUser(), 'review' )
97        ) {
98            $out->addModuleStyles( 'codex-styles' );
99            $out->addModules( 'ext.flaggedRevs.review' );
100        }
101    }
102
103    /**
104     * @inheritDoc
105     */
106    public function onMakeGlobalVariablesScript( &$vars, $out ): void {
107        // Get the review tags on this wiki
108        $levels = FlaggedRevs::getMaxLevel();
109        if ( $levels > 0 ) {
110            $vars['wgFlaggedRevsParams'] = [
111                'tags' => [
112                    FlaggedRevs::getTagName() => [ 'levels' => $levels ]
113                ],
114            ];
115        }
116
117        // Get page-specific meta-data
118        $title = $out->getTitle();
119        $fa = $title->canExist() ? FlaggableWikiPage::getTitleInstance( $title ) : null;
120
121        // Try to only add to relevant pages
122        if ( $fa && $fa->isReviewable() ) {
123            $frev = $fa->getStableRev();
124            $vars['wgStableRevisionId'] = $frev ? $frev->getRevId() : 0;
125        }
126    }
127
128    /**
129     * Add FlaggedRevs css for relevant special pages.
130     * @param OutputPage $out
131     */
132    private static function injectStyleForSpecial( $out ) {
133        $title = $out->getTitle();
134        $spPages = [ 'UnreviewedPages', 'PendingChanges', 'Watchlist',
135            'Recentchanges', 'Contributions', 'Recentchangeslinked' ];
136        foreach ( $spPages as $key ) {
137            if ( $title->isSpecial( $key ) ) {
138                $out->addModuleStyles( 'ext.flaggedRevs.basic' ); // CSS only
139                $out->addModuleStyles( 'codex-styles' );
140                break;
141            }
142        }
143    }
144
145    /**
146     * @inheritDoc
147     * Add tag notice, CSS/JS, protect form link, and set robots policy.
148     */
149    public function onBeforePageDisplay( $out, $skin ): void {
150        if ( $out->getTitle()->getNamespace() === NS_SPECIAL ) {
151            self::maybeAddBacklogNotice( $out ); // RC/Watchlist notice
152            self::injectStyleForSpecial( $out ); // try special page CSS
153        } elseif ( $out->getTitle()->canExist() ) {
154            $view = FlaggablePageView::newFromTitle( $out->getTitle() );
155            $view->addStabilizationLink(); // link on protect form
156            $view->displayTag(); // show notice bar/icon in subtitle
157            if ( $out->isArticleRelated() ) {
158                // Only use this hook if we want to prepend the form.
159                // We prepend the form for diffs, so only handle that case here.
160                if ( $view->diffRevRecordsAreSet() ) {
161                    $view->addReviewForm( $out ); // form to be prepended
162                }
163            }
164            $view->setRobotPolicy(); // set indexing policy
165            self::injectStyleAndJS( $out ); // full CSS/JS
166        }
167    }
168
169    /**
170     * @inheritDoc
171     * Add user preferences (uses prefs-flaggedrevs, prefs-flaggedrevs-ui msgs)
172     */
173    public function onGetPreferences( $user, &$preferences ) {
174        // Box or bar UI
175        $preferences['flaggedrevssimpleui'] =
176            [
177                'type' => 'radio',
178                'section' => 'rc/flaggedrevs-ui',
179                'label-message' => 'flaggedrevs-pref-UI',
180                'options-messages' => [
181                    'flaggedrevs-pref-UI-0' => 0,
182                    'flaggedrevs-pref-UI-1' => 1,
183                ],
184            ];
185        // Default versions...
186        $preferences['flaggedrevsstable'] =
187            [
188                'type' => 'radio',
189                'section' => 'rc/flaggedrevs-ui',
190                'label-message' => 'flaggedrevs-prefs-stable',
191                'options-messages' => [
192                    'flaggedrevs-pref-stable-0' => FR_SHOW_STABLE_DEFAULT,
193                    'flaggedrevs-pref-stable-1' => FR_SHOW_STABLE_ALWAYS,
194                    'flaggedrevs-pref-stable-2' => FR_SHOW_STABLE_NEVER,
195                ],
196            ];
197        // Review-related rights...
198        if ( MediaWikiServices::getInstance()->getPermissionManager()
199            ->userHasRight( $user, 'review' )
200        ) {
201            // Watching reviewed pages
202            $preferences['flaggedrevswatch'] =
203                [
204                    'type' => 'toggle',
205                    'section' => 'watchlist/advancedwatchlist',
206                    'label-message' => 'flaggedrevs-prefs-watch',
207                ];
208            // Diff-to-stable on edit
209            $preferences['flaggedrevseditdiffs'] =
210                [
211                    'type' => 'toggle',
212                    'section' => 'editing/advancedediting',
213                    'label-message' => 'flaggedrevs-prefs-editdiffs',
214                ];
215            // Diff-to-stable on draft view
216            $preferences['flaggedrevsviewdiffs'] =
217                [
218                    'type' => 'toggle',
219                    'section' => 'rc/flaggedrevs-ui',
220                    'label-message' => 'flaggedrevs-prefs-viewdiffs',
221                ];
222        }
223    }
224
225    /**
226     * @inheritDoc
227     * Vector et al: $links is all the tabs (2 levels)
228     */
229    public function onSkinTemplateNavigation__Universal( $skin, &$links ): void {
230        if ( $skin->getTitle()->canExist() ) {
231            $view = FlaggablePageView::newFromTitle( $skin->getTitle() );
232            $view->setActionTabs( $links['actions'] );
233            $view->setViewTabs( $skin, $links['views'] );
234        }
235    }
236
237    /**
238     * @inheritDoc
239     */
240    public function onArticleViewHeader( $article, &$outputDone, &$useParserCache ) {
241        if ( $article->getTitle()->canExist() ) {
242            $view = FlaggablePageView::newFromTitle( $article->getTitle() );
243            $view->addStableLink();
244            $view->setPageContent( $outputDone, $useParserCache );
245        }
246    }
247
248    /**
249     * @inheritDoc
250     */
251    public function onInitializeArticleMaybeRedirect(
252        $title,
253        $request,
254        &$ignoreRedirect,
255        &$target,
256        &$article
257    ) {
258        global $wgParserCacheExpireTime;
259        $wikiPage = $article->getPage();
260
261        $fa = FlaggableWikiPage::getTitleInstance( $title );
262        if ( !$fa->isReviewable() ) {
263            return;
264        }
265        # Viewing an old reviewed version...
266        if ( $request->getVal( 'stableid' ) ) {
267            $ignoreRedirect = true; // don't redirect (same as ?oldid=x)
268            return;
269        }
270        $srev = $fa->getStableRev();
271        $view = FlaggablePageView::newFromTitle( $title );
272        # Check if we are viewing an unsynced stable version...
273        # (Make sure that nothing in this code calls WebRequest::getActionName(): T323254)
274        if ( $srev && $view->showingStable() && $srev->getRevId() != $wikiPage->getLatest() ) {
275            # Check the stable redirect properties from the cache...
276            $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
277            $stableRedirect = $cache->getWithSetCallback(
278                $cache->makeKey( 'flaggedrevs-stable-redirect', $wikiPage->getId() ),
279                $wgParserCacheExpireTime,
280                static function () use ( $fa, $srev ) {
281                    $content = $srev->getRevisionRecord()
282                        ->getContent( SlotRecord::MAIN );
283
284                    return $fa->getRedirectURL( $content->getRedirectTarget() ) ?: '';
285                },
286                [
287                    'touchedCallback' => static function () use ( $wikiPage ) {
288                        return wfTimestampOrNull( TS_UNIX, $wikiPage->getTouched() );
289                    }
290                ]
291            );
292            if ( $stableRedirect ) {
293                $target = $stableRedirect; // use stable redirect
294            } else {
295                $ignoreRedirect = true; // make MW skip redirection
296            }
297            $clearEnvironment = (bool)$target;
298        # Check if the we are viewing a draft or synced stable version...
299        } else {
300            # In both cases, we can just let MW use followRedirect()
301            # on the draft as normal, avoiding any page text hits.
302            $clearEnvironment = $wikiPage->isRedirect();
303        }
304        # Environment will change in MediaWiki::initializeArticle
305        if ( $clearEnvironment ) {
306            $view->clear();
307        }
308    }
309
310    /**
311     * @inheritDoc
312     */
313    public function onTitleGetEditNotices( $title, $oldid, &$notices ) {
314        if ( $title->canExist() ) {
315            $view = FlaggablePageView::newFromTitle( $title );
316            $view->getEditNotices( $title, $oldid, $notices );
317        }
318    }
319
320    /**
321     * @inheritDoc
322     */
323    public function onEditPageBeforeEditButtons( $editPage, &$buttons, &$tabindex ) {
324        if ( $editPage->getTitle()->canExist() ) {
325            $view = FlaggablePageView::newFromTitle( $editPage->getTitle() );
326            $view->changeSaveButton( $editPage, $buttons );
327        }
328    }
329
330    /**
331     * @inheritDoc
332     */
333    public function onEditPageNoSuchSection( $editPage, &$s ) {
334        if ( $editPage->getTitle()->canExist() ) {
335            $view = FlaggablePageView::newFromTitle( $editPage->getTitle() );
336            $view->addToNoSuchSection( $s );
337        }
338    }
339
340    /**
341     * @inheritDoc
342     */
343    public function onPageHistoryBeforeList( $article, $context ) {
344        if ( $article->getTitle()->canExist() ) {
345            $view = FlaggablePageView::newFromTitle( $article->getTitle() );
346            $view->addToHistView();
347        }
348    }
349
350    /**
351     * @inheritDoc
352     */
353    public function onCategoryPageView( $category ) {
354        if ( $category->getTitle()->canExist() ) {
355            $view = FlaggablePageView::newFromTitle( $category->getTitle() );
356            $view->addToCategoryView();
357        }
358    }
359
360    /**
361     * @inheritDoc
362     */
363    public function onSkinAfterContent( &$data, $skin ) {
364        if ( $skin->getOutput()->isArticleRelated()
365            && $skin->getTitle()->canExist()
366        ) {
367            $view = FlaggablePageView::newFromTitle( $skin->getTitle() );
368            // Only use this hook if we want to append the form.
369            // We *prepend* the form for diffs, so skip that case here.
370            if ( !$view->diffRevRecordsAreSet() ) {
371                $view->addReviewForm( $data ); // form to be appended
372            }
373        }
374    }
375
376    /**
377     * @inheritDoc
378     * Registers a filter on Special:NewPages to hide edits that have been reviewed
379     * through FlaggedRevs.
380     */
381    public function onSpecialNewPagesFilters( $specialPage, &$filters ) {
382        if ( !FlaggedRevs::useOnlyIfProtected() ) {
383            $filters['hideReviewed'] = [
384                'msg' => 'flaggedrevs-hidereviewed', 'default' => false
385            ];
386        }
387    }
388
389    /**
390     * @inheritDoc
391     * Registers a filter to hide edits that have been reviewed through
392     * FlaggedRevs.
393     */
394    public function onChangesListSpecialPageStructuredFilters( $specialPage ) {
395        if ( FlaggedRevs::useOnlyIfProtected() ) {
396            return;
397        }
398
399        // Old filter, replaced in structured UI
400        $flaggedRevsUnstructuredGroup = new ChangesListBooleanFilterGroup(
401            [
402                'name' => 'flaggedRevsUnstructured',
403                'priority' => -1,
404                'filters' => [
405                    [
406                        'name' => 'hideReviewed',
407                        'showHide' => 'flaggedrevs-hidereviewed',
408                        'isReplacedInStructuredUi' => true,
409                        'default' => false,
410                        'queryCallable' => function ( $specialClassName, $ctx, $dbr, &$tables,
411                            &$fields, &$conds, &$query_options, &$join_conds
412                        ) {
413                            self::hideReviewedChangesUnconditionally(
414                                $conds
415                            );
416                        },
417                    ],
418                ],
419            ]
420        );
421
422        $specialPage->registerFilterGroup( $flaggedRevsUnstructuredGroup );
423
424        $flaggedRevsGroup = new ChangesListStringOptionsFilterGroup(
425            [
426                'name' => 'flaggedrevs',
427                'title' => 'flaggedrevs',
428                'priority' => -9,
429                'default' => ChangesListStringOptionsFilterGroup::NONE,
430                'isFullCoverage' => true,
431                'filters' => [
432                    [
433                        'name' => 'needreview',
434                        'label' => 'flaggedrevs-rcfilters-need-review-label',
435                        'description' => 'flaggedrevs-rcfilters-need-review-desc',
436                        'cssClassSuffix' => 'need-review',
437                        'isRowApplicableCallable' => static function ( $ctx, $rc ) {
438                            return ( FlaggedRevs::isReviewNamespace( $rc->getAttribute( 'rc_namespace' ) ) &&
439                                $rc->getAttribute( 'rc_type' ) !== RC_EXTERNAL ) &&
440                                (
441                                    !$rc->getAttribute( 'fp_stable' ) ||
442                                    (
443                                        // The rc_timestamp >= fp_pending_since condition implies that
444                                        // fp_pending_since is not null, because all comparisons with null
445                                        // values are false in MySQL. It doesn't work that way in PHP,
446                                        // so we have to explicitly check that fp_pending_since is not null
447                                        $rc->getAttribute( 'fp_pending_since' ) &&
448                                        $rc->getAttribute( 'rc_timestamp' ) >= $rc->getAttribute( 'fp_pending_since' )
449                                    )
450                                );
451                        }
452                    ],
453                    [
454                        'name' => 'reviewed',
455                        'label' => 'flaggedrevs-rcfilters-reviewed-label',
456                        'description' => 'flaggedrevs-rcfilters-reviewed-desc',
457                        'cssClassSuffix' => 'reviewed',
458                        'isRowApplicableCallable' => static function ( $ctx, $rc ) {
459                            return ( FlaggedRevs::isReviewNamespace( $rc->getAttribute( 'rc_namespace' ) ) &&
460                                $rc->getAttribute( 'rc_type' ) !== RC_EXTERNAL ) &&
461                                $rc->getAttribute( 'fp_stable' ) &&
462                                (
463                                    !$rc->getAttribute( 'fp_pending_since' ) ||
464                                    $rc->getAttribute( 'rc_timestamp' ) < $rc->getAttribute( 'fp_pending_since' )
465                                );
466                        }
467                    ],
468                    [
469                        'name' => 'notreviewable',
470                        'label' => 'flaggedrevs-rcfilters-not-reviewable-label',
471                        'description' => 'flaggedrevs-rcfilters-not-reviewable-desc',
472                        'cssClassSuffix' => 'not-reviewable',
473                        'isRowApplicableCallable' => static function ( $ctx, $rc ) {
474                            return !FlaggedRevs::isReviewNamespace( $rc->getAttribute( 'rc_namespace' ) );
475                        }
476                    ],
477                ],
478                'queryCallable' => static function ( $specialClassName, $ctx, $dbr, &$tables,
479                    &$fields, &$conds, &$query_options, &$join_conds, $selectedValues
480                ) {
481                    if ( !$selectedValues || count( $selectedValues ) > 2 ) {
482                        // Nothing/everything was selected, no filter needed
483                        return;
484                    }
485
486                    $namespaces = FlaggedRevs::getReviewNamespaces();
487                    $needReviewCond = 'rc_timestamp >= fp_pending_since OR fp_stable IS NULL';
488                    $reviewedCond = '(fp_pending_since IS NULL OR rc_timestamp < fp_pending_since) ' .
489                        'AND fp_stable IS NOT NULL';
490                    $notReviewableCond = 'rc_namespace NOT IN (' . $dbr->makeList( $namespaces ) .
491                        ') OR rc_type = ' . $dbr->addQuotes( RC_EXTERNAL );
492                    $reviewableCond = 'rc_namespace IN (' . $dbr->makeList( $namespaces ) .
493                        ') AND rc_type != ' . $dbr->addQuotes( RC_EXTERNAL );
494
495                    $filters = [];
496                    if ( in_array( 'needreview', $selectedValues ) ) {
497                        $filters[] = $needReviewCond;
498                    }
499                    if ( in_array( 'reviewed', $selectedValues ) ) {
500                        $filters[] = $reviewedCond;
501                    }
502                    if ( count( $filters ) > 1 ) {
503                        // Both selected, no filter needed
504                        $filters = [];
505                    }
506
507                    if ( in_array( 'notreviewable', $selectedValues ) ) {
508                        $filters[] = $notReviewableCond;
509                        $conds[] = $dbr->makeList( $filters, LIST_OR );
510                    } else {
511                        $filters[] = $reviewableCond;
512                        $conds[] = $dbr->makeList( $filters, LIST_AND );
513                    }
514                }
515            ]
516        );
517
518        $specialPage->registerFilterGroup( $flaggedRevsGroup );
519    }
520
521    /**
522     * @inheritDoc
523     */
524    public function onPageHistoryPager__getQueryInfo( $pager, &$queryInfo ) {
525        $flaggedArticle = FlaggableWikiPage::getTitleInstance( $pager->getTitle() );
526        # Non-content pages cannot be validated. Stable version must exist.
527        if ( $flaggedArticle->isReviewable() && $flaggedArticle->getStableRev() ) {
528            # Highlight flaggedrevs
529            $queryInfo['tables'][] = 'flaggedrevs';
530            $queryInfo['fields'][] = 'fr_rev_id';
531            $queryInfo['fields'][] = 'fr_user';
532            $queryInfo['fields'][] = 'fr_flags';
533            $queryInfo['join_conds']['flaggedrevs'] = [ 'LEFT JOIN', "fr_rev_id = rev_id" ];
534            # Find reviewer name. Sanity check that no extensions added a `user` query.
535            if ( !in_array( 'user', $queryInfo['tables'] ) ) {
536                $queryInfo['tables'][] = 'user';
537                $queryInfo['fields']['reviewer'] = 'user_name';
538                $queryInfo['join_conds']['user'] = [ 'LEFT JOIN', "user_id = fr_user" ];
539            }
540        }
541    }
542
543    /**
544     * @inheritDoc
545     */
546    public function onContribsPager__getQueryInfo( $pager, &$queryInfo ) {
547        global $wgFlaggedRevsProtection;
548
549        if ( $wgFlaggedRevsProtection ) {
550            return;
551        }
552
553        # Highlight flaggedrevs
554        $queryInfo['tables'][] = 'flaggedrevs';
555        $queryInfo['fields'][] = 'fr_rev_id';
556        $queryInfo['join_conds']['flaggedrevs'] = [ 'LEFT JOIN', "fr_rev_id = rev_id" ];
557        # Highlight unchecked content
558        $queryInfo['tables'][] = 'flaggedpages';
559        $queryInfo['fields'][] = 'fp_stable';
560        $queryInfo['fields'][] = 'fp_pending_since';
561        $queryInfo['join_conds']['flaggedpages'] = [ 'LEFT JOIN', "fp_page_id = rev_page" ];
562    }
563
564    /**
565     * @inheritDoc
566     */
567    public function onSpecialNewpagesConditions(
568        $specialPage, $opts, &$conds, &$tables, &$fields, &$join_conds
569    ) {
570        self::makeAllQueryChanges( $conds, $tables, $join_conds, $fields );
571    }
572
573    /**
574     * @inheritDoc
575     */
576    public function onChangesListSpecialPageQuery(
577        $name, &$tables, &$fields, &$conds, &$query_options, &$join_conds, $opts
578    ) {
579        self::addMetadataQueryJoins( $tables, $join_conds, $fields );
580    }
581
582    /**
583     * Make all query changes, both joining for FlaggedRevs metadata and conditionally
584     * hiding reviewed changes
585     *
586     * @param array &$conds Query conditions
587     * @param array &$tables Tables to query
588     * @param array &$join_conds Query join conditions
589     * @param string[] &$fields Fields to query
590     */
591    private static function makeAllQueryChanges(
592        array &$conds, array &$tables, array &$join_conds, array &$fields
593    ) {
594        self::addMetadataQueryJoins( $tables, $join_conds, $fields );
595        self::hideReviewedChangesIfNeeded( $conds );
596    }
597
598    /**
599     * Add FlaggedRevs metadata by adding fields and joins
600     *
601     * @param array &$tables Tables to query
602     * @param array &$join_conds Query join conditions
603     * @param string[] &$fields Fields to query
604     */
605    private static function addMetadataQueryJoins(
606        array &$tables, array &$join_conds, array &$fields
607    ) {
608        $tables[] = 'flaggedpages';
609        $fields[] = 'fp_stable';
610        $fields[] = 'fp_pending_since';
611        $join_conds['flaggedpages'] = [ 'LEFT JOIN', 'fp_page_id = rc_cur_id' ];
612    }
613
614    /**
615     * Checks the request variable and hides reviewed changes if requested
616     *
617     * Must already be joined into the FlaggedRevs tables.
618     *
619     * @param array &$conds Query conditions
620     */
621    private static function hideReviewedChangesIfNeeded(
622        array &$conds
623    ) {
624        if ( RequestContext::getMain()->getRequest()->getBool( 'hideReviewed' ) &&
625            !FlaggedRevs::useOnlyIfProtected()
626        ) {
627            self::hideReviewedChangesUnconditionally( $conds );
628        }
629    }
630
631    /**
632     * Hides reviewed changes unconditionally; assumes you have checked whether to do
633     * so already
634     *
635     * Must already be joined into the FlaggedRevs tables.
636     *
637     * @param array &$conds Query conditions
638     */
639    private static function hideReviewedChangesUnconditionally(
640        array &$conds
641    ) {
642        // Don't filter external changes as FlaggedRevisions doesn't apply to those
643        $conds[] = 'rc_timestamp >= fp_pending_since OR fp_stable IS NULL OR rc_type = ' . RC_EXTERNAL;
644    }
645
646    /**
647     * @inheritDoc
648     * @suppress PhanUndeclaredProperty For HistoryPager->fr_*
649     */
650    public function onPageHistoryLineEnding( $history, &$row, &$s, &$liClasses, &$attribs ) {
651        $fa = FlaggableWikiPage::getTitleInstance( $history->getTitle() );
652        if ( !$fa->isReviewable() ) {
653            return;
654        }
655        # Fetch and process cache the stable revision
656        if ( !isset( $history->fr_stableRevId ) ) {
657            $srev = $fa->getStableRev();
658            $history->fr_stableRevId = $srev ? $srev->getRevId() : null;
659            $history->fr_stableRevUTS = $srev ? // bug 15515
660                wfTimestamp( TS_UNIX, $srev->getRevTimestamp() ) : null;
661            $history->fr_pendingRevs = false;
662        }
663        if ( !$history->fr_stableRevId ) {
664            return;
665        }
666        $title = $history->getTitle();
667        $revId = (int)$row->rev_id;
668        // Pending revision: highlight and add diff link
669        $link = '';
670        $class = '';
671        if ( wfTimestamp( TS_UNIX, $row->rev_timestamp ) > $history->fr_stableRevUTS ) {
672            $class = 'flaggedrevs-pending';
673            $link = $history->msg( 'revreview-hist-pending-difflink',
674                $title->getPrefixedText(), $history->fr_stableRevId, $revId )->parse();
675            $link = '<span class="plainlinks mw-fr-hist-difflink">' . $link . '</span>';
676            $history->fr_pendingRevs = true; // pending rev shown above stable
677        // Reviewed revision: highlight and add link
678        } elseif ( isset( $row->fr_rev_id ) ) {
679            if (
680                !( $row->rev_deleted & RevisionRecord::DELETED_TEXT )
681                && !( $row->rev_deleted & RevisionRecord::DELETED_USER )
682            ) {
683                # Add link to stable version of *this* rev, if any
684                [ $link, $class ] = self::markHistoryRow( $history, $title, $row );
685                # Space out and demark the stable revision
686                if ( $revId == $history->fr_stableRevId && $history->fr_pendingRevs ) {
687                    $liClasses[] = 'fr-hist-stable-margin';
688                }
689            }
690        }
691        # Style the row as needed
692        if ( $class ) {
693            $s = "<span class='$class'>$s</span>";
694        }
695        # Add stable old version link
696        if ( $link ) {
697            $s .= " $link";
698        }
699    }
700
701    /**
702     * Make stable version link and return the css
703     * @param IContextSource $ctx
704     * @param Title $title
705     * @param stdClass $row from history page
706     * @return string[]
707     */
708    private static function markHistoryRow( IContextSource $ctx, Title $title, $row ) {
709        if ( !isset( $row->fr_rev_id ) ) {
710            return [ "", "" ]; // not reviewed
711        }
712        $flags = explode( ',', $row->fr_flags );
713        if ( in_array( 'auto', $flags ) ) {
714            $msg = 'revreview-hist-basic-auto';
715            $css = 'fr-hist-basic-auto';
716        } else {
717            $msg = 'revreview-hist-basic-user';
718            $css = 'fr-hist-basic-user';
719        }
720        if ( isset( $row->reviewer ) ) {
721            $name = $row->reviewer;
722        } else {
723            $reviewer = MediaWikiServices::getInstance()
724                ->getActorStore()
725                ->getUserIdentityByUserId( $row->fr_user );
726            $name = $reviewer ? $reviewer->getName() : false;
727        }
728        $link = $ctx->msg( $msg, $title->getPrefixedDBkey(), $row->rev_id, $name )->parse();
729        $link = "<span class='$css plainlinks'>[$link]</span>";
730        return [ $link, 'flaggedrevs-color-1' ];
731    }
732
733    /**
734     * @inheritDoc
735     * Intercept contribution entries and format them to FlaggedRevs standards
736     */
737    public function onContributionsLineEnding( $contribs, &$ret, $row, &$classes, &$attribs ) {
738        global $wgFlaggedRevsProtection;
739
740        // make sure that we're parsing revisions data
741        if ( !$wgFlaggedRevsProtection && isset( $row->rev_id ) ) {
742            if ( !FlaggedRevs::isReviewNamespace( $row->page_namespace ) ) {
743                // do nothing
744            } elseif ( isset( $row->fr_rev_id ) ) {
745                $classes[] = 'flaggedrevs-color-1';
746            } elseif ( isset( $row->fp_pending_since )
747                && $row->rev_timestamp >= $row->fp_pending_since // bug 15515
748            ) {
749                $classes[] = 'flaggedrevs-pending';
750            } elseif ( !isset( $row->fp_stable ) ) {
751                $classes[] = 'flaggedrevs-unreviewed';
752            }
753        }
754    }
755
756    /**
757     * @inheritDoc
758     */
759    public function onChangesListInsertArticleLink(
760        $list,
761        &$articlelink,
762        &$s,
763        $rc,
764        $unpatrolled,
765        $watched
766    ) {
767        $page = $rc->getPage();
768        if ( !$page || !FlaggedRevs::inReviewNamespace( $page )
769            || !$rc->getAttribute( 'rc_this_oldid' ) // rev, not log
770            || !array_key_exists( 'fp_stable', $rc->getAttributes() )
771        ) {
772            // Confirm that page is in reviewable namespace
773            return;
774        }
775        $rlink = '';
776        $css = '';
777        // page is not reviewed
778        if ( $rc->getAttribute( 'fp_stable' ) == null ) {
779            // Is this a config were pages start off reviewable?
780            // Hide notice from non-reviewers due to vandalism concerns (bug 24002).
781            if ( !FlaggedRevs::useOnlyIfProtected() && MediaWikiServices::getInstance()
782                    ->getPermissionManager()
783                    ->userHasRight( $list->getUser(), 'review' )
784            ) {
785                $rlink = $list->msg( 'revreview-unreviewedpage' )->escaped();
786                $css = 'flaggedrevs-unreviewed';
787            }
788        // page is reviewed and has pending edits (use timestamps; bug 15515)
789        } elseif ( $rc->getAttribute( 'fp_pending_since' ) !== null &&
790            $rc->getAttribute( 'rc_timestamp' ) >= $rc->getAttribute( 'fp_pending_since' )
791        ) {
792            $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
793            $rlink = $linkRenderer->makeLink(
794                $page,
795                $list->msg( 'revreview-reviewlink' )->text(),
796                [ 'title' => $list->msg( 'revreview-reviewlink-title' )->text() ],
797                [ 'oldid' => $rc->getAttribute( 'fp_stable' ), 'diff' => 'cur' ]
798            );
799            $css = 'flaggedrevs-pending';
800        }
801        if ( $rlink != '' ) {
802            $articlelink .= " <span class=\"mw-fr-reviewlink $css\">[$rlink]</span>";
803        }
804    }
805
806    /**
807     * @inheritDoc
808     */
809    public function onArticleUpdateBeforeRedirect( $article, &$sectionAnchor, &$extraQuery ) {
810        if ( $article->getTitle()->canExist() ) {
811            $view = FlaggablePageView::newFromTitle( $article->getTitle() );
812            $view->injectPostEditURLParams( $sectionAnchor, $extraQuery );
813        }
814    }
815
816    /**
817     * @inheritDoc
818     * diff=review param (bug 16923)
819     */
820    public function onNewDifferenceEngine( $titleObj, &$mOldid, &$mNewid, $old, $new ) {
821        if ( $new === 'review' && $titleObj ) {
822            $sRevId = FlaggedRevision::getStableRevId( $titleObj );
823            if ( $sRevId ) {
824                $mOldid = $sRevId; // stable
825                $mNewid = 0; // cur
826            }
827        }
828    }
829
830    /**
831     * @inheritDoc
832     */
833    public function onDifferenceEngineViewHeader( $diff ) {
834        self::injectStyleAndJS( $diff->getOutput() );
835
836        if ( $diff->getTitle()->canExist() ) {
837            $view = FlaggablePageView::newFromTitle( $diff->getTitle() );
838
839            $oldRevRecord = $diff->getOldRevision();
840            $newRevRecord = $diff->getNewRevision();
841            $view->setViewFlags( $diff, $oldRevRecord, $newRevRecord );
842            $view->addToDiffView( $oldRevRecord, $newRevRecord );
843        }
844    }
845
846    /**
847     * @inheritDoc
848     */
849    public function onEditPageGetCheckboxesDefinition( $editPage, &$checkboxes ) {
850        if ( $editPage->getTitle()->canExist() ) {
851            $view = FlaggablePageView::newFromTitle( $editPage->getTitle() );
852            $view->addReviewCheck( $editPage, $checkboxes );
853        }
854    }
855
856    /**
857     * @param OutputPage $out
858     */
859    private static function maybeAddBacklogNotice( OutputPage $out ) {
860        if ( !MediaWikiServices::getInstance()->getPermissionManager()
861            ->userHasRight( $out->getUser(), 'review' ) ) {
862            // Not relevant to user
863            return;
864        }
865        $namespaces = FlaggedRevs::getReviewNamespaces();
866        $watchlist = SpecialPage::getTitleFor( 'Watchlist' );
867        # Add notice to watchlist about pending changes...
868        if ( $out->getTitle()->equals( $watchlist ) && $namespaces ) {
869            $dbr = MediaWikiServices::getInstance()->getConnectionProvider()
870                ->getReplicaDatabase( false, 'watchlist' ); // consistency with watchlist
871            $watchedOutdated = (bool)$dbr->newSelectQueryBuilder()
872                ->select( '1' ) // existence
873                ->from( 'watchlist' )
874                ->join( 'page', null, [
875                    'wl_namespace = page_namespace',
876                    'wl_title = page_title',
877                ] )
878                ->join( 'flaggedpages', null, 'fp_page_id = page_id' )
879                ->where( [
880                    'wl_user' => $out->getUser()->getId(), // this user
881                    'wl_namespace' => $namespaces, // reviewable
882                    $dbr->expr( 'fp_pending_since', '!=', null ), // edits pending
883                ] )
884                ->caller( __METHOD__ )
885                ->fetchField();
886            # Give a notice if pages on the users's wachlist have pending edits
887            if ( $watchedOutdated ) {
888                $css = 'plainlinks fr-watchlist-pending-notice mw-message-box mw-message-box-warning';
889                // @todo: Use Html::warningBox. We can't use it here because warningBox cannot have an id.
890                // Thus we must either remove the need of the id attribute or use two <div>s.
891                $out->prependHTML( "<div id='mw-fr-watchlist-pending-notice' class='$css'>" .
892                    wfMessage( 'flaggedrevs-watched-pending' )->parse() . "</div>" );
893            }
894        }
895    }
896
897    /**
898     * @inheritDoc
899     * Add selector of review "protection" options
900     */
901    public function onProtectionFormAddFormFields( $article, &$fields ) {
902        global $wgFlaggedRevsProtection;
903
904        $wikiPage = $article->getPage();
905        $title = $wikiPage->getTitle();
906        $context = $article->getContext();
907
908        if (
909            !$wgFlaggedRevsProtection
910            || !$wikiPage->exists()
911            || !FlaggedRevs::inReviewNamespace( $wikiPage )
912        ) {
913            return;
914        }
915
916        $user = $context->getUser();
917        $request = $context->getRequest();
918        $mode = $request->wasPosted() ? IDBAccessObject::READ_LATEST : 0;
919        $form = new PageStabilityProtectForm( $user );
920        $form->setTitle( $title );
921
922        $config = FRPageConfig::getStabilitySettings( $title, $mode );
923        $expirySelect = $request->getVal(
924            'mwStabilizeExpirySelection',
925            $config['expiry'] == 'infinity' ? 'infinite' : 'existing'
926        );
927        $isAllowed = $form->isAllowed();
928
929        $expiryOther = $request->getVal( 'mwStabilizeExpiryOther' );
930        if ( $expiryOther ) {
931            $expirySelect = 'othertime'; // mutual exclusion
932        }
933
934        # Get and add restriction levels to an array
935        $effectiveLevels = [ 'none', ...FlaggedRevs::getRestrictionLevels() ];
936        $options = [];
937        foreach ( $effectiveLevels as $limit ) {
938            $msg = $context->msg( 'flaggedrevs-protect-' . $limit );
939            // Default to the key itself if no UI message
940            $options[$msg->isDisabled() ? 'flaggedrevs-protect-' . $limit : $msg->text()] = $limit;
941        }
942
943        # Get and add expiry options to an array
944        $scExpiryOptions = wfMessage( 'protect-expiry-options' )->inContentLanguage()->text();
945        $expiryOptions = [];
946
947        if ( $config['expiry'] != 'infinity' ) {
948            $lang = $context->getLanguage();
949            $timestamp = $lang->userTimeAndDate( $config['expiry'], $user );
950            $date = $lang->userDate( $config['expiry'], $user );
951            $time = $lang->userTime( $config['expiry'], $user );
952            $existingExpiryMessage = $context->msg( 'protect-existing-expiry', $timestamp, $date, $time );
953            $expiryOptions[$existingExpiryMessage->text()] = 'existing';
954        }
955
956        $expiryOptions[$context->msg( 'protect-othertime-op' )->text()] = 'othertime';
957
958        foreach ( explode( ',', $scExpiryOptions ) as $option ) {
959            $pair = explode( ':', $option, 2 );
960            $expiryOptions[$pair[0]] = $pair[1] ?? $pair[0];
961        }
962
963        # Create restriction level select
964        $fields['mwStabilityLevel'] = [
965            'type' => 'select',
966            'name' => 'mwStabilityLevel',
967            'id' => 'mwStabilityLevel',
968            'disabled' => !$isAllowed,
969            'options' => $options,
970            'default' => $request->getVal( 'mwStabilityLevel', FRPageConfig::getProtectionLevel( $config ) ),
971            'section' => 'flaggedrevs-protect-legend',
972        ];
973
974        # Create expiry options select
975        if ( $scExpiryOptions !== '-' ) {
976            $fields['mwStabilizeExpirySelection'] = [
977                'type' => 'select',
978                'name' => 'mwStabilizeExpirySelection',
979                'id' => 'mwStabilizeExpirySelection',
980                'disabled' => !$isAllowed,
981                'label' => $context->msg( 'stabilization-expiry' )->text(),
982                'options' => $expiryOptions,
983                'default' => $expirySelect,
984                'section' => 'flaggedrevs-protect-legend',
985            ];
986        }
987
988        # Create other expiry time input
989        if ( $isAllowed ) {
990            $fields['mwStabilizeExpiryOther'] = [
991                'type' => 'text',
992                'name' => 'mwStabilizeExpiryOther',
993                'id' => 'mwStabilizeExpiryOther',
994                'label' => $context->msg( 'stabilization-othertime' )->text(),
995                'default' => $expiryOther,
996                'section' => 'flaggedrevs-protect-legend'
997            ];
998        }
999
1000        # Add some javascript for expiry dropdowns
1001        $context->getOutput()->addInlineScript( ResourceLoader::makeInlineCodeWithModule( 'oojs-ui-core', "
1002            var changeExpiryDropdown = OO.ui.infuse( $( '#mwStabilizeExpirySelection' ) ),
1003                changeExpiryInput = OO.ui.infuse( $( '#mwStabilizeExpiryOther' ) );
1004
1005            changeExpiryDropdown.on( 'change', function ( val ) {
1006                if ( val !== 'othertime' ) {
1007                    changeExpiryInput.setValue( '' );
1008                }
1009            } );
1010
1011            changeExpiryInput.on( 'change', function ( val ) {
1012                if ( val ) {
1013                    changeExpiryDropdown.setValue( 'othertime' );
1014                }
1015            } );
1016        " ) );
1017    }
1018
1019    /**
1020     * @inheritDoc
1021     * Add stability log extract to protection form
1022     */
1023    public function onProtectionForm__showLogExtract(
1024        $article,
1025        $out
1026    ) {
1027        global $wgFlaggedRevsProtection;
1028        $wikiPage = $article->getPage();
1029        $title = $wikiPage->getTitle();
1030
1031        if (
1032            !$wgFlaggedRevsProtection
1033            || !$wikiPage->exists()
1034            || !FlaggedRevs::inReviewNamespace( $wikiPage )
1035        ) {
1036            return;
1037        }
1038
1039        # Show relevant lines from the stability log:
1040        $logPage = new LogPage( 'stable' );
1041        $out->addHTML( Html::element( 'h2', [], $logPage->getName()->text() ) );
1042        LogEventsList::showLogExtract( $out, 'stable', $title->getPrefixedText() );
1043    }
1044
1045    /**
1046     * @inheritDoc
1047     * Update stability config from request
1048     */
1049    public function onProtectionForm__save( $article, &$errorMsg, $reasonstr ) {
1050        global $wgFlaggedRevsProtection;
1051        $wikiPage = $article->getPage();
1052        $title = $wikiPage->getTitle();
1053        $user = $article->getContext()->getUser();
1054
1055        if (
1056            !$wgFlaggedRevsProtection
1057            || !$wikiPage->exists() // simple custom levels set for action=protect
1058            || !FlaggedRevs::inReviewNamespace( $wikiPage )
1059        ) {
1060            return;
1061        }
1062
1063        $services = MediaWikiServices::getInstance();
1064        if ( $services->getReadOnlyMode()->isReadOnly() || !$services->getPermissionManager()
1065                ->userHasRight( $user, 'stablesettings' )
1066        ) {
1067            // User cannot change anything
1068            return;
1069        }
1070        $form = new PageStabilityProtectForm( $user );
1071        $form->setTitle( $title ); // target page
1072        $request = RequestContext::getMain()->getRequest();
1073        $permission = (string)$request->getVal( 'mwStabilityLevel', '' );
1074        if ( $permission == "none" ) {
1075            $permission = ''; // 'none' => ''
1076        }
1077        $form->setAutoreview( $permission ); // protection level (autoreview restriction)
1078        $form->setWatchThis( null ); // protection form already has a watch check
1079        $form->setReasonExtra( $request->getText( 'mwProtect-reason' ) ); // manual
1080        $form->setReasonSelection( $request->getVal( 'wpProtectReasonSelection' ) ); // dropdown
1081        $form->setExpiryCustom( $request->getVal( 'mwStabilizeExpiryOther' ) ); // manual
1082        $form->setExpirySelection( $request->getVal( 'mwStabilizeExpirySelection' ) ); // dropdown
1083        $form->ready(); // params all set
1084        if ( $request->wasPosted() && $form->isAllowed() ) {
1085            $status = $form->submit();
1086            if ( $status !== true ) {
1087                $errorMsg = wfMessage( $status )->text(); // some error message
1088            }
1089        }
1090    }
1091
1092    /**
1093     * @inheritDoc
1094     */
1095    public function onSpecialPage_initList( &$list ) {
1096        global $wgFlaggedRevsProtection, $wgFlaggedRevsNamespaces;
1097
1098        // Show special pages only if FlaggedRevs is enabled on some namespaces
1099        if ( $wgFlaggedRevsNamespaces ) {
1100            $list['RevisionReview'] = RevisionReview::class; // unlisted
1101            $list['PendingChanges'] = PendingChanges::class;
1102            $list['ValidationStatistics'] = ValidationStatistics::class;
1103            // Protect levels define allowed stability settings
1104            if ( $wgFlaggedRevsProtection ) {
1105                $list['StablePages'] = StablePages::class;
1106            } else {
1107                $list['ConfiguredPages'] = ConfiguredPages::class;
1108                $list['UnreviewedPages'] = UnreviewedPages::class;
1109                $list['Stabilization'] = Stabilization::class; // unlisted
1110            }
1111        }
1112    }
1113
1114    /**
1115     * Adds list of translcuded pages waiting for review to action=info
1116     *
1117     * @param IContextSource $context
1118     * @param array[] &$pageInfo
1119     */
1120    public function onInfoAction( $context, &$pageInfo ) {
1121        if ( FlaggedRevs::inclusionSetting() == FR_INCLUDES_CURRENT ) {
1122            return; // short-circuit
1123        }
1124        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
1125
1126        $linksMigration = MediaWikiServices::getInstance()->getLinksMigration();
1127        [ $nsField, $titleField ] = $linksMigration->getTitleFields( 'templatelinks' );
1128        $queryInfo = $linksMigration->getQueryInfo( 'templatelinks' );
1129        // Keep it in sync with FlaggedRevision::findPendingTemplateChanges()
1130        $ret = $dbr->newSelectQueryBuilder()
1131            ->select( [ $nsField, $titleField ] )
1132            ->tables( $queryInfo['tables'] )
1133            ->leftJoin( 'page', null, [ "page_namespace = $nsField", "page_title = $titleField" ] )
1134            ->join( 'flaggedpages', null, 'fp_page_id = page_id' )
1135            ->where( [
1136                'tl_from' => $context->getTitle()->getArticleID(),
1137                $dbr->expr( 'fp_pending_since', '!=', null )->or( 'fp_stable', '=', null ),
1138            ] )
1139            ->joinConds( $queryInfo['joins'] )
1140            ->caller( __METHOD__ )
1141            ->fetchResultSet();
1142        $titles = [];
1143        foreach ( $ret as $row ) {
1144            $titleValue = new TitleValue( (int)$row->$nsField, $row->$titleField );
1145            $titles[] = MediaWikiServices::getInstance()->getLinkRenderer()->makeLink( $titleValue );
1146        }
1147        if ( $titles ) {
1148            $valueHTML = Html::openElement( 'ul' );
1149            foreach ( $titles as $title ) {
1150                $valueHTML .= Html::rawElement( 'li', [], $title );
1151            }
1152            $valueHTML .= Html::closeElement( 'ul' );
1153        } else {
1154            $valueHTML = $context->msg( 'flaggedrevs-action-info-pages-waiting-for-review-none' )->parse();
1155        }
1156
1157        $pageInfo['header-properties'][] = [
1158            $context->msg( 'flaggedrevs-action-info-pages-waiting-for-review' )->parse(),
1159            $valueHTML
1160        ];
1161    }
1162}