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