diff --git a/README b/README index 38dd23a..1cea2b0 100644 --- a/README +++ b/README @@ -1,2 +1,28 @@ The Contribution Scores extension polls the wiki database to locate contributors with the highest contribution volume. The extension is intended to add a fun metric for contributors to see how much they are helping out. + +--- TEAR HERE --- + +This is a modified version of Extension:Contribution Scores, the original source +code for which can be found here: https://www.mediawiki.org/wiki/Extension:Contribution_Scores +The rest of this document discussed changes made to this copy of the extension. + +This fork developed by Eunakria the config option $wgContribScoreIgnoreMinorEdits, +which is mutually exclusive with $wgContribScoreUseRoughEditCount. + +In addition to providing the aforementioned bug fixes, this fork also fixes two +other unwanted behaviors exhibited by the original extension: + +1. When $wgContribScoreUseRoughEditCount was enabled, the extension would query + the user_editcount field of the user table but not actually join on the user + table, in the parser function {{#cscore}}. This was presumably never noticed + since this parser function is not frequently used. + +2. On some databases, there was a potential for integer underflow when + subtracting unsigned values in both the parser function and contribution + score data. This could occur if $wgContribScoreUseRoughEditCount was enabled + and edits went uncounted in user_editcount, and was resolved by clamping the + right-hand side preimage of subtraction. + +There are probably more bugs I don't know about, both in the original upstream +code *and* my code! In other words, caveat emptor. diff --git a/extension.json b/extension.json index 61f2efe..1c9c6cd 100644 --- a/extension.json +++ b/extension.json @@ -1,7 +1,7 @@ { "name": "ContributionScores", - "author": "Tim Laqua", - "url": "https://www.mediawiki.org/wiki/Extension:Contribution_Scores", + "author": "Tim Laqua (modified by Eunakria)", + "url": "https://git.eunakria.com/Eunakria/mediawiki-extensions-ContributionScores", "descriptionmsg": "contributionscores-desc", "version": "1.26.1", "type": "specialpage", @@ -53,7 +53,11 @@ }, "ContribScoreUseRoughEditCount": { "value": false, - "description": "Set to true to use the rough number of edits in user table, for performance issue." + "description": "Set to true to use the rough number of edits in user table, for performance issue. Mutually exclusive with $ContribScoreIgnoreMinorEdits; this option will be disabled if the other is enabled." + }, + "ContribScoreIgnoreMinorEdits": { + "value": false, + "description": "Set to true to ignore minor edits. Mutually exclusive with $wgContribScoreUseRoughEditCount; if this is enabled, the other option will be disabled." }, "ContribScoreCacheTTL": { "value": 30, diff --git a/src/ContributionScores.php b/src/ContributionScores.php index 7acf0ef..f1456d2 100644 --- a/src/ContributionScores.php +++ b/src/ContributionScores.php @@ -16,6 +16,7 @@ use MediaWiki\User\ActorMigration; * * @ingroup Extensions * @author Tim Laqua + * @author Eunakria */ class ContributionScores extends IncludableSpecialPage { const CONTRIBUTIONSCORES_MAXINCLUDELIMIT = 50; @@ -28,8 +29,19 @@ class ContributionScores extends IncludableSpecialPage { $parser->setFunctionHook( 'cscore', [ self::class, 'efContributionScoresRender' ] ); } + /** + * See extension.json; this function returns whether or not to use the + * "rough edit count" (user_editcount, stored in the user table). Since + * ignoring minor edits is mutually exclusive with this and takes priority, + * we check here. + */ + private static function shouldUseRoughEditCount(): bool { + global $wgContribScoreIgnoreMinorEdits, $wgContribScoreUseRoughEditCount; + return $wgContribScoreUseRoughEditCount && !$wgContribScoreIgnoreMinorEdits; + } + public static function efContributionScoresRender( $parser, $usertext, $metric = 'score' ) { - global $wgContribScoreDisableCache, $wgContribScoreUseRoughEditCount; + global $wgContribScoreDisableCache, $wgContribScoreIgnoreMinorEdits; if ( $wgContribScoreDisableCache ) { $parser->getOutput()->updateCacheExpiry( 0 ); @@ -41,37 +53,81 @@ class ContributionScores extends IncludableSpecialPage { if ( $user instanceof User && $user->isRegistered() ) { global $wgLang; - $revVar = $wgContribScoreUseRoughEditCount ? 'user_editcount' : 'COUNT(rev_id)'; + $revVar = self::shouldUseRoughEditCount() ? 'user_editcount' : 'COUNT(rev_id)'; $revWhere = ActorMigration::newMigration()->getWhere( $dbr, 'rev_user', $user ); + $revConds = (array)$revWhere['conds']; + if ( $wgContribScoreIgnoreMinorEdits ) { + $revConds['rev_minor_edit'] = 0; + } + + # HACK: The original ContributionScores doesn't actually join on + # user despite querying on user... again not our fault, but + # definitely our problem. What a mess... + $revTables = array_merge([ 'revision' ], $revWhere['tables']); + $revJoins = $revWhere['joins']; + if ( self::shouldUseRoughEditCount() && !in_array( 'user', $revTables ) ) { + # Not familiar enough with MediaWiki to know if this is the + # right way to do things, but it seems like it. The + # ActorMigration class is deprecated and I think I'm supposed + # to just join on the bare names? + $revTables[] = 'actor'; + $revTables[] = 'user'; + $revJoins['actor'] = [ 'JOIN', 'rev_actor = actor_id' ]; + $revJoins['user'] = [ 'JOIN', 'actor_user = user_id' ]; + } + + # HACK: This is not our fault (this bug exists in the original + # ContributionScores as well!) but in cases where the rough edit + # count underestimates the number of revisions, on MariaDB the + # unsigned subtraction can underflow, which will throw an error. + # So we have to clamp the subtraction rhs. + # + # Confused by buildLeast's API? Yeah, me too. It eventually + # dispatches to buildSuperlative, which looks like this: + # https://doc.wikimedia.org/mediawiki-core/master/php/SQLPlatform_8php_source.html#l00127 + # The way to smuggle in an existing expression is by passing it as + # an array element with a non-integer key to the $fields argument, + # which is what we do here. + # + # Of course the use of buildLeast is necessitated by the fact that + # MariaDB and Postgres call it "LEAST", but SQLite calls it "MIN" + # (a more intuitive name, but the same as the aggregate function). + # + $clampedRhsSql = $dbr->buildLeast ( + /* $fields: */ [ 'lhs' => $revVar, 'rhs' => 'COUNT(DISTINCT rev_page)' ], + /* $values: */ [] + ); + $wikiRankSql = "COUNT(DISTINCT rev_page) + SQRT($revVar-$clampedRhsSql) * 2"; + if ( $metric == 'score' ) { $row = $dbr->selectRow( - [ 'revision' ] + $revWhere['tables'], - [ 'wiki_rank' => "COUNT(DISTINCT rev_page)+SQRT($revVar-COUNT(DISTINCT rev_page))*2" ], - $revWhere['conds'], + $revTables, + [ 'wiki_rank' => $wikiRankSql ], + $revConds, __METHOD__, [], - $revWhere['joins'] + $revJoins ); $output = $wgLang->formatNum( round( $row->wiki_rank, 0 ) ); } elseif ( $metric == 'changes' ) { $row = $dbr->selectRow( - [ 'revision' ] + $revWhere['tables'], + $revTables, [ 'rev_count' => $revVar ], - $revWhere['conds'], + $revConds, __METHOD__, [], - $revWhere['joins'] + $revJoins ); $output = $wgLang->formatNum( $row->rev_count ); } elseif ( $metric == 'pages' ) { $row = $dbr->selectRow( - [ 'revision' ] + $revWhere['tables'], + $revTables, [ 'page_count' => 'COUNT(DISTINCT rev_page)' ], - $revWhere['conds'], + $revConds, __METHOD__, [], - $revWhere['joins'] + $revJoins ); $output = $wgLang->formatNum( $row->page_count ); } else { @@ -92,7 +148,7 @@ class ContributionScores extends IncludableSpecialPage { */ public static function getContributionScoreData( $days, $limit ) { global $wgContribScoreIgnoreBots, $wgContribScoreIgnoreBlockedUsers, $wgContribScoreIgnoreUsernames, - $wgContribScoreUseRoughEditCount; + $wgContribScoreIgnoreMinorEdits; $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer(); $dbr = $loadBalancer->getConnection( DB_REPLICA ); @@ -105,6 +161,10 @@ class ContributionScores extends IncludableSpecialPage { $sqlWhere = []; + if ( $wgContribScoreIgnoreMinorEdits ) { + $sqlWhere[] = 'rev_minor_edit = 0'; + } + if ( $days > 0 ) { $date = time() - ( 60 * 60 * 24 * $days ); $sqlWhere[] = 'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $date ) ); @@ -114,7 +174,7 @@ class ContributionScores extends IncludableSpecialPage { 'rev_user' => $revUser, 'page_count' => 'COUNT(DISTINCT rev_page)' ]; - if ( $wgContribScoreUseRoughEditCount ) { + if ( self::shouldUseRoughEditCount() ) { $revQuery['tables'][] = 'user'; $revQuery['joins']['user'] = [ 'LEFT JOIN', [ "$revUser != 0", "user_id = $revUser" ] ]; $sqlVars['rev_count'] = 'user_editcount'; @@ -186,6 +246,14 @@ class ContributionScores extends IncludableSpecialPage { $revQuery['joins'] ); + # HACK: See previous comment on the use of buildLeast. Not our fault, + # but still our problem... + $clampedRhsSql = $dbr->buildLeast( + /* $fields: */ [ 'rev_count', 'page_count' ], + /* $values: */ [] + ); + $wikiRankSql = "page_count+SQRT(rev_count-$clampedRhsSql)*2"; + $sqlMostPagesOrRevs = $dbr->unionQueries( [ $sqlMostPages, $sqlMostRevs ], false ); $res = $dbr->select( [ @@ -198,7 +266,7 @@ class ContributionScores extends IncludableSpecialPage { 'user_real_name', 'page_count', 'rev_count', - 'wiki_rank' => 'page_count+SQRT(rev_count-page_count)*2', + 'wiki_rank' => $wikiRankSql, ], [], __METHOD__,