From: Nicolas Weeger Date: Mon, 14 Feb 2005 23:21:07 +0000 (+0000) Subject: Fix for bug http://bugzilla.wikimedia.org/show_bug.cgi?id=1138 (ip block related). X-Git-Tag: 1.5.0alpha1~735 X-Git-Url: https://git.cyclocoop.org/%7B%24admin_url%7Dmembres/modifier.php?a=commitdiff_plain;h=4d11f7c2d39736a65daf8133754b50aa7a89e9ea;p=lhc%2Fweb%2Fwiklou.git Fix for bug bugzilla.wikimedia.org/show_bug.cgi?id=1138 (ip block related). New behaviour: check slave when user asks for the edit page, nothing when previewing, master when saving the page. Don't use memcache, as apparently it was causing issues. --- diff --git a/includes/BlockCache.php b/includes/BlockCache.php index 8f9a5a60aa..6f53e61ac0 100644 --- a/includes/BlockCache.php +++ b/includes/BlockCache.php @@ -1,139 +1 @@ -mMemcKey = $dbName.':ipblocks'; - - if ( !$deferLoad ) { - $this->load(); - } - } - - /** - * Load the blocks from the database and save them to memcached - */ - function loadFromDB() { - global $wgUseMemCached, $wgMemc; - $this->mData = array(); - # Selecting FOR UPDATE is a convenient way to serialise the memcached and DB operations, - # which is necessary even though we don't update the DB - if ( $wgUseMemCached ) { - Block::enumBlocks( 'wfBlockCacheInsert', '', EB_FOR_UPDATE ); - $wgMemc->set( $this->mMemcKey, $this->mData, 0 ); - } else { - Block::enumBlocks( 'wfBlockCacheInsert', '' ); - } - } - - /** - * Load the cache from memcached or, if that's not possible, from the DB - */ - function load() { - global $wgUseMemCached, $wgMemc; - - if ( $this->mData === false) { - # Try memcached - if ( $wgUseMemCached ) { - $this->mData = $wgMemc->get( $this->mMemcKey ); - } - - if ( !is_array( $this->mData ) ) { - $this->loadFromDB(); - } - } - } - - /** - * Add a block to the cache - * - * @param Object &$block Reference to a "Block" object. - */ - function insert( &$block ) { - if ( $block->mUser == 0 ) { - $nb = $block->getNetworkBits(); - $ipint = $block->getIntegerAddr(); - $index = $ipint >> ( 32 - $nb ); - - if ( !array_key_exists( $nb, $this->mData ) ) { - $this->mData[$nb] = array(); - } - - $this->mData[$nb][$index] = 1; - } - } - - /** - * Find out if a given IP address is blocked - * - * @param String $ip IP address - */ - function get( $ip ) { - $this->load(); - $ipint = ip2long( $ip ); - $blocked = false; - - foreach ( $this->mData as $networkBits => $blockInts ) { - if ( array_key_exists( $ipint >> ( 32 - $networkBits ), $blockInts ) ) { - $blocked = true; - break; - } - } - if ( $blocked ) { - # Clear low order bits - if ( $networkBits != 32 ) { - $ip .= '/'.$networkBits; - $ip = Block::normaliseRange( $ip ); - } - $block = new Block(); - $block->load( $ip ); - } else { - $block = false; - } - - return $block; - } - - /** - * Clear the local cache - * There was once a clear() to clear memcached too, but I deleted it - */ - function clearLocal() { - $this->mData = false; - } -} - -/** - * Add a block to the global $wgBlockCache - * - * @param Object $block A "Block"-object - * @param Any $tag unused - */ -function wfBlockCacheInsert( $block, $tag ) { - global $wgBlockCache; - $wgBlockCache->insert( $block ); -} +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->mForUpdate = false; $this->initialiseRange(); } /*static*/ function newFromDB( $address, $user = 0, $killExpired = true ) { $ban = new Block(); $ban->load( $address, $user, $killExpired ); return $ban; } function clear() { $mAddress = $mReason = $mTimestamp = ''; $mUser = $mBy = 0; } /** * Get a ban from the DB, with either the given address or the given username */ function load( $address = '', $user = 0, $killExpired = true ) { $fname = 'Block::load'; wfDebug( "Block::load: '$address', '$user', $killExpired\n" ); $ret = false; $killed = false; if ( $this->forUpdate() ) { $db =& wfGetDB( DB_MASTER ); $options = 'FOR UPDATE'; } else { $db =& wfGetDB( DB_SLAVE ); $options = ''; } $ipblocks = $db->tableName( 'ipblocks' ); if ( 0 == $user && $address=='' ) { $sql = "SELECT * from $ipblocks $options"; } elseif ($address=="") { $sql = "SELECT * FROM $ipblocks WHERE ipb_user={$user} $options"; } elseif ($user=="") { $sql = "SELECT * FROM $ipblocks WHERE ipb_address='" . $db->strencode( $address ) . "' $options"; } elseif ( $options=='' ) { # If there are no optiones (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 ) ) { # User is not blocked $this->clear(); } else { # Get first block $row = $db->fetchObject( $res ); $this->initFromRow( $row ); if ( $killExpired ) { # If requested, delete expired rows do { $killed = $this->deleteIfExpired(); if ( $killed ) { $row = $db->fetchObject( $res ); if ( $row ) { $this->initFromRow( $row ); } } } while ( $killed && $row ); # If there were any left after the killing finished, return true if ( !$row ) { $ret = false; $this->clear(); } else { $ret = true; } } else { $ret = true; } } $db->freeResult( $res ); return $ret; } function initFromRow( $row ) { $this->mAddress = $row->ipb_address; $this->mReason = $row->ipb_reason; $this->mTimestamp = wfTimestamp(TS_MW,$row->ipb_timestamp); $this->mUser = $row->ipb_user; $this->mBy = $row->ipb_by; $this->mAuto = $row->ipb_auto; $this->mId = $row->ipb_id; $this->mExpiry = $row->ipb_expiry ? wfTimestamp(TS_MW,$row->ipb_expiry) : $row->ipb_expiry; $this->initialiseRange(); } function initialiseRange() { if ( $this->mUser == 0 ) { $rangeParts = explode( '/', $this->mAddress ); if ( count( $rangeParts ) == 2 ) { $this->mNetworkBits = $rangeParts[1]; } else { $this->mNetworkBits = 32; } $this->mIntegerAddr = ip2long( $rangeParts[0] ); } else { $this->mNetworkBits = false; $this->mIntegerAddr = false; } } /** * Callback with a Block object for every block */ /*static*/ function enumBlocks( $callback, $tag, $flags = 0 ) { $block = new Block(); if ( $flags & EB_FOR_UPDATE ) { $db =& wfGetDB( DB_MASTER ); $options = 'FOR UPDATE'; $block->forUpdate( true ); } else { $db =& wfGetDB( DB_SLAVE ); $options = ''; } $ipblocks = $db->tableName( 'ipblocks' ); $sql = "SELECT * FROM $ipblocks ORDER BY ipb_timestamp DESC $options"; $res = $db->query( $sql, 'Block::enumBans' ); while ( $row = $db->fetchObject( $res ) ) { $block->initFromRow( $row ); if ( !( $flags & EB_KEEP_EXPIRED ) ) { if ( !$block->deleteIfExpired() ) { $callback( $block, $tag ); } } else { $callback( $block, $tag ); } } wfFreeResult( $res ); } function delete() { $fname = 'Block::delete'; $dbw =& wfGetDB( DB_MASTER ); if ( $this->mAddress == '' ) { $condition = array( 'ipb_id' => $this->mId ); } else { $condition = array( 'ipb_address' => $this->mAddress ); } $dbw->delete( 'ipblocks', $condition, $fname ); $this->clearCache(); } function insert() { wfDebug( "Block::insert; timestamp {$this->mTimestamp}\n" ); $dbw =& wfGetDB( DB_MASTER ); $dbw->insert( 'ipblocks', array( 'ipb_address' => $this->mAddress, 'ipb_user' => $this->mUser, 'ipb_by' => $this->mBy, 'ipb_reason' => $this->mReason, 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp), 'ipb_auto' => $this->mAuto, 'ipb_expiry' => $this->mExpiry ? $dbw->timestamp($this->mExpiry) : $this->mExpiry, ), 'Block::insert' ); $this->clearCache(); } function deleteIfExpired() { if ( $this->isExpired() ) { wfDebug( "Block::deleteIfExpired() -- deleting\n" ); $this->delete(); return true; } else { wfDebug( "Block::deleteIfExpired() -- not expired\n" ); return false; } } function isExpired() { wfDebug( "Block::isExpired() checking current " . wfTimestampNow() . " vs $this->mExpiry\n" ); if ( !$this->mExpiry ) { return false; } else { return wfTimestampNow() > $this->mExpiry; } } function isValid() { return $this->mAddress != ''; } function updateTimestamp() { if ( $this->mAuto ) { $this->mTimestamp = wfTimestamp(); $this->mExpiry = Block::getAutoblockExpiry( $this->mTimestamp ); $dbw =& wfGetDB( DB_MASTER ); $dbw->update( 'ipblocks', array( /* SET */ 'ipb_timestamp' => $dbw->timestamp($this->mTimestamp), 'ipb_expiry' => $dbw->timestamp($this->mExpiry), ), array( /* WHERE */ 'ipb_address' => $this->mAddress ), 'Block::updateTimestamp' ); $this->clearCache(); } } /* private */ function clearCache() { global $wgBlockCache; if ( is_object( $wgBlockCache ) ) { $wgBlockCache->loadFromDB(); } } function getIntegerAddr() { return $this->mIntegerAddr; } function getNetworkBits() { return $this->mNetworkBits; } function forUpdate( $x = NULL ) { return wfSetVar( $this->mForUpdate, $x ); } /* static */ function getAutoblockExpiry( $timestamp ) { global $wgAutoblockExpiry; return wfTimestamp( TS_MW, wfTimestamp( TS_UNIX, $timestamp ) + $wgAutoblockExpiry ); } /* static */ function normaliseRange( $range ) { $parts = explode( '/', $range ); if ( count( $parts ) == 2 ) { $shift = 32 - $parts[1]; $ipint = ip2long( $parts[0] ); $ipint = $ipint >> $shift << $shift; $newip = long2ip( $ipint ); $range = "$newip/{$parts[1]}"; } return $range; }}?> \ No newline at end of file diff --git a/includes/EditPage.php b/includes/EditPage.php index 95e59c84ee..be22b4780a 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -150,7 +150,9 @@ class EditPage { $wgOut->readOnlyPage( $this->mArticle->getContent( true ), true ); return; } - if ( $wgUser->isBlocked() ) { + if ( !$this->preview && $wgUser->isBlocked( !$this->save ) ) { + # When previewing, don't check blocked state - will get caught at save time. + # Also, check when starting edition is done against slave to improve performance. $this->blockedIPpage(); return; } @@ -184,51 +186,21 @@ class EditPage { * @todo document */ function importFormData( &$request ) { - if( $request->wasPosted() ) { - # These fields need to be checked for encoding. - # Also remove trailing whitespace, but don't remove _initial_ - # whitespace from the text boxes. This may be significant formatting. - $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) ); - $this->textbox2 = rtrim( $request->getText( 'wpTextbox2' ) ); - $this->mMetaData = rtrim( $request->getText( 'metadata' ) ); - $this->summary = trim( $request->getText( 'wpSummary' ) ); - - $this->edittime = $request->getVal( 'wpEdittime' ); - if( is_null( $this->edittime ) ) { - # If the form is incomplete, force to preview. - $this->preview = true; - } else { - if( $this->tokenOk( $request ) ) { - # Some browsers will not report any submit button - # if the user hits enter in the comment box. - # The unmarked state will be assumed to be a save, - # if the form seems otherwise complete. - $this->preview = $request->getCheck( 'wpPreview' ); - } else { - # Page might be a hack attempt posted from - # an external site. Preview instead of saving. - $this->preview = true; - } - } - $this->save = !$this->preview; - if( !preg_match( '/^\d{14}$/', $this->edittime )) { - $this->edittime = null; - } - - $this->minoredit = $request->getCheck( 'wpMinoredit' ); - $this->watchthis = $request->getCheck( 'wpWatchthis' ); - } else { - # Not a posted form? Start with nothing. - $this->textbox1 = ''; - $this->textbox2 = ''; - $this->mMetaData = ''; - $this->summary = ''; - $this->edittime = ''; - $this->preview = false; - $this->save = false; - $this->minoredit = false; - $this->watchthis = false; - } + # These fields need to be checked for encoding. + # Also remove trailing whitespace, but don't remove _initial_ + # whitespace from the text boxes. This may be significant formatting. + $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) ); + $this->textbox2 = rtrim( $request->getText( 'wpTextbox2' ) ); + $this->mMetaData = rtrim( $request->getText( 'metadata' ) ); + $this->summary = trim( $request->getText( 'wpSummary' ) ); + + $this->edittime = $request->getVal( 'wpEdittime' ); + if( !preg_match( '/^\d{14}$/', $this->edittime )) $this->edittime = ''; + + $this->preview = $request->getCheck( 'wpPreview' ); + $this->save = $request->wasPosted() && !$this->preview; + $this->minoredit = $request->getCheck( 'wpMinoredit' ); + $this->watchthis = $request->getCheck( 'wpWatchthis' ); $this->oldid = $request->getInt( 'oldid' ); @@ -239,24 +211,15 @@ class EditPage { } /** - * Make sure the form isn't faking a user's credentials. - * - * @param WebRequest $request - * @return bool - * @access private + * Since there is only one text field on the edit form, + * pressing will cause the form to be submitted, but + * the submit button value won't appear in the query, so we + * Fake it here before going back to edit(). This is kind of + * ugly, but it helps some old URLs to still work. */ - function tokenOk( &$request ) { - global $wgUser; - if( $wgUser->getId() == 0 ) { - # Anonymous users may not have a session - # open. Don't tokenize. - return true; - } else { - return $wgUser->matchEditToken( $request->getVal( 'wpEditToken' ) ); - } - } - function submit() { + if( !$this->preview ) $this->save = true; + $this->edit(); } @@ -310,7 +273,8 @@ class EditPage { # Error messages or other handling should be performed by the filter function return; } - if ( $wgUser->isBlocked() ) { + if ( $wgUser->isBlocked( false ) ) { + # Check block state against master, thus 'false'. $this->blockedIPpage(); return; } @@ -580,27 +544,24 @@ class EditPage { $wgOut->setOnloadHandler( 'document.editform.wpTextbox1.focus()' ); } # Prepare a list of templates used by this page - $templates = ''; + $db =& wfGetDB( DB_SLAVE ); + $page = $db->tableName( 'page' ); + $links = $db->tableName( 'links' ); $id = $this->mTitle->getArticleID(); - if ( 0 !== $id ) { - $db =& wfGetDB( DB_SLAVE ); - $page = $db->tableName( 'page' ); - $links = $db->tableName( 'links' ); - $sql = "SELECT page_namespace,page_title,page_id ". - "FROM $page,$links WHERE l_to=page_id AND l_from={$id} and page_namespace=".NS_TEMPLATE; - $res = $db->query( $sql, "EditPage::editform" ); - if ( false !== $res ) { - if ( $db->numRows( $res ) ) { - $templates = '
'. wfMsg( 'templatesused' ) . ''; + $sql = "SELECT page_namespace,page_title,page_id ". + "FROM $page,$links WHERE l_to=page_id AND l_from={$id} and page_namespace=".NS_TEMPLATE; + $res = $db->query( $sql, "EditPage::editform" ); + + if ( $db->numRows( $res ) ) { + $templates = '
'. wfMsg( 'templatesused' ) . ''; + } else { + $templates = ''; } global $wgLivePreview, $wgStylePath; @@ -659,21 +620,6 @@ END section ) . "\" name=\"wpSection\" /> edittime}\" name=\"wpEdittime\" />\n" ); - if ( 0 != $wgUser->getID() ) { - /** - * To make it harder for someone to slip a user a page - * which submits an edit form to the wiki without their - * knowledge, a random token is associated with the login - * session. If it's not passed back with the submission, - * we won't save the page, or render user JavaScript and - * CSS previews. - */ - $token = htmlspecialchars( $wgUser->editToken() ); - $wgOut->addHTML( " -\n" ); - } - - if ( $isConflict ) { require_once( "DifferenceEngine.php" ); $wgOut->addHTML( "

" . wfMsg( "yourdiff" ) . "

\n" ); @@ -716,7 +662,6 @@ END } $parserOutput = $wgParser->parse( $previewtext , $wgTitle, $parserOptions ); $wgOut->addHTML( $parserOutput->mText ); - return $previewhead; } else { # if user want to see preview when he edit an article if( $wgUser->getOption('previewonfirst') and ($this->textbox1 == '')) { @@ -732,8 +677,8 @@ END $previewHTML = $parserOutput->mText; $wgOut->addCategoryLinks($parserOutput->getCategoryLinks()); $wgOut->addLanguageLinks($parserOutput->getLanguageLinks()); - return $previewhead . $previewHTML; } + return $previewhead . $previewHTML; } /** diff --git a/includes/User.php b/includes/User.php index a116389938..5272730f53 100644 --- a/includes/User.php +++ b/includes/User.php @@ -242,8 +242,16 @@ class User { /** * Get blocking information * @access private + * @param bool $bFromSlave Specify whether to check slave or master. To improve performance, + * non-critical checks are done against slaves. Check when actually saving should be done against + * master. + * + * Note that even if $bFromSlave is false, the check is done first against slave, then master. + * The logic is that if blocked on slave, we'll assume it's either blocked on master or + * just slightly outta sync and soon corrected - safer to block slightly more that less. + * And it's cheaper to check slave first, then master if needed, than master always. */ - function getBlockedStatus() { + function getBlockedStatus( $bFromSlave = false ) { global $wgIP, $wgBlockCache, $wgProxyList; if ( -1 != $this->mBlockedby ) { return; } @@ -253,7 +261,8 @@ class User { # User blocking if ( $this->mId ) { $block = new Block(); - if ( $block->load( $wgIP , $this->mId ) ) { + $block->forUpdate( $bFromSlave ); + if ( $block->load( $wgIP , $this->mId ) ) { $this->mBlockedby = $block->mBy; $this->mBlockreason = $block->mReason; } @@ -261,7 +270,14 @@ class User { # IP/range blocking if ( !$this->mBlockedby ) { - $block = $wgBlockCache->get( $wgIP ); + # Check first against slave, and optionally from master. + $block = $wgBlockCache->get( $wgIP, true ); + if ( !block && !$bFromSlave ) + { + # Not blocked: check against master, to make sure. + $wgBlockCache->clearLocal( ); + $block = $wgBlockCache->get( $wgIP, false ); + } if ( $block !== false ) { $this->mBlockedby = $block->mBy; $this->mBlockreason = $block->mReason; @@ -281,8 +297,8 @@ class User { * Check if user is blocked * @return bool True if blocked, false otherwise */ - function isBlocked() { - $this->getBlockedStatus(); + function isBlocked( $bFromSlave = false ) { + $this->getBlockedStatus( $bFromSlave ); if ( 0 === $this->mBlockedby ) { return false; } return true; } @@ -1187,39 +1203,6 @@ class User { } return false; } - - /** - * Initialize (if necessary) and return a session token value - * which can be used in edit forms to show that the user's - * login credentials aren't being hijacked with a foreign form - * submission. - * - * @return string - * @access public - */ - function editToken() { - if( !isset( $_SESSION['wsEditToken'] ) ) { - $token = dechex( mt_rand() ) . dechex( mt_rand() ); - $_SESSION['wsEditToken'] = $token; - } - return $_SESSION['wsEditToken']; - } - - /** - * Check given value against the token value stored in the session. - * A match should confirm that the form was submitted from the - * user's own login session, not a form submission from a third-party - * site. - * - * @param string $val - * @return bool - * @access public - */ - function matchEditToken( $val ) { - if( !isset( $_SESSION['wsEditToken'] ) ) - return false; - return $_SESSION['wsEditToken'] == $val; - } } ?>