MediaWiki
REL1_21
|
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 if ( $this->newSchema() ) { 00162 $set = array( 'user', 'revision' ); 00163 } else { 00164 $set = array( 'user', 'cur', 'old' ); 00165 } 00166 $names = array_map( array( $this, 'lockTable' ), $set ); 00167 $tables = implode( ',', $names ); 00168 00169 $this->db->query( "LOCK TABLES $tables", __METHOD__ ); 00170 } 00171 00172 function lockTable( $table ) { 00173 return $this->db->tableName( $table ) . ' WRITE'; 00174 } 00175 00180 function newSchema() { 00181 return MWInit::classExists( 'Revision' ); 00182 } 00183 00187 function unlock() { 00188 $this->db->query( "UNLOCK TABLES", __METHOD__ ); 00189 } 00190 00196 function getDupes() { 00197 $user = $this->db->tableName( 'user' ); 00198 $result = $this->db->query( 00199 "SELECT user_name,COUNT(*) AS n 00200 FROM $user 00201 GROUP BY user_name 00202 HAVING n > 1", __METHOD__ ); 00203 00204 $list = array(); 00205 foreach ( $result as $row ) { 00206 $list[] = $row->user_name; 00207 } 00208 return $list; 00209 } 00210 00219 function examine( $name, $doDelete ) { 00220 $result = $this->db->select( 'user', 00221 array( 'user_id' ), 00222 array( 'user_name' => $name ), 00223 __METHOD__ ); 00224 00225 $firstRow = $this->db->fetchObject( $result ); 00226 $firstId = $firstRow->user_id; 00227 $this->out( "Record that will be used for '$name' is user_id=$firstId\n" ); 00228 00229 foreach ( $result as $row ) { 00230 $dupeId = $row->user_id; 00231 $this->out( "... dupe id $dupeId: " ); 00232 $edits = $this->editCount( $dupeId ); 00233 if ( $edits > 0 ) { 00234 $this->reassigned++; 00235 $this->out( "has $edits edits! " ); 00236 if ( $doDelete ) { 00237 $this->reassignEdits( $dupeId, $firstId ); 00238 $newEdits = $this->editCount( $dupeId ); 00239 if ( $newEdits == 0 ) { 00240 $this->out( "confirmed cleaned. " ); 00241 } else { 00242 $this->failed++; 00243 $this->out( "WARNING! $newEdits remaining edits for $dupeId; NOT deleting user.\n" ); 00244 continue; 00245 } 00246 } else { 00247 $this->out( "(will need to reassign edits on fix)" ); 00248 } 00249 } else { 00250 $this->out( "ok, no edits. " ); 00251 } 00252 $this->trimmed++; 00253 if ( $doDelete ) { 00254 $this->trimAccount( $dupeId ); 00255 } 00256 $this->out( "\n" ); 00257 } 00258 } 00259 00268 function editCount( $userid ) { 00269 if ( $this->newSchema() ) { 00270 return $this->editCountOn( 'revision', 'rev_user', $userid ); 00271 } else { 00272 return $this->editCountOn( 'cur', 'cur_user', $userid ) + 00273 $this->editCountOn( 'old', 'old_user', $userid ); 00274 } 00275 } 00276 00285 function editCountOn( $table, $field, $userid ) { 00286 return intval( $this->db->selectField( 00287 $table, 00288 'COUNT(*)', 00289 array( $field => $userid ), 00290 __METHOD__ ) ); 00291 } 00292 00298 function reassignEdits( $from, $to ) { 00299 $set = $this->newSchema() 00300 ? array( 'revision' => 'rev_user' ) 00301 : array( 'cur' => 'cur_user', 'old' => 'old_user' ); 00302 foreach ( $set as $table => $field ) { 00303 $this->reassignEditsOn( $table, $field, $from, $to ); 00304 } 00305 } 00306 00314 function reassignEditsOn( $table, $field, $from, $to ) { 00315 $this->out( "reassigning on $table... " ); 00316 $this->db->update( $table, 00317 array( $field => $to ), 00318 array( $field => $from ), 00319 __METHOD__ ); 00320 $this->out( "ok. " ); 00321 } 00322 00328 function trimAccount( $userid ) { 00329 $this->out( "deleting..." ); 00330 $this->db->delete( 'user', array( 'user_id' => $userid ), __METHOD__ ); 00331 $this->out( " ok" ); 00332 } 00333 00334 }