MediaWiki  REL1_22
userDupes.inc
Go to the documentation of this file.
00001 <?php
00035 class UserDupes {
00036     private $db;
00037     private $reassigned;
00038     private $trimmed;
00039     private $failed;
00040     private $outputCallback;
00041 
00042     function __construct( &$database, $outputCallback ) {
00043         $this->db = $database;
00044         $this->outputCallback = $outputCallback;
00045     }
00046 
00051     private function out( $str ) {
00052         call_user_func( $this->outputCallback, $str );
00053     }
00054 
00060     function hasUniqueIndex() {
00061         $info = $this->db->indexInfo( 'user', 'user_name', __METHOD__ );
00062         if ( !$info ) {
00063             $this->out( "WARNING: doesn't seem to have user_name index at all!\n" );
00064             return false;
00065         }
00066 
00067         # Confusingly, 'Non_unique' is 0 for *unique* indexes,
00068         # and 1 for *non-unique* indexes. Pass the crack, MySQL,
00069         # it's obviously some good stuff!
00070         return ( $info[0]->Non_unique == 0 );
00071     }
00072 
00084     function clearDupes() {
00085         return $this->checkDupes( true );
00086     }
00087 
00102     function checkDupes( $doDelete = false ) {
00103         if ( $this->hasUniqueIndex() ) {
00104             echo wfWikiID() . " already has a unique index on its user table.\n";
00105             return true;
00106         }
00107 
00108         $this->lock();
00109 
00110         $this->out( "Checking for duplicate accounts...\n" );
00111         $dupes = $this->getDupes();
00112         $count = count( $dupes );
00113 
00114         $this->out( "Found $count accounts with duplicate records on " . wfWikiID() . ".\n" );
00115         $this->trimmed = 0;
00116         $this->reassigned = 0;
00117         $this->failed = 0;
00118         foreach ( $dupes as $name ) {
00119             $this->examine( $name, $doDelete );
00120         }
00121 
00122         $this->unlock();
00123 
00124         $this->out( "\n" );
00125 
00126         if ( $this->reassigned > 0 ) {
00127             if ( $doDelete ) {
00128                 $this->out( "$this->reassigned duplicate accounts had edits reassigned to a canonical record id.\n" );
00129             } else {
00130                 $this->out( "$this->reassigned duplicate accounts need to have edits reassigned.\n" );
00131             }
00132         }
00133 
00134         if ( $this->trimmed > 0 ) {
00135             if ( $doDelete ) {
00136                 $this->out( "$this->trimmed duplicate user records were deleted from " . wfWikiID() . ".\n" );
00137             } else {
00138                 $this->out( "$this->trimmed duplicate user accounts were found on " . wfWikiID() . " which can be removed safely.\n" );
00139             }
00140         }
00141 
00142         if ( $this->failed > 0 ) {
00143             $this->out( "Something terribly awry; $this->failed duplicate accounts were not removed.\n" );
00144             return false;
00145         }
00146 
00147         if ( $this->trimmed == 0 || $doDelete ) {
00148             $this->out( "It is now safe to apply the unique index on user_name.\n" );
00149             return true;
00150         } else {
00151             $this->out( "Run this script again with the --fix option to automatically delete them.\n" );
00152             return false;
00153         }
00154     }
00155 
00160     function lock() {
00161         $set = array( 'user', 'revision' );
00162         $names = array_map( array( $this, 'lockTable' ), $set );
00163         $tables = implode( ',', $names );
00164 
00165         $this->db->query( "LOCK TABLES $tables", __METHOD__ );
00166     }
00167 
00168     function lockTable( $table ) {
00169         return $this->db->tableName( $table ) . ' WRITE';
00170     }
00171 
00175     function unlock() {
00176         $this->db->query( "UNLOCK TABLES", __METHOD__ );
00177     }
00178 
00184     function getDupes() {
00185         $user = $this->db->tableName( 'user' );
00186         $result = $this->db->query(
00187              "SELECT user_name,COUNT(*) AS n
00188                 FROM $user
00189             GROUP BY user_name
00190               HAVING n > 1", __METHOD__ );
00191 
00192         $list = array();
00193         foreach ( $result as $row ) {
00194             $list[] = $row->user_name;
00195         }
00196         return $list;
00197     }
00198 
00207     function examine( $name, $doDelete ) {
00208         $result = $this->db->select( 'user',
00209             array( 'user_id' ),
00210             array( 'user_name' => $name ),
00211             __METHOD__ );
00212 
00213         $firstRow = $this->db->fetchObject( $result );
00214         $firstId = $firstRow->user_id;
00215         $this->out( "Record that will be used for '$name' is user_id=$firstId\n" );
00216 
00217         foreach ( $result as $row ) {
00218             $dupeId = $row->user_id;
00219             $this->out( "... dupe id $dupeId: " );
00220             $edits = $this->editCount( $dupeId );
00221             if ( $edits > 0 ) {
00222                 $this->reassigned++;
00223                 $this->out( "has $edits edits! " );
00224                 if ( $doDelete ) {
00225                     $this->reassignEdits( $dupeId, $firstId );
00226                     $newEdits = $this->editCount( $dupeId );
00227                     if ( $newEdits == 0 ) {
00228                         $this->out( "confirmed cleaned. " );
00229                     } else {
00230                         $this->failed++;
00231                         $this->out( "WARNING! $newEdits remaining edits for $dupeId; NOT deleting user.\n" );
00232                         continue;
00233                     }
00234                 } else {
00235                     $this->out( "(will need to reassign edits on fix)" );
00236                 }
00237             } else {
00238                 $this->out( "ok, no edits. " );
00239             }
00240             $this->trimmed++;
00241             if ( $doDelete ) {
00242                 $this->trimAccount( $dupeId );
00243             }
00244             $this->out( "\n" );
00245         }
00246     }
00247 
00256     function editCount( $userid ) {
00257         return intval( $this->db->selectField(
00258             'revision',
00259             'COUNT(*)',
00260             array( 'rev_user' => $userid ),
00261             __METHOD__ ) );
00262     }
00263 
00269     function reassignEdits( $from, $to ) {
00270         $this->out( 'reassigning... ' );
00271         $this->db->update( 'revision',
00272             array( 'rev_user' => $to ),
00273             array( 'rev_user' => $from ),
00274             __METHOD__ );
00275         $this->out( "ok. " );
00276     }
00277 
00283     function trimAccount( $userid ) {
00284         $this->out( "deleting..." );
00285         $this->db->delete( 'user', array( 'user_id' => $userid ), __METHOD__ );
00286         $this->out( " ok" );
00287     }
00288 
00289 }