MediaWiki  REL1_24
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 
00065             return false;
00066         }
00067 
00068         # Confusingly, 'Non_unique' is 0 for *unique* indexes,
00069         # and 1 for *non-unique* indexes. Pass the crack, MySQL,
00070         # it's obviously some good stuff!
00071         return ( $info[0]->Non_unique == 0 );
00072     }
00073 
00085     function clearDupes() {
00086         return $this->checkDupes( true );
00087     }
00088 
00103     function checkDupes( $doDelete = false ) {
00104         if ( $this->hasUniqueIndex() ) {
00105             echo wfWikiID() . " already has a unique index on its user table.\n";
00106 
00107             return true;
00108         }
00109 
00110         $this->lock();
00111 
00112         $this->out( "Checking for duplicate accounts...\n" );
00113         $dupes = $this->getDupes();
00114         $count = count( $dupes );
00115 
00116         $this->out( "Found $count accounts with duplicate records on " . wfWikiID() . ".\n" );
00117         $this->trimmed = 0;
00118         $this->reassigned = 0;
00119         $this->failed = 0;
00120         foreach ( $dupes as $name ) {
00121             $this->examine( $name, $doDelete );
00122         }
00123 
00124         $this->unlock();
00125 
00126         $this->out( "\n" );
00127 
00128         if ( $this->reassigned > 0 ) {
00129             if ( $doDelete ) {
00130                 $this->out( "$this->reassigned duplicate accounts had edits "
00131                     . "reassigned to a canonical record id.\n" );
00132             } else {
00133                 $this->out( "$this->reassigned duplicate accounts need to have edits reassigned.\n" );
00134             }
00135         }
00136 
00137         if ( $this->trimmed > 0 ) {
00138             if ( $doDelete ) {
00139                 $this->out( "$this->trimmed duplicate user records were deleted from "
00140                     . wfWikiID() . ".\n" );
00141             } else {
00142                 $this->out( "$this->trimmed duplicate user accounts were found on "
00143                     . wfWikiID() . " which can be removed safely.\n" );
00144             }
00145         }
00146 
00147         if ( $this->failed > 0 ) {
00148             $this->out( "Something terribly awry; $this->failed duplicate accounts were not removed.\n" );
00149 
00150             return false;
00151         }
00152 
00153         if ( $this->trimmed == 0 || $doDelete ) {
00154             $this->out( "It is now safe to apply the unique index on user_name.\n" );
00155 
00156             return true;
00157         } else {
00158             $this->out( "Run this script again with the --fix option to automatically delete them.\n" );
00159 
00160             return false;
00161         }
00162     }
00163 
00168     function lock() {
00169         $set = array( 'user', 'revision' );
00170         $names = array_map( array( $this, 'lockTable' ), $set );
00171         $tables = implode( ',', $names );
00172 
00173         $this->db->query( "LOCK TABLES $tables", __METHOD__ );
00174     }
00175 
00176     function lockTable( $table ) {
00177         return $this->db->tableName( $table ) . ' WRITE';
00178     }
00179 
00183     function unlock() {
00184         $this->db->query( "UNLOCK TABLES", __METHOD__ );
00185     }
00186 
00192     function getDupes() {
00193         $user = $this->db->tableName( 'user' );
00194         $result = $this->db->query(
00195             "SELECT user_name,COUNT(*) AS n
00196                 FROM $user
00197             GROUP BY user_name
00198               HAVING n > 1", __METHOD__ );
00199 
00200         $list = array();
00201         foreach ( $result as $row ) {
00202             $list[] = $row->user_name;
00203         }
00204 
00205         return $list;
00206     }
00207 
00216     function examine( $name, $doDelete ) {
00217         $result = $this->db->select( 'user',
00218             array( 'user_id' ),
00219             array( 'user_name' => $name ),
00220             __METHOD__ );
00221 
00222         $firstRow = $this->db->fetchObject( $result );
00223         $firstId = $firstRow->user_id;
00224         $this->out( "Record that will be used for '$name' is user_id=$firstId\n" );
00225 
00226         foreach ( $result as $row ) {
00227             $dupeId = $row->user_id;
00228             $this->out( "... dupe id $dupeId: " );
00229             $edits = $this->editCount( $dupeId );
00230             if ( $edits > 0 ) {
00231                 $this->reassigned++;
00232                 $this->out( "has $edits edits! " );
00233                 if ( $doDelete ) {
00234                     $this->reassignEdits( $dupeId, $firstId );
00235                     $newEdits = $this->editCount( $dupeId );
00236                     if ( $newEdits == 0 ) {
00237                         $this->out( "confirmed cleaned. " );
00238                     } else {
00239                         $this->failed++;
00240                         $this->out( "WARNING! $newEdits remaining edits for $dupeId; NOT deleting user.\n" );
00241                         continue;
00242                     }
00243                 } else {
00244                     $this->out( "(will need to reassign edits on fix)" );
00245                 }
00246             } else {
00247                 $this->out( "ok, no edits. " );
00248             }
00249             $this->trimmed++;
00250             if ( $doDelete ) {
00251                 $this->trimAccount( $dupeId );
00252             }
00253             $this->out( "\n" );
00254         }
00255     }
00256 
00265     function editCount( $userid ) {
00266         return intval( $this->db->selectField(
00267             'revision',
00268             'COUNT(*)',
00269             array( 'rev_user' => $userid ),
00270             __METHOD__ ) );
00271     }
00272 
00278     function reassignEdits( $from, $to ) {
00279         $this->out( 'reassigning... ' );
00280         $this->db->update( 'revision',
00281             array( 'rev_user' => $to ),
00282             array( 'rev_user' => $from ),
00283             __METHOD__ );
00284         $this->out( "ok. " );
00285     }
00286 
00292     function trimAccount( $userid ) {
00293         $this->out( "deleting..." );
00294         $this->db->delete( 'user', array( 'user_id' => $userid ), __METHOD__ );
00295         $this->out( " ok" );
00296     }
00297 }