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