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