jenkins-bot has submitted this change and it was merged. ( 
https://gerrit.wikimedia.org/r/392415 )

Change subject: Move Contributions and ChangesList hook handlers to their 
dedicated files
......................................................................


Move Contributions and ChangesList hook handlers to their dedicated files

It's just moving the code around and makes the Hook class bearably clean

Change-Id: I35247553444d595330f149c6c5b1f29165ee1e40
---
M extension.json
M includes/Hooks.php
A includes/Hooks/ChangesListHooksHandler.php
A includes/Hooks/ContributionsHooksHandler.php
A tests/phpunit/includes/Hooks/ChangesListHooksHandlerTest.php
A tests/phpunit/includes/Hooks/ContributionsHookHandlerTest.php
M tests/phpunit/includes/HooksTest.php
7 files changed, 1,339 insertions(+), 1,223 deletions(-)

Approvals:
  jenkins-bot: Verified
  Awight: Looks good to me, approved



diff --git a/extension.json b/extension.json
index c3ce04a..63f226b 100644
--- a/extension.json
+++ b/extension.json
@@ -13,6 +13,8 @@
                "ORES\\Cache": "includes/Cache.php",
                "ORES\\Hooks": "includes/Hooks.php",
                "ORES\\Hooks\\ApiHooksHandler": 
"includes/Hooks/ApiHooksHandler.php",
+               "ORES\\Hooks\\ChangesListHooksHandler": 
"includes/Hooks/ChangesListHooksHandler.php",
+               "ORES\\Hooks\\ContributionsHooksHandler": 
"includes/Hooks/ContributionsHooksHandler.php",
                "ORES\\Hooks\\PreferencesHookHandler": 
"includes/Hooks/PreferencesHookHandler.php",
                "ORES\\FetchScoreJob": "includes/FetchScoreJob.php",
                "ORES\\Range": "includes/Range.php",
@@ -50,19 +52,19 @@
                        "ORES\\Hooks::onBeforePageDisplay"
                ],
                "ChangesListSpecialPageStructuredFilters": [
-                       "ORES\\Hooks::onChangesListSpecialPageStructuredFilters"
+                       
"ORES\\Hooks\\ChangesListHooksHandler::onChangesListSpecialPageStructuredFilters"
                ],
                "ChangesListSpecialPageQuery": [
-                       "ORES\\Hooks::onChangesListSpecialPageQuery"
+                       
"ORES\\Hooks\\ChangesListHooksHandler::onChangesListSpecialPageQuery"
                ],
                "ContribsPager::getQueryInfo": [
-                       "ORES\\Hooks::onContribsGetQueryInfo"
+                       
"ORES\\Hooks\\ContributionsHooksHandler::onContribsGetQueryInfo"
                ],
                "EnhancedChangesListModifyBlockLineData": [
-                       "ORES\\Hooks::onEnhancedChangesListModifyBlockLineData"
+                       
"ORES\\Hooks\\ChangesListHooksHandler::onEnhancedChangesListModifyBlockLineData"
                ],
                "EnhancedChangesListModifyLineData": [
-                       "ORES\\Hooks::onEnhancedChangesListModifyLineData"
+                       
"ORES\\Hooks\\ChangesListHooksHandler::onEnhancedChangesListModifyLineData"
                ],
                "GetBetaFeaturePreferences": [
                        "ORES\\Hooks::onGetBetaFeaturePreferences"
@@ -74,7 +76,7 @@
                        "ORES\\Hooks::onLoadExtensionSchemaUpdates"
                ],
                "OldChangesListRecentChangesLine": [
-                       "ORES\\Hooks::onOldChangesListRecentChangesLine"
+                       
"ORES\\Hooks\\ChangesListHooksHandler::onOldChangesListRecentChangesLine"
                ],
                "RecentChange_save": [
                        "ORES\\Hooks::onRecentChange_save"
@@ -83,13 +85,13 @@
                        "ORES\\Hooks::onRecentChangesPurgeRows"
                ],
                "SpecialContributions::formatRow::flags": [
-                       "ORES\\Hooks::onSpecialContributionsFormatRowFlags"
+                       
"ORES\\Hooks\\ContributionsHooksHandler::onSpecialContributionsFormatRowFlags"
                ],
                "ContributionsLineEnding": [
-                       "ORES\\Hooks::onContributionsLineEnding"
+                       
"ORES\\Hooks\\ContributionsHooksHandler::onContributionsLineEnding"
                ],
                "SpecialContributions::getForm::filters": [
-                       "ORES\\Hooks::onSpecialContributionsGetFormFilters"
+                       
"ORES\\Hooks\\ContributionsHooksHandler::onSpecialContributionsGetFormFilters"
                ]
        },
        "ResourceFileModulePaths": {
diff --git a/includes/Hooks.php b/includes/Hooks.php
index b0d53aa..78c71c3 100644
--- a/includes/Hooks.php
+++ b/includes/Hooks.php
@@ -3,29 +3,18 @@
 namespace ORES;
 
 use BetaFeatures;
-use ChangesList;
-use ChangesListBooleanFilterGroup;
-use ChangesListSpecialPage;
-use ChangesListStringOptionsFilterGroup;
-use ContribsPager;
 use DatabaseUpdater;
-use EnhancedChangesList;
 use Exception;
-use FormOptions;
 use JobQueueGroup;
-use Html;
 use IContextSource;
 use MediaWiki\Logger\LoggerFactory;
 use OutputPage;
-use RCCacheEntry;
 use RecentChange;
 use RequestContext;
 use Skin;
-use SpecialContributions;
 use SpecialRecentChanges;
 use SpecialWatchlist;
 use User;
-use Xml;
 
 class Hooks {
        // The oresDamagingPref preference uses these names for historical 
reasons
@@ -117,601 +106,6 @@
                        $revIds[] = $row->rc_this_oldid;
                }
                Cache::instance()->purgeRows( $revIds );
-       }
-
-       public static function onChangesListSpecialPageStructuredFilters(
-               ChangesListSpecialPage $clsp
-       ) {
-               // ORES is disabled on Recentchangeslinked: T163063
-               if ( !self::oresUiEnabled( $clsp->getUser() ) || 
$clsp->getName() === 'Recentchangeslinked' ) {
-                       return;
-               }
-
-               $stats = Stats::newFromGlobalState();
-
-               $changeTypeGroup = $clsp->getFilterGroup( 'changeType' );
-               $logFilter = $changeTypeGroup->getFilter( 'hidelog' );
-
-               if ( self::isModelEnabled( 'damaging' ) ) {
-                       if ( $clsp instanceof SpecialRecentChanges ) {
-                               $damagingDefault = $clsp->getUser()->getOption( 
'oresRCHideNonDamaging' );
-                       } elseif ( $clsp instanceof SpecialWatchlist ) {
-                               $damagingDefault = $clsp->getUser()->getOption( 
'oresWatchlistHideNonDamaging' );
-                       } else {
-                               $damagingDefault = false;
-                       }
-
-                       $damagingLevels = $stats->getThresholds( 'damaging' );
-                       $filters = [];
-                       if ( isset( $damagingLevels[ 'likelygood' ] ) ) {
-                               $filters[ 'likelygood' ] = [
-                                       'name' => 'likelygood',
-                                       'label' => 
'ores-rcfilters-damaging-likelygood-label',
-                                       'description' => 
'ores-rcfilters-damaging-likelygood-desc',
-                                       'cssClassSuffix' => 
'damaging-likelygood',
-                                       'isRowApplicableCallable' => 
self::makeApplicableCallback(
-                                               'damaging',
-                                               $damagingLevels['likelygood']
-                                       ),
-                               ];
-                       }
-                       if ( isset( $damagingLevels[ 'maybebad' ] ) ) {
-                               $filters[ 'maybebad' ] = [
-                                       'name' => 'maybebad',
-                                       'label' => 
'ores-rcfilters-damaging-maybebad-label',
-                                       'description' => 
'ores-rcfilters-damaging-maybebad-desc',
-                                       'cssClassSuffix' => 'damaging-maybebad',
-                                       'isRowApplicableCallable' => 
self::makeApplicableCallback(
-                                               'damaging',
-                                               $damagingLevels['maybebad']
-                                       ),
-                               ];
-                       }
-                       if ( isset( $damagingLevels[ 'likelybad' ] ) ) {
-                               $descMsg = isset( $filters[ 'maybebad' ] ) ?
-                                       
'ores-rcfilters-damaging-likelybad-desc-low' :
-                                       
'ores-rcfilters-damaging-likelybad-desc-high';
-                               $filters[ 'likelybad' ] = [
-                                       'name' => 'likelybad',
-                                       'label' => 
'ores-rcfilters-damaging-likelybad-label',
-                                       'description' => $descMsg,
-                                       'cssClassSuffix' => 
'damaging-likelybad',
-                                       'isRowApplicableCallable' => 
self::makeApplicableCallback(
-                                               'damaging',
-                                               $damagingLevels['likelybad']
-                                       ),
-                               ];
-                       }
-                       if ( isset( $damagingLevels[ 'verylikelybad' ] ) ) {
-                               $filters[ 'verylikelybad' ] = [
-                                       'name' => 'verylikelybad',
-                                       'label' => 
'ores-rcfilters-damaging-verylikelybad-label',
-                                       'description' => 
'ores-rcfilters-damaging-verylikelybad-desc',
-                                       'cssClassSuffix' => 
'damaging-verylikelybad',
-                                       'isRowApplicableCallable' => 
self::makeApplicableCallback(
-                                               'damaging',
-                                               $damagingLevels['verylikelybad']
-                                       ),
-                               ];
-                       }
-
-                       if ( $filters ) {
-                               $newDamagingGroup = new 
ChangesListStringOptionsFilterGroup( [
-                                       'name' => 'damaging',
-                                       'title' => 
'ores-rcfilters-damaging-title',
-                                       'whatsThisHeader' => 
'ores-rcfilters-damaging-whats-this-header',
-                                       'whatsThisBody' => 
'ores-rcfilters-damaging-whats-this-body',
-                                       'whatsThisUrl' => 
'https://www.mediawiki.org/wiki/' .
-                                               
'Special:MyLanguage/Help:New_filters_for_edit_review/Quality_and_Intent_Filters',
-                                       'whatsThisLinkText' => 
'ores-rcfilters-whats-this-link-text',
-                                       'priority' => 2,
-                                       'filters' => array_values( $filters ),
-                                       'default' => 
ChangesListStringOptionsFilterGroup::NONE,
-                                       'isFullCoverage' => false,
-                                       'queryCallable' => function ( 
$specialClassName, $ctx, $dbr, &$tables, &$fields,
-                                                       &$conds, 
&$query_options, &$join_conds, $selectedValues ) {
-                                               $condition = 
self::buildRangeFilter( 'damaging', $selectedValues );
-                                               if ( $condition ) {
-                                                       $conds[] = $condition;
-
-                                                       // Filter out 
incompatible types; log actions and external rows are not scorable
-                                                       $conds[] = 'rc_type NOT 
IN (' . $dbr->makeList( [ RC_LOG, RC_EXTERNAL ] ) . ')';
-                                                       // Make the joins INNER 
JOINs instead of LEFT JOINs
-                                                       
$join_conds['ores_damaging_mdl'][0] = 'INNER JOIN';
-                                                       
$join_conds['ores_damaging_cls'][0] = 'INNER JOIN';
-                                                       // Performance hack: 
add STRAIGHT_JOIN (T146111) but not for Watchlist (T176456 / T164796)
-                                                       if ( $specialClassName 
!== 'SpecialWatchlist' ) {
-                                                               
$query_options[] = 'STRAIGHT_JOIN';
-                                                       }
-                                               }
-                                       },
-                               ] );
-
-                               $newDamagingGroup->conflictsWith(
-                                       $logFilter,
-                                       
'ores-rcfilters-ores-conflicts-logactions-global',
-                                       
'ores-rcfilters-damaging-conflicts-logactions',
-                                       
'ores-rcfilters-logactions-conflicts-ores'
-                               );
-
-                               if ( isset( $filters[ 'maybebad' ] ) && isset( 
$filters[ 'likelybad' ] ) ) {
-                                       $newDamagingGroup->getFilter( 
'maybebad' )->setAsSupersetOf(
-                                               $newDamagingGroup->getFilter( 
'likelybad' )
-                                       );
-                               }
-
-                               if ( isset( $filters[ 'likelybad' ] ) && isset( 
$filters[ 'verylikelybad' ] ) ) {
-                                       $newDamagingGroup->getFilter( 
'likelybad' )->setAsSupersetOf(
-                                               $newDamagingGroup->getFilter( 
'verylikelybad' )
-                                       );
-                               }
-
-                               // Transitive closure
-                               if ( isset( $filters[ 'maybebad' ] ) && isset( 
$filters[ 'verylikelybad' ] ) ) {
-                                       $newDamagingGroup->getFilter( 
'maybebad' )->setAsSupersetOf(
-                                               $newDamagingGroup->getFilter( 
'verylikelybad' )
-                                       );
-                               }
-
-                               if ( $damagingDefault ) {
-                                       $newDamagingGroup->setDefault( 
self::getDamagingLevelPreference( $clsp->getUser() ) );
-                               }
-
-                               if ( $clsp->getUser()->getBoolOption( 
'oresHighlight' ) ) {
-                                       $levelsColors = [
-                                               'maybebad' => 'c3',
-                                               'likelybad' => 'c4',
-                                               'verylikelybad' => 'c5',
-                                       ];
-
-                                       $prefLevel = 
self::getDamagingLevelPreference( $clsp->getUser() );
-                                       $allLevels = array_keys( $levelsColors 
);
-                                       $applicableLevels = array_slice( 
$allLevels, array_search( $prefLevel, $allLevels ) );
-                                       $applicableLevels = array_intersect( 
$applicableLevels, array_keys( $filters ) );
-
-                                       foreach ( $applicableLevels as $level ) 
{
-                                               $newDamagingGroup
-                                                       ->getFilter( $level )
-                                                       
->setDefaultHighlightColor( $levelsColors[ $level ] );
-                                       }
-                               }
-
-                               $clsp->registerFilterGroup( $newDamagingGroup );
-                       }
-
-                       // I don't think we need to register a conflict here, 
since
-                       // if we're showing non-damaging, that won't conflict 
with
-                       // anything.
-                       $legacyDamagingGroup = new 
ChangesListBooleanFilterGroup( [
-                               'name' => 'ores',
-                               'filters' => [
-                                       [
-                                               'name' => 'hidenondamaging',
-                                               'showHide' => 
'ores-damaging-filter',
-                                               'isReplacedInStructuredUi' => 
true,
-                                               'default' => $damagingDefault,
-                                               'queryCallable' => function ( 
$specialClassName, $ctx, $dbr, &$tables,
-                                                               &$fields, 
&$conds, &$query_options, &$join_conds ) {
-                                                       
self::hideNonDamagingFilter( $fields, $conds, true, $ctx->getUser() );
-                                                       // Filter out 
incompatible types; log actions and external rows are not scorable
-                                                       $conds[] = 'rc_type NOT 
IN (' . $dbr->makeList( [ RC_LOG, RC_EXTERNAL ] ) . ')';
-                                                       // Filter out patrolled 
edits: the 'r' doesn't appear for them
-                                                       $conds['rc_patrolled'] 
= 0;
-                                                       // Make the joins INNER 
JOINs instead of LEFT JOINs
-                                                       
$join_conds['ores_damaging_mdl'][0] = 'INNER JOIN';
-                                                       
$join_conds['ores_damaging_cls'][0] = 'INNER JOIN';
-                                                       // Performance hack: 
add STRAIGHT_JOIN (T146111) but not for Watchlist (T176456 / T164796)
-                                                       if ( $specialClassName 
!== 'SpecialWatchlist' ) {
-                                                               
$query_options[] = 'STRAIGHT_JOIN';
-                                                       }
-                                               },
-                                       ]
-                               ],
-
-                       ] );
-
-                       $clsp->registerFilterGroup( $legacyDamagingGroup );
-               }
-               if ( self::isModelEnabled( 'goodfaith' ) ) {
-                       $goodfaithLevels = $stats->getThresholds( 'goodfaith' );
-                       $filters = [];
-                       if ( isset( $goodfaithLevels['likelygood'] ) ) {
-                               $filters[ 'likelygood' ] = [
-                                       'name' => 'likelygood',
-                                       'label' => 
'ores-rcfilters-goodfaith-good-label',
-                                       'description' => 
'ores-rcfilters-goodfaith-good-desc',
-                                       'cssClassSuffix' => 'goodfaith-good',
-                                       'isRowApplicableCallable' => 
self::makeApplicableCallback(
-                                               'goodfaith',
-                                               $goodfaithLevels['likelygood']
-                                       ),
-                               ];
-                       }
-                       if ( isset( $goodfaithLevels['maybebad'] ) ) {
-                               $filters[ 'maybebad' ] = [
-                                       'name' => 'maybebad',
-                                       'label' => 
'ores-rcfilters-goodfaith-maybebad-label',
-                                       'description' => 
'ores-rcfilters-goodfaith-maybebad-desc',
-                                       'cssClassSuffix' => 
'goodfaith-maybebad',
-                                       'isRowApplicableCallable' => 
self::makeApplicableCallback(
-                                               'goodfaith',
-                                               $goodfaithLevels['maybebad']
-                                       ),
-                               ];
-                       }
-                       if ( isset( $goodfaithLevels['likelybad'] ) ) {
-                               $descMsg = isset( $filters[ 'maybebad' ] ) ?
-                                       'ores-rcfilters-goodfaith-bad-desc-low' 
:
-                                       
'ores-rcfilters-goodfaith-bad-desc-high';
-                               $filters[ 'likelybad' ] = [
-                                       'name' => 'likelybad',
-                                       'label' => 
'ores-rcfilters-goodfaith-bad-label',
-                                       'description' => $descMsg,
-                                       'cssClassSuffix' => 'goodfaith-bad',
-                                       'isRowApplicableCallable' => 
self::makeApplicableCallback(
-                                               'goodfaith',
-                                               $goodfaithLevels['likelybad']
-                                       ),
-                               ];
-                       }
-                       if ( isset( $goodfaithLevels['verylikelybad'] ) ) {
-                               $filters[ 'verylikelybad' ] = [
-                                       'name' => 'verylikelybad',
-                                       'label' => 
'ores-rcfilters-goodfaith-verylikelybad-label',
-                                       'description' => 
'ores-rcfilters-goodfaith-verylikelybad-desc',
-                                       'cssClassSuffix' => 
'goodfaith-verylikelybad',
-                                       'isRowApplicableCallable' => 
self::makeApplicableCallback(
-                                               'goodfaith',
-                                               
$goodfaithLevels['verylikelybad']
-                                       ),
-                               ];
-                       }
-
-                       if ( $filters ) {
-                               $goodfaithGroup = new 
ChangesListStringOptionsFilterGroup( [
-                                       'name' => 'goodfaith',
-                                       'title' => 
'ores-rcfilters-goodfaith-title',
-                                       'whatsThisHeader' => 
'ores-rcfilters-goodfaith-whats-this-header',
-                                       'whatsThisBody' => 
'ores-rcfilters-goodfaith-whats-this-body',
-                                       'whatsThisUrl' => 
'https://www.mediawiki.org/wiki/' .
-                                               
'Special:MyLanguage/Help:New_filters_for_edit_review/Quality_and_Intent_Filters',
-                                       'whatsThisLinkText' => 
'ores-rcfilters-whats-this-link-text',
-                                       'priority' => 1,
-                                       'filters' => array_values( $filters ),
-                                       'default' => 
ChangesListStringOptionsFilterGroup::NONE,
-                                       'isFullCoverage' => false,
-                                       'queryCallable' => function ( 
$specialClassName, $ctx, $dbr, &$tables, &$fields,
-                                               &$conds, &$query_options, 
&$join_conds, $selectedValues ) {
-                                               $condition = 
self::buildRangeFilter( 'goodfaith', $selectedValues );
-                                               if ( $condition ) {
-                                                       $conds[] = $condition;
-
-                                                       // Filter out 
incompatible types; log actions and external rows are not scorable
-                                                       $conds[] = 'rc_type NOT 
IN (' . $dbr->makeList( [ RC_LOG, RC_EXTERNAL ] ) . ')';
-                                                       // Make the joins INNER 
JOINs instead of LEFT JOINs
-                                                       
$join_conds['ores_goodfaith_mdl'][0] = 'INNER JOIN';
-                                                       
$join_conds['ores_goodfaith_cls'][0] = 'INNER JOIN';
-                                                       // Performance hack: 
add STRAIGHT_JOIN (T146111) but not for Watchlist (T176456 / T164796)
-                                                       if ( $specialClassName 
!== 'SpecialWatchlist' ) {
-                                                               
$query_options[] = 'STRAIGHT_JOIN';
-                                                       }
-                                               }
-                                       },
-                               ] );
-
-                               if ( isset( $filters['maybebad'] ) && isset( 
$filters['likelybad'] ) ) {
-                                       $goodfaithGroup->getFilter( 'maybebad' 
)->setAsSupersetOf(
-                                               $goodfaithGroup->getFilter( 
'likelybad' )
-                                       );
-                               }
-
-                               if ( isset( $filters['likelybad'] ) && isset( 
$filters['verylikelybad'] ) ) {
-                                       $goodfaithGroup->getFilter( 'likelybad' 
)->setAsSupersetOf(
-                                               $goodfaithGroup->getFilter( 
'verylikelybad' )
-                                       );
-                               }
-
-                               if ( isset( $filters['maybebad'] ) && isset( 
$filters['verylikelybad'] ) ) {
-                                       $goodfaithGroup->getFilter( 'maybebad' 
)->setAsSupersetOf(
-                                               $goodfaithGroup->getFilter( 
'verylikelybad' )
-                                       );
-                               }
-
-                               $goodfaithGroup->conflictsWith(
-                                       $logFilter,
-                                       
'ores-rcfilters-ores-conflicts-logactions-global',
-                                       
'ores-rcfilters-goodfaith-conflicts-logactions',
-                                       
'ores-rcfilters-logactions-conflicts-ores'
-                               );
-
-                               $clsp->registerFilterGroup( $goodfaithGroup );
-                       }
-               }
-       }
-
-       public static function onChangesListSpecialPageQuery(
-               $name, array &$tables, array &$fields, array &$conds,
-               array &$query_options, array &$join_conds, FormOptions $opts
-       ) {
-               global $wgUser;
-
-               // ORES is disabled on Recentchangeslinked: T163063
-               if ( !self::oresUiEnabled( $wgUser ) || $name === 
'Recentchangeslinked' ) {
-                       return;
-               }
-
-               if ( self::isModelEnabled( 'damaging' ) ) {
-                       self::joinWithOresTables(
-                               'damaging',
-                               'rc_this_oldid',
-                               $tables,
-                               $fields,
-                               $join_conds
-                       );
-               }
-               if ( self::isModelEnabled( 'goodfaith' ) ) {
-                       self::joinWithOresTables(
-                               'goodfaith',
-                               'rc_this_oldid',
-                               $tables,
-                               $fields,
-                               $join_conds
-                       );
-               }
-       }
-
-       /**
-        * Label recent changes with ORES scores (for each change in an 
expanded group)
-        *
-        * @param EnhancedChangesList $ecl
-        * @param array &$data
-        * @param RCCacheEntry[] $block
-        * @param RCCacheEntry $rcObj
-        * @param string[] &$classes
-        */
-       public static function onEnhancedChangesListModifyLineData(
-               EnhancedChangesList $ecl,
-               array &$data,
-               array $block,
-               RCCacheEntry $rcObj,
-               array &$classes
-       ) {
-               if ( !self::oresUiEnabled( $ecl->getUser() ) ) {
-                       return;
-               }
-
-               self::processRecentChangesList( $rcObj, $data, $classes, 
$ecl->getContext() );
-       }
-
-       /**
-        * Label recent changes with ORES scores (for top-level ungrouped lines)
-        *
-        * @param EnhancedChangesList $ecl
-        * @param array &$data
-        * @param RCCacheEntry $rcObj
-        */
-       public static function onEnhancedChangesListModifyBlockLineData(
-               EnhancedChangesList $ecl,
-               array &$data,
-               RCCacheEntry $rcObj
-       ) {
-               if ( !self::oresUiEnabled( $ecl->getUser() ) ) {
-                       return;
-               }
-
-               $classes = [];
-               self::processRecentChangesList( $rcObj, $data, $classes, 
$ecl->getContext() );
-               $data['attribs']['class'] = array_merge( 
$data['attribs']['class'], $classes );
-       }
-
-       /**
-        * Hook for formatting recent changes links
-        * @see 
https://www.mediawiki.org/wiki/Manual:Hooks/OldChangesListRecentChangesLine
-        *
-        * @param ChangesList &$changesList
-        * @param string &$s
-        * @param RecentChange $rc
-        * @param string[] &$classes
-        * @return bool|void
-        */
-       public static function onOldChangesListRecentChangesLine(
-               ChangesList &$changesList,
-               &$s,
-               $rc,
-               &$classes = []
-       ) {
-               if ( !self::oresUiEnabled( $changesList->getUser() ) ) {
-                       return;
-               }
-
-               $damaging = self::getScoreRecentChangesList( $rc, 
$changesList->getContext() );
-               if ( $damaging ) {
-                       // Add highlight class
-                       if ( self::isHighlightEnabled( $changesList ) ) {
-                               $classes[] = 'ores-highlight';
-                       }
-
-                       // Add damaging class and flag
-                       if ( self::isDamagingFlagEnabled( $changesList ) ) {
-                               $classes[] = 'damaging';
-
-                               $separator = ' <span 
class="mw-changeslist-separator">. .</span> ';
-                               if ( strpos( $s, $separator ) === false ) {
-                                       return;
-                               }
-
-                               $parts = explode( $separator, $s );
-                               $parts[1] = ChangesList::flag( 'damaging' ) . 
$parts[1];
-                               $s = implode( $separator, $parts );
-                       }
-               }
-
-               return true;
-       }
-
-       /**
-        * Filter out non-damaging changes from Special:Contributions
-        *
-        * @param ContribsPager $pager
-        * @param array &$query
-        */
-       public static function onContribsGetQueryInfo(
-               ContribsPager $pager,
-               &$query
-       ) {
-               if ( !self::oresUiEnabled( $pager->getUser() ) ) {
-                       return;
-               }
-
-               if ( self::isModelEnabled( 'damaging' ) ) {
-                       $request = $pager->getContext()->getRequest();
-
-                       self::joinWithOresTables(
-                               'damaging',
-                               'rev_id',
-                               $query['tables'],
-                               $query['fields'],
-                               $query['join_conds']
-                       );
-
-                       self::hideNonDamagingFilter(
-                               $query['fields'],
-                               $query['conds'],
-                               $request->getVal( 'hidenondamaging' ),
-                               $pager->getUser()
-                       );
-               }
-       }
-
-       public static function onSpecialContributionsFormatRowFlags(
-               RequestContext $context,
-               $row,
-               array &$flags
-       ) {
-               if ( !self::oresUiEnabled( $context->getUser() ) ) {
-                       return;
-               }
-
-               // Doesn't have ores score, skipping.
-               if ( !isset( $row->ores_damaging_score ) ) {
-                       return;
-               }
-
-               self::addRowData( $context, $row->rev_id, 
(float)$row->ores_damaging_score, 'damaging' );
-
-               if (
-                       self::isDamagingFlagEnabled( $context ) &&
-                       $row->ores_damaging_score > 
$row->ores_damaging_threshold
-               ) {
-                       // Prepend the "r" flag
-                       array_unshift( $flags, ChangesList::flag( 'damaging' ) 
);
-               }
-       }
-
-       public static function onContributionsLineEnding(
-               ContribsPager $pager,
-               &$ret,
-               $row,
-               array &$classes
-       ) {
-               if ( !self::oresUiEnabled( $pager->getUser() ) ) {
-                       return;
-               }
-
-               // Doesn't have ores score or threshold is not set properly, 
skipping.
-               if ( !isset( $row->ores_damaging_score ) || !isset( 
$row->ores_damaging_threshold ) ) {
-                       return;
-               }
-
-               if ( $row->ores_damaging_score > $row->ores_damaging_threshold 
) {
-                       if ( self::isHighlightEnabled( $pager ) ) {
-                               $classes[] = 'ores-highlight';
-                       }
-                       if ( self::isDamagingFlagEnabled( $pager ) ) {
-                               $classes[] = 'damaging';
-                       }
-               }
-       }
-
-       /**
-        * Hook into Special:Contributions filters
-        *
-        * @param SpecialContributions $page
-        * @param string[] &$filters HTML
-        */
-       public static function onSpecialContributionsGetFormFilters(
-               SpecialContributions $page,
-               array &$filters
-       ) {
-               if ( !self::oresUiEnabled( $page->getUser() ) || 
!self::isModelEnabled( 'damaging' ) ) {
-                       return;
-               }
-
-               $filters[] = Html::rawElement(
-                       'span',
-                       [ 'class' => 'mw-input-with-label' ],
-                       Xml::checkLabel(
-                               $page->msg( 'ores-hide-nondamaging-filter' 
)->text(),
-                               'hidenondamaging',
-                               'ores-hide-nondamaging',
-                               $page->getContext()->getRequest()->getVal( 
'hidenondamaging' ),
-                               [ 'class' => 'mw-input' ]
-                       )
-               );
-       }
-
-       /**
-        * Internal helper to label matching rows
-        *
-        * @param RCCacheEntry $rcObj
-        * @param string[] &$data
-        * @param string[] &$classes
-        * @param IContextSource $context
-        */
-       protected static function processRecentChangesList(
-               RCCacheEntry $rcObj,
-               array &$data,
-               array &$classes = [],
-               IContextSource $context
-       ) {
-               $damaging = self::getScoreRecentChangesList( $rcObj, $context );
-
-               if ( $damaging && self::isDamagingFlagEnabled( $context ) ) {
-                       $classes[] = 'damaging';
-                       $data['recentChangesFlags']['damaging'] = true;
-               }
-       }
-
-       /**
-        * Check if we should flag a row. As a side effect, also adds score 
data for this row.
-        * @param RecentChange $rcObj
-        * @param IContextSource $context
-        * @return bool
-        */
-       public static function getScoreRecentChangesList( $rcObj, 
IContextSource $context ) {
-               global $wgUser;
-               $threshold = $rcObj->getAttribute( 'ores_damaging_threshold' );
-               if ( $threshold === null ) {
-                       $threshold = self::getThreshold( 'damaging', $wgUser );
-               }
-               $score = $rcObj->getAttribute( 'ores_damaging_score' );
-               $patrolled = $rcObj->getAttribute( 'rc_patrolled' );
-               $type = $rcObj->getAttribute( 'rc_type' );
-
-               // Log actions and external rows are not scorable; if such a 
row does have a score, ignore it
-               if ( !$score || $threshold === null || in_array( $type, [ 
RC_LOG, RC_EXTERNAL ] ) ) {
-                       // Shorten out
-                       return false;
-               }
-
-               self::addRowData(
-                       $context,
-                       $rcObj->getAttribute( 'rc_this_oldid' ),
-                       (float)$score,
-                       'damaging'
-               );
-
-               return $score && $score >= $threshold && !$patrolled;
        }
 
        /**
@@ -870,7 +264,7 @@
         * @param IContextSource $context
         * @return bool Whether highlights should be shown
         */
-       private static function isHighlightEnabled( IContextSource $context ) {
+       public static function isHighlightEnabled( IContextSource $context ) {
                // Was previously controlled by different preferences than the 
"r", but they're currently
                // the same.
                return self::isDamagingFlagEnabled( $context );
@@ -880,7 +274,7 @@
         * @param IContextSource $context
         * @return bool Whether the damaging flag ("r") should be shown
         */
-       private static function isDamagingFlagEnabled( IContextSource $context 
) {
+       public static function isDamagingFlagEnabled( IContextSource $context ) 
{
                $user = $context->getUser();
 
                if ( !self::oresUiEnabled( $user ) ) {
@@ -916,7 +310,7 @@
         * @param float $score
         * @param string $model
         */
-       private static function addRowData( IContextSource $context, 
$revisionId, $score, $model ) {
+       public static function addRowData( IContextSource $context, 
$revisionId, $score, $model ) {
                $out = $context->getOutput();
                $data = $out->getProperty( 'oresData' );
                if ( !isset( $data[$revisionId] ) ) {
@@ -926,7 +320,7 @@
                $out->setProperty( 'oresData', $data );
        }
 
-       private static function joinWithOresTables(
+       public static function joinWithOresTables(
                $type,
                $revIdField,
                array &$tables,
@@ -956,7 +350,7 @@
                ] ];
        }
 
-       private static function hideNonDamagingFilter(
+       public static function hideNonDamagingFilter(
                array &$fields,
                array &$conds,
                $hidenondamaging,
@@ -975,64 +369,6 @@
                        // Filter out non-damaging edits.
                        $conds[] = 'ores_damaging_cls.oresc_probability > ' . 
$dbr->addQuotes( $threshold );
                }
-       }
-
-       private static function buildRangeFilter( $name, $filterValue ) {
-               $stats = Stats::newFromGlobalState();
-               $thresholds = $stats->getThresholds( $name );
-
-               $selectedLevels = is_array( $filterValue ) ? $filterValue :
-                       explode( ',', strtolower( $filterValue ) );
-               $selectedLevels = array_intersect(
-                       $selectedLevels,
-                       array_keys( $thresholds )
-               );
-
-               if ( $selectedLevels ) {
-                       $ranges = [];
-                       foreach ( $selectedLevels as $level ) {
-                               $range = new Range(
-                                       $thresholds[$level]['min'],
-                                       $thresholds[$level]['max']
-                               );
-
-                               $result = array_filter(
-                                       $ranges,
-                                       function ( Range $r ) use ( $range ) {
-                                               return $r->overlaps( $range );
-                                       }
-                               );
-                               $overlap = reset( $result );
-                               if ( $overlap ) {
-                                       $overlap->combineWith( $range );
-                               } else {
-                                       $ranges[] = $range;
-                               }
-                       }
-
-                       $betweenConditions = array_map(
-                               function ( Range $range ) use ( $name ) {
-                                       $min = $range->getMin();
-                                       $max = $range->getMax();
-                                       return 
"ores_{$name}_cls.oresc_probability BETWEEN $min AND $max";
-                               },
-                               $ranges
-                       );
-
-                       return \wfGetDB( DB_REPLICA )->makeList( 
$betweenConditions, \IDatabase::LIST_OR );
-               }
-       }
-
-       private static function makeApplicableCallback( $model, array 
$levelData ) {
-               return function ( $ctx, $rc ) use ( $model, $levelData ) {
-                       $score = $rc->getAttribute( "ores_{$model}_score" );
-                       $type = $rc->getAttribute( 'rc_type' );
-                       // Log actions and external rows are not scorable; if 
such a row does have a score, ignore it
-                       if ( $score === null || in_array( $type, [ RC_LOG, 
RC_EXTERNAL ] ) ) {
-                               return false;
-                       }
-                       return $levelData['min'] <= $score && $score <= 
$levelData['max'];
-               };
        }
 
 }
diff --git a/includes/Hooks/ChangesListHooksHandler.php 
b/includes/Hooks/ChangesListHooksHandler.php
new file mode 100644
index 0000000..d211f18
--- /dev/null
+++ b/includes/Hooks/ChangesListHooksHandler.php
@@ -0,0 +1,564 @@
+<?php
+
+namespace ORES\Hooks;
+
+use ChangesList;
+use ChangesListBooleanFilterGroup;
+use ChangesListSpecialPage;
+use ChangesListStringOptionsFilterGroup;
+use EnhancedChangesList;
+use FormOptions;
+use IContextSource;
+use ORES\Hooks;
+use ORES\Range;
+use ORES\Stats;
+use RCCacheEntry;
+use RecentChange;
+use SpecialRecentChanges;
+use SpecialWatchlist;
+
+class ChangesListHooksHandler {
+
+       public static function onChangesListSpecialPageStructuredFilters(
+               ChangesListSpecialPage $clsp
+       ) {
+               // ORES is disabled on Recentchangeslinked: T163063
+               if ( !Hooks::oresUiEnabled( $clsp->getUser() ) || 
$clsp->getName() === 'Recentchangeslinked'
+               ) {
+                       return;
+               }
+
+               $stats = Stats::newFromGlobalState();
+
+               $changeTypeGroup = $clsp->getFilterGroup( 'changeType' );
+               $logFilter = $changeTypeGroup->getFilter( 'hidelog' );
+
+               if ( Hooks::isModelEnabled( 'damaging' ) ) {
+                       if ( $clsp instanceof SpecialRecentChanges ) {
+                               $damagingDefault = $clsp->getUser()->getOption( 
'oresRCHideNonDamaging' );
+                       } elseif ( $clsp instanceof SpecialWatchlist ) {
+                               $damagingDefault = $clsp->getUser()->getOption( 
'oresWatchlistHideNonDamaging' );
+                       } else {
+                               $damagingDefault = false;
+                       }
+
+                       $damagingLevels = $stats->getThresholds( 'damaging' );
+                       $filters = [];
+                       if ( isset( $damagingLevels[ 'likelygood' ] ) ) {
+                               $filters[ 'likelygood' ] = [
+                                       'name' => 'likelygood',
+                                       'label' => 
'ores-rcfilters-damaging-likelygood-label',
+                                       'description' => 
'ores-rcfilters-damaging-likelygood-desc',
+                                       'cssClassSuffix' => 
'damaging-likelygood',
+                                       'isRowApplicableCallable' => 
self::makeApplicableCallback(
+                                               'damaging',
+                                               $damagingLevels['likelygood']
+                                       ),
+                               ];
+                       }
+                       if ( isset( $damagingLevels[ 'maybebad' ] ) ) {
+                               $filters[ 'maybebad' ] = [
+                                       'name' => 'maybebad',
+                                       'label' => 
'ores-rcfilters-damaging-maybebad-label',
+                                       'description' => 
'ores-rcfilters-damaging-maybebad-desc',
+                                       'cssClassSuffix' => 'damaging-maybebad',
+                                       'isRowApplicableCallable' => 
self::makeApplicableCallback(
+                                               'damaging',
+                                               $damagingLevels['maybebad']
+                                       ),
+                               ];
+                       }
+                       if ( isset( $damagingLevels[ 'likelybad' ] ) ) {
+                               $descMsg = isset( $filters[ 'maybebad' ] ) ?
+                                       
'ores-rcfilters-damaging-likelybad-desc-low' :
+                                       
'ores-rcfilters-damaging-likelybad-desc-high';
+                               $filters[ 'likelybad' ] = [
+                                       'name' => 'likelybad',
+                                       'label' => 
'ores-rcfilters-damaging-likelybad-label',
+                                       'description' => $descMsg,
+                                       'cssClassSuffix' => 
'damaging-likelybad',
+                                       'isRowApplicableCallable' => 
self::makeApplicableCallback(
+                                               'damaging',
+                                               $damagingLevels['likelybad']
+                                       ),
+                               ];
+                       }
+                       if ( isset( $damagingLevels[ 'verylikelybad' ] ) ) {
+                               $filters[ 'verylikelybad' ] = [
+                                       'name' => 'verylikelybad',
+                                       'label' => 
'ores-rcfilters-damaging-verylikelybad-label',
+                                       'description' => 
'ores-rcfilters-damaging-verylikelybad-desc',
+                                       'cssClassSuffix' => 
'damaging-verylikelybad',
+                                       'isRowApplicableCallable' => 
self::makeApplicableCallback(
+                                               'damaging',
+                                               $damagingLevels['verylikelybad']
+                                       ),
+                               ];
+                       }
+
+                       if ( $filters ) {
+                               $newDamagingGroup = new 
ChangesListStringOptionsFilterGroup( [
+                                       'name' => 'damaging',
+                                       'title' => 
'ores-rcfilters-damaging-title',
+                                       'whatsThisHeader' => 
'ores-rcfilters-damaging-whats-this-header',
+                                       'whatsThisBody' => 
'ores-rcfilters-damaging-whats-this-body',
+                                       'whatsThisUrl' => 
'https://www.mediawiki.org/wiki/' .
+                                               
'Special:MyLanguage/Help:New_filters_for_edit_review/Quality_and_Intent_Filters',
+                                       'whatsThisLinkText' => 
'ores-rcfilters-whats-this-link-text',
+                                       'priority' => 2,
+                                       'filters' => array_values( $filters ),
+                                       'default' => 
ChangesListStringOptionsFilterGroup::NONE,
+                                       'isFullCoverage' => false,
+                                       'queryCallable' => function ( 
$specialClassName, $ctx, $dbr, &$tables,
+                                                       &$fields, &$conds, 
&$query_options, &$join_conds, $selectedValues ) {
+                                               $condition = 
self::buildRangeFilter( 'damaging', $selectedValues );
+                                               if ( $condition ) {
+                                                       $conds[] = $condition;
+
+                                                       // Filter out 
incompatible types; log actions and external rows are not scorable
+                                                       $conds[] = 'rc_type NOT 
IN (' . $dbr->makeList( [ RC_LOG, RC_EXTERNAL ] ) . ')';
+                                                       // Make the joins INNER 
JOINs instead of LEFT JOINs
+                                                       
$join_conds['ores_damaging_mdl'][0] = 'INNER JOIN';
+                                                       
$join_conds['ores_damaging_cls'][0] = 'INNER JOIN';
+                                                       // Performance hack: 
add STRAIGHT_JOIN (T146111) but not for Watchlist (T176456 / T164796)
+                                                       if ( $specialClassName 
!== 'SpecialWatchlist' ) {
+                                                               
$query_options[] = 'STRAIGHT_JOIN';
+                                                       }
+                                               }
+                                       },
+                               ] );
+
+                               $newDamagingGroup->conflictsWith(
+                                       $logFilter,
+                                       
'ores-rcfilters-ores-conflicts-logactions-global',
+                                       
'ores-rcfilters-damaging-conflicts-logactions',
+                                       
'ores-rcfilters-logactions-conflicts-ores'
+                               );
+
+                               if ( isset( $filters[ 'maybebad' ] ) && isset( 
$filters[ 'likelybad' ] ) ) {
+                                       $newDamagingGroup->getFilter( 
'maybebad' )->setAsSupersetOf(
+                                               $newDamagingGroup->getFilter( 
'likelybad' )
+                                       );
+                               }
+
+                               if ( isset( $filters[ 'likelybad' ] ) && isset( 
$filters[ 'verylikelybad' ] ) ) {
+                                       $newDamagingGroup->getFilter( 
'likelybad' )->setAsSupersetOf(
+                                               $newDamagingGroup->getFilter( 
'verylikelybad' )
+                                       );
+                               }
+
+                               // Transitive closure
+                               if ( isset( $filters[ 'maybebad' ] ) && isset( 
$filters[ 'verylikelybad' ] ) ) {
+                                       $newDamagingGroup->getFilter( 
'maybebad' )->setAsSupersetOf(
+                                               $newDamagingGroup->getFilter( 
'verylikelybad' )
+                                       );
+                               }
+
+                               if ( $damagingDefault ) {
+                                       $newDamagingGroup->setDefault( 
Hooks::getDamagingLevelPreference( $clsp->getUser
+                                       () ) );
+                               }
+
+                               if ( $clsp->getUser()->getBoolOption( 
'oresHighlight' ) ) {
+                                       $levelsColors = [
+                                               'maybebad' => 'c3',
+                                               'likelybad' => 'c4',
+                                               'verylikelybad' => 'c5',
+                                       ];
+
+                                       $prefLevel = 
Hooks::getDamagingLevelPreference( $clsp->getUser() );
+                                       $allLevels = array_keys( $levelsColors 
);
+                                       $applicableLevels = array_slice( 
$allLevels, array_search( $prefLevel, $allLevels ) );
+                                       $applicableLevels = array_intersect( 
$applicableLevels, array_keys( $filters ) );
+
+                                       foreach ( $applicableLevels as $level ) 
{
+                                               $newDamagingGroup
+                                                       ->getFilter( $level )
+                                                       
->setDefaultHighlightColor( $levelsColors[ $level ] );
+                                       }
+                               }
+
+                               $clsp->registerFilterGroup( $newDamagingGroup );
+                       }
+
+                       // I don't think we need to register a conflict here, 
since
+                       // if we're showing non-damaging, that won't conflict 
with
+                       // anything.
+                       $legacyDamagingGroup = new 
ChangesListBooleanFilterGroup( [
+                               'name' => 'ores',
+                               'filters' => [
+                                       [
+                                               'name' => 'hidenondamaging',
+                                               'showHide' => 
'ores-damaging-filter',
+                                               'isReplacedInStructuredUi' => 
true,
+                                               'default' => $damagingDefault,
+                                               'queryCallable' => function ( 
$specialClassName, $ctx, $dbr, &$tables,
+                                                               &$fields, 
&$conds, &$query_options, &$join_conds ) {
+                                                       
Hooks::hideNonDamagingFilter( $fields, $conds, true, $ctx->getUser() );
+                                                       // Filter out 
incompatible types; log actions and external rows are not scorable
+                                                       $conds[] = 'rc_type NOT 
IN (' . $dbr->makeList( [ RC_LOG, RC_EXTERNAL ] ) . ')';
+                                                       // Filter out patrolled 
edits: the 'r' doesn't appear for them
+                                                       $conds['rc_patrolled'] 
= 0;
+                                                       // Make the joins INNER 
JOINs instead of LEFT JOINs
+                                                       
$join_conds['ores_damaging_mdl'][0] = 'INNER JOIN';
+                                                       
$join_conds['ores_damaging_cls'][0] = 'INNER JOIN';
+                                                       // Performance hack: 
add STRAIGHT_JOIN (T146111) but not for Watchlist (T176456 / T164796)
+                                                       if ( $specialClassName 
!== 'SpecialWatchlist' ) {
+                                                               
$query_options[] = 'STRAIGHT_JOIN';
+                                                       }
+                                               },
+                                       ]
+                               ],
+
+                       ] );
+
+                       $clsp->registerFilterGroup( $legacyDamagingGroup );
+               }
+               if ( Hooks::isModelEnabled( 'goodfaith' ) ) {
+                       $goodfaithLevels = $stats->getThresholds( 'goodfaith' );
+                       $filters = [];
+                       if ( isset( $goodfaithLevels['likelygood'] ) ) {
+                               $filters[ 'likelygood' ] = [
+                                       'name' => 'likelygood',
+                                       'label' => 
'ores-rcfilters-goodfaith-good-label',
+                                       'description' => 
'ores-rcfilters-goodfaith-good-desc',
+                                       'cssClassSuffix' => 'goodfaith-good',
+                                       'isRowApplicableCallable' => 
self::makeApplicableCallback(
+                                               'goodfaith',
+                                               $goodfaithLevels['likelygood']
+                                       ),
+                               ];
+                       }
+                       if ( isset( $goodfaithLevels['maybebad'] ) ) {
+                               $filters[ 'maybebad' ] = [
+                                       'name' => 'maybebad',
+                                       'label' => 
'ores-rcfilters-goodfaith-maybebad-label',
+                                       'description' => 
'ores-rcfilters-goodfaith-maybebad-desc',
+                                       'cssClassSuffix' => 
'goodfaith-maybebad',
+                                       'isRowApplicableCallable' => 
self::makeApplicableCallback(
+                                               'goodfaith',
+                                               $goodfaithLevels['maybebad']
+                                       ),
+                               ];
+                       }
+                       if ( isset( $goodfaithLevels['likelybad'] ) ) {
+                               $descMsg = isset( $filters[ 'maybebad' ] ) ?
+                                       'ores-rcfilters-goodfaith-bad-desc-low' 
:
+                                       
'ores-rcfilters-goodfaith-bad-desc-high';
+                               $filters[ 'likelybad' ] = [
+                                       'name' => 'likelybad',
+                                       'label' => 
'ores-rcfilters-goodfaith-bad-label',
+                                       'description' => $descMsg,
+                                       'cssClassSuffix' => 'goodfaith-bad',
+                                       'isRowApplicableCallable' => 
self::makeApplicableCallback(
+                                               'goodfaith',
+                                               $goodfaithLevels['likelybad']
+                                       ),
+                               ];
+                       }
+                       if ( isset( $goodfaithLevels['verylikelybad'] ) ) {
+                               $filters[ 'verylikelybad' ] = [
+                                       'name' => 'verylikelybad',
+                                       'label' => 
'ores-rcfilters-goodfaith-verylikelybad-label',
+                                       'description' => 
'ores-rcfilters-goodfaith-verylikelybad-desc',
+                                       'cssClassSuffix' => 
'goodfaith-verylikelybad',
+                                       'isRowApplicableCallable' => 
self::makeApplicableCallback(
+                                               'goodfaith',
+                                               
$goodfaithLevels['verylikelybad']
+                                       ),
+                               ];
+                       }
+
+                       if ( $filters ) {
+                               $goodfaithGroup = new 
ChangesListStringOptionsFilterGroup( [
+                                       'name' => 'goodfaith',
+                                       'title' => 
'ores-rcfilters-goodfaith-title',
+                                       'whatsThisHeader' => 
'ores-rcfilters-goodfaith-whats-this-header',
+                                       'whatsThisBody' => 
'ores-rcfilters-goodfaith-whats-this-body',
+                                       'whatsThisUrl' => 
'https://www.mediawiki.org/wiki/' .
+                                               
'Special:MyLanguage/Help:New_filters_for_edit_review/Quality_and_Intent_Filters',
+                                       'whatsThisLinkText' => 
'ores-rcfilters-whats-this-link-text',
+                                       'priority' => 1,
+                                       'filters' => array_values( $filters ),
+                                       'default' => 
ChangesListStringOptionsFilterGroup::NONE,
+                                       'isFullCoverage' => false,
+                                       'queryCallable' => function ( 
$specialClassName, $ctx, $dbr, &$tables, &$fields,
+                                                       &$conds, 
&$query_options, &$join_conds, $selectedValues ) {
+                                               $condition = 
self::buildRangeFilter( 'goodfaith', $selectedValues );
+                                               if ( $condition ) {
+                                                       $conds[] = $condition;
+
+                                                       // Filter out 
incompatible types; log actions and external rows are not scorable
+                                                       $conds[] = 'rc_type NOT 
IN (' . $dbr->makeList( [ RC_LOG, RC_EXTERNAL ] ) . ')';
+                                                       // Make the joins INNER 
JOINs instead of LEFT JOINs
+                                                       
$join_conds['ores_goodfaith_mdl'][0] = 'INNER JOIN';
+                                                       
$join_conds['ores_goodfaith_cls'][0] = 'INNER JOIN';
+                                                       // Performance hack: 
add STRAIGHT_JOIN (T146111) but not for Watchlist (T176456 / T164796)
+                                                       if ( $specialClassName 
!== 'SpecialWatchlist' ) {
+                                                               
$query_options[] = 'STRAIGHT_JOIN';
+                                                       }
+                                               }
+                                       },
+                               ] );
+
+                               if ( isset( $filters['maybebad'] ) && isset( 
$filters['likelybad'] ) ) {
+                                       $goodfaithGroup->getFilter( 'maybebad' 
)->setAsSupersetOf(
+                                               $goodfaithGroup->getFilter( 
'likelybad' )
+                                       );
+                               }
+
+                               if ( isset( $filters['likelybad'] ) && isset( 
$filters['verylikelybad'] ) ) {
+                                       $goodfaithGroup->getFilter( 'likelybad' 
)->setAsSupersetOf(
+                                               $goodfaithGroup->getFilter( 
'verylikelybad' )
+                                       );
+                               }
+
+                               if ( isset( $filters['maybebad'] ) && isset( 
$filters['verylikelybad'] ) ) {
+                                       $goodfaithGroup->getFilter( 'maybebad' 
)->setAsSupersetOf(
+                                               $goodfaithGroup->getFilter( 
'verylikelybad' )
+                                       );
+                               }
+
+                               $goodfaithGroup->conflictsWith(
+                                       $logFilter,
+                                       
'ores-rcfilters-ores-conflicts-logactions-global',
+                                       
'ores-rcfilters-goodfaith-conflicts-logactions',
+                                       
'ores-rcfilters-logactions-conflicts-ores'
+                               );
+
+                               $clsp->registerFilterGroup( $goodfaithGroup );
+                       }
+               }
+       }
+
+       public static function onChangesListSpecialPageQuery(
+               $name, array &$tables, array &$fields, array &$conds,
+               array &$query_options, array &$join_conds, FormOptions $opts
+       ) {
+               global $wgUser;
+
+               // ORES is disabled on Recentchangeslinked: T163063
+               if ( !Hooks::oresUiEnabled( $wgUser ) || $name === 
'Recentchangeslinked' ) {
+                       return;
+               }
+
+               if ( Hooks::isModelEnabled( 'damaging' ) ) {
+                       Hooks::joinWithOresTables(
+                               'damaging',
+                               'rc_this_oldid',
+                               $tables,
+                               $fields,
+                               $join_conds
+                       );
+               }
+               if ( Hooks::isModelEnabled( 'goodfaith' ) ) {
+                       Hooks::joinWithOresTables(
+                               'goodfaith',
+                               'rc_this_oldid',
+                               $tables,
+                               $fields,
+                               $join_conds
+                       );
+               }
+       }
+
+       /**
+        * Label recent changes with ORES scores (for each change in an 
expanded group)
+        *
+        * @param EnhancedChangesList $ecl
+        * @param array &$data
+        * @param RCCacheEntry[] $block
+        * @param RCCacheEntry $rcObj
+        * @param string[] &$classes
+        */
+       public static function onEnhancedChangesListModifyLineData(
+               EnhancedChangesList $ecl,
+               array &$data,
+               array $block,
+               RCCacheEntry $rcObj,
+               array &$classes
+       ) {
+               if ( !Hooks::oresUiEnabled( $ecl->getUser() ) ) {
+                       return;
+               }
+
+               self::processRecentChangesList( $rcObj, $data, $classes, 
$ecl->getContext() );
+       }
+
+       /**
+        * Label recent changes with ORES scores (for top-level ungrouped lines)
+        *
+        * @param EnhancedChangesList $ecl
+        * @param array &$data
+        * @param RCCacheEntry $rcObj
+        */
+       public static function onEnhancedChangesListModifyBlockLineData(
+               EnhancedChangesList $ecl,
+               array &$data,
+               RCCacheEntry $rcObj
+       ) {
+               if ( !Hooks::oresUiEnabled( $ecl->getUser() ) ) {
+                       return;
+               }
+
+               $classes = [];
+               self::processRecentChangesList( $rcObj, $data, $classes, 
$ecl->getContext() );
+               $data['attribs']['class'] = array_merge( 
$data['attribs']['class'], $classes );
+       }
+
+       /**
+        * Internal helper to label matching rows
+        *
+        * @param RCCacheEntry $rcObj
+        * @param string[] &$data
+        * @param string[] &$classes
+        * @param IContextSource $context
+        */
+       protected static function processRecentChangesList(
+               RCCacheEntry $rcObj,
+               array &$data,
+               array &$classes = [],
+               IContextSource $context
+       ) {
+               $damaging = self::getScoreRecentChangesList( $rcObj, $context );
+
+               if ( $damaging && Hooks::isDamagingFlagEnabled( $context ) ) {
+                       $classes[] = 'damaging';
+                       $data['recentChangesFlags']['damaging'] = true;
+               }
+       }
+
+       /**
+        * Hook for formatting recent changes links
+        * @see 
https://www.mediawiki.org/wiki/Manual:Hooks/OldChangesListRecentChangesLine
+        *
+        * @param ChangesList &$changesList
+        * @param string &$s
+        * @param RecentChange $rc
+        * @param string[] &$classes
+        * @return bool|void
+        */
+       public static function onOldChangesListRecentChangesLine(
+               ChangesList &$changesList,
+               &$s,
+               $rc,
+               &$classes = []
+       ) {
+               if ( !Hooks::oresUiEnabled( $changesList->getUser() ) ) {
+                       return;
+               }
+
+               $damaging = self::getScoreRecentChangesList( $rc, 
$changesList->getContext() );
+               if ( $damaging ) {
+                       // Add highlight class
+                       if ( Hooks::isHighlightEnabled( $changesList ) ) {
+                               $classes[] = 'ores-highlight';
+                       }
+
+                       // Add damaging class and flag
+                       if ( Hooks::isDamagingFlagEnabled( $changesList ) ) {
+                               $classes[] = 'damaging';
+
+                               $separator = ' <span 
class="mw-changeslist-separator">. .</span> ';
+                               if ( strpos( $s, $separator ) === false ) {
+                                       return;
+                               }
+
+                               $parts = explode( $separator, $s );
+                               $parts[1] = ChangesList::flag( 'damaging' ) . 
$parts[1];
+                               $s = implode( $separator, $parts );
+                       }
+               }
+       }
+
+       /**
+        * Check if we should flag a row. As a side effect, also adds score 
data for this row.
+        * @param RecentChange $rcObj
+        * @param IContextSource $context
+        * @return bool
+        */
+       public static function getScoreRecentChangesList( $rcObj, 
IContextSource $context ) {
+               global $wgUser;
+               $threshold = $rcObj->getAttribute( 'ores_damaging_threshold' );
+               if ( $threshold === null ) {
+                       $threshold = Hooks::getThreshold( 'damaging', $wgUser );
+               }
+               $score = $rcObj->getAttribute( 'ores_damaging_score' );
+               $patrolled = $rcObj->getAttribute( 'rc_patrolled' );
+               $type = $rcObj->getAttribute( 'rc_type' );
+
+               // Log actions and external rows are not scorable; if such a 
row does have a score, ignore it
+               if ( !$score || $threshold === null || in_array( $type, [ 
RC_LOG, RC_EXTERNAL ] ) ) {
+                       // Shorten out
+                       return false;
+               }
+
+               Hooks::addRowData(
+                       $context,
+                       $rcObj->getAttribute( 'rc_this_oldid' ),
+                       (float)$score,
+                       'damaging'
+               );
+
+               return $score && $score >= $threshold && !$patrolled;
+       }
+
+       private static function makeApplicableCallback( $model, array 
$levelData ) {
+               return function ( $ctx, $rc ) use ( $model, $levelData ) {
+                       $score = $rc->getAttribute( "ores_{$model}_score" );
+                       $type = $rc->getAttribute( 'rc_type' );
+                       // Log actions and external rows are not scorable; if 
such a row does have a score, ignore it
+                       if ( $score === null || in_array( $type, [ RC_LOG, 
RC_EXTERNAL ] ) ) {
+                               return false;
+                       }
+                       return $levelData['min'] <= $score && $score <= 
$levelData['max'];
+               };
+       }
+
+       private static function buildRangeFilter( $name, $filterValue ) {
+               $stats = Stats::newFromGlobalState();
+               $thresholds = $stats->getThresholds( $name );
+
+               $selectedLevels = is_array( $filterValue ) ? $filterValue :
+                       explode( ',', strtolower( $filterValue ) );
+               $selectedLevels = array_intersect(
+                       $selectedLevels,
+                       array_keys( $thresholds )
+               );
+
+               if ( $selectedLevels ) {
+                       $ranges = [];
+                       foreach ( $selectedLevels as $level ) {
+                               $range = new Range(
+                                       $thresholds[$level]['min'],
+                                       $thresholds[$level]['max']
+                               );
+
+                               $result = array_filter(
+                                       $ranges,
+                                       function ( Range $r ) use ( $range ) {
+                                               return $r->overlaps( $range );
+                                       }
+                               );
+                               $overlap = reset( $result );
+                               if ( $overlap ) {
+                                       $overlap->combineWith( $range );
+                               } else {
+                                       $ranges[] = $range;
+                               }
+                       }
+
+                       $betweenConditions = array_map(
+                               function ( Range $range ) use ( $name ) {
+                                       $min = $range->getMin();
+                                       $max = $range->getMax();
+                                       return 
"ores_{$name}_cls.oresc_probability BETWEEN $min AND $max";
+                               },
+                               $ranges
+                       );
+
+                       return \wfGetDB( DB_REPLICA )->makeList( 
$betweenConditions, \IDatabase::LIST_OR );
+               }
+       }
+
+}
diff --git a/includes/Hooks/ContributionsHooksHandler.php 
b/includes/Hooks/ContributionsHooksHandler.php
new file mode 100644
index 0000000..903db4a
--- /dev/null
+++ b/includes/Hooks/ContributionsHooksHandler.php
@@ -0,0 +1,126 @@
+<?php
+
+namespace ORES\Hooks;
+
+use ChangesList;
+use ContribsPager;
+use Html;
+use ORES\Hooks;
+use RequestContext;
+use SpecialContributions;
+use Xml;
+
+class ContributionsHooksHandler {
+
+       /**
+        * Filter out non-damaging changes from Special:Contributions
+        *
+        * @param ContribsPager $pager
+        * @param array &$query
+        */
+       public static function onContribsGetQueryInfo(
+               ContribsPager $pager,
+               &$query
+       ) {
+               if ( !Hooks::oresUiEnabled( $pager->getUser() ) ) {
+                       return;
+               }
+
+               if ( Hooks::isModelEnabled( 'damaging' ) ) {
+                       $request = $pager->getContext()->getRequest();
+
+                       Hooks::joinWithOresTables(
+                               'damaging',
+                               'rev_id',
+                               $query['tables'],
+                               $query['fields'],
+                               $query['join_conds']
+                       );
+
+                       Hooks::hideNonDamagingFilter(
+                               $query['fields'],
+                               $query['conds'],
+                               $request->getVal( 'hidenondamaging' ),
+                               $pager->getUser()
+                       );
+               }
+       }
+
+       public static function onSpecialContributionsFormatRowFlags(
+               RequestContext $context,
+               $row,
+               array &$flags
+       ) {
+               if ( !Hooks::oresUiEnabled( $context->getUser() ) ) {
+                       return;
+               }
+
+               // Doesn't have ores score, skipping.
+               if ( !isset( $row->ores_damaging_score ) ) {
+                       return;
+               }
+
+               Hooks::addRowData( $context, $row->rev_id, 
(float)$row->ores_damaging_score, 'damaging' );
+
+               if (
+                       Hooks::isDamagingFlagEnabled( $context ) &&
+                       $row->ores_damaging_score > 
$row->ores_damaging_threshold
+               ) {
+                       // Prepend the "r" flag
+                       array_unshift( $flags, ChangesList::flag( 'damaging' ) 
);
+               }
+       }
+
+       public static function onContributionsLineEnding(
+               ContribsPager $pager,
+               &$ret,
+               $row,
+               array &$classes
+       ) {
+               if ( !Hooks::oresUiEnabled( $pager->getUser() ) ) {
+                       return;
+               }
+
+               // Doesn't have ores score or threshold is not set properly, 
skipping.
+               if ( !isset( $row->ores_damaging_score ) || !isset( 
$row->ores_damaging_threshold ) ) {
+                       return;
+               }
+
+               if ( $row->ores_damaging_score > $row->ores_damaging_threshold 
) {
+                       if ( Hooks::isHighlightEnabled( $pager ) ) {
+                               $classes[] = 'ores-highlight';
+                       }
+                       if ( Hooks::isDamagingFlagEnabled( $pager ) ) {
+                               $classes[] = 'damaging';
+                       }
+               }
+       }
+
+       /**
+        * Hook into Special:Contributions filters
+        *
+        * @param SpecialContributions $page
+        * @param string[] &$filters HTML
+        */
+       public static function onSpecialContributionsGetFormFilters(
+               SpecialContributions $page,
+               array &$filters
+       ) {
+               if ( !Hooks::oresUiEnabled( $page->getUser() ) || 
!Hooks::isModelEnabled( 'damaging' ) ) {
+                       return;
+               }
+
+               $filters[] = Html::rawElement(
+                       'span',
+                       [ 'class' => 'mw-input-with-label' ],
+                       Xml::checkLabel(
+                               $page->msg( 'ores-hide-nondamaging-filter' 
)->text(),
+                               'hidenondamaging',
+                               'ores-hide-nondamaging',
+                               $page->getContext()->getRequest()->getVal( 
'hidenondamaging' ),
+                               [ 'class' => 'mw-input' ]
+                       )
+               );
+       }
+
+}
diff --git a/tests/phpunit/includes/Hooks/ChangesListHooksHandlerTest.php 
b/tests/phpunit/includes/Hooks/ChangesListHooksHandlerTest.php
new file mode 100644
index 0000000..3880651
--- /dev/null
+++ b/tests/phpunit/includes/Hooks/ChangesListHooksHandlerTest.php
@@ -0,0 +1,403 @@
+<?php
+
+namespace ORES\Tests\Hooks;
+
+use ChangesList;
+use Config;
+use EnhancedChangesList;
+use FauxRequest;
+use FormOptions;
+use IContextSource;
+use ORES\Hooks\ChangesListHooksHandler;
+use RCCacheEntry;
+use RecentChange;
+use RequestContext;
+use SpecialPage;
+use User;
+
+/**
+ * @group ORES
+ * @covers ORES\Hooks\ChangesListHooksHandler
+ */
+class ChangesListHooksHandlerTest extends \MediaWikiTestCase {
+
+       protected $user;
+
+       protected $context;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->setMwGlobals( [
+                       'wgOresFiltersThresholds' => [
+                               'damaging' => [
+                                       'maybebad' => [ 'min' => 0.16, 'max' => 
1 ],
+                                       'likelybad' => [ 'min' => 0.56, 'max' 
=> 1 ],
+                               ]
+                       ],
+                       'wgOresWikiId' => 'testwiki',
+               ] );
+
+               $this->user = static::getTestUser()->getUser();
+               $this->user->setOption( 'ores-enabled', 1 );
+               $this->user->setOption( 'oresDamagingPref', 'maybebad' );
+               $this->user->setOption( 'oresHighlight', 1 );
+               $this->user->setOption( 'ores-damaging-flag-rc', 1 );
+               $this->user->saveSettings();
+
+               $this->context = self::getContext( $this->user );
+       }
+
+       public function testOresRCObj() {
+               $row = new \stdClass();
+               $row->ores_damaging_threshold = 0.2;
+               $row->ores_damaging_score = 0.3;
+               $row->rc_patrolled = 0;
+               $row->rc_timestamp = '20150921134808';
+               $row->rc_deleted = 0;
+               $row->rc_comment = '';
+               $row->rc_comment_text = '';
+               $row->rc_comment_data = null;
+
+               $rc = RecentChange::newFromRow( $row );
+               $this->assertTrue( 
ChangesListHooksHandler::getScoreRecentChangesList( $rc, $this->context ) );
+
+               $row->ores_damaging_threshold = 0.4;
+               $rc = RecentChange::newFromRow( $row );
+               $this->assertFalse( 
ChangesListHooksHandler::getScoreRecentChangesList( $rc, $this->context ) );
+       }
+
+       /**
+        * @dataProvider onChangesListSpecialPageQuery_provider
+        */
+       public function testOnChangesListSpecialPageQuery( $modelConfig, 
$expectedQuery ) {
+               $this->setMwGlobals( [
+                       'wgUser' => $this->user,
+                       'wgOresModels' => $modelConfig
+               ] );
+               $tables = [];
+               $fields = [];
+               $conds = [];
+               $query_options = [];
+               $join_conds = [];
+               ChangesListHooksHandler::onChangesListSpecialPageQuery(
+                       '',
+                       $tables,
+                       $fields,
+                       $conds,
+                       $query_options,
+                       $join_conds,
+                       new FormOptions()
+               );
+               $this->assertSame( $expectedQuery['tables'], $tables );
+               $this->assertSame( $expectedQuery['fields'], $fields );
+               $this->assertSame( $expectedQuery['join_conds'], $join_conds );
+       }
+
+       public function onChangesListSpecialPageQuery_provider() {
+               return [
+                       [
+                               [ 'damaging' => false, 'goodfaith' => false ],
+                               [
+                                       'tables' => [],
+                                       'fields' => [],
+                                       'join_conds' => []
+                               ]
+                       ],
+                       [
+                               [ 'damaging' => true, 'goodfaith' => false ],
+                               [
+                                       'tables' => [
+                                               'ores_damaging_mdl' => 
'ores_model',
+                                               'ores_damaging_cls' => 
'ores_classification'
+                                       ],
+                                       'fields' => [
+                                               'ores_damaging_score' => 
'ores_damaging_cls.oresc_probability',
+                                       ],
+                                       'join_conds' => [
+                                               'ores_damaging_mdl' => [ 'LEFT 
JOIN',
+                                                       [
+                                                               
'ores_damaging_mdl.oresm_is_current' => 1,
+                                                               
'ores_damaging_mdl.oresm_name' => 'damaging'
+                                                       ]
+                                               ],
+                                               'ores_damaging_cls' => [ 'LEFT 
JOIN',
+                                                       [
+                                                               
'ores_damaging_cls.oresc_model = ores_damaging_mdl.oresm_id',
+                                                               'rc_this_oldid 
= ores_damaging_cls.oresc_rev',
+                                                               
'ores_damaging_cls.oresc_class' => 1
+                                                       ]
+                                               ]
+                                       ]
+                               ]
+                       ],
+                       [
+                               [ 'damaging' => false, 'goodfaith' => true ],
+                               [
+                                       'tables' => [
+                                               'ores_goodfaith_mdl' => 
'ores_model',
+                                               'ores_goodfaith_cls' => 
'ores_classification'
+                                       ],
+                                       'fields' => [
+                                               'ores_goodfaith_score' => 
'ores_goodfaith_cls.oresc_probability',
+                                       ],
+                                       'join_conds' => [
+                                               'ores_goodfaith_mdl' => [ 'LEFT 
JOIN',
+                                                       [
+                                                               
'ores_goodfaith_mdl.oresm_is_current' => 1,
+                                                               
'ores_goodfaith_mdl.oresm_name' => 'goodfaith'
+                                                       ]
+                                               ],
+                                               'ores_goodfaith_cls' => [ 'LEFT 
JOIN',
+                                                       [
+                                                               
'ores_goodfaith_cls.oresc_model = ores_goodfaith_mdl.oresm_id',
+                                                               'rc_this_oldid 
= ores_goodfaith_cls.oresc_rev',
+                                                               
'ores_goodfaith_cls.oresc_class' => 1
+                                                       ]
+                                               ]
+                                       ]
+                               ]
+                       ],
+                       [
+                               [ 'damaging' => true, 'goodfaith' => true ],
+                               [
+                                       'tables' => [
+                                               'ores_damaging_mdl' => 
'ores_model',
+                                               'ores_damaging_cls' => 
'ores_classification',
+                                               'ores_goodfaith_mdl' => 
'ores_model',
+                                               'ores_goodfaith_cls' => 
'ores_classification'
+                                       ],
+                                       'fields' => [
+                                               'ores_damaging_score' => 
'ores_damaging_cls.oresc_probability',
+                                               'ores_goodfaith_score' => 
'ores_goodfaith_cls.oresc_probability',
+                                       ],
+                                       'join_conds' => [
+                                               'ores_damaging_mdl' => [ 'LEFT 
JOIN',
+                                                       [
+                                                               
'ores_damaging_mdl.oresm_is_current' => 1,
+                                                               
'ores_damaging_mdl.oresm_name' => 'damaging'
+                                                       ]
+                                               ],
+                                               'ores_damaging_cls' => [ 'LEFT 
JOIN',
+                                                       [
+                                                               
'ores_damaging_cls.oresc_model = ores_damaging_mdl.oresm_id',
+                                                               'rc_this_oldid 
= ores_damaging_cls.oresc_rev',
+                                                               
'ores_damaging_cls.oresc_class' => 1
+                                                       ]
+                                               ],
+                                               'ores_goodfaith_mdl' => [ 'LEFT 
JOIN',
+                                                       [
+                                                               
'ores_goodfaith_mdl.oresm_is_current' => 1,
+                                                               
'ores_goodfaith_mdl.oresm_name' => 'goodfaith'
+                                                       ]
+                                               ],
+                                               'ores_goodfaith_cls' => [ 'LEFT 
JOIN',
+                                                       [
+                                                               
'ores_goodfaith_cls.oresc_model = ores_goodfaith_mdl.oresm_id',
+                                                               'rc_this_oldid 
= ores_goodfaith_cls.oresc_rev',
+                                                               
'ores_goodfaith_cls.oresc_class' => 1
+                                                       ]
+                                               ]
+                                       ]
+                               ]
+                       ]
+               ];
+       }
+
+       public function testOnEnhancedChangesListModifyLineDataDamaging() {
+               $row = new \stdClass();
+               $row->ores_damaging_threshold = 0.2;
+               $row->ores_damaging_score = 0.3;
+               $row->rc_patrolled = 0;
+               $row->rc_timestamp = '20150921134808';
+               $row->rc_deleted = 0;
+               $row->rc_comment = '';
+               $row->rc_comment_text = '';
+               $row->rc_comment_data = null;
+               $rc = RecentChange::newFromRow( $row );
+               $rc = RCCacheEntry::newFromParent( $rc );
+
+               $ecl = $this->getMockBuilder( EnhancedChangesList::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $ecl->expects( $this->any() )
+                       ->method( 'getUser' )
+                       ->will( $this->returnValue( $this->user ) );
+
+               $ecl->expects( $this->any() )
+                       ->method( 'getContext' )
+                       ->will( $this->returnValue( $this->context ) );
+
+               $data = [];
+               $block = [];
+               $classes = [];
+
+               ChangesListHooksHandler::onEnhancedChangesListModifyLineData(
+                       $ecl,
+                       $data,
+                       $block,
+                       $rc,
+                       $classes
+               );
+
+               $this->assertSame( [ 'recentChangesFlags' => [ 'damaging' => 
true ] ], $data );
+               $this->assertSame( [], $block );
+               $this->assertSame( [ 'damaging' ], $classes );
+       }
+
+       public function testOnEnhancedChangesListModifyLineDataNonDamaging() {
+               $row = new \stdClass();
+               $row->ores_damaging_threshold = 0.4;
+               $row->ores_damaging_score = 0.3;
+               $row->rc_patrolled = 0;
+               $row->rc_timestamp = '20150921134808';
+               $row->rc_deleted = 0;
+               $row->rc_comment = '';
+               $row->rc_comment_text = '';
+               $row->rc_comment_data = null;
+               $rc = RecentChange::newFromRow( $row );
+               $rc = RCCacheEntry::newFromParent( $rc );
+
+               $ecl = $this->getMockBuilder( EnhancedChangesList::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $ecl->expects( $this->any() )
+                       ->method( 'getUser' )
+                       ->will( $this->returnValue( $this->user ) );
+
+               $ecl->expects( $this->any() )
+                       ->method( 'getTitle' )
+                       ->will( $this->returnValue( SpecialPage::getTitleFor( 
'Recentchanges' ) ) );
+
+               $ecl->expects( $this->any() )
+                       ->method( 'getContext' )
+                       ->will( $this->returnValue( $this->context ) );
+
+               $data = [];
+               $block = [];
+               $classes = [];
+
+               ChangesListHooksHandler::onEnhancedChangesListModifyLineData(
+                       $ecl,
+                       $data,
+                       $block,
+                       $rc,
+                       $classes
+               );
+
+               $this->assertSame( [], $data );
+               $this->assertSame( [], $block );
+               $this->assertSame( [], $classes );
+       }
+
+       public function testOnOldChangesListModifyLineDataDamaging() {
+               $row = new \stdClass();
+               $row->ores_damaging_threshold = 0.2;
+               $row->ores_damaging_score = 0.3;
+               $row->rc_patrolled = 0;
+               $row->rc_timestamp = '20150921134808';
+               $row->rc_deleted = 0;
+               $row->rc_comment = '';
+               $row->rc_comment_text = '';
+               $row->rc_comment_data = null;
+               $rc = RecentChange::newFromRow( $row );
+               $rc = RCCacheEntry::newFromParent( $rc );
+
+               $config = $this->getMockBuilder( Config::class )->getMock();
+               $config->expects( $this->any() )
+                       ->method( 'get' )
+                       ->will( $this->returnValue( false ) );
+
+               $cl = $this->getMockBuilder( ChangesList::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $cl->expects( $this->any() )
+                       ->method( 'getUser' )
+                       ->will( $this->returnValue( $this->user ) );
+
+               $cl->expects( $this->any() )
+                       ->method( 'getRequest' )
+                       ->will( $this->returnValue( new FauxRequest() ) );
+
+               $cl->expects( $this->any() )
+                       ->method( 'getConfig' )
+                       ->will( $this->returnValue( $config ) );
+
+               $cl->expects( $this->any() )
+                       ->method( 'getTitle' )
+                       ->will( $this->returnValue( SpecialPage::getTitleFor( 
'Recentchanges' ) ) );
+
+               $cl->expects( $this->any() )
+                       ->method( 'getContext' )
+                       ->will( $this->returnValue( $this->context ) );
+
+               $classes = [];
+
+               $s = ' <span class="mw-changeslist-separator">. .</span> ';
+               ChangesListHooksHandler::onOldChangesListRecentChangesLine( 
$cl, $s, $rc, $classes );
+
+               $this->assertSame(
+                       ' <span class="mw-changeslist-separator">. .</span>' .
+                       ' <abbr class="ores-damaging" title="This edit needs 
review">r</abbr>',
+                       $s
+               );
+               $this->assertSame( [ 'ores-highlight', 'damaging' ], $classes );
+       }
+
+       public function testOnOldChangesListModifyLineDataNonDamaging() {
+               $row = new \stdClass();
+               $row->ores_damaging_threshold = 0.4;
+               $row->ores_damaging_score = 0.3;
+               $row->rc_patrolled = 0;
+               $row->rc_timestamp = '20150921134808';
+               $row->rc_deleted = 0;
+               $row->rc_comment = '';
+               $row->rc_comment_text = '';
+               $row->rc_comment_data = null;
+               $rc = RecentChange::newFromRow( $row );
+               $rc = RCCacheEntry::newFromParent( $rc );
+
+               $cl = $this->getMockBuilder( ChangesList::class )
+                       ->disableOriginalConstructor()
+                       ->getMock();
+
+               $cl->expects( $this->any() )
+                       ->method( 'getUser' )
+                       ->will( $this->returnValue( $this->user ) );
+
+               $cl->expects( $this->any() )
+                       ->method( 'getTitle' )
+                       ->will( $this->returnValue( SpecialPage::getTitleFor( 
'Recentchanges' ) ) );
+
+               $cl->expects( $this->any() )
+                       ->method( 'getContext' )
+                       ->will( $this->returnValue( $this->context ) );
+
+               $classes = [];
+
+               $s = ' <span class="mw-changeslist-separator">. .</span> ';
+               ChangesListHooksHandler::onOldChangesListRecentChangesLine( 
$cl, $s, $rc, $classes );
+
+               $this->assertSame( ' <span class="mw-changeslist-separator">. 
.</span> ', $s );
+               $this->assertSame( [], $classes );
+       }
+
+       /**
+        * @param User $user
+        *
+        * @return IContextSource
+        */
+       private static function getContext( User $user ) {
+               $context = new RequestContext();
+
+               $context->setLanguage( 'en' );
+               $context->setUser( $user );
+               $context->setTitle( SpecialPage::getTitleFor( 'Recentchanges' ) 
);
+
+               return $context;
+       }
+
+}
diff --git a/tests/phpunit/includes/Hooks/ContributionsHookHandlerTest.php 
b/tests/phpunit/includes/Hooks/ContributionsHookHandlerTest.php
new file mode 100644
index 0000000..77fc6fb
--- /dev/null
+++ b/tests/phpunit/includes/Hooks/ContributionsHookHandlerTest.php
@@ -0,0 +1,224 @@
+<?php
+
+namespace ORES\Tests;
+
+use ContribsPager;
+use IContextSource;
+use ORES\Hooks\ContributionsHooksHandler;
+use RequestContext;
+use SpecialPage;
+use User;
+
+/**
+ * @group ORES
+ * @covers ORES\Hooks\ContributionsHooksHandler
+ */
+class ContributionsHookHandlerTest extends \MediaWikiTestCase {
+
+       protected $user;
+
+       protected $context;
+
+       protected function setUp() {
+               parent::setUp();
+
+               $this->setMwGlobals( [
+                       'wgOresFiltersThresholds' => [
+                               'damaging' => [
+                                       'maybebad' => [ 'min' => 0.16, 'max' => 
1 ],
+                                       'likelybad' => [ 'min' => 0.56, 'max' 
=> 1 ],
+                               ]
+                       ],
+                       'wgOresWikiId' => 'testwiki',
+               ] );
+
+               $this->user = static::getTestUser()->getUser();
+               $this->user->setOption( 'ores-enabled', 1 );
+               $this->user->setOption( 'oresDamagingPref', 'maybebad' );
+               $this->user->setOption( 'oresHighlight', 1 );
+               $this->user->setOption( 'ores-damaging-flag-rc', 1 );
+               $this->user->saveSettings();
+
+               $this->context = self::getContext( $this->user );
+       }
+
+       /**
+        * @param User $user
+        *
+        * @return IContextSource
+        */
+       private static function getContext( User $user ) {
+               $context = new RequestContext();
+
+               $context->setLanguage( 'en' );
+               $context->setUser( $user );
+               $context->setTitle( SpecialPage::getTitleFor( 'Recentchanges' ) 
);
+
+               return $context;
+       }
+
+       public function provideOnContribsGetQueryInfo() {
+               $expected = [
+                       'tables' => [
+                               'ores_damaging_mdl' => 'ores_model',
+                               'ores_damaging_cls' => 'ores_classification'
+                       ],
+                       'fields' => [
+                               'ores_damaging_score' => 
'ores_damaging_cls.oresc_probability',
+                               'ores_damaging_threshold' => "'0.16'"
+                       ],
+                       'conds' => [],
+                       'join_conds' => [
+                               'ores_damaging_mdl' => [
+                                       'LEFT JOIN',
+                                       [
+                                               
'ores_damaging_mdl.oresm_is_current' => 1,
+                                               'ores_damaging_mdl.oresm_name' 
=> 'damaging'
+                                       ]
+                               ],
+                               'ores_damaging_cls' => [
+                                       'LEFT JOIN',
+                                       [
+                                               'ores_damaging_cls.oresc_model 
= ores_damaging_mdl.oresm_id',
+                                               'rev_id = 
ores_damaging_cls.oresc_rev',
+                                               'ores_damaging_cls.oresc_class' 
=> 1
+                                       ]
+                               ]
+                       ],
+               ];
+
+               $expectedDamaging = $expected;
+               $expectedDamaging['conds'] = [ 
'ores_damaging_cls.oresc_probability > \'0.16\'' ];
+
+               return [
+                       'all' => [ $expected, false ],
+                       'damaging only' => [ $expectedDamaging, true ]
+               ];
+       }
+
+       /**
+        * @dataProvider provideOnContribsGetQueryInfo
+        */
+       public function testOnContribsGetQueryInfo( array $expected, 
$nonDamaging ) {
+               $cp =
+                       $this->getMockBuilder( ContribsPager::class 
)->disableOriginalConstructor()->getMock();
+
+               $cp->expects( $this->any() )
+                       ->method( 'getUser' )
+                       ->will( $this->returnValue( $this->user ) );
+
+               $cp->expects( $this->any() )
+                       ->method( 'getTitle' )
+                       ->will( $this->returnValue( SpecialPage::getTitleFor( 
'Contributions' ) ) );
+
+               $cp->expects( $this->any() )
+                       ->method( 'getContext' )
+                       ->will( $this->returnValue( $this->context ) );
+
+               if ( $nonDamaging === true ) {
+                       $this->context->getRequest()->setVal( 
'hidenondamaging', true );
+               }
+
+               $query = [
+                       'tables' => [],
+                       'fields' => [],
+                       'conds' => [],
+                       'options' => [],
+                       'join_conds' => [],
+               ];
+               ContributionsHooksHandler::onContribsGetQueryInfo( $cp, $query 
);
+
+               $this->assertSame( $expected['tables'], $query['tables'] );
+               $this->assertSame( $expected['fields'], $query['fields'] );
+               $this->assertSame( $expected['conds'], $query['conds'] );
+               $this->assertSame( $expected['join_conds'], 
$query['join_conds'] );
+       }
+
+       public function testOnSpecialContributionsFormatRowFlagsDamaging() {
+               $row = new \stdClass();
+               $row->ores_damaging_threshold = 0.2;
+               $row->ores_damaging_score = 0.3;
+               $row->rev_id = 0;
+
+               $flags = [];
+
+               
ContributionsHooksHandler::onSpecialContributionsFormatRowFlags( 
$this->context, $row, $flags );
+
+               $this->assertSame( [ '<abbr class="ores-damaging" title="This 
edit needs review">r</abbr>' ],
+                       $flags );
+       }
+
+       public function testOnSpecialContributionsFormatRowFlagsNonDamaging() {
+               $row = new \stdClass();
+               $row->ores_damaging_threshold = 0.4;
+               $row->ores_damaging_score = 0.3;
+               $row->rev_id = 0;
+
+               $flags = [];
+
+               
ContributionsHooksHandler::onSpecialContributionsFormatRowFlags( 
$this->context, $row, $flags );
+
+               $this->assertSame( [], $flags );
+       }
+
+       public function testOnContributionsLineEndingDamaging() {
+               $cp =
+                       $this->getMockBuilder( ContribsPager::class 
)->disableOriginalConstructor()->getMock();
+
+               $cp->expects( $this->any() )
+                       ->method( 'getUser' )
+                       ->will( $this->returnValue( $this->user ) );
+
+               $cp->expects( $this->any() )
+                       ->method( 'getTitle' )
+                       ->will( $this->returnValue( SpecialPage::getTitleFor( 
'Contributions' ) ) );
+
+               $cp->expects( $this->any() )
+                       ->method( 'getContext' )
+                       ->will( $this->returnValue( $this->context ) );
+
+               $row = new \stdClass();
+               $row->ores_damaging_threshold = 0.2;
+               $row->ores_damaging_score = 0.3;
+               $row->rev_id = 0;
+
+               $ret = [];
+               $classes = [];
+
+               ContributionsHooksHandler::onContributionsLineEnding( $cp, 
$ret, $row, $classes );
+
+               $this->assertSame( [ 'ores-highlight', 'damaging' ], $classes );
+               $this->assertSame( [], $ret );
+       }
+
+       public function testOnContributionsLineEndingNonDamaging() {
+               $cp =
+                       $this->getMockBuilder( ContribsPager::class 
)->disableOriginalConstructor()->getMock();
+
+               $cp->expects( $this->any() )
+                       ->method( 'getUser' )
+                       ->will( $this->returnValue( $this->user ) );
+
+               $cp->expects( $this->any() )
+                       ->method( 'getTitle' )
+                       ->will( $this->returnValue( SpecialPage::getTitleFor( 
'Contributions' ) ) );
+
+               $cp->expects( $this->any() )
+                       ->method( 'getContext' )
+                       ->will( $this->returnValue( $this->context ) );
+
+               $row = new \stdClass();
+               $row->ores_damaging_threshold = 0.4;
+               $row->ores_damaging_score = 0.3;
+               $row->rev_id = 0;
+
+               $ret = [];
+               $classes = [];
+
+               ContributionsHooksHandler::onContributionsLineEnding( $cp, 
$ret, $row, $classes );
+
+               $this->assertSame( [], $classes );
+               $this->assertSame( [], $ret );
+       }
+
+}
diff --git a/tests/phpunit/includes/HooksTest.php 
b/tests/phpunit/includes/HooksTest.php
index 7dff0a8..fd374a2 100644
--- a/tests/phpunit/includes/HooksTest.php
+++ b/tests/phpunit/includes/HooksTest.php
@@ -2,24 +2,12 @@
 
 namespace ORES\Tests;
 
-use ChangesList;
-use Config;
-use ContribsPager;
-use EnhancedChangesList;
-use EventRelayerNull;
-use FauxRequest;
-use FormOptions;
-use HashBagOStuff;
 use IContextSource;
-use MediaWiki\MediaWikiServices;
-use ORES;
+use ORES\Hooks;
 use ORES\Hooks\PreferencesHookHandler;
-use RCCacheEntry;
-use RecentChange;
 use RequestContext;
 use SpecialPage;
 use User;
-use WANObjectCache;
 
 /**
  * @group ORES
@@ -66,512 +54,15 @@
                $this->user->setOption( 'oresDamagingPref', 'maybebad' );
                $this->assertEquals(
                        0.16,
-                       ORES\Hooks::getThreshold( 'damaging', $this->user )
+                       Hooks::getThreshold( 'damaging', $this->user )
                );
 
                // b/c
                $this->user->setOption( 'oresDamagingPref', 'soft' );
                $this->assertEquals(
                        0.56,
-                       ORES\Hooks::getThreshold( 'damaging', $this->user )
+                       Hooks::getThreshold( 'damaging', $this->user )
                );
-       }
-
-       public function testOresRCObj() {
-               $row = new \stdClass();
-               $row->ores_damaging_threshold = 0.2;
-               $row->ores_damaging_score = 0.3;
-               $row->rc_patrolled = 0;
-               $row->rc_timestamp = '20150921134808';
-               $row->rc_deleted = 0;
-               $row->rc_comment = '';
-               $row->rc_comment_text = '';
-               $row->rc_comment_data = null;
-
-               $rc = RecentChange::newFromRow( $row );
-               $this->assertTrue( ORES\Hooks::getScoreRecentChangesList( $rc, 
$this->context ) );
-
-               $row->ores_damaging_threshold = 0.4;
-               $rc = RecentChange::newFromRow( $row );
-               $this->assertFalse( ORES\Hooks::getScoreRecentChangesList( $rc, 
$this->context ) );
-       }
-
-       /**
-        * @dataProvider onChangesListSpecialPageQuery_provider
-        */
-       public function testOnChangesListSpecialPageQuery( $modelConfig, 
$expectedQuery ) {
-               $this->setMwGlobals( [
-                       'wgUser' => $this->user,
-                       'wgOresModels' => $modelConfig
-               ] );
-               $tables = [];
-               $fields = [];
-               $conds = [];
-               $query_options = [];
-               $join_conds = [];
-               ORES\Hooks::onChangesListSpecialPageQuery(
-                       '',
-                       $tables,
-                       $fields,
-                       $conds,
-                       $query_options,
-                       $join_conds,
-                       new FormOptions()
-               );
-               $this->assertSame( $expectedQuery['tables'], $tables );
-               $this->assertSame( $expectedQuery['fields'], $fields );
-               $this->assertSame( $expectedQuery['join_conds'], $join_conds );
-       }
-
-       public function onChangesListSpecialPageQuery_provider() {
-               return [
-                       [
-                               [ 'damaging' => false, 'goodfaith' => false ],
-                               [
-                                       'tables' => [],
-                                       'fields' => [],
-                                       'join_conds' => []
-                               ]
-                       ],
-                       [
-                               [ 'damaging' => true, 'goodfaith' => false ],
-                               [
-                                       'tables' => [
-                                               'ores_damaging_mdl' => 
'ores_model',
-                                               'ores_damaging_cls' => 
'ores_classification'
-                                       ],
-                                       'fields' => [
-                                               'ores_damaging_score' => 
'ores_damaging_cls.oresc_probability',
-                                       ],
-                                       'join_conds' => [
-                                               'ores_damaging_mdl' => [ 'LEFT 
JOIN',
-                                                       [
-                                                               
'ores_damaging_mdl.oresm_is_current' => 1,
-                                                               
'ores_damaging_mdl.oresm_name' => 'damaging'
-                                                       ]
-                                               ],
-                                               'ores_damaging_cls' => [ 'LEFT 
JOIN',
-                                                       [
-                                                               
'ores_damaging_cls.oresc_model = ores_damaging_mdl.oresm_id',
-                                                               'rc_this_oldid 
= ores_damaging_cls.oresc_rev',
-                                                               
'ores_damaging_cls.oresc_class' => 1
-                                                       ]
-                                               ]
-                                       ]
-                               ]
-                       ],
-                       [
-                               [ 'damaging' => false, 'goodfaith' => true ],
-                               [
-                                       'tables' => [
-                                               'ores_goodfaith_mdl' => 
'ores_model',
-                                               'ores_goodfaith_cls' => 
'ores_classification'
-                                       ],
-                                       'fields' => [
-                                               'ores_goodfaith_score' => 
'ores_goodfaith_cls.oresc_probability',
-                                       ],
-                                       'join_conds' => [
-                                               'ores_goodfaith_mdl' => [ 'LEFT 
JOIN',
-                                                       [
-                                                               
'ores_goodfaith_mdl.oresm_is_current' => 1,
-                                                               
'ores_goodfaith_mdl.oresm_name' => 'goodfaith'
-                                                       ]
-                                               ],
-                                               'ores_goodfaith_cls' => [ 'LEFT 
JOIN',
-                                                       [
-                                                               
'ores_goodfaith_cls.oresc_model = ores_goodfaith_mdl.oresm_id',
-                                                               'rc_this_oldid 
= ores_goodfaith_cls.oresc_rev',
-                                                               
'ores_goodfaith_cls.oresc_class' => 1
-                                                       ]
-                                               ]
-                                       ]
-                               ]
-                       ],
-                       [
-                               [ 'damaging' => true, 'goodfaith' => true ],
-                               [
-                                       'tables' => [
-                                               'ores_damaging_mdl' => 
'ores_model',
-                                               'ores_damaging_cls' => 
'ores_classification',
-                                               'ores_goodfaith_mdl' => 
'ores_model',
-                                               'ores_goodfaith_cls' => 
'ores_classification'
-                                       ],
-                                       'fields' => [
-                                               'ores_damaging_score' => 
'ores_damaging_cls.oresc_probability',
-                                               'ores_goodfaith_score' => 
'ores_goodfaith_cls.oresc_probability',
-                                       ],
-                                       'join_conds' => [
-                                               'ores_damaging_mdl' => [ 'LEFT 
JOIN',
-                                                       [
-                                                               
'ores_damaging_mdl.oresm_is_current' => 1,
-                                                               
'ores_damaging_mdl.oresm_name' => 'damaging'
-                                                       ]
-                                               ],
-                                               'ores_damaging_cls' => [ 'LEFT 
JOIN',
-                                                       [
-                                                               
'ores_damaging_cls.oresc_model = ores_damaging_mdl.oresm_id',
-                                                               'rc_this_oldid 
= ores_damaging_cls.oresc_rev',
-                                                               
'ores_damaging_cls.oresc_class' => 1
-                                                       ]
-                                               ],
-                                               'ores_goodfaith_mdl' => [ 'LEFT 
JOIN',
-                                                       [
-                                                               
'ores_goodfaith_mdl.oresm_is_current' => 1,
-                                                               
'ores_goodfaith_mdl.oresm_name' => 'goodfaith'
-                                                       ]
-                                               ],
-                                               'ores_goodfaith_cls' => [ 'LEFT 
JOIN',
-                                                       [
-                                                               
'ores_goodfaith_cls.oresc_model = ores_goodfaith_mdl.oresm_id',
-                                                               'rc_this_oldid 
= ores_goodfaith_cls.oresc_rev',
-                                                               
'ores_goodfaith_cls.oresc_class' => 1
-                                                       ]
-                                               ]
-                                       ]
-                               ]
-                       ]
-               ];
-       }
-
-       public function testOnEnhancedChangesListModifyLineDataDamaging() {
-               $row = new \stdClass();
-               $row->ores_damaging_threshold = 0.2;
-               $row->ores_damaging_score = 0.3;
-               $row->rc_patrolled = 0;
-               $row->rc_timestamp = '20150921134808';
-               $row->rc_deleted = 0;
-               $row->rc_comment = '';
-               $row->rc_comment_text = '';
-               $row->rc_comment_data = null;
-               $rc = RecentChange::newFromRow( $row );
-               $rc = RCCacheEntry::newFromParent( $rc );
-
-               $ecl = $this->getMockBuilder( EnhancedChangesList::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $ecl->expects( $this->any() )
-                       ->method( 'getUser' )
-                       ->will( $this->returnValue( $this->user ) );
-
-               $ecl->expects( $this->any() )
-                       ->method( 'getContext' )
-                       ->will( $this->returnValue( $this->context ) );
-
-               $data = [];
-               $block = [];
-               $classes = [];
-
-               ORES\Hooks::onEnhancedChangesListModifyLineData( $ecl, $data, 
$block, $rc, $classes );
-
-               $this->assertSame( [ 'recentChangesFlags' => [ 'damaging' => 
true ] ], $data );
-               $this->assertSame( [], $block );
-               $this->assertSame( [ 'damaging' ], $classes );
-       }
-
-       public function testOnEnhancedChangesListModifyLineDataNonDamaging() {
-               $row = new \stdClass();
-               $row->ores_damaging_threshold = 0.4;
-               $row->ores_damaging_score = 0.3;
-               $row->rc_patrolled = 0;
-               $row->rc_timestamp = '20150921134808';
-               $row->rc_deleted = 0;
-               $row->rc_comment = '';
-               $row->rc_comment_text = '';
-               $row->rc_comment_data = null;
-               $rc = RecentChange::newFromRow( $row );
-               $rc = RCCacheEntry::newFromParent( $rc );
-
-               $ecl = $this->getMockBuilder( EnhancedChangesList::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $ecl->expects( $this->any() )
-                       ->method( 'getUser' )
-                       ->will( $this->returnValue( $this->user ) );
-
-               $ecl->expects( $this->any() )
-                       ->method( 'getTitle' )
-                       ->will( $this->returnValue( SpecialPage::getTitleFor( 
'Recentchanges' ) ) );
-
-               $ecl->expects( $this->any() )
-                       ->method( 'getContext' )
-                       ->will( $this->returnValue( $this->context ) );
-
-               $data = [];
-               $block = [];
-               $classes = [];
-
-               ORES\Hooks::onEnhancedChangesListModifyLineData( $ecl, $data, 
$block, $rc, $classes );
-
-               $this->assertSame( [], $data );
-               $this->assertSame( [], $block );
-               $this->assertSame( [], $classes );
-       }
-
-       public function testOnOldChangesListModifyLineDataDamaging() {
-               $row = new \stdClass();
-               $row->ores_damaging_threshold = 0.2;
-               $row->ores_damaging_score = 0.3;
-               $row->rc_patrolled = 0;
-               $row->rc_timestamp = '20150921134808';
-               $row->rc_deleted = 0;
-               $row->rc_comment = '';
-               $row->rc_comment_text = '';
-               $row->rc_comment_data = null;
-               $rc = RecentChange::newFromRow( $row );
-               $rc = RCCacheEntry::newFromParent( $rc );
-
-               $config = $this->getMockBuilder( Config::class )->getMock();
-               $config->expects( $this->any() )
-                       ->method( 'get' )
-                       ->will( $this->returnValue( false ) );
-
-               $cl = $this->getMockBuilder( ChangesList::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $cl->expects( $this->any() )
-                       ->method( 'getUser' )
-                       ->will( $this->returnValue( $this->user ) );
-
-               $cl->expects( $this->any() )
-                       ->method( 'getRequest' )
-                       ->will( $this->returnValue( new FauxRequest() ) );
-
-               $cl->expects( $this->any() )
-                       ->method( 'getConfig' )
-                       ->will( $this->returnValue( $config ) );
-
-               $cl->expects( $this->any() )
-                       ->method( 'getTitle' )
-                       ->will( $this->returnValue( SpecialPage::getTitleFor( 
'Recentchanges' ) ) );
-
-               $cl->expects( $this->any() )
-                       ->method( 'getContext' )
-                       ->will( $this->returnValue( $this->context ) );
-
-               $classes = [];
-
-               $s = ' <span class="mw-changeslist-separator">. .</span> ';
-               ORES\Hooks::onOldChangesListRecentChangesLine( $cl, $s, $rc, 
$classes );
-
-               $this->assertSame(
-                       ' <span class="mw-changeslist-separator">. .</span>' .
-                       ' <abbr class="ores-damaging" title="This edit needs 
review">r</abbr>',
-                       $s
-               );
-               $this->assertSame( [ 'ores-highlight', 'damaging' ], $classes );
-       }
-
-       public function testOnOldChangesListModifyLineDataNonDamaging() {
-               $row = new \stdClass();
-               $row->ores_damaging_threshold = 0.4;
-               $row->ores_damaging_score = 0.3;
-               $row->rc_patrolled = 0;
-               $row->rc_timestamp = '20150921134808';
-               $row->rc_deleted = 0;
-               $row->rc_comment = '';
-               $row->rc_comment_text = '';
-               $row->rc_comment_data = null;
-               $rc = RecentChange::newFromRow( $row );
-               $rc = RCCacheEntry::newFromParent( $rc );
-
-               $cl = $this->getMockBuilder( ChangesList::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $cl->expects( $this->any() )
-                       ->method( 'getUser' )
-                       ->will( $this->returnValue( $this->user ) );
-
-               $cl->expects( $this->any() )
-                       ->method( 'getTitle' )
-                       ->will( $this->returnValue( SpecialPage::getTitleFor( 
'Recentchanges' ) ) );
-
-               $cl->expects( $this->any() )
-                       ->method( 'getContext' )
-                       ->will( $this->returnValue( $this->context ) );
-
-               $classes = [];
-
-               $s = ' <span class="mw-changeslist-separator">. .</span> ';
-               ORES\Hooks::onOldChangesListRecentChangesLine( $cl, $s, $rc, 
$classes );
-
-               $this->assertSame( ' <span class="mw-changeslist-separator">. 
.</span> ', $s );
-               $this->assertSame( [], $classes );
-       }
-
-       public function provideOnContribsGetQueryInfo() {
-               $expected = [
-                       'tables' => [
-                               'ores_damaging_mdl' => 'ores_model',
-                               'ores_damaging_cls' => 'ores_classification'
-                       ],
-                       'fields' => [
-                               'ores_damaging_score' => 
'ores_damaging_cls.oresc_probability',
-                               'ores_damaging_threshold' => "'0.16'"
-                       ],
-                       'conds' => [],
-                       'join_conds' => [
-                               'ores_damaging_mdl' => [
-                                       'LEFT JOIN',
-                                       [
-                                               
'ores_damaging_mdl.oresm_is_current' => 1,
-                                               'ores_damaging_mdl.oresm_name' 
=> 'damaging'
-                                       ]
-                               ],
-                               'ores_damaging_cls' => [
-                                       'LEFT JOIN',
-                                       [
-                                               'ores_damaging_cls.oresc_model 
= ores_damaging_mdl.oresm_id',
-                                               'rev_id = 
ores_damaging_cls.oresc_rev',
-                                               'ores_damaging_cls.oresc_class' 
=> 1
-                                       ]
-                               ]
-                       ],
-               ];
-
-               $expectedDamaging = $expected;
-               $expectedDamaging['conds'] = [ 
'ores_damaging_cls.oresc_probability > \'0.16\'' ];
-
-               return [
-                       'all' => [ $expected, false ],
-                       'damaging only' => [ $expectedDamaging, true ]
-               ];
-       }
-
-       /**
-        * @dataProvider provideOnContribsGetQueryInfo
-        */
-       public function testOnContribsGetQueryInfo( array $expected, 
$nonDamaging ) {
-               $cp = $this->getMockBuilder( ContribsPager::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $cp->expects( $this->any() )
-                       ->method( 'getUser' )
-                       ->will( $this->returnValue( $this->user ) );
-
-               $cp->expects( $this->any() )
-                       ->method( 'getTitle' )
-                       ->will( $this->returnValue( SpecialPage::getTitleFor( 
'Contributions' ) ) );
-
-               $cp->expects( $this->any() )
-                       ->method( 'getContext' )
-                       ->will( $this->returnValue( $this->context ) );
-
-               if ( $nonDamaging === true ) {
-                       $this->context->getRequest()->setVal( 
'hidenondamaging', true );
-               }
-
-               $query = [
-                       'tables' => [],
-                       'fields' => [],
-                       'conds' => [],
-                       'options' => [],
-                       'join_conds' => [],
-               ];
-               ORES\Hooks::onContribsGetQueryInfo(
-                       $cp,
-                       $query
-               );
-
-               $this->assertSame( $expected['tables'], $query['tables'] );
-               $this->assertSame( $expected['fields'], $query['fields'] );
-               $this->assertSame( $expected['conds'], $query['conds'] );
-               $this->assertSame( $expected['join_conds'], 
$query['join_conds'] );
-       }
-
-       public function testOnSpecialContributionsFormatRowFlagsDamaging() {
-               $row = new \stdClass();
-               $row->ores_damaging_threshold = 0.2;
-               $row->ores_damaging_score = 0.3;
-               $row->rev_id = 0;
-
-               $flags = [];
-
-               ORES\Hooks::onSpecialContributionsFormatRowFlags( 
$this->context, $row, $flags );
-
-               $this->assertSame(
-                       [ '<abbr class="ores-damaging" title="This edit needs 
review">r</abbr>' ],
-                       $flags
-               );
-       }
-
-       public function testOnSpecialContributionsFormatRowFlagsNonDamaging() {
-               $row = new \stdClass();
-               $row->ores_damaging_threshold = 0.4;
-               $row->ores_damaging_score = 0.3;
-               $row->rev_id = 0;
-
-               $flags = [];
-
-               ORES\Hooks::onSpecialContributionsFormatRowFlags( 
$this->context, $row, $flags );
-
-               $this->assertSame( [], $flags );
-       }
-
-       public function testOnContributionsLineEndingDamaging() {
-               $cp = $this->getMockBuilder( ContribsPager::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $cp->expects( $this->any() )
-                       ->method( 'getUser' )
-                       ->will( $this->returnValue( $this->user ) );
-
-               $cp->expects( $this->any() )
-                       ->method( 'getTitle' )
-                       ->will( $this->returnValue( SpecialPage::getTitleFor( 
'Contributions' ) ) );
-
-               $cp->expects( $this->any() )
-                       ->method( 'getContext' )
-                       ->will( $this->returnValue( $this->context ) );
-
-               $row = new \stdClass();
-               $row->ores_damaging_threshold = 0.2;
-               $row->ores_damaging_score = 0.3;
-               $row->rev_id = 0;
-
-               $ret = [];
-               $classes = [];
-
-               ORES\Hooks::onContributionsLineEnding( $cp, $ret, $row, 
$classes );
-
-               $this->assertSame( [ 'ores-highlight', 'damaging' ], $classes );
-               $this->assertSame( [], $ret );
-       }
-
-       public function testOnContributionsLineEndingNonDamaging() {
-               $cp = $this->getMockBuilder( ContribsPager::class )
-                       ->disableOriginalConstructor()
-                       ->getMock();
-
-               $cp->expects( $this->any() )
-                       ->method( 'getUser' )
-                       ->will( $this->returnValue( $this->user ) );
-
-               $cp->expects( $this->any() )
-                       ->method( 'getTitle' )
-                       ->will( $this->returnValue( SpecialPage::getTitleFor( 
'Contributions' ) ) );
-
-               $cp->expects( $this->any() )
-                       ->method( 'getContext' )
-                       ->will( $this->returnValue( $this->context ) );
-
-               $row = new \stdClass();
-               $row->ores_damaging_threshold = 0.4;
-               $row->ores_damaging_score = 0.3;
-               $row->rev_id = 0;
-
-               $ret = [];
-               $classes = [];
-
-               ORES\Hooks::onContributionsLineEnding( $cp, $ret, $row, 
$classes );
-
-               $this->assertSame( [], $classes );
-               $this->assertSame( [], $ret );
        }
 
        public function testOnGetPreferencesEnabled() {
@@ -584,7 +75,7 @@
        public function testOnGetBetaFeaturePreferences_on() {
                $this->setMwGlobals( 'wgOresExtensionStatus', 'on' );
                $prefs = [];
-               ORES\Hooks::onGetBetaFeaturePreferences( $this->user, $prefs );
+               Hooks::onGetBetaFeaturePreferences( $this->user, $prefs );
 
                $this->assertSame( 0, count( $prefs ) );
        }
@@ -592,7 +83,7 @@
        public function testOnGetBetaFeaturePreferences_off() {
                $this->setMwGlobals( 'wgOresExtensionStatus', 'off' );
                $prefs = [];
-               ORES\Hooks::onGetBetaFeaturePreferences( $this->user, $prefs );
+               Hooks::onGetBetaFeaturePreferences( $this->user, $prefs );
 
                $this->assertSame( 0, count( $prefs ) );
        }
@@ -600,7 +91,7 @@
        public function testOnGetBetaFeaturePreferences_beta() {
                $this->setMwGlobals( 'wgOresExtensionStatus', 'beta' );
                $prefs = [];
-               ORES\Hooks::onGetBetaFeaturePreferences( $this->user, $prefs );
+               Hooks::onGetBetaFeaturePreferences( $this->user, $prefs );
 
                $this->assertSame( 1, count( $prefs ) );
                $this->assertArrayHasKey( 'ores-enabled', $prefs );
@@ -619,36 +110,6 @@
                $context->setTitle( SpecialPage::getTitleFor( 'Recentchanges' ) 
);
 
                return $context;
-       }
-
-       private function mockStatsInCache() {
-               $cache = new WANObjectCache( [
-                       'cache' => new HashBagOStuff(),
-                       'pool' => 'testcache-hash',
-                       'relayer' => new EventRelayerNull( [] )
-               ] );
-
-               $invalidStats = [ 'will trigger the use of' => 'default values' 
];
-
-               $cache->set(
-                       $cache->makeKey( 'ORES', 'test_stats', 'damaging' ),
-                       $invalidStats,
-                       \WANObjectCache::TTL_DAY
-               );
-
-               $cache->set(
-                       $cache->makeKey( 'ORES', 'test_stats', 'goodfaith' ),
-                       $invalidStats,
-                       \WANObjectCache::TTL_DAY
-               );
-
-               MediaWikiServices::getInstance()->resetServiceForTesting( 
'MainWANObjectCache' );
-               MediaWikiServices::getInstance()->redefineService(
-                       'MainWANObjectCache',
-                       function () use ( $cache ) {
-                               return $cache;
-                       }
-               );
        }
 
 }

-- 
To view, visit https://gerrit.wikimedia.org/r/392415
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: I35247553444d595330f149c6c5b1f29165ee1e40
Gerrit-PatchSet: 2
Gerrit-Project: mediawiki/extensions/ORES
Gerrit-Branch: master
Gerrit-Owner: Ladsgroup <ladsgr...@gmail.com>
Gerrit-Reviewer: Awight <awi...@wikimedia.org>
Gerrit-Reviewer: Thiemo Mättig (WMDE) <thiemo.kr...@wikimedia.de>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
MediaWiki-commits@lists.wikimedia.org
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to