Add basic IP range support to Special:Contributions

This works by using the new table introduced with T156318.

The only thing that differs from normal Special:Contribs is we are
showing the IP address next to each entry. This is it how it is
displayed if you request to see newbie contributions:
https://en.wikipedia.org/wiki/Special:Contributions?contribs=newbie

For the time being, Special:DeletedContributions does not support
IP ranges. Various other irrelevant links such as Uploads and Logs
are also hidden.

Refer to P4725 for a way to automate creation of edits by random
IPs in your dev environment.

IP::isValidBlock() has been deprecated with this dependent change:
https://gerrit.wikimedia.org/r/#/c/373165/

Bug: T163562
Change-Id: Ice1bdae3d16cf365da14c6df0e8d91d2b914e064
This commit is contained in:
MusikAnimal 2017-04-21 12:17:59 -04:00
parent f884d157f7
commit d09554b6ef
24 changed files with 535 additions and 35 deletions

View file

@ -25,6 +25,8 @@ section).
to plain class names, using the 'factory' key in the module description
array. This allows dependency injection to be used for ResourceLoader modules.
* $wgExceptionHooks has been removed.
* (T163562) $wgRangeContributionsCIDRLimit was introduced to control the size
of IP ranges that can be queried at Special:Contributions.
* (T45547) $wgUsePigLatinVariant added (off by default).
* (T152540) MediaWiki now supports a section ID escaping style that allows to display
non-Latin characters verbatim on many modern browsers. This is controlled by the
@ -44,6 +46,8 @@ section).
* (T37247) Output from Parser::parse() will now be wrapped in a div with
class="mw-parser-output" by default. This may be changed or disabled using
ParserOptions::setWrapOutputClass().
* (T163562) Added ability to search for contributions within an IP ranges
at Special:Contributions.
* Added 'ChangeTagsAllowedAdd' hook, enabling extensions to allow software-
specific tags to be added by users.
* Added a 'ParserOptionsRegister' hook to allow extensions to register

View file

@ -1117,6 +1117,7 @@ $wgAutoloadLocalClasses = [
'PopulateFilearchiveSha1' => __DIR__ . '/maintenance/populateFilearchiveSha1.php',
'PopulateImageSha1' => __DIR__ . '/maintenance/populateImageSha1.php',
'PopulateInterwiki' => __DIR__ . '/maintenance/populateInterwiki.php',
'PopulateIpChanges' => __DIR__ . '/maintenance/populateIpChanges.php',
'PopulateLogSearch' => __DIR__ . '/maintenance/populateLogSearch.php',
'PopulateLogUsertext' => __DIR__ . '/maintenance/populateLogUsertext.php',
'PopulatePPSortKey' => __DIR__ . '/maintenance/populatePPSortKey.php',

View file

@ -8727,6 +8727,18 @@ $wgCSPFalsePositiveUrls = [
'https://ad.lkqd.net/vpaid/vpaid.js' => true,
];
/**
* Shortest CIDR limits that can be checked in any individual range check
* at Special:Contributions.
*
* @var array
* @since 1.30
*/
$wgRangeContributionsCIDRLimit = [
'IPv4' => 16,
'IPv6' => 32,
];
/**
* The following variables define 3 user experience levels:
*

View file

@ -1401,7 +1401,7 @@ class Revision implements IDBAccessObject {
*
* @param IDatabase $dbw (master connection)
* @throws MWException
* @return int
* @return int The revision ID
*/
public function insertOn( $dbw ) {
global $wgDefaultExternalStore, $wgContentHandlerUseDB;
@ -1518,6 +1518,16 @@ class Revision implements IDBAccessObject {
);
}
// Insert IP revision into ip_changes for use when querying for a range.
if ( $this->mUser === 0 && IP::isValid( $this->mUserText ) ) {
$ipcRow = [
'ipc_rev_id' => $this->mId,
'ipc_rev_timestamp' => $row['rev_timestamp'],
'ipc_hex' => IP::toHex( $row['rev_user_text'] ),
];
$dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
}
// Avoid PHP 7.1 warning of passing $this by reference
$revision = $this;
Hooks::run( 'RevisionInsertComplete', [ &$revision, $data, $flags ] );

View file

@ -83,7 +83,8 @@ abstract class DatabaseUpdater {
FixDefaultJsonContentPages::class,
CleanupEmptyCategories::class,
AddRFCAndPMIDInterwiki::class,
PopulatePPSortKey::class
PopulatePPSortKey::class,
PopulateIpChanges::class,
];
/**

View file

@ -735,6 +735,17 @@ class PageArchive {
] );
$revision->insertOn( $dbw );
// Also restore reference to the revision in ip_changes if it was an IP edit.
if ( (int)$row->ar_rev_id === 0 && IP::isValid( $row->ar_user_text ) ) {
$ipcRow = [
'ipc_rev_id' => $row->ar_rev_id,
'ipc_rev_timestamp' => $row->ar_timestamp,
'ipc_hex' => IP::toHex( $row->ar_user_text ),
];
$dbw->insert( 'ip_changes', $ipcRow, __METHOD__ );
}
$restored++;
Hooks::run( 'ArticleRevisionUndeleted',

View file

@ -2833,9 +2833,14 @@ class WikiPage implements Page, IDBAccessObject {
'FOR UPDATE',
$commentQuery['joins']
);
// Build their equivalent archive rows
$rowsInsert = [];
$revids = [];
/** @var int[] Revision IDs of edits that were made by IPs */
$ipRevIds = [];
foreach ( $res as $row ) {
$comment = $revCommentStore->getComment( $row );
$rowInsert = [
@ -2861,6 +2866,12 @@ class WikiPage implements Page, IDBAccessObject {
}
$rowsInsert[] = $rowInsert;
$revids[] = $row->rev_id;
// Keep track of IP edits, so that the corresponding rows can
// be deleted in the ip_changes table.
if ( (int)$row->rev_user === 0 && IP::isValid( $row->rev_user_text ) ) {
$ipRevIds[] = $row->rev_id;
}
}
// Copy them into the archive table
$dbw->insert( 'archive', $rowsInsert, __METHOD__ );
@ -2879,6 +2890,11 @@ class WikiPage implements Page, IDBAccessObject {
$dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
}
// Also delete records from ip_changes as applicable.
if ( count( $ipRevIds ) > 0 ) {
$dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
}
// Log the deletion, if the page was suppressed, put it in the suppression log instead
$logtype = $suppress ? 'suppress' : 'delete';

View file

@ -103,7 +103,12 @@ class SpecialContributions extends IncludableSpecialPage {
'pagetitle',
$this->msg( 'contributions-title', $target )->plain()
)->inContentLanguage() );
$this->getSkin()->setRelevantUser( $userObj );
# For IP ranges, we want the contributionsSub, but not the skin-dependent
# links under 'Tools', which may include irrelevant links like 'Logs'.
if ( !IP::isValidRange( $target ) ) {
$this->getSkin()->setRelevantUser( $userObj );
}
} else {
$out->addSubtitle( $this->msg( 'sp-contributions-newbies-sub' ) );
$out->setHTMLTitle( $this->msg(
@ -206,7 +211,12 @@ class SpecialContributions extends IncludableSpecialPage {
'associated' => $this->opts['associated'],
] );
if ( !$pager->getNumRows() ) {
if ( IP::isValidRange( $target ) && !$pager->isQueryableRange( $target ) ) {
// Valid range, but outside CIDR limit.
$limits = $this->getConfig()->get( 'RangeContributionsCIDRLimit' );
$limit = $limits[ IP::isIPv4( $target ) ? 'IPv4' : 'IPv6' ];
$out->addWikiMsg( 'sp-contributions-outofrange', $limit );
} elseif ( !$pager->getNumRows() ) {
$out->addWikiMsg( 'nocontribs', $target );
} else {
# Show a message about replica DB lag, if applicable
@ -223,11 +233,14 @@ class SpecialContributions extends IncludableSpecialPage {
}
$out->addHTML( $output );
}
$out->preventClickjacking( $pager->getPreventClickjacking() );
# Show the appropriate "footer" message - WHOIS tools, etc.
if ( $this->opts['contribs'] == 'newbie' ) {
$message = 'sp-contributions-footer-newbies';
} elseif ( IP::isValidRange( $target ) ) {
$message = 'sp-contributions-footer-anon-range';
} elseif ( IP::isIPAddress( $target ) ) {
$message = 'sp-contributions-footer-anon';
} elseif ( $userObj->isAnon() ) {
@ -258,8 +271,11 @@ class SpecialContributions extends IncludableSpecialPage {
*/
protected function contributionsSub( $userObj ) {
if ( $userObj->isAnon() ) {
// Show a warning message that the user being searched for doesn't exists
if ( !User::isIP( $userObj->getName() ) ) {
// Show a warning message that the user being searched for doesn't exists.
// User::isIP returns true for IP address and usemod IPs like '123.123.123.xxx',
// but returns false for IP ranges. We don't want to suggest either of these are
// valid usernames which we would with the 'contributions-userdoesnotexist' message.
if ( !User::isIP( $userObj->getName() ) && !$userObj->isIPRange() ) {
$this->getOutput()->wrapWikiMsg(
"<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>",
[
@ -286,7 +302,13 @@ class SpecialContributions extends IncludableSpecialPage {
// Do not expose the autoblocks, since that may lead to a leak of accounts' IPs,
// and also this will display a totally irrelevant log entry as a current block.
if ( !$this->including() ) {
$block = Block::newFromTarget( $userObj, $userObj );
// For IP ranges you must give Block::newFromTarget the CIDR string and not a user object.
if ( $userObj->isIPRange() ) {
$block = Block::newFromTarget( $userObj->getName(), $userObj->getName() );
} else {
$block = Block::newFromTarget( $userObj, $userObj );
}
if ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
if ( $block->getType() == Block::TYPE_RANGE ) {
$nt = MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget();
@ -332,10 +354,14 @@ class SpecialContributions extends IncludableSpecialPage {
$talkpage = $target->getTalkPage();
$linkRenderer = $sp->getLinkRenderer();
$tools['user-talk'] = $linkRenderer->makeLink(
$talkpage,
$sp->msg( 'sp-contributions-talk' )->text()
);
# No talk pages for IP ranges.
if ( !IP::isValidRange( $username ) ) {
$tools['user-talk'] = $linkRenderer->makeLink(
$talkpage,
$sp->msg( 'sp-contributions-talk' )->text()
);
}
if ( ( $id !== null ) || ( $id === null && IP::isIPAddress( $username ) ) ) {
if ( $sp->getUser()->isAllowed( 'block' ) ) { # Block / Change block / Unblock links
@ -374,24 +400,28 @@ class SpecialContributions extends IncludableSpecialPage {
);
}
}
# Uploads
$tools['uploads'] = $linkRenderer->makeKnownLink(
SpecialPage::getTitleFor( 'Listfiles', $username ),
$sp->msg( 'sp-contributions-uploads' )->text()
);
# Other logs link
$tools['logs'] = $linkRenderer->makeKnownLink(
SpecialPage::getTitleFor( 'Log', $username ),
$sp->msg( 'sp-contributions-logs' )->text()
);
# Add link to deleted user contributions for priviledged users
if ( $sp->getUser()->isAllowed( 'deletedhistory' ) ) {
$tools['deletedcontribs'] = $linkRenderer->makeKnownLink(
SpecialPage::getTitleFor( 'DeletedContributions', $username ),
$sp->msg( 'sp-contributions-deleted', $username )->text()
# Don't show some links for IP ranges
if ( !IP::isValidRange( $username ) ) {
# Uploads
$tools['uploads'] = $linkRenderer->makeKnownLink(
SpecialPage::getTitleFor( 'Listfiles', $username ),
$sp->msg( 'sp-contributions-uploads' )->text()
);
# Other logs link
$tools['logs'] = $linkRenderer->makeKnownLink(
SpecialPage::getTitleFor( 'Log', $username ),
$sp->msg( 'sp-contributions-logs' )->text()
);
# Add link to deleted user contributions for priviledged users
if ( $sp->getUser()->isAllowed( 'deletedhistory' ) ) {
$tools['deletedcontribs'] = $linkRenderer->makeKnownLink(
SpecialPage::getTitleFor( 'DeletedContributions', $username ),
$sp->msg( 'sp-contributions-deleted', $username )->text()
);
}
}
# Add a link to change user rights for privileged users

View file

@ -87,6 +87,10 @@ class ContribsPager extends RangeChronologicalPager {
}
$this->getDateRangeCond( $startTimestamp, $endTimestamp );
// This property on IndexPager is set by $this->getIndexField() in parent::__construct().
// We need to reassign it here so that it is used when the actual query is ran.
$this->mIndexField = $this->getIndexField();
// Most of this code will use the 'contributions' group DB, which can map to replica DBs
// with extra user based indexes or partioning by user. The additional metadata
// queries should use a regular replica DB since the lookup pattern is not all by user.
@ -207,6 +211,12 @@ class ContribsPager extends RangeChronologicalPager {
'join_conds' => $join_cond
];
// For IPv6, we use ipc_rev_timestamp on ip_changes as the index field,
// which will be referenced when parsing the results of a query.
if ( self::isQueryableRange( $this->target ) ) {
$queryInfo['fields'][] = 'ipc_rev_timestamp';
}
ChangeTags::modifyDisplayQuery(
$queryInfo['tables'],
$queryInfo['fields'],
@ -257,8 +267,18 @@ class ContribsPager extends RangeChronologicalPager {
$condition['rev_user'] = $uid;
$index = 'user_timestamp';
} else {
$condition['rev_user_text'] = $this->target;
$index = 'usertext_timestamp';
$ipRangeConds = $this->getIpRangeConds( $this->mDb, $this->target );
if ( $ipRangeConds ) {
$tables[] = 'ip_changes';
$join_conds['ip_changes'] = [
'LEFT JOIN', [ 'ipc_rev_id = rev_id' ]
];
$condition[] = $ipRangeConds;
} else {
$condition['rev_user_text'] = $this->target;
$index = 'usertext_timestamp';
}
}
}
@ -305,8 +325,57 @@ class ContribsPager extends RangeChronologicalPager {
return [];
}
function getIndexField() {
return 'rev_timestamp';
/**
* Get SQL conditions for an IP range, if applicable
* @param IDatabase $db
* @param string $ip The IP address or CIDR
* @return string|false SQL for valid IP ranges, false if invalid
*/
private function getIpRangeConds( $db, $ip ) {
// First make sure it is a valid range and they are not outside the CIDR limit
if ( !$this->isQueryableRange( $ip ) ) {
return false;
}
list( $start, $end ) = IP::parseRange( $ip );
return 'ipc_hex BETWEEN ' . $db->addQuotes( $start ) . ' AND ' . $db->addQuotes( $end );
}
/**
* Is the given IP a range and within the CIDR limit?
*
* @param string $ipRange
* @return bool True if it is valid
* @since 1.30
*/
public function isQueryableRange( $ipRange ) {
$limits = $this->getConfig()->get( 'RangeContributionsCIDRLimit' );
$bits = IP::parseCIDR( $ipRange )[1];
if (
( $bits === false ) ||
( IP::isIPv4( $ipRange ) && $bits < $limits['IPv4'] ) ||
( IP::isIPv6( $ipRange ) && $bits < $limits['IPv6'] )
) {
return false;
}
return true;
}
/**
* Override of getIndexField() in IndexPager.
* For IP ranges, it's faster to use the replicated ipc_rev_timestamp
* on the `ip_changes` table than the rev_timestamp on the `revision` table.
* @return string Name of field
*/
public function getIndexField() {
if ( self::isQueryableRange( $this->target ) ) {
return 'ipc_rev_timestamp';
} else {
return 'rev_timestamp';
}
}
function doBatchLookups() {
@ -400,6 +469,7 @@ class ContribsPager extends RangeChronologicalPager {
# Mark current revisions
$topmarktext = '';
$user = $this->getUser();
if ( $row->rev_id === $row->page_latest ) {
$topmarktext .= '<span class="mw-uctop">' . $this->messages['uctop'] . '</span>';
$classes[] = 'mw-contributions-current';
@ -473,8 +543,10 @@ class ContribsPager extends RangeChronologicalPager {
# Show user names for /newbies as there may be different users.
# Note that only unprivileged users have rows with hidden user names excluded.
# When querying for an IP range, we want to always show user and user talk links.
$userlink = '';
if ( $this->contribs == 'newbie' && !$rev->isDeleted( Revision::DELETED_USER ) ) {
if ( ( $this->contribs == 'newbie' && !$rev->isDeleted( Revision::DELETED_USER ) )
|| self::isQueryableRange( $this->target ) ) {
$userlink = ' . . ' . $lang->getDirMark()
. Linker::userLink( $rev->getUser(), $rev->getUserText() );
$userlink .= ' ' . $this->msg( 'parentheses' )->rawParams(

View file

@ -827,6 +827,16 @@ class User implements IDBAccessObject {
|| IP::isIPv6( $name );
}
/**
* Is the user an IP range?
*
* @since 1.30
* @return bool
*/
public function isIPRange() {
return IP::isValidRange( $this->mName );
}
/**
* Is the input a valid username?
*

View file

@ -2459,7 +2459,9 @@
"sp-contributions-explain": "",
"sp-contributions-footer": "-",
"sp-contributions-footer-anon": "-",
"sp-contributions-footer-anon-range": "-",
"sp-contributions-footer-newbies": "-",
"sp-contributions-outofrange": "Unable to show any results. The requested IP range is larger than the CIDR limit of /$1.",
"whatlinkshere": "What links here",
"whatlinkshere-title": "Pages that link to \"$1\"",
"whatlinkshere-summary": "",

View file

@ -91,6 +91,7 @@
"Mormegil",
"Mpradeep",
"Murma174",
"MusikAnimal",
"Najami",
"Naudefj",
"Nemo bis",
@ -2649,7 +2650,9 @@
"sp-contributions-explain": "{{optional}}",
"sp-contributions-footer": "{{ignored}}This is the footer for users that are not anonymous or newbie on [[Special:Contributions]].",
"sp-contributions-footer-anon": "{{ignored}}This is the footer for anonymous users on [[Special:Contributions]].",
"sp-contributions-footer-anon-range": "{{ignored}}This is the footer for IP ranges on [[Special:Contributions]].",
"sp-contributions-footer-newbies": "{{ignored}}This is the footer for newbie users on [[Special:Contributions]].",
"sp-contributions-outofrange": "Message shown when a user tries to view contributions of an IP range that's too large. $1 is the numerical limit imposed on the CIDR range.",
"whatlinkshere": "The text of the link in the toolbox (on the left, below the search menu) going to [[Special:WhatLinksHere]].\n\nSee also:\n* {{msg-mw|Whatlinkshere}}\n* {{msg-mw|Accesskey-t-whatlinkshere}}\n* {{msg-mw|Tooltip-t-whatlinkshere}}",
"whatlinkshere-title": "Title of the special page [[Special:WhatLinksHere]]. This page appears when you click on the 'What links here' button in the toolbox. $1 is the name of the page concerned.",
"whatlinkshere-summary": "{{doc-specialpagesummary|whatlinkshere}}",

View file

@ -86,6 +86,7 @@ class DeleteOldRevisions extends Maintenance {
if ( $delete && $count ) {
$this->output( "Deleting..." );
$dbw->delete( 'revision', [ 'rev_id' => $oldRevs ], __METHOD__ );
$dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $oldRevs ], __METHOD__ );
$this->output( "done.\n" );
}

View file

@ -92,6 +92,9 @@ class DeleteOrphanedRevisions extends Maintenance {
$id = [ $id ];
}
$dbw->delete( 'revision', [ 'rev_id' => $id ], __METHOD__ );
// Delete from ip_changes should a record exist.
$dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $id ], __METHOD__ );
}
}

View file

@ -0,0 +1,122 @@
<?php
/**
* Find all revisions by logged out users and copy the rev_id,
* rev_timestamp, and a hex representation of rev_user_text to the
* new ip_changes table. This table is used to efficiently query for
* contributions within an IP range.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Maintenance
*/
require_once __DIR__ . '/Maintenance.php';
use MediaWiki\MediaWikiServices;
/**
* Maintenance script that will find all rows in the revision table where
* rev_user = 0 (user is an IP), and copy relevant fields to ip_changes so
* that historical data will be available when querying for IP ranges.
*
* @ingroup Maintenance
*/
class PopulateIpChanges extends LoggedUpdateMaintenance {
public function __construct() {
parent::__construct();
$this->addDescription( <<<TEXT
This script will find all rows in the revision table where the user is an IP,
and copy relevant fields to the ip_changes table. This backfilled data will
then be available when querying for IP ranges at Special:Contributions.
TEXT
);
$this->addOption( 'rev-id', 'The rev_id to start copying from. Default: 0', false, true );
$this->addOption(
'throttle',
'Wait this many milliseconds after copying each batch of revisions. Default: 0',
false,
true
);
$this->addOption( 'force', 'Run regardless of whether the database says it\'s been run already' );
}
public function doDBUpdates() {
$lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
$dbw = $this->getDB( DB_MASTER );
$throttle = intval( $this->getOption( 'throttle', 0 ) );
$start = $this->getOption( 'rev-id', 0 );
$end = $dbw->selectField( 'revision', 'MAX(rev_id)', false, __METHOD__ );
$blockStart = $start;
$revCount = 0;
$this->output( "Copying IP revisions to ip_changes, from rev_id $start to rev_id $end\n" );
while ( $blockStart <= $end ) {
$cond = "rev_id > $blockStart AND rev_user = 0 ORDER BY rev_id ASC LIMIT " . $this->mBatchSize;
$rows = $dbw->select(
'revision',
[ 'rev_id', 'rev_timestamp', 'rev_user_text' ],
$cond,
__METHOD__
);
if ( !$rows || $rows->numRows() === 0 ) {
break;
}
$this->output( "...copying $this->mBatchSize revisions starting with rev_id $blockStart\n" );
foreach ( $rows as $row ) {
// Double-check to make sure this is an IP, e.g. not maintenance user or imported revision.
if ( !IP::isValid( $row->rev_user_text ) ) {
continue;
}
$dbw->insert(
'ip_changes',
[
'ipc_rev_id' => $row->rev_id,
'ipc_rev_timestamp' => $row->rev_timestamp,
'ipc_hex' => IP::toHex( $row->rev_user_text ),
],
__METHOD__,
'IGNORE'
);
$blockStart = (int)$row->rev_id;
$revCount++;
}
$blockStart++;
$lbFactory->waitForReplication();
usleep( $throttle * 1000 );
}
$this->output( "$revCount IP revisions copied.\n" );
return true;
}
protected function getUpdateKey() {
return 'populate ip_changes';
}
}
$maintClass = "PopulateIpChanges";
require_once RUN_MAINTENANCE_IF_MAIN;

View file

@ -1147,7 +1147,7 @@ class ParserTestRunner {
*/
private function listTables() {
$tables = [ 'user', 'user_properties', 'user_former_groups', 'page', 'page_restrictions',
'protected_titles', 'revision', 'text', 'pagelinks', 'imagelinks',
'protected_titles', 'revision', 'ip_changes', 'text', 'pagelinks', 'imagelinks',
'categorylinks', 'templatelinks', 'externallinks', 'langlinks', 'iwlinks',
'site_stats', 'ipblocks', 'image', 'oldimage',
'recentchanges', 'watchlist', 'interwiki', 'logging', 'log_search',

View file

@ -1303,7 +1303,7 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
private function resetDB( $db, $tablesUsed ) {
if ( $db ) {
$userTables = [ 'user', 'user_groups', 'user_properties' ];
$pageTables = [ 'page', 'revision', 'revision_comment_temp', 'comment' ];
$pageTables = [ 'page', 'revision', 'ip_changes', 'revision_comment_temp', 'comment' ];
$coreDBDataTables = array_merge( $userTables, $pageTables );
// If any of the user or page tables were marked as used, we should clear all of them.

View file

@ -0,0 +1,110 @@
<?php
/**
* Test class for page archiving.
*
* @group ContentHandler
* @group Database
* ^--- important, causes temporary tables to be used instead of the real database
*
* @group medium
* ^--- important, causes tests not to fail with timeout
*/
class PageArchiveTest extends MediaWikiTestCase {
/**
* @var WikiPage $archivedPage
*/
private $archivedPage;
/**
* A logged out user who edited the page before it was archived.
* @var string $ipEditor
*/
private $ipEditor;
/**
* Revision ID of the IP edit
* @var int $ipRevId
*/
private $ipRevId;
function __construct( $name = null, array $data = [], $dataName = '' ) {
parent::__construct( $name, $data, $dataName );
$this->tablesUsed = array_merge(
$this->tablesUsed,
[
'page',
'revision',
'ip_changes',
'text',
'archive',
'recentchanges',
'logging',
'page_props',
]
);
}
protected function setUp() {
parent::setUp();
// First create our dummy page
$page = Title::newFromText( 'PageArchiveTest_thePage' );
$page = new WikiPage( $page );
$content = ContentHandler::makeContent(
'testing',
$page->getTitle(),
CONTENT_MODEL_WIKITEXT
);
$page->doEditContent( $content, 'testing', EDIT_NEW );
// Insert IP revision
$this->ipEditor = '2600:387:ed7:947e:8c16:a1ad:dd34:1dd7';
$rev = new Revision( [
'text' => 'Lorem Ipsum',
'comment' => 'just a test',
'page' => $page->getId(),
'user_text' => $this->ipEditor,
] );
$dbw = wfGetDB( DB_MASTER );
$this->ipRevId = $rev->insertOn( $dbw );
// Delete the page
$page->doDeleteArticleReal( 'Just a test deletion' );
$this->archivedPage = new PageArchive( $page->getTitle() );
}
/**
* @covers PageArchive::undelete
*/
public function testUndeleteRevisions() {
// First make sure old revisions are archived
$dbr = wfGetDB( DB_REPLICA );
$res = $dbr->select( 'archive', '*', [ 'ar_rev_id' => $this->ipRevId ] );
$row = $res->fetchObject();
$this->assertEquals( $this->ipEditor, $row->ar_user_text );
// Should not be in revision
$res = $dbr->select( 'revision', '*', [ 'rev_id' => $this->ipRevId ] );
$this->assertFalse( $res->fetchObject() );
// Should not be in ip_changes
$res = $dbr->select( 'ip_changes', '*', [ 'ipc_rev_id' => $this->ipRevId ] );
$this->assertFalse( $res->fetchObject() );
// Restore the page
$this->archivedPage->undelete( [] );
// Should be back in revision
$res = $dbr->select( 'revision', '*', [ 'rev_id' => $this->ipRevId ] );
$row = $res->fetchObject();
$this->assertEquals( $this->ipEditor, $row->rev_user_text );
// Should be back in ip_changes
$res = $dbr->select( 'ip_changes', '*', [ 'ipc_rev_id' => $this->ipRevId ] );
$row = $res->fetchObject();
$this->assertEquals( IP::toHex( $this->ipEditor ), $row->ipc_hex );
}
}

View file

@ -22,6 +22,7 @@ class RevisionStorageTest extends MediaWikiTestCase {
$this->tablesUsed = array_merge( $this->tablesUsed,
[ 'page',
'revision',
'ip_changes',
'text',
'recentchanges',
@ -440,6 +441,25 @@ class RevisionStorageTest extends MediaWikiTestCase {
$this->assertEquals( 'some testing text', $rev->getContent()->getNativeData() );
}
/**
* @covers Revision::insertOn
*/
public function testInsertOn() {
$ip = '2600:387:ed7:947e:8c16:a1ad:dd34:1dd7';
$orig = $this->makeRevision( [
'user_text' => $ip
] );
// Make sure the revision was copied to ip_changes
$dbr = wfGetDB( DB_REPLICA );
$res = $dbr->select( 'ip_changes', '*', [ 'ipc_rev_id' => $orig->getId() ] );
$row = $res->fetchObject();
$this->assertEquals( IP::toHex( $ip ), $row->ipc_hex );
$this->assertEquals( $orig->getTimestamp(), $row->ipc_rev_timestamp );
}
public static function provideUserWasLastToEdit() {
return [
[ # 0

View file

@ -18,6 +18,7 @@ class WikiPageTest extends MediaWikiLangTestCase {
[ 'page',
'revision',
'archive',
'ip_changes',
'text',
'recentchanges',

View file

@ -3,7 +3,20 @@
/**
* @group Database
*/
class ContribsPagerTest extends \PHPUnit_Framework_TestCase {
class ContribsPagerTest extends MediaWikiTestCase {
/** @var ContribsPager */
private $pager;
function setUp() {
$context = new RequestContext();
$this->pager = new ContribsPager( $context, [
'start' => '2017-01-01',
'end' => '2017-02-02',
] );
parent::setUp();
}
/**
* @dataProvider dateFilterOptionProcessingProvider
* @param array $inputOpts Input options
@ -47,4 +60,58 @@ class ContribsPagerTest extends \PHPUnit_Framework_TestCase {
'end' => '2012-12-31' ] ],
];
}
/**
* @covers ContribsPager::isQueryableRange
* @dataProvider provideQueryableRanges
*/
public function testQueryableRanges( $ipRange ) {
$this->setMwGlobals( [
'wgRangeContributionsCIDRLimit' => [
'IPv4' => 16,
'IPv6' => 32,
],
] );
$this->assertTrue(
$this->pager->isQueryableRange( $ipRange ),
"$ipRange is a queryable IP range"
);
}
public function provideQueryableRanges() {
return [
[ '116.17.184.5/32' ],
[ '0.17.184.5/16' ],
[ '2000::/32' ],
[ '2001:db8::/128' ],
];
}
/**
* @covers ContribsPager::isQueryableRange
* @dataProvider provideUnqueryableRanges
*/
public function testUnqueryableRanges( $ipRange ) {
$this->setMwGlobals( [
'wgRangeContributionsCIDRLimit' => [
'IPv4' => 16,
'IPv6' => 32,
],
] );
$this->assertFalse(
$this->pager->isQueryableRange( $ipRange ),
"$ipRange is not a queryable IP range"
);
}
public function provideUnqueryableRanges() {
return [
[ '116.17.184.5/33' ],
[ '0.17.184.5/15' ],
[ '2000::/31' ],
[ '2001:db8::/9999' ],
];
}
}

View file

@ -217,6 +217,8 @@ class UserTest extends MediaWikiTestCase {
[ 'Ab/cd', false, 'Contains slash' ],
[ 'Ab cd', true, 'Whitespace' ],
[ '192.168.1.1', false, 'IP' ],
[ '116.17.184.5/32', false, 'IP range' ],
[ '::e:f:2001/96', false, 'IPv6 range' ],
[ 'User:Abcd', false, 'Reserved Namespace' ],
[ '12abcd232', true, 'Starts with Numbers' ],
[ '?abcd', true, 'Start with ? mark' ],

View file

@ -30,6 +30,7 @@ class TextPassDumperDatabaseTest extends DumpTestCase {
function addDBData() {
$this->tablesUsed[] = 'page';
$this->tablesUsed[] = 'revision';
$this->tablesUsed[] = 'ip_changes';
$this->tablesUsed[] = 'text';
$this->mergeMwGlobalArrayValue( 'wgContentHandlers', [

View file

@ -29,6 +29,7 @@ class BackupDumperPageTest extends DumpTestCase {
$this->tablesUsed[] = 'page';
$this->tablesUsed[] = 'revision';
$this->tablesUsed[] = 'ip_changes';
$this->tablesUsed[] = 'text';
try {