Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
12.05% |
47 / 390 |
|
0.00% |
0 / 15 |
CRAP | |
0.00% |
0 / 1 |
ChangesListHooksHandler | |
12.05% |
47 / 390 |
|
0.00% |
0 / 15 |
4544.33 | |
0.00% |
0 / 1 |
onChangesListSpecialPageStructuredFilters | |
60.00% |
9 / 15 |
|
0.00% |
0 / 1 |
8.30 | |||
handleDamaging | |
0.00% |
0 / 107 |
|
0.00% |
0 / 1 |
272 | |||
handleGoodFaith | |
0.00% |
0 / 54 |
|
0.00% |
0 / 1 |
110 | |||
handleRevertrisk | |
0.00% |
0 / 42 |
|
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 | |
92.31% |
12 / 13 |
|
0.00% |
0 / 1 |
6.02 | |||
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 MediaWiki\Context\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\IReadableDatabase; |
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, |
116 | IReadableDatabase $dbr, |
117 | &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedValues |
118 | ) { |
119 | $databaseQueryBuilder = ORESServices::getDatabaseQueryBuilder(); |
120 | $condition = $databaseQueryBuilder->buildQuery( |
121 | 'damaging', |
122 | $selectedValues |
123 | ); |
124 | if ( $condition ) { |
125 | $conds[] = $condition; |
126 | |
127 | // Filter out incompatible types; log actions and external rows are not scorable |
128 | $conds[] = $dbr->expr( 'rc_type', '!=', [ RC_LOG, RC_EXTERNAL ] ); |
129 | // Make the joins INNER JOINs instead of LEFT JOINs |
130 | $join_conds['ores_damaging_mdl'][0] = 'INNER JOIN'; |
131 | $join_conds['ores_damaging_cls'][0] = 'INNER JOIN'; |
132 | if ( self::shouldStraightJoin( $specialClassName ) ) { |
133 | $query_options[] = 'STRAIGHT_JOIN'; |
134 | } |
135 | } |
136 | }, |
137 | ] ); |
138 | |
139 | $newDamagingGroup->conflictsWith( |
140 | $logFilter, |
141 | 'ores-rcfilters-ores-conflicts-logactions-global', |
142 | 'ores-rcfilters-damaging-conflicts-logactions', |
143 | 'ores-rcfilters-logactions-conflicts-ores' |
144 | ); |
145 | |
146 | if ( isset( $filters[ 'maybebad' ] ) && isset( $filters[ 'likelybad' ] ) ) { |
147 | $newDamagingGroup->getFilter( 'maybebad' )->setAsSupersetOf( |
148 | $newDamagingGroup->getFilter( 'likelybad' ) |
149 | ); |
150 | } |
151 | |
152 | if ( isset( $filters[ 'likelybad' ] ) && isset( $filters[ 'verylikelybad' ] ) ) { |
153 | $newDamagingGroup->getFilter( 'likelybad' )->setAsSupersetOf( |
154 | $newDamagingGroup->getFilter( 'verylikelybad' ) |
155 | ); |
156 | } |
157 | |
158 | // Transitive closure |
159 | if ( isset( $filters[ 'maybebad' ] ) && isset( $filters[ 'verylikelybad' ] ) ) { |
160 | $newDamagingGroup->getFilter( 'maybebad' )->setAsSupersetOf( |
161 | $newDamagingGroup->getFilter( 'verylikelybad' ) |
162 | ); |
163 | } |
164 | |
165 | if ( $damagingDefault ) { |
166 | $newDamagingGroup->setDefault( Helpers::getDamagingLevelPreference( $clsp->getUser(), |
167 | $clsp->getPageTitle() ) ); |
168 | } |
169 | |
170 | if ( $highlightDefault ) { |
171 | $levelsColors = [ |
172 | 'maybebad' => 'c3', |
173 | 'likelybad' => 'c4', |
174 | 'verylikelybad' => 'c5', |
175 | ]; |
176 | |
177 | $prefLevel = Helpers::getDamagingLevelPreference( |
178 | $clsp->getUser(), |
179 | $clsp->getPageTitle() |
180 | ); |
181 | $allLevels = array_keys( $levelsColors ); |
182 | $applicableLevels = array_slice( $allLevels, array_search( $prefLevel, $allLevels ) ); |
183 | $applicableLevels = array_intersect( $applicableLevels, array_keys( $filters ) ); |
184 | |
185 | foreach ( $applicableLevels as $level ) { |
186 | $newDamagingGroup |
187 | ->getFilter( $level ) |
188 | ->setDefaultHighlightColor( $levelsColors[$level] ); |
189 | } |
190 | } |
191 | |
192 | $clsp->registerFilterGroup( $newDamagingGroup ); |
193 | } |
194 | |
195 | // I don't think we need to register a conflict here, since |
196 | // if we're showing non-damaging, that won't conflict with |
197 | // anything. |
198 | $legacyDamagingGroup = new ChangesListBooleanFilterGroup( [ |
199 | 'name' => 'ores', |
200 | 'filters' => [ |
201 | [ |
202 | 'name' => 'hidenondamaging', |
203 | 'showHide' => 'ores-damaging-filter', |
204 | 'isReplacedInStructuredUi' => true, |
205 | 'default' => $damagingDefault, |
206 | 'queryCallable' => function ( $specialClassName, IContextSource $ctx, |
207 | IReadableDatabase $dbr, |
208 | &$tables, &$fields, &$conds, &$query_options, &$join_conds |
209 | ) { |
210 | Helpers::hideNonDamagingFilter( $fields, $conds, true, $ctx->getUser(), |
211 | $ctx->getTitle() ); |
212 | // Filter out incompatible types; log actions and external rows are not scorable |
213 | $conds[] = $dbr->expr( 'rc_type', '!=', [ RC_LOG, RC_EXTERNAL ] ); |
214 | // Filter out patrolled edits: the 'r' doesn't appear for them |
215 | $conds['rc_patrolled'] = RecentChange::PRC_UNPATROLLED; |
216 | // Make the joins INNER JOINs instead of LEFT JOINs |
217 | $join_conds['ores_damaging_mdl'][0] = 'INNER JOIN'; |
218 | $join_conds['ores_damaging_cls'][0] = 'INNER JOIN'; |
219 | if ( self::shouldStraightJoin( $specialClassName ) ) { |
220 | $query_options[] = 'STRAIGHT_JOIN'; |
221 | } |
222 | }, |
223 | ] |
224 | ], |
225 | |
226 | ] ); |
227 | |
228 | $clsp->registerFilterGroup( $legacyDamagingGroup ); |
229 | } |
230 | |
231 | /** |
232 | * @param ChangesListSpecialPage $clsp |
233 | * @param ThresholdLookup $thresholdLookup |
234 | * @param ChangesListFilter $logFilter |
235 | */ |
236 | private static function handleGoodFaith( |
237 | ChangesListSpecialPage $clsp, |
238 | ThresholdLookup $thresholdLookup, |
239 | ChangesListFilter $logFilter |
240 | ) { |
241 | $filters = self::getGoodFaithStructuredFiltersOnChangesList( |
242 | $thresholdLookup->getThresholds( 'goodfaith' ) |
243 | ); |
244 | |
245 | if ( !$filters ) { |
246 | return; |
247 | } |
248 | $goodfaithGroup = new ChangesListStringOptionsFilterGroup( [ |
249 | 'name' => 'goodfaith', |
250 | 'title' => 'ores-rcfilters-goodfaith-title', |
251 | 'whatsThisHeader' => 'ores-rcfilters-goodfaith-whats-this-header', |
252 | 'whatsThisBody' => 'ores-rcfilters-goodfaith-whats-this-body', |
253 | 'whatsThisUrl' => 'https://www.mediawiki.org/wiki/' . |
254 | 'Special:MyLanguage/Help:New_filters_for_edit_review/Quality_and_Intent_Filters', |
255 | 'whatsThisLinkText' => 'ores-rcfilters-whats-this-link-text', |
256 | 'priority' => 1, |
257 | 'filters' => array_values( $filters ), |
258 | 'default' => ChangesListStringOptionsFilterGroup::NONE, |
259 | 'isFullCoverage' => false, |
260 | 'queryCallable' => function ( $specialClassName, $ctx, |
261 | IReadableDatabase $dbr, |
262 | &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedValues |
263 | ) { |
264 | $databaseQueryBuilder = ORESServices::getDatabaseQueryBuilder(); |
265 | $condition = $databaseQueryBuilder->buildQuery( |
266 | 'goodfaith', |
267 | $selectedValues |
268 | ); |
269 | if ( $condition ) { |
270 | $conds[] = $condition; |
271 | |
272 | // Filter out incompatible types; log actions and external rows are not scorable |
273 | $conds[] = $dbr->expr( 'rc_type', '!=', [ RC_LOG, RC_EXTERNAL ] ); |
274 | // Make the joins INNER JOINs instead of LEFT JOINs |
275 | $join_conds['ores_goodfaith_mdl'][0] = 'INNER JOIN'; |
276 | $join_conds['ores_goodfaith_cls'][0] = 'INNER JOIN'; |
277 | if ( self::shouldStraightJoin( $specialClassName ) ) { |
278 | $query_options[] = 'STRAIGHT_JOIN'; |
279 | } |
280 | } |
281 | }, |
282 | ] ); |
283 | |
284 | if ( isset( $filters['maybebad'] ) && isset( $filters['likelybad'] ) ) { |
285 | $goodfaithGroup->getFilter( 'maybebad' )->setAsSupersetOf( |
286 | $goodfaithGroup->getFilter( 'likelybad' ) |
287 | ); |
288 | } |
289 | |
290 | if ( isset( $filters['likelybad'] ) && isset( $filters['verylikelybad'] ) ) { |
291 | $goodfaithGroup->getFilter( 'likelybad' )->setAsSupersetOf( |
292 | $goodfaithGroup->getFilter( 'verylikelybad' ) |
293 | ); |
294 | } |
295 | |
296 | if ( isset( $filters['maybebad'] ) && isset( $filters['verylikelybad'] ) ) { |
297 | $goodfaithGroup->getFilter( 'maybebad' )->setAsSupersetOf( |
298 | $goodfaithGroup->getFilter( 'verylikelybad' ) |
299 | ); |
300 | } |
301 | |
302 | $goodfaithGroup->conflictsWith( |
303 | $logFilter, |
304 | 'ores-rcfilters-ores-conflicts-logactions-global', |
305 | 'ores-rcfilters-goodfaith-conflicts-logactions', |
306 | 'ores-rcfilters-logactions-conflicts-ores' |
307 | ); |
308 | |
309 | $clsp->registerFilterGroup( $goodfaithGroup ); |
310 | } |
311 | |
312 | private static function handleRevertrisk( |
313 | ChangesListSpecialPage $clsp, |
314 | ThresholdLookup $thresholdLookup, |
315 | ChangesListFilter $logFilter |
316 | ) { |
317 | $filters = self::getRevertriskStructuredFiltersOnChangesList( |
318 | $thresholdLookup->getThresholds( 'revertrisklanguageagnostic' ) |
319 | ); |
320 | |
321 | if ( !$filters ) { |
322 | return; |
323 | } |
324 | $revertriskGroup = new ChangesListStringOptionsFilterGroup( [ |
325 | 'name' => 'revertrisklanguageagnostic', |
326 | 'title' => 'ores-rcfilters-revertrisklanguageagnostic-title', |
327 | 'whatsThisHeader' => 'ores-rcfilters-revertrisklanguageagnostic-whats-this-header', |
328 | 'whatsThisBody' => 'ores-rcfilters-revertrisklanguageagnostic-whats-this-body', |
329 | 'whatsThisUrl' => 'https://www.mediawiki.org/wiki/' . |
330 | 'Special:MyLanguage/Help:New_filters_for_edit_review/Quality_and_Intent_Filters#Revert_risk', |
331 | 'whatsThisLinkText' => 'ores-rcfilters-whats-this-link-text', |
332 | 'priority' => 1, |
333 | 'filters' => array_values( $filters ), |
334 | 'default' => ChangesListStringOptionsFilterGroup::NONE, |
335 | 'isFullCoverage' => false, |
336 | 'queryCallable' => function ( $specialClassName, $ctx, |
337 | IReadableDatabase $dbr, |
338 | &$tables, &$fields, &$conds, &$query_options, &$join_conds, $selectedValues |
339 | ) { |
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 ): array { |
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 ): array { |
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 ): array { |
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 | $pos = strpos( $s, $separator ); |
636 | if ( $pos !== false ) { |
637 | $pos += strlen( $separator ); |
638 | $s = substr_replace( $s, ChangesList::flag( 'damaging' ), $pos, 0 ); |
639 | } |
640 | } |
641 | } |
642 | } |
643 | |
644 | /** |
645 | * Check if we should flag a row. As a side effect, also adds score data for this row. |
646 | * @param RecentChange $rcObj |
647 | * @param IContextSource $context |
648 | * @return bool |
649 | */ |
650 | public static function getScoreRecentChangesList( RecentChange $rcObj, IContextSource $context ) { |
651 | $threshold = $rcObj->getAttribute( 'ores_damaging_threshold' ); |
652 | if ( $threshold === null ) { |
653 | try { |
654 | $threshold = |
655 | Helpers::getThreshold( 'damaging', $context->getUser(), $context->getTitle() ); |
656 | } catch ( Exception $exception ) { |
657 | return false; |
658 | } |
659 | |
660 | } |
661 | $score = $rcObj->getAttribute( 'ores_damaging_score' ); |
662 | $patrolled = $rcObj->getAttribute( 'rc_patrolled' ); |
663 | $type = $rcObj->getAttribute( 'rc_type' ); |
664 | |
665 | // Log actions and external rows are not scorable; if such a row does have a score, ignore it |
666 | if ( !$score || $threshold === null || in_array( $type, [ RC_LOG, RC_EXTERNAL ] ) ) { |
667 | // Shorten out |
668 | return false; |
669 | } |
670 | |
671 | $score = (float)$score; |
672 | Helpers::addRowData( $context, $rcObj->getAttribute( 'rc_this_oldid' ), $score, |
673 | 'damaging' ); |
674 | |
675 | return $score >= $threshold && !$patrolled; |
676 | } |
677 | |
678 | private static function makeApplicableCallback( string $model, array $levelData ) { |
679 | return static function ( $ctx, RecentChange $rc ) use ( $model, $levelData ) { |
680 | $score = $rc->getAttribute( "ores_{$model}_score" ); |
681 | $type = $rc->getAttribute( 'rc_type' ); |
682 | // Log actions and external rows are not scorable; if such a row does have a score, ignore it |
683 | if ( $score === null || in_array( $type, [ RC_LOG, RC_EXTERNAL ] ) ) { |
684 | return false; |
685 | } |
686 | return $levelData['min'] <= $score && $score <= $levelData['max']; |
687 | }; |
688 | } |
689 | |
690 | } |