From 1d9922db6442dec82e4be07c45c2e8a3a2035602 Mon Sep 17 00:00:00 2001 From: Tim Starling Date: Mon, 10 Jul 2006 06:30:03 +0000 Subject: [PATCH] * Allow blocks on anonymous users only. * Allow or disallow account creation from blocked IP addressess on a per-block basis. * Prevent duplicate blocks. * Fixed the problem of expiry and unblocking erroneously affecting multiple blocks. * Fixed confusing lack of error message when a blocked user attempts to create an account. * Fixed inefficiency of Special:Ipblocklist in the presence of large numbers of blocks; added indexes and implemented an indexed pager. --- RELEASE-NOTES | 11 +- includes/Block.php | 258 ++++++++++++------- includes/SpecialBlockip.php | 51 +++- includes/SpecialIpblocklist.php | 258 +++++++++++++------ includes/SpecialUserlogin.php | 34 ++- includes/User.php | 27 +- languages/Messages.php | 10 + maintenance/archives/patch-ipb_anon_only.sql | 43 ++++ maintenance/mysql5/tables.sql | 20 +- maintenance/tables.sql | 62 +++-- maintenance/updaters.inc | 1 + 11 files changed, 549 insertions(+), 226 deletions(-) create mode 100644 maintenance/archives/patch-ipb_anon_only.sql diff --git a/RELEASE-NOTES b/RELEASE-NOTES index fbd40189b2..c4959bf95e 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -25,9 +25,7 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN == Major new features == -* None! - - +* (bug 550) Allow blocks on anonymous users only. == Changes since 1.7 == @@ -36,6 +34,13 @@ it from source control: http://www.mediawiki.org/wiki/Download_from_SVN * (bug 6586) Regression in "unblocked" subtitle * Don't put empty-page message into view-source when page text is blank * (bug 6587) Remove redundant "allnonarticles" message +* Block improvements: Allow blocks on anonymous users only. Optionally allow + or disallow account creation from blocked IP addresses. Prevent duplicate + blocks. Fixed the problem of expiry and unblocking erroneously affecting + multiple blocks. Fixed confusing lack of error message when a blocked user + attempts to create an account. Fixed inefficiency of Special:Ipblocklist in + the presence of large numbers of blocks; added indexes and implemented an + indexed pager. == Languages updated == diff --git a/includes/Block.php b/includes/Block.php index 26fa444d23..7bee08f47f 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -9,7 +9,6 @@ * All the functions in this class assume the object is either explicitly * loaded or filled. It is not load-on-demand. There are no accessors. * - * To use delete(), you only need to fill $mAddress * Globals used: $wgAutoblockExpiry, $wgAntiLockFlags * * @todo This could be used everywhere, but it isn't. @@ -18,7 +17,7 @@ class Block { /* public*/ var $mAddress, $mUser, $mBy, $mReason, $mTimestamp, $mAuto, $mId, $mExpiry, - $mRangeStart, $mRangeEnd; + $mRangeStart, $mRangeEnd, $mAnonOnly; /* private */ var $mNetworkBits, $mIntegerAddr, $mForUpdate, $mFromMaster, $mByName; const EB_KEEP_EXPIRED = 1; @@ -26,19 +25,18 @@ class Block const EB_RANGE_ONLY = 4; function Block( $address = '', $user = '', $by = 0, $reason = '', - $timestamp = '' , $auto = 0, $expiry = '' ) + $timestamp = '' , $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0 ) { + $this->mId = 0; $this->mAddress = $address; $this->mUser = $user; $this->mBy = $by; $this->mReason = $reason; $this->mTimestamp = wfTimestamp(TS_MW,$timestamp); $this->mAuto = $auto; - if( empty( $expiry ) ) { - $this->mExpiry = $expiry; - } else { - $this->mExpiry = wfTimestamp( TS_MW, $expiry ); - } + $this->mAnonOnly = $anonOnly; + $this->mCreateAccount = $createAccount; + $this->mExpiry = self::decodeExpiry( $expiry ); $this->mForUpdate = false; $this->mFromMaster = false; @@ -46,19 +44,36 @@ class Block $this->initialiseRange(); } - /*static*/ function newFromDB( $address, $user = 0, $killExpired = true ) + static function newFromDB( $address, $user = 0, $killExpired = true ) { - $ban = new Block(); - $ban->load( $address, $user, $killExpired ); - return $ban; + $block = new Block(); + $block->load( $address, $user, $killExpired ); + if ( $block->isValid() ) { + return $block; + } else { + return null; + } + } + + static function newFromID( $id ) + { + $dbr =& wfGetDB( DB_SLAVE ); + $res = $dbr->resultObject( $dbr->select( 'ipblocks', '*', + array( 'ipb_id' => $id ), __METHOD__ ) ); + $block = new Block; + if ( $block->loadFromResult( $res ) ) { + return $block; + } else { + return null; + } } function clear() { $this->mAddress = $this->mReason = $this->mTimestamp = ''; - $this->mUser = $this->mBy = 0; + $this->mId = $this->mAnonOnly = $this->mCreateAccount = + $this->mAuto = $this->mUser = $this->mBy = 0; $this->mByName = false; - } /** @@ -70,56 +85,80 @@ class Block if ( $this->mForUpdate || $this->mFromMaster ) { $db =& wfGetDB( DB_MASTER ); if ( !$this->mForUpdate || ($wgAntiLockFlags & ALF_NO_BLOCK_LOCK) ) { - $options = ''; + $options = array(); } else { - $options = 'FOR UPDATE'; + $options = array( 'FOR UPDATE' ); } } else { $db =& wfGetDB( DB_SLAVE ); - $options = ''; + $options = array(); } return $db; } /** * Get a ban from the DB, with either the given address or the given username + * + * @param string $address The IP address of the user, or blank to skip IP blocks + * @param integer $user The user ID, or zero for anonymous users + * @param bool $killExpired Whether to delete expired rows while loading + * */ function load( $address = '', $user = 0, $killExpired = true ) { - $fname = 'Block::load'; wfDebug( "Block::load: '$address', '$user', $killExpired\n" ); - $options = ''; + $options = array(); $db =& $this->getDBOptions( $options ); $ret = false; $killed = false; - $ipblocks = $db->tableName( 'ipblocks' ); if ( 0 == $user && $address == '' ) { # Invalid user specification, not blocked $this->clear(); return false; - } elseif ( $address == '' ) { - $sql = "SELECT * FROM $ipblocks WHERE ipb_user={$user} $options"; - } elseif ( $user == '' ) { - $sql = "SELECT * FROM $ipblocks WHERE ipb_address=" . $db->addQuotes( $address ) . " $options"; - } elseif ( $options == '' ) { - # If there are no options (e.g. FOR UPDATE), use a UNION - # so that the query can make efficient use of indices - $sql = "SELECT * FROM $ipblocks WHERE ipb_address='" . $db->strencode( $address ) . - "' UNION SELECT * FROM $ipblocks WHERE ipb_user={$user}"; - } else { - # If there are options, a UNION can not be used, use one - # SELECT instead. Will do a full table scan. - $sql = "SELECT * FROM $ipblocks WHERE (ipb_address='" . $db->strencode( $address ) . - "' OR ipb_user={$user}) $options"; } - $res = $db->query( $sql, $fname ); - if ( 0 != $db->numRows( $res ) ) { + # Try user block + if ( $user ) { + $res = $db->resultObject( $db->select( 'ipblocks', '*', array( 'ipb_user' => $user ), + __METHOD__, $options ) ); + if ( $this->loadFromResult( $res, $killExpired ) ) { + return true; + } + } + + # Try IP block + if ( $address ) { + $conds = array( 'ipb_address' => $address ); + if ( $user ) { + $conds['ipb_anon_only'] = 0; + } + $res = $db->resultObject( $db->select( 'ipblocks', '*', $conds, __METHOD__, $options ) ); + if ( $this->loadFromResult( $res, $killExpired ) ) { + return true; + } + } + + # Try range block + if ( $this->loadRange( $address, $killExpired, $user == 0 ) ) { + return true; + } + + # Give up + $this->clear(); + return false; + } + + /** + * Fill in member variables from a result wrapper + */ + function loadFromResult( ResultWrapper $res, $killExpired = true ) { + $ret = false; + if ( 0 != $res->numRows() ) { # Get first block - $row = $db->fetchObject( $res ); + $row = $res->fetchObject(); $this->initFromRow( $row ); if ( $killExpired ) { @@ -127,7 +166,7 @@ class Block do { $killed = $this->deleteIfExpired(); if ( $killed ) { - $row = $db->fetchObject( $res ); + $row = $res->fetchObject(); if ( $row ) { $this->initFromRow( $row ); } @@ -135,26 +174,14 @@ class Block } while ( $killed && $row ); # If there were any left after the killing finished, return true - if ( !$row ) { - $ret = false; - $this->clear(); - } else { + if ( $row ) { $ret = true; } } else { $ret = true; } } - $db->freeResult( $res ); - - # No blocks found yet? Try looking for range blocks - if ( !$ret && $address != '' ) { - $ret = $this->loadRange( $address, $killExpired ); - } - if ( !$ret ) { - $this->clear(); - } - + $res->free(); return $ret; } @@ -162,10 +189,8 @@ class Block * Search the database for any range blocks matching the given address, and * load the row if one is found. */ - function loadRange( $address, $killExpired = true ) + function loadRange( $address, $killExpired = true, $isAnon = true ) { - $fname = 'Block::loadRange'; - $iaddr = wfIP2Hex( $address ); if ( $iaddr === false ) { # Invalid address @@ -176,27 +201,19 @@ class Block # Blocks should not cross a /16 boundary. $range = substr( $iaddr, 0, 4 ); - $options = ''; + $options = array(); $db =& $this->getDBOptions( $options ); - $ipblocks = $db->tableName( 'ipblocks' ); - $sql = "SELECT * FROM $ipblocks WHERE ipb_range_start LIKE '$range%' ". - "AND ipb_range_start <= '$iaddr' AND ipb_range_end >= '$iaddr' $options"; - $res = $db->query( $sql, $fname ); - $row = $db->fetchObject( $res ); - - $success = false; - if ( $row ) { - # Found a row, initialise this object - $this->initFromRow( $row ); - - # Is it expired? - if ( !$killExpired || !$this->deleteIfExpired() ) { - # No, return true - $success = true; - } + $conds = array( + "ipb_range_start LIKE '$range%'", + "ipb_range_start <= '$iaddr'", + "ipb_range_end >= '$iaddr'" + ); + if ( !$isAnon ) { + $conds['ipb_anon_only'] = 0; } - $db->freeResult( $res ); + $res = $db->resultObject( $db->select( 'ipblocks', '*', $conds, __METHOD__, $options ) ); + $success = $this->loadFromResult( $res, $killExpired ); return $success; } @@ -220,10 +237,10 @@ class Block $this->mUser = $row->ipb_user; $this->mBy = $row->ipb_by; $this->mAuto = $row->ipb_auto; + $this->mAnonOnly = $row->ipb_anon_only; + $this->mCreateAccount = $row->ipb_create_account; $this->mId = $row->ipb_id; - $this->mExpiry = $row->ipb_expiry ? - wfTimestamp(TS_MW,$row->ipb_expiry) : - $row->ipb_expiry; + $this->mExpiry = self::decodeExpiry( $row->ipb_expiry ); if ( isset( $row->user_name ) ) { $this->mByName = $row->user_name; } else { @@ -304,24 +321,27 @@ class Block function delete() { - $fname = 'Block::delete'; if (wfReadOnly()) { - return; + return false; } - $dbw =& wfGetDB( DB_MASTER ); - - if ( $this->mAddress == '' ) { - $condition = array( 'ipb_id' => $this->mId ); - } else { - $condition = array( 'ipb_address' => $this->mAddress ); + if ( !$this->mId ) { + throw new MWException( "Block::delete() now requires that the mId member be filled\n" ); } - return( $dbw->delete( 'ipblocks', $condition, $fname ) > 0 ? true : false ); + + $dbw =& wfGetDB( DB_MASTER ); + $dbw->delete( 'ipblocks', array( 'ipb_id' => $this->mId ), __METHOD__ ); + return $dbw->affectedRows() > 0; } function insert() { wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" ); $dbw =& wfGetDB( DB_MASTER ); + $dbw->begin(); + + # Don't collide with expired blocks + Block::purgeExpired(); + $ipb_id = $dbw->nextSequenceValue('ipblocks_ipb_id_val'); $dbw->insert( 'ipblocks', array( @@ -332,13 +352,16 @@ class Block 'ipb_reason' => $this->mReason, 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp), 'ipb_auto' => $this->mAuto, - 'ipb_expiry' => $this->mExpiry ? - $dbw->timestamp($this->mExpiry) : - $this->mExpiry, + 'ipb_anon_only' => $this->mAnonOnly, + 'ipb_create_account' => $this->mCreateAccount, + 'ipb_expiry' => self::encodeExpiry( $this->mExpiry, $dbw ), 'ipb_range_start' => $this->mRangeStart, 'ipb_range_end' => $this->mRangeEnd, - ), 'Block::insert' + ), 'Block::insert', array( 'IGNORE' ) ); + $affected = $dbw->affectedRows(); + $dbw->commit(); + return $affected; } function deleteIfExpired() @@ -417,13 +440,43 @@ class Block return wfSetVar( $this->mFromMaster, $x ); } - /* static */ function getAutoblockExpiry( $timestamp ) + function getRedactedName() { + if ( $this->mAuto ) { + return '#' . $this->mId; + } else { + return $this->mAddress; + } + } + + /** + * Encode expiry for DB + */ + static function encodeExpiry( $expiry, $db ) { + if ( $expiry == '' || $expiry == Block::infinity() ) { + return Block::infinity(); + } else { + return $db->timestamp( $expiry ); + } + } + + /** + * Decode expiry which has come from the DB + */ + static function decodeExpiry( $expiry ) { + if ( $expiry == '' || $expiry == Block::infinity() ) { + return Block::infinity(); + } else { + return wfTimestamp( TS_MW, $expiry ); + } + } + + static function getAutoblockExpiry( $timestamp ) { global $wgAutoblockExpiry; return wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $timestamp ) + $wgAutoblockExpiry ); } - /* static */ function normaliseRange( $range ) + static function normaliseRange( $range ) { $parts = explode( '/', $range ); if ( count( $parts ) == 2 ) { @@ -436,5 +489,28 @@ class Block return $range; } + /** + * Purge expired blocks from the ipblocks table + */ + static function purgeExpired() { + $dbw =& wfGetDB( DB_MASTER ); + $dbw->delete( 'ipblocks', array( 'ipb_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), __METHOD__ ); + } + + static function infinity() { + # This is a special keyword for timestamps in PostgreSQL, and + # works with CHAR(14) as well because "i" sorts after all numbers. + return 'infinity'; + + /* + static $infinity; + if ( !isset( $infinity ) ) { + $dbr =& wfGetDB( DB_SLAVE ); + $infinity = $dbr->bigTimestamp(); + } + return $infinity; + */ + } + } ?> diff --git a/includes/SpecialBlockip.php b/includes/SpecialBlockip.php index b3f67ab10d..4ca376b465 100644 --- a/includes/SpecialBlockip.php +++ b/includes/SpecialBlockip.php @@ -46,6 +46,15 @@ class IPBlockForm { $this->BlockReason = $wgRequest->getText( 'wpBlockReason' ); $this->BlockExpiry = $wgRequest->getVal( 'wpBlockExpiry', wfMsg('ipbotheroption') ); $this->BlockOther = $wgRequest->getVal( 'wpBlockOther', '' ); + $this->BlockAnonOnly = $wgRequest->getBool( 'wpAnonOnly' ); + + # Unchecked checkboxes are not included in the form data at all, so having one + # that is true by default is a bit tricky + if ( $wgRequest->wasPosted() ) { + $this->BlockCreateAccount = $wgRequest->getBool( 'wpCreateAccount', false ); + } else { + $this->BlockCreateAccount = $wgRequest->getBool( 'wpCreateAccount', true ); + } } function showForm( $err ) { @@ -64,6 +73,8 @@ class IPBlockForm { $mIpbothertime = wfMsgHtml( 'ipbotheroption' ); $mIpbreason = wfMsgHtml( 'ipbreason' ); $mIpbsubmit = wfMsgHtml( 'ipbsubmit' ); + $mIpbanononly = wfMsgHtml( 'ipbanononly' ); + $mIpbcreateaccount = wfMsgHtml( 'ipbcreateaccount' ); $titleObj = Title::makeTitle( NS_SPECIAL, 'Blockip' ); $action = $titleObj->escapeLocalURL( "action=submit" ); @@ -77,6 +88,8 @@ class IPBlockForm { $scBlockReason = htmlspecialchars( $this->BlockReason ); $scBlockOtherTime = htmlspecialchars( $this->BlockOther ); $scBlockExpiryOptions = htmlspecialchars( wfMsgForContent( 'ipboptions' ) ); + $anonOnlyChecked = $this->BlockAnonOnly ? 'checked' : ''; + $createAccountChecked = $this->BlockCreateAccount ? 'checked' : ''; $showblockoptions = $scBlockExpiryOptions != '-'; if (!$showblockoptions) @@ -102,7 +115,7 @@ class IPBlockForm { {$mIpaddress}: - + "); @@ -133,6 +146,24 @@ class IPBlockForm {   + + + + +   + + + + + +   + @@ -188,7 +219,7 @@ class IPBlockForm { } if ( $expirestr == 'infinite' || $expirestr == 'indefinite' ) { - $expiry = ''; + $expiry = Block::infinity(); } else { # Convert GNU-style date, on error returns -1 for PHP <5.1 and false for PHP >=5.1 $expiry = strtotime( $expirestr ); @@ -199,20 +230,24 @@ class IPBlockForm { } $expiry = wfTimestamp( TS_MW, $expiry ); - } # Create block # Note: for a user block, ipb_address is only for display purposes - $ban = new Block( $this->BlockAddress, $userId, $wgUser->getID(), - $this->BlockReason, wfTimestampNow(), 0, $expiry ); + $block = new Block( $this->BlockAddress, $userId, $wgUser->getID(), + $this->BlockReason, wfTimestampNow(), 0, $expiry, $this->BlockAnonOnly, + $this->BlockCreateAccount ); - if (wfRunHooks('BlockIp', array(&$ban, &$wgUser))) { + if (wfRunHooks('BlockIp', array(&$block, &$wgUser))) { - $ban->insert(); + if ( !$block->insert() ) { + $this->showForm( wfMsg( 'ipb_already_blocked', + htmlspecialchars( $this->BlockAddress ) ) ); + return; + } - wfRunHooks('BlockIpComplete', array($ban, $wgUser)); + wfRunHooks('BlockIpComplete', array($block, $wgUser)); # Make log entry $log = new LogPage( 'block' ); diff --git a/includes/SpecialIpblocklist.php b/includes/SpecialIpblocklist.php index a4f960a156..024385b76a 100644 --- a/includes/SpecialIpblocklist.php +++ b/includes/SpecialIpblocklist.php @@ -12,13 +12,15 @@ function wfSpecialIpblocklist() { global $wgUser, $wgOut, $wgRequest; $ip = $wgRequest->getVal( 'wpUnblockAddress', $wgRequest->getVal( 'ip' ) ); + $id = $wgRequest->getVal( 'id' ); $reason = $wgRequest->getText( 'wpUnblockReason' ); $action = $wgRequest->getText( 'action' ); + $successip = $wgRequest->getVal( 'successip' ); - $ipu = new IPUnblockForm( $ip, $reason ); + $ipu = new IPUnblockForm( $ip, $id, $reason ); if ( "success" == $action ) { - $ipu->showList( $wgOut->parse( wfMsg( 'unblocked', $ip ) ) ); + $ipu->showList( $wgOut->parse( wfMsg( 'unblocked', $successip ) ) ); } else if ( "submit" == $action && $wgRequest->wasPosted() && $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) ) ) { if ( ! $wgUser->isAllowed('block') ) { @@ -39,10 +41,11 @@ function wfSpecialIpblocklist() { * @subpackage SpecialPage */ class IPUnblockForm { - var $ip, $reason; + var $ip, $reason, $id; - function IPUnblockForm( $ip, $reason ) { + function IPUnblockForm( $ip, $id, $reason ) { $this->ip = $ip; + $this->id = $id; $this->reason = $reason; } @@ -64,13 +67,27 @@ class IPUnblockForm { } $token = htmlspecialchars( $wgUser->editToken() ); + $addressPart = false; + if ( $this->id ) { + $block = Block::newFromID( $this->id ); + if ( $block ) { + $encName = htmlspecialchars( $block->getRedactedName() ); + $encId = htmlspecialchars( $this->id ); + $addressPart = $encName . ""; + } + } + if ( !$addressPart ) { + $addressPart = "ip ) . "\" />"; + } + $wgOut->addHTML( "
@@ -94,27 +111,46 @@ class IPUnblockForm { function doSubmit() { global $wgOut; - $block = new Block(); - $this->ip = trim( $this->ip ); - - if ( $this->ip{0} == "#" ) { - $block->mId = substr( $this->ip, 1 ); + if ( $this->id ) { + $block = Block::newFromID( $this->id ); + if ( $block ) { + $this->ip = $block->getRedactedName(); + } } else { - $block->mAddress = $this->ip; + $block = new Block(); + $this->ip = trim( $this->ip ); + if ( substr( $this->ip, 0, 1 ) == "#" ) { + $id = substr( $this->ip, 1 ); + $block = Block::newFromID( $id ); + } else { + $block = Block::newFromDB( $this->ip ); + if ( !$block ) { + $block = null; + } + } + } + $success = false; + if ( $block ) { + # Delete block + if ( $block->delete() ) { + # Make log entry + $log = new LogPage( 'block' ); + $log->addEntry( 'unblock', Title::makeTitle( NS_USER, $this->ip ), $this->reason ); + $success = true; + } } - # Delete block (if it exists) - # We should probably check for errors rather than just declaring success - $block->delete(); - - # Make log entry - $log = new LogPage( 'block' ); - $log->addEntry( 'unblock', Title::makeTitle( NS_USER, $this->ip ), $this->reason ); - - # Report to the user - $titleObj = Title::makeTitle( NS_SPECIAL, "Ipblocklist" ); - $success = $titleObj->getFullURL( "action=success&ip=" . urlencode( $this->ip ) ); - $wgOut->redirect( $success ); + if ( $success ) { + # Report to the user + $titleObj = Title::makeTitle( NS_SPECIAL, "Ipblocklist" ); + $success = $titleObj->getFullURL( "action=success&successip=" . urlencode( $this->ip ) ); + $wgOut->redirect( $success ); + } else { + if ( !$this->ip && $this->id ) { + $this->ip = '#' . $this->id; + } + $this->showForm( wfMsg( 'ipb_cant_unblock', htmlspecialchars( $this->id ) ) ); + } } function showList( $msg ) { @@ -124,29 +160,43 @@ class IPUnblockForm { if ( "" != $msg ) { $wgOut->setSubtitle( $msg ); } - global $wgRequest; - list( $this->limit, $this->offset ) = $wgRequest->getLimitOffset(); - $this->counter = 0; - $paging = '

' . wfViewPrevNext( $this->offset, $this->limit, - Title::makeTitle( NS_SPECIAL, 'Ipblocklist' ), - 'ip=' . urlencode( $this->ip ) ) . "

\n"; - $wgOut->addHTML( $paging ); + // Purge expired entries on one in every 10 queries + if ( !mt_rand( 0, 10 ) ) { + Block::purgeExpired(); + } - $search = $this->searchForm(); - $wgOut->addHTML( $search ); - - $wgOut->addHTML( "\n" ); - $wgOut->addHTML( $paging ); + $s .= $pager->getNavigationBar(); + $wgOut->addHTML( $s ); } function searchForm() { - global $wgTitle; + global $wgTitle, $wgRequest; return wfElement( 'form', array( 'action' => $wgTitle->getLocalUrl() ), @@ -158,7 +208,7 @@ class IPUnblockForm { wfElement( 'input', array( 'type' => 'hidden', 'name' => 'limit', - 'value' => $this->limit ) ). + 'value' => $wgRequest->getText( 'limit' ) ) ) . wfElement( 'input', array( 'name' => 'ip', 'value' => $this->ip ) ) . @@ -171,33 +221,10 @@ class IPUnblockForm { /** * Callback function to output a block */ - function addRow( $block, $tag ) { - global $wgOut, $wgUser, $wgLang; - - if( $this->ip != '' ) { - if( $block->mAuto ) { - if( stristr( $block->mId, $this->ip ) == false ) { - return; - } - } else { - if( stristr( $block->mAddress, $this->ip ) == false ) { - return; - } - } - } + function formatRow( $block ) { + global $wgUser, $wgLang; - // Loading blocks is fast; displaying them is slow. - // Quick hack for paging. - $this->counter++; - if( $this->counter <= $this->offset ) { - return; - } - if( $this->counter - $this->offset > $this->limit ) { - return; - } - - $fname = 'IPUnblockForm-addRow'; - wfProfileIn( $fname ); + wfProfileIn( __METHOD__ ); static $sk=null, $msg=null; @@ -205,14 +232,15 @@ class IPUnblockForm { $sk = $wgUser->getSkin(); if( is_null( $msg ) ) { $msg = array(); - foreach( array( 'infiniteblock', 'expiringblock', 'contribslink', 'unblocklink' ) as $key ) { + $keys = array( 'infiniteblock', 'expiringblock', 'contribslink', 'unblocklink', + 'anononlyblock', 'createaccountblock' ); + foreach( $keys as $key ) { $msg[$key] = wfMsgHtml( $key ); } $msg['blocklistline'] = wfMsg( 'blocklistline' ); $msg['contribslink'] = wfMsg( 'contribslink' ); } - # Prepare links to the blocker's user and talk pages $blocker_name = $block->getByName(); $blocker = $sk->MakeLinkObj( Title::makeTitle( NS_USER, $blocker_name ), $blocker_name ); @@ -220,35 +248,101 @@ class IPUnblockForm { # Prepare links to the block target's user and contribs. pages (as applicable, don't do it for autoblocks) if( $block->mAuto ) { - $target = '#' . $block->mId; # Hide the IP addresses of auto-blocks; privacy + $target = $block->getRedactedName(); # Hide the IP addresses of auto-blocks; privacy } else { $target = $sk->makeLinkObj( Title::makeTitle( NS_USER, $block->mAddress ), $block->mAddress ); $target .= ' (' . $sk->makeKnownLinkObj( Title::makeTitle( NS_SPECIAL, 'Contributions' ), $msg['contribslink'], 'target=' . urlencode( $block->mAddress ) ) . ')'; } - # Prep the address for the unblock link, masking autoblocks as before - $addr = $block->mAuto ? '#' . $block->mId : $block->mAddress; - $formattedTime = $wgLang->timeanddate( $block->mTimestamp, true ); - if ( $block->mExpiry === "" ) { - $formattedExpiry = $msg['infiniteblock']; + $properties = array(); + if ( $block->mExpiry === "" || $block->mExpiry === Block::infinity() ) { + $properties[] = $msg['infiniteblock']; } else { - $formattedExpiry = wfMsgReplaceArgs( $msg['expiringblock'], + $properties[] = wfMsgReplaceArgs( $msg['expiringblock'], array( $wgLang->timeanddate( $block->mExpiry, true ) ) ); } + if ( $block->mAnonOnly ) { + $properties[] = $msg['anononlyblock']; + } + if ( $block->mCreateAccount ) { + $properties[] = $msg['createaccountblock']; + } + $properties = implode( ', ', $properties ); - $line = wfMsgReplaceArgs( $msg['blocklistline'], array( $formattedTime, $blocker, $target, $formattedExpiry ) ); + $line = wfMsgReplaceArgs( $msg['blocklistline'], array( $formattedTime, $blocker, $target, $properties ) ); - $wgOut->addHTML( "
  • {$line}" ); + $s = "
  • {$line}"; if ( $wgUser->isAllowed('block') ) { $titleObj = Title::makeTitle( NS_SPECIAL, "Ipblocklist" ); - $wgOut->addHTML( ' (' . $sk->makeKnownLinkObj($titleObj, $msg['unblocklink'], 'action=unblock&ip=' . urlencode( $addr ) ) . ')' ); + $s .= ' (' . $sk->makeKnownLinkObj($titleObj, $msg['unblocklink'], 'action=unblock&id=' . urlencode( $block->mId ) ) . ')'; } - $wgOut->addHTML( $sk->commentBlock( $block->mReason ) ); - $wgOut->addHTML( "
  • \n" ); - wfProfileOut( $fname ); + $s .= $sk->commentBlock( $block->mReason ); + $s .= "\n"; + wfProfileOut( __METHOD__ ); + return $s; + } +} + +class IPBlocklistPager extends ReverseChronologicalPager { + public $mForm, $mConds; + + function __construct( $form, $conds = array() ) { + $this->mForm = $form; + $this->mConds = $conds; + parent::__construct(); + } + + function getStartBody() { + wfProfileIn( __METHOD__ ); + # Do a link batch query + $this->mResult->seek( 0 ); + $lb = new LinkBatch; + + /* + while ( $row = $this->mResult->fetchObject() ) { + $lb->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) ); + $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) ); + $lb->addObj( Title::makeTitleSafe( NS_USER, $row->ipb_address ) ); + $lb->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->ipb_address ) ); + }*/ + # Faster way + # Usernames and titles are in fact related by a simple substitution of space -> underscore + # The last few lines of Title::secureAndSplit() tell the story. + while ( $row = $this->mResult->fetchObject() ) { + $name = str_replace( ' ', '_', $row->user_name ); + $lb->add( NS_USER, $name ); + $lb->add( NS_USER_TALK, $name ); + $name = str_replace( ' ', '_', $row->ipb_address ); + $lb->add( NS_USER, $name ); + $lb->add( NS_USER_TALK, $name ); + } + $lb->execute(); + wfProfileOut( __METHOD__ ); + return ''; + } + + function formatRow( $row ) { + $block = new Block; + $block->initFromRow( $row ); + return $this->mForm->formatRow( $block ); + } + + function getQueryInfo() { + $conds = $this->mConds; + $conds[] = 'ipb_expiry>' . $this->mDb->addQuotes( $this->mDb->timestamp() ); + $conds[] = 'ipb_by=user_id'; + return array( + 'tables' => array( 'ipblocks', 'user' ), + 'fields' => 'ipblocks.*,user_name', + 'conds' => $conds, + ); + } + + function getIndexField() { + return 'ipb_timestamp'; } } diff --git a/includes/SpecialUserlogin.php b/includes/SpecialUserlogin.php index 4ee35b1bfb..5f72ee94e6 100644 --- a/includes/SpecialUserlogin.php +++ b/includes/SpecialUserlogin.php @@ -470,6 +470,27 @@ class LoginForm { $wgOut->returnToMain( false ); } + /** */ + function userBlockedMessage() { + global $wgOut; + + # Let's be nice about this, it's likely that this feature will be used + # for blocking large numbers of innocent people, e.g. range blocks on + # schools. Don't blame it on the user. There's a small chance that it + # really is the user's fault, i.e. the username is blocked and they + # haven't bothered to log out before trying to create an account to + # evade it, but we'll leave that to their guilty conscience to figure + # out. + + $wgOut->setPageTitle( wfMsg( 'cantcreateaccounttitle' ) ); + $wgOut->setRobotpolicy( 'noindex,nofollow' ); + $wgOut->setArticleRelated( false ); + + $ip = wfGetIP(); + $wgOut->addWikiText( wfMsg( 'cantcreateaccounttext', $ip ) ); + $wgOut->returnToMain( false ); + } + /** * @private */ @@ -477,9 +498,14 @@ class LoginForm { global $wgUser, $wgOut, $wgAllowRealName, $wgEnableEmail; global $wgCookiePrefix, $wgAuth, $wgLoginLanguageSelector; - if ( $this->mType == 'signup' && !$wgUser->isAllowedToCreateAccount() ) { - $this->userNotPrivilegedMessage(); - return; + if ( $this->mType == 'signup' ) { + if ( !$wgUser->isAllowed( 'createaccount' ) ) { + $this->userNotPrivilegedMessage(); + return; + } elseif ( $wgUser->isBlockedFromCreateAccount() ) { + $this->userBlockedMessage(); + return; + } } if ( '' == $this->mName ) { @@ -570,7 +596,7 @@ class LoginForm { function showCreateOrLoginLink( &$user ) { if( $this->mType == 'signup' ) { return( true ); - } elseif( $user->isAllowedToCreateAccount() ) { + } elseif( $user->isAllowed( 'createaccount' ) ) { return( true ); } else { return( false ); diff --git a/includes/User.php b/includes/User.php index f24262844d..3d4d6fcf85 100644 --- a/includes/User.php +++ b/includes/User.php @@ -24,6 +24,7 @@ class User { */ var $mBlockedby; //!< var $mBlockreason; //!< + var $mBlock; //!< var $mDataLoaded; //!< var $mEmail; //!< var $mEmailAuthenticated; //!< @@ -114,8 +115,6 @@ class User { */ function __sleep() { return array( -'mBlockedby', -'mBlockreason', 'mDataLoaded', 'mEmail', 'mEmailAuthenticated', @@ -436,16 +435,17 @@ class User { $ip = wfGetIP(); # User/IP blocking - $block = new Block(); - $block->fromMaster( !$bFromSlave ); - if ( $block->load( $ip , $this->mId ) ) { + $this->mBlock = new Block(); + $this->mBlock->fromMaster( !$bFromSlave ); + if ( $this->mBlock->load( $ip , $this->mId ) ) { wfDebug( "$fname: Found block.\n" ); - $this->mBlockedby = $block->mBy; - $this->mBlockreason = $block->mReason; + $this->mBlockedby = $this->mBlock->mBy; + $this->mBlockreason = $this->mBlock->mReason; if ( $this->isLoggedIn() ) { $this->spreadBlock(); } } else { + $this->mBlock = null; wfDebug( "$fname: No block.\n" ); } @@ -694,6 +694,8 @@ class User { $user->loadFromDatabase(); } else { wfDebug( "User::loadFromSession() got from cache!\n" ); + # Set block status to unloaded, that should be loaded every time + $user->mBlockedby = -1; } if ( isset( $_SESSION['wsToken'] ) ) { @@ -1532,13 +1534,13 @@ class User { } $userblock = Block::newFromDB( '', $this->mId ); - if ( !$userblock->isValid() ) { + if ( !$userblock ) { return; } # Check if this IP address is already blocked $ipblock = Block::newFromDB( wfGetIP() ); - if ( $ipblock->isValid() ) { + if ( $ipblock ) { # If the user is already blocked. Then check if the autoblock would # excede the user block. If it would excede, then do nothing, else # prolong block time @@ -1612,8 +1614,13 @@ class User { return $confstr; } + function isBlockedFromCreateAccount() { + $this->getBlockedStatus(); + return $this->mBlock && $this->mBlock->mCreateAccount; + } + function isAllowedToCreateAccount() { - return $this->isAllowed( 'createaccount' ) && !$this->isBlocked(); + return $this->isAllowed( 'createaccount' ) && !$this->isBlockedFromCreateAccount(); } /** diff --git a/languages/Messages.php b/languages/Messages.php index d7b2e8acd1..1f02b2bb19 100644 --- a/languages/Messages.php +++ b/languages/Messages.php @@ -555,6 +555,10 @@ the text into a text file and save it for later.', 'nocreatetitle' => 'Page creation limited', 'nocreatetext' => 'This site has restricted the ability to create new pages. You can go back and edit an existing page, or [[Special:Userlogin|log in or create an account]].', +'cantcreateaccounttitle' => 'Can\'t create account', +'cantcreateaccounttext' => 'Account creation from this IP address ($1) has been blocked. +This is probably due to persistent vandalism from your school or Internet service +provider. ', # History pages # @@ -1271,6 +1275,8 @@ pages that were vandalized).", 'ipadressorusername' => 'IP Address or username', 'ipbexpiry' => 'Expiry', 'ipbreason' => 'Reason', +'ipbanononly' => 'Block anonymous users only', +'ipbcreateaccount' => 'Prevent account creation', 'ipbsubmit' => 'Block this user', 'ipbother' => 'Other time', 'ipboptions' => '2 hours:2 hours,1 day:1 day,3 days:3 days,1 week:1 week,2 weeks:2 weeks,1 month:1 month,3 months:3 months,6 months:6 months,1 year:1 year,infinite:infinite', @@ -1288,6 +1294,8 @@ to a previously blocked IP address or username.', 'blocklistline' => "$1, $2 blocked $3 ($4)", 'infiniteblock' => 'infinite', 'expiringblock' => 'expires $1', +'anononlyblock' => 'anon. only', +'createaccountblock' => 'account creation blocked', 'ipblocklistempty' => 'The blocklist is empty.', 'blocklink' => 'block', 'unblocklink' => 'unblock', @@ -1301,8 +1309,10 @@ the list of currently operational bans and blocks.', 'unblocklogentry' => 'unblocked $1', 'range_block_disabled' => 'The sysop ability to create range blocks is disabled.', 'ipb_expiry_invalid' => 'Expiry time invalid.', +'ipb_already_blocked' => '"$1" is already blocked', 'ip_range_invalid' => 'Invalid IP range.', 'proxyblocker' => 'Proxy blocker', +'ipb_cant_unblock' => 'Error: Block ID $1 not found. It may have been unblocked already.', 'proxyblockreason' => 'Your IP address has been blocked because it is an open proxy. Please contact your Internet service provider or tech support and inform them of this serious security problem.', 'proxyblocksuccess' => 'Done.', 'sorbs' => 'SORBS DNSBL', diff --git a/maintenance/archives/patch-ipb_anon_only.sql b/maintenance/archives/patch-ipb_anon_only.sql new file mode 100644 index 0000000000..ea7d889e86 --- /dev/null +++ b/maintenance/archives/patch-ipb_anon_only.sql @@ -0,0 +1,43 @@ +-- Add extra option fields to the ipblocks table, add some extra indexes, +-- convert infinity values in ipb_expiry to something that sorts better, +-- extend ipb_address and range fields, add a unique index for block conflict +-- detection. + +-- Conflicts in the new unique index can be handled by creating a new +-- table and inserting into it instead of doing an ALTER TABLE. + + +DROP TABLE IF EXISTS /*$wgDBprefix*/ipblocks_newunique; + +CREATE TABLE /*$wgDBprefix*/ipblocks_newunique ( + ipb_id int(8) NOT NULL auto_increment, + ipb_address tinyblob NOT NULL default '', + ipb_user int(8) unsigned NOT NULL default '0', + ipb_by int(8) unsigned NOT NULL default '0', + ipb_reason tinyblob NOT NULL default '', + ipb_timestamp char(14) binary NOT NULL default '', + ipb_auto boolean NOT NULL default 0, + ipb_anon_only boolean NOT NULL default 0, + ipb_create_account boolean NOT NULL default 1, + ipb_expiry char(14) binary NOT NULL default '', + ipb_range_start tinyblob NOT NULL default '', + ipb_range_end tinyblob NOT NULL default '', + + PRIMARY KEY ipb_id (ipb_id), + UNIQUE INDEX ipb_address_unique (ipb_address(255), ipb_user, ipb_auto), + INDEX ipb_user (ipb_user), + INDEX ipb_range (ipb_range_start(8), ipb_range_end(8)), + INDEX ipb_timestamp (ipb_timestamp), + INDEX ipb_expiry (ipb_expiry) + +) TYPE=InnoDB; + +INSERT IGNORE INTO /*$wgDBprefix*/ipblocks_newunique + (ipb_id, ipb_address, ipb_user, ipb_by, ipb_reason, ipb_timestamp, ipb_auto, ipb_expiry, ipb_range_start, ipb_range_end) + SELECT ipb_id, ipb_address, ipb_user, ipb_by, ipb_reason, ipb_timestamp, ipb_auto, ipb_expiry, ipb_range_start, ipb_range_end + FROM /*$wgDBprefix*/ipblocks; + +DROP TABLE IF EXISTS /*$wgDBprefix*/ipblocks_old; +RENAME TABLE /*$wgDBprefix*/ipblocks TO /*$wgDBprefix*/ipblocks_old; +RENAME TABLE /*$wgDBprefix*/ipblocks_newunique TO /*$wgDBprefix*/ipblocks; + diff --git a/maintenance/mysql5/tables.sql b/maintenance/mysql5/tables.sql index cc6818d39c..0ee6e2aa71 100644 --- a/maintenance/mysql5/tables.sql +++ b/maintenance/mysql5/tables.sql @@ -583,8 +583,14 @@ CREATE TABLE /*$wgDBprefix*/ipblocks ( -- Indicates that the IP address was banned because a banned -- user accessed a page through it. If this is 1, ipb_address -- will be hidden, and the block identified by block ID number. - ipb_auto tinyint(1) NOT NULL default '0', + ipb_auto boolean NOT NULL default '0', + -- If set to 1, block applies only to logged-out users + ipb_anon_only boolean NOT NULL default 0, + + -- Block prevents account creation from matching IP addresses + ipb_create_account boolean NOT NULL default 1, + -- Time at which the block will expire. ipb_expiry char(14) binary NOT NULL default '', @@ -594,9 +600,15 @@ CREATE TABLE /*$wgDBprefix*/ipblocks ( ipb_range_end varchar(32) NOT NULL default '', PRIMARY KEY ipb_id (ipb_id), - INDEX ipb_address (ipb_address), + + -- Unique index to support "user already blocked" messages + -- Any new options which prevent collisions should be included + UNIQUE INDEX ipb_address (ipb_address(255), ipb_user, ipb_auto, ipb_anon_only), + INDEX ipb_user (ipb_user), - INDEX ipb_range (ipb_range_start(8), ipb_range_end(8)) + INDEX ipb_range (ipb_range_start(8), ipb_range_end(8)), + INDEX ipb_timestamp (ipb_timestamp), + INDEX ipb_expiry (ipb_expiry) ) TYPE=InnoDB, DEFAULT CHARSET=utf8; @@ -1006,4 +1018,4 @@ CREATE TABLE /*$wgDBprefix*/querycache_info ( UNIQUE KEY ( qci_type ) -) TYPE=InnoDB; \ No newline at end of file +) TYPE=InnoDB; diff --git a/maintenance/tables.sql b/maintenance/tables.sql index 288d4a0636..b00115e371 100644 --- a/maintenance/tables.sql +++ b/maintenance/tables.sql @@ -552,7 +552,7 @@ CREATE TABLE /*$wgDBprefix*/ipblocks ( ipb_id int(8) NOT NULL auto_increment, -- Blocked IP address in dotted-quad form or user name. - ipb_address varchar(40) binary NOT NULL default '', + ipb_address tinyblob NOT NULL default '', -- Blocked user ID or 0 for IP blocks. ipb_user int(8) unsigned NOT NULL default '0', @@ -570,20 +570,32 @@ CREATE TABLE /*$wgDBprefix*/ipblocks ( -- Indicates that the IP address was banned because a banned -- user accessed a page through it. If this is 1, ipb_address -- will be hidden, and the block identified by block ID number. - ipb_auto tinyint(1) NOT NULL default '0', + ipb_auto boolean NOT NULL default 0, + + -- If set to 1, block applies only to logged-out users + ipb_anon_only boolean NOT NULL default 0, + + -- Block prevents account creation from matching IP addresses + ipb_create_account boolean NOT NULL default 1, -- Time at which the block will expire. ipb_expiry char(14) binary NOT NULL default '', -- Start and end of an address range, in hexadecimal -- Size chosen to allow IPv6 - ipb_range_start varchar(32) NOT NULL default '', - ipb_range_end varchar(32) NOT NULL default '', + ipb_range_start tinyblob NOT NULL default '', + ipb_range_end tinyblob NOT NULL default '', PRIMARY KEY ipb_id (ipb_id), - INDEX ipb_address (ipb_address), + + -- Unique index to support "user already blocked" messages + -- Any new options which prevent collisions should be included + UNIQUE INDEX ipb_address (ipb_address(255), ipb_user, ipb_auto, ipb_anon_only), + INDEX ipb_user (ipb_user), - INDEX ipb_range (ipb_range_start(8), ipb_range_end(8)) + INDEX ipb_range (ipb_range_start(8), ipb_range_end(8)), + INDEX ipb_timestamp (ipb_timestamp), + INDEX ipb_expiry (ipb_expiry) ) TYPE=InnoDB; @@ -913,10 +925,10 @@ CREATE TABLE /*$wgDBprefix*/objectcache ( -- Cache of interwiki transclusion -- CREATE TABLE /*$wgDBprefix*/transcache ( - tc_url VARCHAR(255) NOT NULL, - tc_contents TEXT, - tc_time INT NOT NULL, - UNIQUE INDEX tc_url_idx(tc_url) + tc_url VARCHAR(255) NOT NULL, + tc_contents TEXT, + tc_time INT NOT NULL, + UNIQUE INDEX tc_url_idx(tc_url) ) TYPE=InnoDB; CREATE TABLE /*$wgDBprefix*/logging ( @@ -951,14 +963,14 @@ CREATE TABLE /*$wgDBprefix*/logging ( ) TYPE=InnoDB; CREATE TABLE /*$wgDBprefix*/trackbacks ( - tb_id integer AUTO_INCREMENT PRIMARY KEY, - tb_page integer REFERENCES page(page_id) ON DELETE CASCADE, - tb_title varchar(255) NOT NULL, - tb_url varchar(255) NOT NULL, - tb_ex text, - tb_name varchar(255), - - INDEX (tb_page) + tb_id integer AUTO_INCREMENT PRIMARY KEY, + tb_page integer REFERENCES page(page_id) ON DELETE CASCADE, + tb_title varchar(255) NOT NULL, + tb_url varchar(255) NOT NULL, + tb_ex text, + tb_name varchar(255), + + INDEX (tb_page) ) TYPE=InnoDB; @@ -986,13 +998,15 @@ CREATE TABLE /*$wgDBprefix*/job ( -- Details of updates to cached special pages CREATE TABLE /*$wgDBprefix*/querycache_info ( - -- Special page name - -- Corresponds to a qc_type value - qci_type varchar(32) NOT NULL default '', + -- Special page name + -- Corresponds to a qc_type value + qci_type varchar(32) NOT NULL default '', - -- Timestamp of last update - qci_timestamp char(14) NOT NULL default '19700101000000', + -- Timestamp of last update + qci_timestamp char(14) NOT NULL default '19700101000000', - UNIQUE KEY ( qci_type ) +UNIQUE KEY ( qci_type ) ) TYPE=InnoDB; + +-- vim: sw=2 sts=2 et diff --git a/maintenance/updaters.inc b/maintenance/updaters.inc index 164a00cf09..0174168353 100644 --- a/maintenance/updaters.inc +++ b/maintenance/updaters.inc @@ -56,6 +56,7 @@ $wgNewFields = array( array( 'interwiki', 'iw_trans', 'patch-interwiki-trans.sql' ), array( 'ipblocks', 'ipb_range_start', 'patch-ipb_range_start.sql' ), array( 'site_stats', 'ss_images', 'patch-ss_images.sql' ), + array( 'ipblocks', 'ipb_anon_only', 'patch-ipb_anon_only.sql' ), ); function rename_table( $from, $to, $patch ) { -- 2.20.1
    {$ipa}: - ip ) . "\" /> + {$addressPart}