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