X-Git-Url: https://git.cyclocoop.org/?a=blobdiff_plain;f=maintenance%2FuserDupes.inc;h=31bae8ed0bca470aa099779e3c0f6ce6b9319474;hb=392af46809d831514f49618cdef1e1529d7fddf4;hp=e0b24988891d9ddc92f1a07d7e86755920d28817;hpb=b76be11a85418b32235eaec3fd42e3a0d9af83dc;p=lhc%2Fweb%2Fwiklou.git diff --git a/maintenance/userDupes.inc b/maintenance/userDupes.inc index e0b2498889..31bae8ed0b 100644 --- a/maintenance/userDupes.inc +++ b/maintenance/userDupes.inc @@ -1,54 +1,71 @@ -# http://www.mediawiki.org/ -# -# 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., -# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -# http://www.gnu.org/copyleft/gpl.html +/** + * Helper class for update.php and upgrade1_5.php. + * + * Copyright © 2005 Brion Vibber + * http://www.mediawiki.org/ + * + * 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 + */ /** * Look for duplicate user table entries and optionally prune them. + * @ingroup Maintenance */ class UserDupes { var $db; - var $unresolvable; + var $reassigned; var $trimmed; - - function UserDupes( &$database ) { - $this->db =& $database; + var $failed; + private $outputCallback; + + function __construct( &$database, $outputCallback ) { + $this->db = $database; + $this->outputCallback = $outputCallback; + } + + /** + * Output some text via the output callback provided + * @param $str String Text to print + */ + private function out( $str ) { + call_user_func( $this->outputCallback, $str ); } - + /** * Check if this database's user table has already had a unique * user_name index applied. * @return bool */ function hasUniqueIndex() { - $fname = 'UserDupes::hasUniqueIndex'; - #if( $wgDatabase->indexExists( 'image', 'PRIMARY' ) ) { - $info = $this->db->indexInfo( 'user', 'user_name', $fname ); - if( !$info ) { - echo "WARNING: doesn't seem to have user_name index at all!\n"; + $info = $this->db->indexInfo( 'user', 'user_name', __METHOD__ ); + if ( !$info ) { + $this->out( "WARNING: doesn't seem to have user_name index at all!\n" ); return false; } - + # Confusingly, 'Non_unique' is 0 for *unique* indexes, # and 1 for *non-unique* indexes. Pass the crack, MySQL, # it's obviously some good stuff! - return ( $info->Non_unique == 0 ); + return ( $info[0]->Non_unique == 0 ); } - + /** * Checks the database for duplicate user account records * and remove them in preparation for application of a unique @@ -63,208 +80,251 @@ class UserDupes { function clearDupes() { return $this->checkDupes( true ); } - + /** * Checks the database for duplicate user account records * in preparation for application of a unique index on the * user_name field. Returns true if the table is clean or * if duplicates can be resolved automatically. * - * May return false if there are unresolvable problems. + * Returns false if there are duplicates and resolution was + * not requested. (If doing resolution, edits may be reassigned.) * Status information will be echo'd to stdout. * - * @param bool $doDelete pass true to actually remove things - * from the database; false to just check. + * @param $doDelete bool: pass true to actually remove things + * from the database; false to just check. * @return bool */ - function checkDupes( $doDelete=false ) { - global $wgDBname; - - if( $this->hasUniqueIndex() ) { - echo "$wgDBname already has a unique index on its user table.\n"; + function checkDupes( $doDelete = false ) { + if ( $this->hasUniqueIndex() ) { + echo wfWikiID() . " already has a unique index on its user table.\n"; return true; } - + $this->lock(); - - echo "Checking for duplicate accounts...\n"; + + $this->out( "Checking for duplicate accounts...\n" ); $dupes = $this->getDupes(); $count = count( $dupes ); - - echo "Found $count accounts with duplicate records on $wgDBname.\n"; - $this->trimmed = 0; - $this->unresolvable = 0; - foreach( $dupes as $name ) { + + $this->out( "Found $count accounts with duplicate records on " . wfWikiID() . ".\n" ); + $this->trimmed = 0; + $this->reassigned = 0; + $this->failed = 0; + foreach ( $dupes as $name ) { $this->examine( $name, $doDelete ); } - + $this->unlock(); - - if( $this->trimmed > 0 ) { - echo "\n"; - if( $doDelete ) { - echo "$this->trimmed duplicate user records were deleted from $wgDBname.\n"; - if( $this->unresolvable == 0 ) { - echo "It is now safe to apply the unique index on user_name.\n"; - } + + $this->out( "\n" ); + + if ( $this->reassigned > 0 ) { + if ( $doDelete ) { + $this->out( "$this->reassigned duplicate accounts had edits reassigned to a canonical record id.\n" ); + } else { + $this->out( "$this->reassigned duplicate accounts need to have edits reassigned.\n" ); + } + } + + if ( $this->trimmed > 0 ) { + if ( $doDelete ) { + $this->out( "$this->trimmed duplicate user records were deleted from " . wfWikiID() . ".\n" ); } else { - echo "$this->trimmed duplicate user accounts were found on $wgDBname which can be removed safely.\n"; - echo "Run this script again with the --fix option to automatically delete them.\n"; + $this->out( "$this->trimmed duplicate user accounts were found on " . wfWikiID() . " which can be removed safely.\n" ); } } - - if( $this->unresolvable > 0 ) { - echo "\n"; - echo "There were $this->unresolvable unresolvable accounts on $wgDBname.\n"; - echo "These accounts have edits credited to duplicate records,\n"; - echo "and need to be cleaned up manually before the unique index\n"; - echo "for user_name can be applied.\n"; + + if ( $this->failed > 0 ) { + $this->out( "Something terribly awry; $this->failed duplicate accounts were not removed.\n" ); return false; - } else { + } + + if ( $this->trimmed == 0 || $doDelete ) { + $this->out( "It is now safe to apply the unique index on user_name.\n" ); return true; + } else { + $this->out( "Run this script again with the --fix option to automatically delete them.\n" ); + return false; } } - + /** * We don't want anybody to mess with our stuff... * @access private */ function lock() { - $fname = 'UserDupes::lock'; - global $wgVersion; - if( version_compare( $wgVersion, '1.5alpha', 'ge' ) ) { + if ( $this->newSchema() ) { $set = array( 'user', 'revision' ); } else { $set = array( 'user', 'cur', 'old' ); } $names = array_map( array( $this, 'lockTable' ), $set ); $tables = implode( ',', $names ); - - $result = $this->db->query( "LOCK TABLES $tables", $fname ); + + $this->db->query( "LOCK TABLES $tables", __METHOD__ ); } - + function lockTable( $table ) { return $this->db->tableName( $table ) . ' WRITE'; } - + + /** + * @return bool + * @access private + */ + function newSchema() { + return MWInit::classExists( 'Revision' ); + } + /** * @access private */ function unlock() { - $fname = 'UserDupes::unlock'; - $result = $this->db->query( "UNLOCK TABLES", $fname ); + $this->db->query( "UNLOCK TABLES", __METHOD__ ); } - + /** * Grab usernames for which multiple records are present in the database. * @return array * @access private */ function getDupes() { - $fname = 'UserDupes::listDupes'; $user = $this->db->tableName( 'user' ); $result = $this->db->query( "SELECT user_name,COUNT(*) AS n - FROM $user + FROM $user GROUP BY user_name - HAVING n > 1", $fname ); - + HAVING n > 1", __METHOD__ ); + $list = array(); - while( $row = $this->db->fetchObject( $result ) ) { + foreach ( $result as $row ) { $list[] = $row->user_name; } - $this->db->freeResult( $result ); - return $list; } - + /** * Examine user records for the given name. Try to see which record * will be the one that actually gets used, then check remaining records * for edits. If the dupes have no edits, we can safely remove them. - * @param string $name - * @param bool $doDelete + * @param $name string + * @param $doDelete bool * @access private */ function examine( $name, $doDelete ) { - $fname = 'UserDupes::listDupes'; $result = $this->db->select( 'user', array( 'user_id' ), array( 'user_name' => $name ), - $fname ); - + __METHOD__ ); + $firstRow = $this->db->fetchObject( $result ); $firstId = $firstRow->user_id; - echo "Record that will be used for '$name' is user_id=$firstId\n"; - - while( $row = $this->db->fetchObject( $result ) ) { + $this->out( "Record that will be used for '$name' is user_id=$firstId\n" ); + + foreach ( $result as $row ) { $dupeId = $row->user_id; - echo "... dupe id $dupeId: "; + $this->out( "... dupe id $dupeId: " ); $edits = $this->editCount( $dupeId ); - if( $edits > 0 ) { - $this->unresolvable++; - echo "has $edits edits! MANUAL INTERVENTION REQUIRED.\n"; - continue; - } else { - $this->trimmed++; - echo "ok, no edits. "; - if( $doDelete ) { - $this->trimAccount( $dupeId ); + if ( $edits > 0 ) { + $this->reassigned++; + $this->out( "has $edits edits! " ); + if ( $doDelete ) { + $this->reassignEdits( $dupeId, $firstId ); + $newEdits = $this->editCount( $dupeId ); + if ( $newEdits == 0 ) { + $this->out( "confirmed cleaned. " ); + } else { + $this->failed++; + $this->out( "WARNING! $newEdits remaining edits for $dupeId; NOT deleting user.\n" ); + continue; + } + } else { + $this->out( "(will need to reassign edits on fix)" ); } - echo "\n"; + } else { + $this->out( "ok, no edits. " ); } + $this->trimmed++; + if ( $doDelete ) { + $this->trimAccount( $dupeId ); + } + $this->out( "\n" ); } - $this->db->freeResult( $result ); } - + /** * Count the number of edits attributed to this user. * Does not currently check log table or other things * where it might show up... - * @param int $userid + * @param $userid int * @return int * @access private */ function editCount( $userid ) { - global $wgVersion; - if( version_compare( $wgVersion, '1.5alpha', 'ge' ) ) { + if ( $this->newSchema() ) { return $this->editCountOn( 'revision', 'rev_user', $userid ); } else { return $this->editCountOn( 'cur', 'cur_user', $userid ) + $this->editCountOn( 'old', 'old_user', $userid ); } } - + /** * Count the number of hits on a given table for this account. - * @param string $table - * @param string $field - * @param int $userid + * @param $table string + * @param $field string + * @param $userid int * @return int * @access private */ function editCountOn( $table, $field, $userid ) { - $fname = 'UserDupes::editCountOn'; - return IntVal( $this->db->selectField( + return intval( $this->db->selectField( $table, 'COUNT(*)', array( $field => $userid ), - $fname ) ); + __METHOD__ ) ); + } + + /** + * @param $from int + * @param $to int + * @access private + */ + function reassignEdits( $from, $to ) { + $set = $this->newSchema() + ? array( 'revision' => 'rev_user' ) + : array( 'cur' => 'cur_user', 'old' => 'old_user' ); + foreach ( $set as $table => $field ) { + $this->reassignEditsOn( $table, $field, $from, $to ); + } + } + + /** + * @param $table string + * @param $field string + * @param $from int + * @param $to int + * @access private + */ + function reassignEditsOn( $table, $field, $from, $to ) { + $this->out( "reassigning on $table... " ); + $this->db->update( $table, + array( $field => $to ), + array( $field => $from ), + __METHOD__ ); + $this->out( "ok. " ); } - + /** * Remove a user account line. - * @param int $userid + * @param $userid int * @access private */ function trimAccount( $userid ) { - $fname = 'UserDupes::trimAccount'; - echo "deleting..."; - $this->db->delete( 'user', array( 'user_id' => $userid ), $fname ); - echo " ok"; + $this->out( "deleting..." ); + $this->db->delete( 'user', array( 'user_id' => $userid ), __METHOD__ ); + $this->out( " ok" ); } - -} - -?> \ No newline at end of file +}