Add $wgContribScoreIgnoreMinorEdits, bug fixes

A comprehensive list of changes made in this commit is provided in the
README for this fork.
This commit is contained in:
2026-07-10 13:05:29 -04:00
parent ebe2898f76
commit a7f94ae854
3 changed files with 116 additions and 18 deletions

26
README
View File

@@ -1,2 +1,28 @@
The Contribution Scores extension polls the wiki database to locate contributors with the highest contribution volume. 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. 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.

View File

@@ -1,7 +1,7 @@
{ {
"name": "ContributionScores", "name": "ContributionScores",
"author": "Tim Laqua", "author": "Tim Laqua (modified by Eunakria)",
"url": "https://www.mediawiki.org/wiki/Extension:Contribution_Scores", "url": "https://git.eunakria.com/Eunakria/mediawiki-extensions-ContributionScores",
"descriptionmsg": "contributionscores-desc", "descriptionmsg": "contributionscores-desc",
"version": "1.26.1", "version": "1.26.1",
"type": "specialpage", "type": "specialpage",
@@ -53,7 +53,11 @@
}, },
"ContribScoreUseRoughEditCount": { "ContribScoreUseRoughEditCount": {
"value": false, "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": { "ContribScoreCacheTTL": {
"value": 30, "value": 30,

View File

@@ -16,6 +16,7 @@ use MediaWiki\User\ActorMigration;
* *
* @ingroup Extensions * @ingroup Extensions
* @author Tim Laqua <t.laqua@gmail.com> * @author Tim Laqua <t.laqua@gmail.com>
* @author Eunakria <eunakria@gmail.com>
*/ */
class ContributionScores extends IncludableSpecialPage { class ContributionScores extends IncludableSpecialPage {
const CONTRIBUTIONSCORES_MAXINCLUDELIMIT = 50; const CONTRIBUTIONSCORES_MAXINCLUDELIMIT = 50;
@@ -28,8 +29,19 @@ class ContributionScores extends IncludableSpecialPage {
$parser->setFunctionHook( 'cscore', [ self::class, 'efContributionScoresRender' ] ); $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' ) { public static function efContributionScoresRender( $parser, $usertext, $metric = 'score' ) {
global $wgContribScoreDisableCache, $wgContribScoreUseRoughEditCount; global $wgContribScoreDisableCache, $wgContribScoreIgnoreMinorEdits;
if ( $wgContribScoreDisableCache ) { if ( $wgContribScoreDisableCache ) {
$parser->getOutput()->updateCacheExpiry( 0 ); $parser->getOutput()->updateCacheExpiry( 0 );
@@ -41,37 +53,81 @@ class ContributionScores extends IncludableSpecialPage {
if ( $user instanceof User && $user->isRegistered() ) { if ( $user instanceof User && $user->isRegistered() ) {
global $wgLang; 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 ); $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' ) { if ( $metric == 'score' ) {
$row = $dbr->selectRow( $row = $dbr->selectRow(
[ 'revision' ] + $revWhere['tables'], $revTables,
[ 'wiki_rank' => "COUNT(DISTINCT rev_page)+SQRT($revVar-COUNT(DISTINCT rev_page))*2" ], [ 'wiki_rank' => $wikiRankSql ],
$revWhere['conds'], $revConds,
__METHOD__, __METHOD__,
[], [],
$revWhere['joins'] $revJoins
); );
$output = $wgLang->formatNum( round( $row->wiki_rank, 0 ) ); $output = $wgLang->formatNum( round( $row->wiki_rank, 0 ) );
} elseif ( $metric == 'changes' ) { } elseif ( $metric == 'changes' ) {
$row = $dbr->selectRow( $row = $dbr->selectRow(
[ 'revision' ] + $revWhere['tables'], $revTables,
[ 'rev_count' => $revVar ], [ 'rev_count' => $revVar ],
$revWhere['conds'], $revConds,
__METHOD__, __METHOD__,
[], [],
$revWhere['joins'] $revJoins
); );
$output = $wgLang->formatNum( $row->rev_count ); $output = $wgLang->formatNum( $row->rev_count );
} elseif ( $metric == 'pages' ) { } elseif ( $metric == 'pages' ) {
$row = $dbr->selectRow( $row = $dbr->selectRow(
[ 'revision' ] + $revWhere['tables'], $revTables,
[ 'page_count' => 'COUNT(DISTINCT rev_page)' ], [ 'page_count' => 'COUNT(DISTINCT rev_page)' ],
$revWhere['conds'], $revConds,
__METHOD__, __METHOD__,
[], [],
$revWhere['joins'] $revJoins
); );
$output = $wgLang->formatNum( $row->page_count ); $output = $wgLang->formatNum( $row->page_count );
} else { } else {
@@ -92,7 +148,7 @@ class ContributionScores extends IncludableSpecialPage {
*/ */
public static function getContributionScoreData( $days, $limit ) { public static function getContributionScoreData( $days, $limit ) {
global $wgContribScoreIgnoreBots, $wgContribScoreIgnoreBlockedUsers, $wgContribScoreIgnoreUsernames, global $wgContribScoreIgnoreBots, $wgContribScoreIgnoreBlockedUsers, $wgContribScoreIgnoreUsernames,
$wgContribScoreUseRoughEditCount; $wgContribScoreIgnoreMinorEdits;
$loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer(); $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
$dbr = $loadBalancer->getConnection( DB_REPLICA ); $dbr = $loadBalancer->getConnection( DB_REPLICA );
@@ -105,6 +161,10 @@ class ContributionScores extends IncludableSpecialPage {
$sqlWhere = []; $sqlWhere = [];
if ( $wgContribScoreIgnoreMinorEdits ) {
$sqlWhere[] = 'rev_minor_edit = 0';
}
if ( $days > 0 ) { if ( $days > 0 ) {
$date = time() - ( 60 * 60 * 24 * $days ); $date = time() - ( 60 * 60 * 24 * $days );
$sqlWhere[] = 'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $date ) ); $sqlWhere[] = 'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $date ) );
@@ -114,7 +174,7 @@ class ContributionScores extends IncludableSpecialPage {
'rev_user' => $revUser, 'rev_user' => $revUser,
'page_count' => 'COUNT(DISTINCT rev_page)' 'page_count' => 'COUNT(DISTINCT rev_page)'
]; ];
if ( $wgContribScoreUseRoughEditCount ) { if ( self::shouldUseRoughEditCount() ) {
$revQuery['tables'][] = 'user'; $revQuery['tables'][] = 'user';
$revQuery['joins']['user'] = [ 'LEFT JOIN', [ "$revUser != 0", "user_id = $revUser" ] ]; $revQuery['joins']['user'] = [ 'LEFT JOIN', [ "$revUser != 0", "user_id = $revUser" ] ];
$sqlVars['rev_count'] = 'user_editcount'; $sqlVars['rev_count'] = 'user_editcount';
@@ -186,6 +246,14 @@ class ContributionScores extends IncludableSpecialPage {
$revQuery['joins'] $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 ); $sqlMostPagesOrRevs = $dbr->unionQueries( [ $sqlMostPages, $sqlMostRevs ], false );
$res = $dbr->select( $res = $dbr->select(
[ [
@@ -198,7 +266,7 @@ class ContributionScores extends IncludableSpecialPage {
'user_real_name', 'user_real_name',
'page_count', 'page_count',
'rev_count', 'rev_count',
'wiki_rank' => 'page_count+SQRT(rev_count-page_count)*2', 'wiki_rank' => $wikiRankSql,
], ],
[], [],
__METHOD__, __METHOD__,