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