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:
parent
f884d157f7
commit
d09554b6ef
24 changed files with 535 additions and 35 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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 ] );
|
||||
|
|
|
|||
|
|
@ -83,7 +83,8 @@ abstract class DatabaseUpdater {
|
|||
FixDefaultJsonContentPages::class,
|
||||
CleanupEmptyCategories::class,
|
||||
AddRFCAndPMIDInterwiki::class,
|
||||
PopulatePPSortKey::class
|
||||
PopulatePPSortKey::class,
|
||||
PopulateIpChanges::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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": "",
|
||||
|
|
|
|||
|
|
@ -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}}",
|
||||
|
|
|
|||
|
|
@ -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" );
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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__ );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
122
maintenance/populateIpChanges.php
Normal file
122
maintenance/populateIpChanges.php
Normal 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;
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
110
tests/phpunit/includes/PageArchiveTest.php
Normal file
110
tests/phpunit/includes/PageArchiveTest.php
Normal 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 );
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ class WikiPageTest extends MediaWikiLangTestCase {
|
|||
[ 'page',
|
||||
'revision',
|
||||
'archive',
|
||||
'ip_changes',
|
||||
'text',
|
||||
|
||||
'recentchanges',
|
||||
|
|
|
|||
|
|
@ -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' ],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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' ],
|
||||
|
|
|
|||
|
|
@ -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', [
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ class BackupDumperPageTest extends DumpTestCase {
|
|||
|
||||
$this->tablesUsed[] = 'page';
|
||||
$this->tablesUsed[] = 'revision';
|
||||
$this->tablesUsed[] = 'ip_changes';
|
||||
$this->tablesUsed[] = 'text';
|
||||
|
||||
try {
|
||||
|
|
|
|||
Loading…
Reference in a new issue