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
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
* (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
'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',
'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:
*
*
* @param IDatabase $dbw (master connection)
* @throws MWException
- * @return int
+ * @return int The revision ID
*/
public function insertOn( $dbw ) {
global $wgDefaultExternalStore, $wgContentHandlerUseDB;
);
}
+ // 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 ] );
FixDefaultJsonContentPages::class,
CleanupEmptyCategories::class,
AddRFCAndPMIDInterwiki::class,
- PopulatePPSortKey::class
+ PopulatePPSortKey::class,
+ PopulateIpChanges::class,
];
/**
] );
$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',
'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 = [
}
$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__ );
$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';
'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(
'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
}
$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() ) {
*/
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>",
[
// 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();
$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
);
}
}
- # 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()
- );
+ # 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()
+ );
- # 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()
+ # 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
}
$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.
'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'],
$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';
+ }
}
}
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() {
# 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';
# 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(
|| 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?
*
"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": "",
"Mormegil",
"Mpradeep",
"Murma174",
+ "MusikAnimal",
"Najami",
"Naudefj",
"Nemo bis",
"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}}",
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" );
}
$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__ );
}
}
--- /dev/null
+<?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;
*/
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',
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.
--- /dev/null
+<?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 );
+ }
+}
$this->tablesUsed = array_merge( $this->tablesUsed,
[ 'page',
'revision',
+ 'ip_changes',
'text',
'recentchanges',
$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
[ 'page',
'revision',
'archive',
+ 'ip_changes',
'text',
'recentchanges',
/**
* @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
'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' ],
+ ];
+ }
}
[ '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' ],
function addDBData() {
$this->tablesUsed[] = 'page';
$this->tablesUsed[] = 'revision';
+ $this->tablesUsed[] = 'ip_changes';
$this->tablesUsed[] = 'text';
$this->mergeMwGlobalArrayValue( 'wgContentHandlers', [
$this->tablesUsed[] = 'page';
$this->tablesUsed[] = 'revision';
+ $this->tablesUsed[] = 'ip_changes';
$this->tablesUsed[] = 'text';
try {