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