Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
12.02% |
47 / 391 |
|
0.00% |
0 / 15 |
CRAP | |
0.00% |
0 / 1 |
ChangesListHooksHandler | |
12.02% |
47 / 391 |
|
0.00% |
0 / 15 |
4549.02 | |
0.00% |
0 / 1 |
onChangesListSpecialPageStructuredFilters | |
60.00% |
9 / 15 |
|
0.00% |
0 / 1 |
8.30 | |||
handleDamaging | |
0.00% |
0 / 103 |
|
0.00% |
0 / 1 |
272 | |||
handleGoodFaith | |
0.00% |
0 / 52 |
|
0.00% |
0 / 1 |
110 | |||
handleRevertrisk | |
0.00% |
0 / 48 |
|
0.00% |
0 / 1 |
20 | |||
shouldStraightJoin | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDamagingStructuredFiltersOnChangesList | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
42 | |||
getGoodFaithStructuredFiltersOnChangesList | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
42 | |||
getRevertriskStructuredFiltersOnChangesList | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
onChangesListSpecialPageQuery | |
76.92% |
10 / 13 |
|
0.00% |
0 / 1 |
6.44 | |||
onEnhancedChangesListModifyLineData | |
66.67% |
2 / 3 |
|
0.00% |
0 / 1 |
2.15 | |||
onEnhancedChangesListModifyBlockLineData | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
processRecentChangesList | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
onOldChangesListRecentChangesLine | |
85.71% |
12 / 14 |
|
0.00% |
0 / 1 |
6.10 | |||
getScoreRecentChangesList | |
66.67% |
10 / 15 |
|
0.00% |
0 / 1 |
8.81 | |||
makeApplicableCallback | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | /** |
3 | * This program is free software: you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation, either version 3 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License |
14 | * along with this program. If not, see <http://www.gnu.org/licenses/>. |
15 | */ |
16 | |
17 | namespace ORES\Hooks; |
18 | |
19 | use ChangesList; |
20 | use ChangesListBooleanFilterGroup; |
21 | use ChangesListFilter; |
22 | use ChangesListStringOptionsFilterGroup; |
23 | use EnhancedChangesList; |
24 | use Exception; |
25 | use IContextSource; |
26 | use MediaWiki\Hook\EnhancedChangesListModifyBlockLineDataHook; |
27 | use MediaWiki\Hook\EnhancedChangesListModifyLineDataHook; |
28 | use MediaWiki\Hook\OldChangesListRecentChangesLineHook; |
29 | use MediaWiki\MediaWikiServices; |
30 | use MediaWiki\SpecialPage\ChangesListSpecialPage; |
31 | use MediaWiki\SpecialPage\Hook\ChangesListSpecialPageQueryHook; |
32 | use MediaWiki\SpecialPage\Hook\ChangesListSpecialPageStructuredFiltersHook; |
33 | use MediaWiki\Specials\SpecialRecentChanges; |
34 | use MediaWiki\Specials\SpecialWatchlist; |
35 | use ORES\Services\ORESServices; |
36 | use ORES\Storage\ThresholdLookup; |
37 | use RecentChange; |
38 | use Wikimedia\Rdbms\IDatabase; |
39 | |
40 | class ChangesListHooksHandler implements |
41 | ChangesListSpecialPageStructuredFiltersHook, |
42 | ChangesListSpecialPageQueryHook, |
43 | EnhancedChangesListModifyBlockLineDataHook, |
44 | EnhancedChangesListModifyLineDataHook, |
45 | OldChangesListRecentChangesLineHook |
46 | { |
47 | |
48 | public function onChangesListSpecialPageStructuredFilters( |
49 | $clsp |
50 | ) { |
51 | if ( !Helpers::oresUiEnabled() ) { |
52 | return; |
53 | } |
54 | |
55 | $thresholdLookup = ORESServices::getThresholdLookup(); |
56 | $changeTypeGroup = $clsp->getFilterGroup( 'changeType' ); |
57 | $logFilter = $changeTypeGroup->getFilter( 'hidelog' ); |
58 | try { |
59 | if ( Helpers::isModelEnabled( 'revertrisklanguageagnostic' ) ) { |
60 | self::handleRevertrisk( $clsp, $thresholdLookup, $logFilter ); |
61 | } |
62 | if ( Helpers::isModelEnabled( 'damaging' ) ) { |
63 | self::handleDamaging( $clsp, $thresholdLookup, $logFilter ); |
64 | } |
65 | if ( Helpers::isModelEnabled( 'goodfaith' ) ) { |
66 | self::handleGoodFaith( $clsp, $thresholdLookup, $logFilter ); |
67 | } |
68 | } catch ( Exception $exception ) { |
69 | ORESServices::getLogger()->error( |
70 | 'Error in ChangesListHookHandler: ' . $exception->getMessage() |
71 | ); |
72 | } |
73 | } |
74 | |
75 | /** |
76 | * @param ChangesListSpecialPage $clsp |
77 | * @param ThresholdLookup $thresholdLookup |
78 | * @param ChangesListFilter $logFilter |
79 | */ |
80 | private static function handleDamaging( |
81 | ChangesListSpecialPage $clsp, |
82 | ThresholdLookup $thresholdLookup, |
83 | ChangesListFilter $logFilter |
84 | |
85 | ) { |
86 | $userOptionsLookup = MediaWikiServices::getInstance()->getUserOptionsLookup(); |
87 | if ( $clsp instanceof SpecialRecentChanges ) { |
88 | $damagingDefault = $userOptionsLookup->getOption( $clsp->getUser(), 'oresRCHideNonDamaging' ); |
89 | $highlightDefault = $userOptionsLookup->getBoolOption( $clsp->getUser(), 'ores-damaging-flag-rc' ); |
90 | } elseif ( $clsp instanceof SpecialWatchlist ) { |
91 | $damagingDefault = $userOptionsLookup->getOption( $clsp->getUser(), 'oresWatchlistHideNonDamaging' ); |
92 | $highlightDefault = $userOptionsLookup->getBoolOption( $clsp->getUser(), 'oresHighlight' ); |
93 | } else { |
94 | $damagingDefault = false; |
95 | $highlightDefault = false; |
96 | } |
97 | |
98 | $filters = self::getDamagingStructuredFiltersOnChangesList( |
99 | $thresholdLookup->getThresholds( 'damaging' ) |
100 | ); |
101 | |
102 | if ( $filters ) { |
103 | $newDamagingGroup = new ChangesListStringOptionsFilterGroup( [ |
104 | 'name' => 'damaging', |
105 | 'title' => 'ores-rcfilters-damaging-title', |
106 | 'whatsThisHeader' => 'ores-rcfilters-damaging-whats-this-header', |
107 | 'whatsThisBody' => 'ores-rcfilters-damaging-whats-this-body', |
108 | 'whatsThisUrl' => 'https://www.mediawiki.org/wiki/' . |
109 | 'Special:MyLanguage/Help:New_filters_for_edit_review/Quality_and_Intent_Filters', |
110 | 'whatsThisLinkText' => 'ores-rcfilters-whats-this-link-text', |
111 | 'priority' => 2, |
112 | 'filters' => array_values( $filters ), |
113 | 'default' => ChangesListStringOptionsFilterGroup::NONE, |
114 | 'isFullCoverage' => false, |
115 | 'queryCallable' => function ( $specialClassName, $ctx, IDatabase $dbr, &$tables, |
116 | &$fields, &$conds, &$query_options, &$join_conds, $selectedValues ) { |
117 | $databaseQueryBuilder = ORESServices::getDatabaseQueryBuilder(); |
118 | $condition = $databaseQueryBuilder->buildQuery( |
119 | 'damaging', |
120 | $selectedValues |
121 | ); |
122 | if ( $condition ) { |
123 | $conds[] = $condition; |
124 | |
125 | // Filter out incompatible types; log actions and external rows are not scorable |
126 | $conds[] = $dbr->expr( 'rc_type', '!=', [ RC_LOG, RC_EXTERNAL ] ); |
127 | // Make the joins INNER JOINs instead of LEFT JOINs |
128 | $join_conds['ores_damaging_mdl'][0] = 'INNER JOIN'; |
129 | $join_conds['ores_damaging_cls'][0] = 'INNER JOIN'; |
130 | if ( self::shouldStraightJoin( $specialClassName ) ) { |
131 | $query_options[] = 'STRAIGHT_JOIN'; |
132 | } |
133 | } |
134 | }, |
135 | ] ); |
136 | |
137 | $newDamagingGroup->conflictsWith( |
138 | $logFilter, |
139 | 'ores-rcfilters-ores-conflicts-logactions-global', |
140 | 'ores-rcfilters-damaging-conflicts-logactions', |
141 | 'ores-rcfilters-logactions-conflicts-ores' |
142 | ); |
143 | |
144 | if ( isset( $filters[ 'maybebad' ] ) && isset( $filters[ 'likelybad' ] ) ) { |
145 | $newDamagingGroup->getFilter( 'maybebad' )->setAsSupersetOf( |
146 | $newDamagingGroup->getFilter( 'likelybad' ) |
147 | ); |
148 | } |
149 | |
150 | if ( isset( $filters[ 'likelybad' ] ) && isset( $filters[ 'verylikelybad' ] ) ) { |
151 | $newDamagingGroup->getFilter( 'likelybad' )->setAsSupersetOf( |
152 | $newDamagingGroup->getFilter( 'verylikelybad' ) |
153 | ); |
154 | } |
155 | |
156 | // Transitive closure |
157 | if ( isset( $filters[ 'maybebad' ] ) && isset( $filters[ 'verylikelybad' ] ) ) { |
158 | $newDamagingGroup->getFilter( 'maybebad' )->setAsSupersetOf( |
159 | $newDamagingGroup->getFilter( 'verylikelybad' ) |
160 | ); |
161 | } |
162 | |
163 | if ( $damagingDefault ) { |
164 | $newDamagingGroup->setDefault( Helpers::getDamagingLevelPreference( $clsp->getUser(), |
165 | $clsp->getPageTitle() ) ); |
166 | } |
167 | |
168 | if ( $highlightDefault ) { |
169 | $levelsColors = [ |
170 | 'maybebad' => 'c3', |
171 | 'likelybad' => 'c4', |
172 | 'verylikelybad' => 'c5', |
173 | ]; |
174 | |
175 | $prefLevel = Helpers::getDamagingLevelPreference( |
176 | $clsp->getUser(), |
177 | $clsp->getPageTitle() |
178 | ); |
179 | $allLevels = array_keys( $levelsColors ); |
180 | $applicableLevels = array_slice( $allLevels, array_search( $prefLevel, $allLevels ) ); |
181 | $applicableLevels = array_intersect( $applicableLevels, array_keys( $filters ) ); |
182 | |
183 | foreach ( $applicableLevels as $level ) { |
184 | $newDamagingGroup |
185 | ->getFilter( $level ) |
186 | ->setDefaultHighlightColor( $levelsColors[$level] ); |
187 | } |
188 | } |
189 | |
190 | $clsp->registerFilterGroup( $newDamagingGroup ); |
191 | } |
192 | |
193 | // I don't think we need to register a conflict here, since |
194 | // if we're showing non-damaging, that won't conflict with |
195 | // anything. |
196 | $legacyDamagingGroup = new ChangesListBooleanFilterGroup( [ |
197 | 'name' => 'ores', |
198 | 'filters' => [ |
199 | [ |
200 | 'name' => 'hidenondamaging', |
201 | 'showHide' => 'ores-damaging-filter', |
202 | 'isReplacedInStructuredUi' => true, |
203 | 'default' => $damagingDefault, |
204 | 'queryCallable' => function ( $specialClassName, IContextSource $ctx, IDatabase $dbr, &$tables, |
205 | &$fields, &$conds, &$query_options, &$join_conds ) { |
206 | Helpers::hideNonDamagingFilter( $fields, $conds, true, $ctx->getUser(), |
207 | $ctx->getTitle() ); |
208 | // Filter out incompatible types; log actions and external rows are not scorable |
209 | $conds[] = $dbr->expr( 'rc_type', '!=', [ RC_LOG, RC_EXTERNAL ] ); |
210 | // Filter out patrolled edits: the 'r' doesn't appear for them |
211 | $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED; |
212 | // Make the joins INNER JOINs instead of LEFT JOINs |
213 | $join_conds['ores_damaging_mdl'][0] = 'INNER JOIN'; |
214 | $join_conds['ores_damaging_cls'][0] = 'INNER JOIN'; |
215 | if ( self::shouldStraightJoin( $specialClassName ) ) { |
216 | $query_options[] = 'STRAIGHT_JOIN'; |
217 | } |
218 | }, |
219 | ] |
220 | ], |
221 | |
222 | ] ); |
223 | |
224 | $clsp->registerFilterGroup( $legacyDamagingGroup ); |
225 | } |
226 | |
227 | /** |
228 | * @param ChangesListSpecialPage $clsp |
229 | * @param ThresholdLookup $thresholdLookup |
230 | * @param ChangesListFilter $logFilter |
231 | */ |
232 | private static function handleGoodFaith( |
233 | ChangesListSpecialPage $clsp, |
234 | ThresholdLookup $thresholdLookup, |
235 | ChangesListFilter $logFilter |
236 | ) { |
237 | $filters = self::getGoodFaithStructuredFiltersOnChangesList( |
238 | $thresholdLookup->getThresholds( 'goodfaith' ) |
239 | ); |
240 | |
241 | if ( !$filters ) { |
242 | return; |
243 | } |
244 | $goodfaithGroup = new ChangesListStringOptionsFilterGroup( [ |
245 | 'name' => 'goodfaith', |
246 | 'title' => 'ores-rcfilters-goodfaith-title', |
247 | 'whatsThisHeader' => 'ores-rcfilters-goodfaith-whats-this-header', |
248 | 'whatsThisBody' => 'ores-rcfilters-goodfaith-whats-this-body', |
249 | 'whatsThisUrl' => 'https://www.mediawiki.org/wiki/' . |
250 | 'Special:MyLanguage/Help:New_filters_for_edit_review/Quality_and_Intent_Filters', |
251 | 'whatsThisLinkText' => 'ores-rcfilters-whats-this-link-text', |
252 | 'priority' => 1, |
253 | 'filters' => array_values( $filters ), |
254 | 'default' => ChangesListStringOptionsFilterGroup::NONE, |
255 | 'isFullCoverage' => false, |
256 | 'queryCallable' => function ( $specialClassName, $ctx, IDatabase $dbr, &$tables, &$fields, |
257 | &$conds, &$query_options, &$join_conds, $selectedValues ) { |
258 | $databaseQueryBuilder = ORESServices::getDatabaseQueryBuilder(); |
259 | $condition = $databaseQueryBuilder->buildQuery( |
260 | 'goodfaith', |
261 | $selectedValues |
262 | ); |
263 | if ( $condition ) { |
264 | $conds[] = $condition; |
265 | |
266 | // Filter out incompatible types; log actions and external rows are not scorable |
267 | $conds[] = $dbr->expr( 'rc_type', '!=', [ RC_LOG, RC_EXTERNAL ] ); |
268 | // Make the joins INNER JOINs instead of LEFT JOINs |
269 | $join_conds['ores_goodfaith_mdl'][0] = 'INNER JOIN'; |
270 | $join_conds['ores_goodfaith_cls'][0] = 'INNER JOIN'; |
271 | if ( self::shouldStraightJoin( $specialClassName ) ) { |
272 | $query_options[] = 'STRAIGHT_JOIN'; |
273 | } |
274 | } |
275 | }, |
276 | ] ); |
277 | |
278 | if ( isset( $filters['maybebad'] ) && isset( $filters['likelybad'] ) ) { |
279 | $goodfaithGroup->getFilter( 'maybebad' )->setAsSupersetOf( |
280 | $goodfaithGroup->getFilter( 'likelybad' ) |
281 | ); |
282 | } |
283 | |
284 | if ( isset( $filters['likelybad'] ) && isset( $filters['verylikelybad'] ) ) { |
285 | $goodfaithGroup->getFilter( 'likelybad' )->setAsSupersetOf( |
286 | $goodfaithGroup->getFilter( 'verylikelybad' ) |
287 | ); |
288 | } |
289 | |
290 | if ( isset( $filters['maybebad'] ) && isset( $filters['verylikelybad'] ) ) { |
291 | $goodfaithGroup->getFilter( 'maybebad' )->setAsSupersetOf( |
292 | $goodfaithGroup->getFilter( 'verylikelybad' ) |
293 | ); |
294 | } |
295 | |
296 | $goodfaithGroup->conflictsWith( |
297 | $logFilter, |
298 | 'ores-rcfilters-ores-conflicts-logactions-global', |
299 | 'ores-rcfilters-goodfaith-conflicts-logactions', |
300 | 'ores-rcfilters-logactions-conflicts-ores' |
301 | ); |
302 | |
303 | $clsp->registerFilterGroup( $goodfaithGroup ); |
304 | } |
305 | |
306 | private static function handleRevertrisk( |
307 | ChangesListSpecialPage $clsp, |
308 | $thresholdLookup, |
309 | ChangesListFilter $logFilter |
310 | ) { |
311 | $filters = self::getRevertriskStructuredFiltersOnChangesList( |
312 | $thresholdLookup->getThresholds( 'revertrisklanguageagnostic' ) |
313 | ); |
314 | |
315 | if ( !$filters ) { |
316 | return; |
317 | } |
318 | $revertriskGroup = new ChangesListStringOptionsFilterGroup( [ |
319 | 'name' => 'revertrisklanguageagnostic', |
320 | 'title' => 'ores-rcfilters-revertrisklanguageagnostic-title', |
321 | 'whatsThisHeader' => 'ores-rcfilters-revertrisklanguageagnostic-whats-this-header', |
322 | 'whatsThisBody' => 'ores-rcfilters-revertrisklanguageagnostic-whats-this-body', |
323 | 'whatsThisUrl' => 'https://www.mediawiki.org/wiki/' . |
324 | 'Special:MyLanguage/Help:New_filters_for_edit_review/Quality_and_Intent_Filters#Revert_risk', |
325 | 'whatsThisLinkText' => 'ores-rcfilters-whats-this-link-text', |
326 | 'priority' => 1, |
327 | 'filters' => array_values( $filters ), |
328 | 'default' => ChangesListStringOptionsFilterGroup::NONE, |
329 | 'isFullCoverage' => false, |
330 | 'queryCallable' => function ( |
331 | $specialClassName, |
332 | $ctx, |
333 | IDatabase $dbr, |
334 | &$tables, |
335 | &$fields, |
336 | &$conds, |
337 | &$query_options, |
338 | &$join_conds, |
339 | $selectedValues ) { |
340 | $databaseQueryBuilder = ORESServices::getDatabaseQueryBuilder(); |
341 | $condition = $databaseQueryBuilder->buildQuery( |
342 | 'revertrisklanguageagnostic', |
343 | $selectedValues |
344 | ); |
345 | if ( $condition ) { |
346 | $conds[] = $condition; |
347 | |
348 | // Filter out incompatible types; log actions and external rows are not scorable |
349 | $conds[] = $dbr->expr( 'rc_type', '!=', [ RC_LOG, RC_EXTERNAL ] ); |
350 | // Make the joins INNER JOINs instead of LEFT JOINs |
351 | $join_conds['ores_revertrisklanguageagnostic_mdl'][0] = 'INNER JOIN'; |
352 | $join_conds['ores_revertrisklanguageagnostic_cls'][0] = 'INNER JOIN'; |
353 | if ( self::shouldStraightJoin( $specialClassName ) ) { |
354 | $query_options[] = 'STRAIGHT_JOIN'; |
355 | } |
356 | } |
357 | }, |
358 | ] ); |
359 | |
360 | $revertriskGroup->conflictsWith( |
361 | $logFilter, |
362 | 'ores-rcfilters-ores-conflicts-logactions-global', |
363 | 'ores-rcfilters-revertrisklanguageagnosticconflicts-logactions', |
364 | 'ores-rcfilters-logactions-conflicts-ores' |
365 | ); |
366 | |
367 | $clsp->registerFilterGroup( $revertriskGroup ); |
368 | } |
369 | |
370 | private static function shouldStraightJoin( $specialClassName ) { |
371 | // Performance hack: add STRAIGHT_JOIN (T146111) but not for Watchlist (T176456 / T164796) |
372 | // New theory is that STRAIGHT JOIN should be used for unfiltered queries (RecentChanges) |
373 | // but not for filtered queries (Watchlist and RecentChangesLinked) (T179718) |
374 | return $specialClassName === 'SpecialRecentChanges'; |
375 | } |
376 | |
377 | private static function getDamagingStructuredFiltersOnChangesList( array $damagingLevels ) { |
378 | $filters = []; |
379 | if ( isset( $damagingLevels[ 'likelygood' ] ) ) { |
380 | $filters[ 'likelygood' ] = [ |
381 | 'name' => 'likelygood', |
382 | 'label' => 'ores-rcfilters-damaging-likelygood-label', |
383 | 'description' => 'ores-rcfilters-damaging-likelygood-desc', |
384 | 'cssClassSuffix' => 'damaging-likelygood', |
385 | 'isRowApplicableCallable' => self::makeApplicableCallback( |
386 | 'damaging', |
387 | $damagingLevels['likelygood'] |
388 | ), |
389 | ]; |
390 | } |
391 | if ( isset( $damagingLevels[ 'maybebad' ] ) ) { |
392 | $filters[ 'maybebad' ] = [ |
393 | 'name' => 'maybebad', |
394 | 'label' => 'ores-rcfilters-damaging-maybebad-label', |
395 | 'description' => 'ores-rcfilters-damaging-maybebad-desc', |
396 | 'cssClassSuffix' => 'damaging-maybebad', |
397 | 'isRowApplicableCallable' => self::makeApplicableCallback( |
398 | 'damaging', |
399 | $damagingLevels['maybebad'] |
400 | ), |
401 | ]; |
402 | } |
403 | if ( isset( $damagingLevels[ 'likelybad' ] ) ) { |
404 | $descMsg = isset( $filters[ 'maybebad' ] ) ? |
405 | 'ores-rcfilters-damaging-likelybad-desc-low' : |
406 | 'ores-rcfilters-damaging-likelybad-desc-high'; |
407 | $filters[ 'likelybad' ] = [ |
408 | 'name' => 'likelybad', |
409 | 'label' => 'ores-rcfilters-damaging-likelybad-label', |
410 | 'description' => $descMsg, |
411 | 'cssClassSuffix' => 'damaging-likelybad', |
412 | 'isRowApplicableCallable' => self::makeApplicableCallback( |
413 | 'damaging', |
414 | $damagingLevels['likelybad'] |
415 | ), |
416 | ]; |
417 | } |
418 | if ( isset( $damagingLevels[ 'verylikelybad' ] ) ) { |
419 | $filters[ 'verylikelybad' ] = [ |
420 | 'name' => 'verylikelybad', |
421 | 'label' => 'ores-rcfilters-damaging-verylikelybad-label', |
422 | 'description' => 'ores-rcfilters-damaging-verylikelybad-desc', |
423 | 'cssClassSuffix' => 'damaging-verylikelybad', |
424 | 'isRowApplicableCallable' => self::makeApplicableCallback( |
425 | 'damaging', |
426 | $damagingLevels['verylikelybad'] |
427 | ), |
428 | ]; |
429 | } |
430 | return $filters; |
431 | } |
432 | |
433 | private static function getGoodFaithStructuredFiltersOnChangesList( array $goodfaithLevels ) { |
434 | $filters = []; |
435 | if ( isset( $goodfaithLevels['likelygood'] ) ) { |
436 | $filters[ 'likelygood' ] = [ |
437 | 'name' => 'likelygood', |
438 | 'label' => 'ores-rcfilters-goodfaith-good-label', |
439 | 'description' => 'ores-rcfilters-goodfaith-good-desc', |
440 | 'cssClassSuffix' => 'goodfaith-good', |
441 | 'isRowApplicableCallable' => self::makeApplicableCallback( |
442 | 'goodfaith', |
443 | $goodfaithLevels['likelygood'] |
444 | ), |
445 | ]; |
446 | } |
447 | if ( isset( $goodfaithLevels['maybebad'] ) ) { |
448 | $filters[ 'maybebad' ] = [ |
449 | 'name' => 'maybebad', |
450 | 'label' => 'ores-rcfilters-goodfaith-maybebad-label', |
451 | 'description' => 'ores-rcfilters-goodfaith-maybebad-desc', |
452 | 'cssClassSuffix' => 'goodfaith-maybebad', |
453 | 'isRowApplicableCallable' => self::makeApplicableCallback( |
454 | 'goodfaith', |
455 | $goodfaithLevels['maybebad'] |
456 | ), |
457 | ]; |
458 | } |
459 | if ( isset( $goodfaithLevels['likelybad'] ) ) { |
460 | $descMsg = isset( $filters[ 'maybebad' ] ) ? |
461 | 'ores-rcfilters-goodfaith-bad-desc-low' : |
462 | 'ores-rcfilters-goodfaith-bad-desc-high'; |
463 | $filters[ 'likelybad' ] = [ |
464 | 'name' => 'likelybad', |
465 | 'label' => 'ores-rcfilters-goodfaith-bad-label', |
466 | 'description' => $descMsg, |
467 | 'cssClassSuffix' => 'goodfaith-bad', |
468 | 'isRowApplicableCallable' => self::makeApplicableCallback( |
469 | 'goodfaith', |
470 | $goodfaithLevels['likelybad'] |
471 | ), |
472 | ]; |
473 | } |
474 | if ( isset( $goodfaithLevels['verylikelybad'] ) ) { |
475 | $filters[ 'verylikelybad' ] = [ |
476 | 'name' => 'verylikelybad', |
477 | 'label' => 'ores-rcfilters-goodfaith-verylikelybad-label', |
478 | 'description' => 'ores-rcfilters-goodfaith-verylikelybad-desc', |
479 | 'cssClassSuffix' => 'goodfaith-verylikelybad', |
480 | 'isRowApplicableCallable' => self::makeApplicableCallback( |
481 | 'goodfaith', |
482 | $goodfaithLevels['verylikelybad'] |
483 | ), |
484 | ]; |
485 | } |
486 | |
487 | return $filters; |
488 | } |
489 | |
490 | private static function getRevertriskStructuredFiltersOnChangesList( array $revertriskLevels ) { |
491 | $filters = []; |
492 | if ( isset( $revertriskLevels['revertrisk'] ) ) { |
493 | $filters[ 'revertrisk' ] = [ |
494 | 'name' => 'revertrisk', |
495 | 'label' => 'ores-rcfilters-revertrisklanguageagnostic-revertrisk-label', |
496 | 'description' => 'ores-rcfilters-revertrisklanguageagnostic-revertrisk-desc', |
497 | 'cssClassSuffix' => 'revertrisklanguageagnostic-revertrisk', |
498 | 'isRowApplicableCallable' => self::makeApplicableCallback( |
499 | 'revertrisk', |
500 | $revertriskLevels['revertrisk'] |
501 | ), |
502 | ]; |
503 | } |
504 | |
505 | return $filters; |
506 | } |
507 | |
508 | public function onChangesListSpecialPageQuery( |
509 | $name, &$tables, &$fields, |
510 | &$conds, &$query_options, &$join_conds, $opts |
511 | ) { |
512 | if ( !Helpers::oresUiEnabled() ) { |
513 | return; |
514 | } |
515 | try { |
516 | if ( Helpers::isModelEnabled( 'damaging' ) ) { |
517 | Helpers::joinWithOresTables( 'damaging', 'rc_this_oldid', $tables, $fields, |
518 | $join_conds ); |
519 | } |
520 | if ( Helpers::isModelEnabled( 'goodfaith' ) ) { |
521 | Helpers::joinWithOresTables( 'goodfaith', 'rc_this_oldid', $tables, $fields, |
522 | $join_conds ); |
523 | } |
524 | if ( Helpers::isModelEnabled( 'revertrisklanguageagnostic' ) ) { |
525 | Helpers::joinWithOresTables( 'revertrisklanguageagnostic', 'rc_this_oldid', $tables, $fields, |
526 | $join_conds ); |
527 | } |
528 | } catch ( Exception $exception ) { |
529 | return; |
530 | } |
531 | } |
532 | |
533 | /** |
534 | * Label recent changes with ORES scores (for each change in an expanded group) |
535 | * |
536 | * @param EnhancedChangesList $ecl |
537 | * @param array &$data |
538 | * @param RecentChange[] $block |
539 | * @param RecentChange $rcObj |
540 | * @param string[] &$classes |
541 | * @param string[] &$attribs |
542 | */ |
543 | public function onEnhancedChangesListModifyLineData( |
544 | $ecl, |
545 | &$data, |
546 | $block, |
547 | $rcObj, |
548 | &$classes, |
549 | &$attribs |
550 | ) { |
551 | if ( !Helpers::oresUiEnabled() ) { |
552 | return; |
553 | } |
554 | |
555 | self::processRecentChangesList( $rcObj, $data, $classes, $ecl->getContext() ); |
556 | } |
557 | |
558 | /** |
559 | * Label recent changes with ORES scores (for top-level ungrouped lines) |
560 | * |
561 | * @param EnhancedChangesList $ecl |
562 | * @param array &$data |
563 | * @param RecentChange $rcObj |
564 | */ |
565 | public function onEnhancedChangesListModifyBlockLineData( |
566 | $ecl, |
567 | &$data, |
568 | $rcObj |
569 | ) { |
570 | if ( !Helpers::oresUiEnabled() ) { |
571 | return; |
572 | } |
573 | |
574 | $classes = []; |
575 | self::processRecentChangesList( $rcObj, $data, $classes, $ecl->getContext() ); |
576 | $data['attribs']['class'] = array_merge( $data['attribs']['class'], $classes ); |
577 | } |
578 | |
579 | /** |
580 | * Internal helper to label matching rows |
581 | * |
582 | * @param RecentChange $rcObj |
583 | * @param string[] &$data |
584 | * @param string[] &$classes |
585 | * @param IContextSource $context |
586 | */ |
587 | protected static function processRecentChangesList( |
588 | RecentChange $rcObj, |
589 | array &$data, |
590 | array &$classes, |
591 | IContextSource $context |
592 | ) { |
593 | $damaging = self::getScoreRecentChangesList( $rcObj, $context ); |
594 | |
595 | if ( $damaging && Helpers::isDamagingFlagEnabled( $context ) ) { |
596 | $classes[] = 'damaging'; |
597 | $data['recentChangesFlags']['damaging'] = true; |
598 | } |
599 | } |
600 | |
601 | /** |
602 | * Hook for formatting recent changes links |
603 | * @see https://www.mediawiki.org/wiki/Manual:Hooks/OldChangesListRecentChangesLine |
604 | * |
605 | * @param ChangesList $changesList |
606 | * @param string &$s |
607 | * @param RecentChange $rc |
608 | * @param string[] &$classes |
609 | * @param string[] &$attribs |
610 | * @return bool|void |
611 | */ |
612 | public function onOldChangesListRecentChangesLine( |
613 | $changesList, |
614 | &$s, |
615 | $rc, |
616 | &$classes, |
617 | &$attribs |
618 | ) { |
619 | if ( !Helpers::oresUiEnabled() ) { |
620 | return; |
621 | } |
622 | |
623 | $damaging = self::getScoreRecentChangesList( $rc, $changesList->getContext() ); |
624 | if ( $damaging ) { |
625 | // Add highlight class |
626 | if ( Helpers::isHighlightEnabled( $changesList ) ) { |
627 | $classes[] = 'ores-highlight'; |
628 | } |
629 | |
630 | // Add damaging class and flag |
631 | if ( Helpers::isDamagingFlagEnabled( $changesList ) ) { |
632 | $classes[] = 'damaging'; |
633 | |
634 | $separator = ' <span class="mw-changeslist-separator"></span> '; |
635 | if ( strpos( $s, $separator ) === false ) { |
636 | return; |
637 | } |
638 | |
639 | $parts = explode( $separator, $s ); |
640 | $parts[1] = ChangesList::flag( 'damaging' ) . $parts[1]; |
641 | $s = implode( $separator, $parts ); |
642 | } |
643 | } |
644 | } |
645 | |
646 | /** |
647 | * Check if we should flag a row. As a side effect, also adds score data for this row. |
648 | * @param RecentChange $rcObj |
649 | * @param IContextSource $context |
650 | * @return bool |
651 | */ |
652 | public static function getScoreRecentChangesList( RecentChange $rcObj, IContextSource $context ) { |
653 | $threshold = $rcObj->getAttribute( 'ores_damaging_threshold' ); |
654 | if ( $threshold === null ) { |
655 | try { |
656 | $threshold = |
657 | Helpers::getThreshold( 'damaging', $context->getUser(), $context->getTitle() ); |
658 | } catch ( Exception $exception ) { |
659 | return false; |
660 | } |
661 | |
662 | } |
663 | $score = $rcObj->getAttribute( 'ores_damaging_score' ); |
664 | $patrolled = $rcObj->getAttribute( 'rc_patrolled' ); |
665 | $type = $rcObj->getAttribute( 'rc_type' ); |
666 | |
667 | // Log actions and external rows are not scorable; if such a row does have a score, ignore it |
668 | if ( !$score || $threshold === null || in_array( $type, [ RC_LOG, RC_EXTERNAL ] ) ) { |
669 | // Shorten out |
670 | return false; |
671 | } |
672 | |
673 | $score = (float)$score; |
674 | Helpers::addRowData( $context, $rcObj->getAttribute( 'rc_this_oldid' ), $score, |
675 | 'damaging' ); |
676 | |
677 | return $score >= $threshold && !$patrolled; |
678 | } |
679 | |
680 | private static function makeApplicableCallback( $model, array $levelData ) { |
681 | return static function ( $ctx, RecentChange $rc ) use ( $model, $levelData ) { |
682 | $score = $rc->getAttribute( "ores_{$model}_score" ); |
683 | $type = $rc->getAttribute( 'rc_type' ); |
684 | // Log actions and external rows are not scorable; if such a row does have a score, ignore it |
685 | if ( $score === null || in_array( $type, [ RC_LOG, RC_EXTERNAL ] ) ) { |
686 | return false; |
687 | } |
688 | return $levelData['min'] <= $score && $score <= $levelData['max']; |
689 | }; |
690 | } |
691 | |
692 | } |