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