MediaWiki
REL1_22
|
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 }