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