MediaWiki REL1_32
userDupes.inc
Go to the documentation of this file.
1<?php
35class UserDupes {
36 private $db;
37 private $reassigned;
38 private $trimmed;
39 private $failed;
41
42 function __construct( &$database, $outputCallback ) {
43 $this->db = $database;
44 $this->outputCallback = $outputCallback;
45 }
46
51 private function out( $str ) {
52 call_user_func( $this->outputCallback, $str );
53 }
54
60 function hasUniqueIndex() {
61 $info = $this->db->indexInfo( 'user', 'user_name', __METHOD__ );
62 if ( !$info ) {
63 $this->out( "WARNING: doesn't seem to have user_name index at all!\n" );
64
65 return false;
66 }
67
68 # Confusingly, 'Non_unique' is 0 for *unique* indexes,
69 # and 1 for *non-unique* indexes. Pass the crack, MySQL,
70 # it's obviously some good stuff!
71 return ( $info[0]->Non_unique == 0 );
72 }
73
85 function clearDupes() {
86 return $this->checkDupes( true );
87 }
88
103 function checkDupes( $doDelete = false ) {
104 if ( $this->hasUniqueIndex() ) {
105 echo wfWikiID() . " already has a unique index on its user table.\n";
106
107 return true;
108 }
109
110 $this->lock();
111
112 $this->out( "Checking for duplicate accounts...\n" );
113 $dupes = $this->getDupes();
114 $count = count( $dupes );
115
116 $this->out( "Found $count accounts with duplicate records on " . wfWikiID() . ".\n" );
117 $this->trimmed = 0;
118 $this->reassigned = 0;
119 $this->failed = 0;
120 foreach ( $dupes as $name ) {
121 $this->examine( $name, $doDelete );
122 }
123
124 $this->unlock();
125
126 $this->out( "\n" );
127
128 if ( $this->reassigned > 0 ) {
129 if ( $doDelete ) {
130 $this->out( "$this->reassigned duplicate accounts had edits "
131 . "reassigned to a canonical record id.\n" );
132 } else {
133 $this->out( "$this->reassigned duplicate accounts need to have edits reassigned.\n" );
134 }
135 }
136
137 if ( $this->trimmed > 0 ) {
138 if ( $doDelete ) {
139 $this->out( "$this->trimmed duplicate user records were deleted from "
140 . wfWikiID() . ".\n" );
141 } else {
142 $this->out( "$this->trimmed duplicate user accounts were found on "
143 . wfWikiID() . " which can be removed safely.\n" );
144 }
145 }
146
147 if ( $this->failed > 0 ) {
148 $this->out( "Something terribly awry; $this->failed duplicate accounts were not removed.\n" );
149
150 return false;
151 }
152
153 if ( $this->trimmed == 0 || $doDelete ) {
154 $this->out( "It is now safe to apply the unique index on user_name.\n" );
155
156 return true;
157 } else {
158 $this->out( "Run this script again with the --fix option to automatically delete them.\n" );
159
160 return false;
161 }
162 }
163
168 function lock() {
169 $set = [ 'user', 'revision' ];
170 $names = array_map( [ $this, 'lockTable' ], $set );
171 $tables = implode( ',', $names );
172
173 $this->db->query( "LOCK TABLES $tables", __METHOD__ );
174 }
175
176 function lockTable( $table ) {
177 return $this->db->tableName( $table ) . ' WRITE';
178 }
179
183 function unlock() {
184 $this->db->query( "UNLOCK TABLES", __METHOD__ );
185 }
186
192 function getDupes() {
193 $user = $this->db->tableName( 'user' );
194 $result = $this->db->query(
195 "SELECT user_name,COUNT(*) AS n
196 FROM $user
197 GROUP BY user_name
198 HAVING n > 1", __METHOD__ );
199
200 $list = [];
201 foreach ( $result as $row ) {
202 $list[] = $row->user_name;
203 }
204
205 return $list;
206 }
207
216 function examine( $name, $doDelete ) {
217 $result = $this->db->select( 'user',
218 [ 'user_id' ],
219 [ 'user_name' => $name ],
220 __METHOD__ );
221
222 $firstRow = $this->db->fetchObject( $result );
223 $firstId = $firstRow->user_id;
224 $this->out( "Record that will be used for '$name' is user_id=$firstId\n" );
225
226 foreach ( $result as $row ) {
227 $dupeId = $row->user_id;
228 $this->out( "... dupe id $dupeId: " );
229 $edits = $this->editCount( $dupeId );
230 if ( $edits > 0 ) {
231 $this->reassigned++;
232 $this->out( "has $edits edits! " );
233 if ( $doDelete ) {
234 $this->reassignEdits( $dupeId, $firstId );
235 $newEdits = $this->editCount( $dupeId );
236 if ( $newEdits == 0 ) {
237 $this->out( "confirmed cleaned. " );
238 } else {
239 $this->failed++;
240 $this->out( "WARNING! $newEdits remaining edits for $dupeId; NOT deleting user.\n" );
241 continue;
242 }
243 } else {
244 $this->out( "(will need to reassign edits on fix)" );
245 }
246 } else {
247 $this->out( "ok, no edits. " );
248 }
249 $this->trimmed++;
250 if ( $doDelete ) {
251 $this->trimAccount( $dupeId );
252 }
253 $this->out( "\n" );
254 }
255 }
256
265 function editCount( $userid ) {
266 return intval( $this->db->selectField(
267 'revision',
268 'COUNT(*)',
269 [ 'rev_user' => $userid ],
270 __METHOD__ ) );
271 }
272
278 function reassignEdits( $from, $to ) {
279 $this->out( 'reassigning... ' );
280 $this->db->update( 'revision',
281 [ 'rev_user' => $to ],
282 [ 'rev_user' => $from ],
283 __METHOD__ );
284 $this->out( "ok. " );
285 }
286
292 function trimAccount( $userid ) {
293 $this->out( "deleting..." );
294 $this->db->delete( 'user', [ 'user_id' => $userid ], __METHOD__ );
295 $this->out( " ok" );
296 }
297}
wfWikiID()
Get an ASCII string identifying this wiki This is used as a prefix in memcached keys.
Look for duplicate user table entries and optionally prune them.
Definition userDupes.inc:35
lock()
We don't want anybody to mess with our stuff...
editCount( $userid)
Count the number of edits attributed to this user.
__construct(&$database, $outputCallback)
Definition userDupes.inc:42
getDupes()
Grab usernames for which multiple records are present in the database.
examine( $name, $doDelete)
Examine user records for the given name.
trimAccount( $userid)
Remove a user account line.
out( $str)
Output some text via the output callback provided.
Definition userDupes.inc:51
lockTable( $table)
checkDupes( $doDelete=false)
Checks the database for duplicate user account records in preparation for application of a unique ind...
clearDupes()
Checks the database for duplicate user account records and remove them in preparation for application...
Definition userDupes.inc:85
reassignEdits( $from, $to)
hasUniqueIndex()
Check if this database's user table has already had a unique user_name index applied.
Definition userDupes.inc:60
this hook is for auditing only RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist & $tables
Definition hooks.txt:1035