MediaWiki REL1_31
rebuildrecentchanges.php
Go to the documentation of this file.
1<?php
26require_once __DIR__ . '/Maintenance.php';
27
30
38 private $cutoffFrom;
40 private $cutoffTo;
41
42 public function __construct() {
43 parent::__construct();
44 $this->addDescription( 'Rebuild recent changes' );
45
46 $this->addOption(
47 'from',
48 "Only rebuild rows in requested time range (in YYYYMMDDHHMMSS format)",
49 false,
50 true
51 );
52 $this->addOption(
53 'to',
54 "Only rebuild rows in requested time range (in YYYYMMDDHHMMSS format)",
55 false,
56 true
57 );
58 $this->setBatchSize( 200 );
59 }
60
61 public function execute() {
62 if (
63 ( $this->hasOption( 'from' ) && !$this->hasOption( 'to' ) ) ||
64 ( !$this->hasOption( 'from' ) && $this->hasOption( 'to' ) )
65 ) {
66 $this->fatalError( "Both 'from' and 'to' must be given, or neither" );
67 }
68
69 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
70 $this->rebuildRecentChangesTablePass1( $lbFactory );
71 $this->rebuildRecentChangesTablePass2( $lbFactory );
72 $this->rebuildRecentChangesTablePass3( $lbFactory );
73 $this->rebuildRecentChangesTablePass4( $lbFactory );
74 $this->rebuildRecentChangesTablePass5( $lbFactory );
75 if ( !( $this->hasOption( 'from' ) && $this->hasOption( 'to' ) ) ) {
76 $this->purgeFeeds();
77 }
78 $this->output( "Done.\n" );
79 }
80
84 private function rebuildRecentChangesTablePass1( ILBFactory $lbFactory ) {
85 $dbw = $this->getDB( DB_MASTER );
86 $commentStore = CommentStore::getStore();
87
88 if ( $this->hasOption( 'from' ) && $this->hasOption( 'to' ) ) {
89 $this->cutoffFrom = wfTimestamp( TS_UNIX, $this->getOption( 'from' ) );
90 $this->cutoffTo = wfTimestamp( TS_UNIX, $this->getOption( 'to' ) );
91
92 $sec = $this->cutoffTo - $this->cutoffFrom;
93 $days = $sec / 24 / 3600;
94 $this->output( "Rebuilding range of $sec seconds ($days days)\n" );
95 } else {
96 global $wgRCMaxAge;
97
98 $days = $wgRCMaxAge / 24 / 3600;
99 $this->output( "Rebuilding \$wgRCMaxAge=$wgRCMaxAge seconds ($days days)\n" );
100
101 $this->cutoffFrom = time() - $wgRCMaxAge;
102 $this->cutoffTo = time();
103 }
104
105 $this->output( "Clearing recentchanges table for time range...\n" );
106 $rcids = $dbw->selectFieldValues(
107 'recentchanges',
108 'rc_id',
109 [
110 'rc_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
111 'rc_timestamp < ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) )
112 ]
113 );
114 foreach ( array_chunk( $rcids, $this->getBatchSize() ) as $rcidBatch ) {
115 $dbw->delete( 'recentchanges', [ 'rc_id' => $rcidBatch ], __METHOD__ );
116 $lbFactory->waitForReplication();
117 }
118
119 $this->output( "Loading from page and revision tables...\n" );
120
121 $commentQuery = $commentStore->getJoin( 'rev_comment' );
122 $actorQuery = ActorMigration::newMigration()->getJoin( 'rev_user' );
123 $res = $dbw->select(
124 [ 'revision', 'page' ] + $commentQuery['tables'] + $actorQuery['tables'],
125 [
126 'rev_timestamp',
127 'rev_minor_edit',
128 'rev_id',
129 'rev_deleted',
130 'page_namespace',
131 'page_title',
132 'page_is_new',
133 'page_id'
134 ] + $commentQuery['fields'] + $actorQuery['fields'],
135 [
136 'rev_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
137 'rev_timestamp < ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) )
138 ],
139 __METHOD__,
140 [ 'ORDER BY' => 'rev_timestamp DESC' ],
141 [
142 'page' => [ 'JOIN', 'rev_page=page_id' ],
143 ] + $commentQuery['joins'] + $actorQuery['joins']
144 );
145
146 $this->output( "Inserting from page and revision tables...\n" );
147 $inserted = 0;
148 $actorMigration = ActorMigration::newMigration();
149 foreach ( $res as $row ) {
150 $comment = $commentStore->getComment( 'rev_comment', $row );
151 $user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
152 $dbw->insert(
153 'recentchanges',
154 [
155 'rc_timestamp' => $row->rev_timestamp,
156 'rc_namespace' => $row->page_namespace,
157 'rc_title' => $row->page_title,
158 'rc_minor' => $row->rev_minor_edit,
159 'rc_bot' => 0,
160 'rc_new' => $row->page_is_new,
161 'rc_cur_id' => $row->page_id,
162 'rc_this_oldid' => $row->rev_id,
163 'rc_last_oldid' => 0, // is this ok?
164 'rc_type' => $row->page_is_new ? RC_NEW : RC_EDIT,
165 'rc_source' => $row->page_is_new ? RecentChange::SRC_NEW : RecentChange::SRC_EDIT,
166 'rc_deleted' => $row->rev_deleted
167 ] + $commentStore->insert( $dbw, 'rc_comment', $comment )
168 + $actorMigration->getInsertValues( $dbw, 'rc_user', $user ),
169 __METHOD__
170 );
171
172 $rcid = $dbw->insertId();
173 $dbw->update(
174 'change_tag',
175 [ 'ct_rc_id' => $rcid ],
176 [ 'ct_rev_id' => $row->rev_id ],
177 __METHOD__
178 );
179
180 if ( ( ++$inserted % $this->getBatchSize() ) == 0 ) {
181 $lbFactory->waitForReplication();
182 }
183 }
184 }
185
190 private function rebuildRecentChangesTablePass2( ILBFactory $lbFactory ) {
191 $dbw = $this->getDB( DB_MASTER );
192
193 $this->output( "Updating links and size differences...\n" );
194
195 # Fill in the rc_last_oldid field, which points to the previous edit
196 $res = $dbw->select(
197 'recentchanges',
198 [ 'rc_cur_id', 'rc_this_oldid', 'rc_timestamp' ],
199 [
200 "rc_timestamp > " . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
201 "rc_timestamp < " . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) )
202 ],
203 __METHOD__,
204 [ 'ORDER BY' => 'rc_cur_id,rc_timestamp' ]
205 );
206
207 $lastCurId = 0;
208 $lastOldId = 0;
209 $lastSize = null;
210 $updated = 0;
211 foreach ( $res as $obj ) {
212 $new = 0;
213
214 if ( $obj->rc_cur_id != $lastCurId ) {
215 # Switch! Look up the previous last edit, if any
216 $lastCurId = intval( $obj->rc_cur_id );
217 $emit = $obj->rc_timestamp;
218
219 $row = $dbw->selectRow(
220 'revision',
221 [ 'rev_id', 'rev_len' ],
222 [ 'rev_page' => $lastCurId, "rev_timestamp < " . $dbw->addQuotes( $emit ) ],
223 __METHOD__,
224 [ 'ORDER BY' => 'rev_timestamp DESC' ]
225 );
226 if ( $row ) {
227 $lastOldId = intval( $row->rev_id );
228 # Grab the last text size if available
229 $lastSize = !is_null( $row->rev_len ) ? intval( $row->rev_len ) : null;
230 } else {
231 # No previous edit
232 $lastOldId = 0;
233 $lastSize = null;
234 $new = 1; // probably true
235 }
236 }
237
238 if ( $lastCurId == 0 ) {
239 $this->output( "Uhhh, something wrong? No curid\n" );
240 } else {
241 # Grab the entry's text size
242 $size = (int)$dbw->selectField(
243 'revision',
244 'rev_len',
245 [ 'rev_id' => $obj->rc_this_oldid ],
246 __METHOD__
247 );
248
249 $dbw->update(
250 'recentchanges',
251 [
252 'rc_last_oldid' => $lastOldId,
253 'rc_new' => $new,
254 'rc_type' => $new ? RC_NEW : RC_EDIT,
255 'rc_source' => $new === 1 ? RecentChange::SRC_NEW : RecentChange::SRC_EDIT,
256 'rc_old_len' => $lastSize,
257 'rc_new_len' => $size,
258 ],
259 [
260 'rc_cur_id' => $lastCurId,
261 'rc_this_oldid' => $obj->rc_this_oldid,
262 'rc_timestamp' => $obj->rc_timestamp // index usage
263 ],
264 __METHOD__
265 );
266
267 $lastOldId = intval( $obj->rc_this_oldid );
268 $lastSize = $size;
269
270 if ( ( ++$updated % $this->getBatchSize() ) == 0 ) {
271 $lbFactory->waitForReplication();
272 }
273 }
274 }
275 }
276
280 private function rebuildRecentChangesTablePass3( ILBFactory $lbFactory ) {
282
283 $dbw = $this->getDB( DB_MASTER );
284 $commentStore = CommentStore::getStore();
285
286 $this->output( "Loading from user, page, and logging tables...\n" );
287
288 $commentQuery = $commentStore->getJoin( 'log_comment' );
289 $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
290 $res = $dbw->select(
291 [ 'logging', 'page' ] + $commentQuery['tables'] + $actorQuery['tables'],
292 [
293 'log_timestamp',
294 'log_namespace',
295 'log_title',
296 'page_id',
297 'log_type',
298 'log_action',
299 'log_id',
300 'log_params',
301 'log_deleted'
302 ] + $commentQuery['fields'] + $actorQuery['fields'],
303 [
304 'log_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
305 'log_timestamp < ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) ),
306 // Some logs don't go in RC since they are private.
307 // @FIXME: core/extensions also have spammy logs that don't go in RC.
308 'log_type' => array_diff( $wgLogTypes, array_keys( $wgLogRestrictions ) ),
309 ],
310 __METHOD__,
311 [ 'ORDER BY' => 'log_timestamp DESC' ],
312 [
313 'page' =>
314 [ 'LEFT JOIN', [ 'log_namespace=page_namespace', 'log_title=page_title' ] ]
315 ] + $commentQuery['joins'] + $actorQuery['joins']
316 );
317
318 $field = $dbw->fieldInfo( 'recentchanges', 'rc_cur_id' );
319
320 $inserted = 0;
321 $actorMigration = ActorMigration::newMigration();
322 foreach ( $res as $row ) {
323 $comment = $commentStore->getComment( 'log_comment', $row );
324 $user = User::newFromAnyId( $row->log_user, $row->log_user_text, $row->log_actor );
325 $dbw->insert(
326 'recentchanges',
327 [
328 'rc_timestamp' => $row->log_timestamp,
329 'rc_namespace' => $row->log_namespace,
330 'rc_title' => $row->log_title,
331 'rc_minor' => 0,
332 'rc_bot' => 0,
333 'rc_patrolled' => $row->log_type == 'upload' ? 0 : 2,
334 'rc_new' => 0,
335 'rc_this_oldid' => 0,
336 'rc_last_oldid' => 0,
337 'rc_type' => RC_LOG,
338 'rc_source' => RecentChange::SRC_LOG,
339 'rc_cur_id' => $field->isNullable()
340 ? $row->page_id
341 : (int)$row->page_id, // NULL => 0,
342 'rc_log_type' => $row->log_type,
343 'rc_log_action' => $row->log_action,
344 'rc_logid' => $row->log_id,
345 'rc_params' => $row->log_params,
346 'rc_deleted' => $row->log_deleted
347 ] + $commentStore->insert( $dbw, 'rc_comment', $comment )
348 + $actorMigration->getInsertValues( $dbw, 'rc_user', $user ),
349 __METHOD__
350 );
351
352 $rcid = $dbw->insertId();
353 $dbw->update(
354 'change_tag',
355 [ 'ct_rc_id' => $rcid ],
356 [ 'ct_log_id' => $row->log_id ],
357 __METHOD__
358 );
359
360 if ( ( ++$inserted % $this->getBatchSize() ) == 0 ) {
361 $lbFactory->waitForReplication();
362 }
363 }
364 }
365
369 private function rebuildRecentChangesTablePass4( ILBFactory $lbFactory ) {
371
372 $dbw = $this->getDB( DB_MASTER );
373
374 $userQuery = User::getQueryInfo();
375
376 # @FIXME: recognize other bot account groups (not the same as users with 'bot' rights)
377 # @NOTE: users with 'bot' rights choose when edits are bot edits or not. That information
378 # may be lost at this point (aside from joining on the patrol log table entries).
379 $botgroups = [ 'bot' ];
380 $autopatrolgroups = $wgUseRCPatrol ? User::getGroupsWithPermission( 'autopatrol' ) : [];
381
382 # Flag our recent bot edits
383 if ( $botgroups ) {
384 $this->output( "Flagging bot account edits...\n" );
385
386 # Find all users that are bots
387 $res = $dbw->select(
388 array_merge( [ 'user_groups' ], $userQuery['tables'] ),
389 $userQuery['fields'],
390 [ 'ug_group' => $botgroups ],
391 __METHOD__,
392 [ 'DISTINCT' ],
393 [ 'user_groups' => [ 'JOIN', 'user_id = ug_user' ] ] + $userQuery['joins']
394 );
395
396 $botusers = [];
397 foreach ( $res as $obj ) {
398 $botusers[] = User::newFromRow( $obj );
399 }
400
401 # Fill in the rc_bot field
402 if ( $botusers ) {
403 $actorQuery = ActorMigration::newMigration()->getWhere( $dbw, 'rc_user', $botusers, false );
404 $rcids = [];
405 foreach ( $actorQuery['orconds'] as $cond ) {
406 $rcids = array_merge( $rcids, $dbw->selectFieldValues(
407 [ 'recentchanges' ] + $actorQuery['tables'],
408 'rc_id',
409 [
410 "rc_timestamp > " . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
411 "rc_timestamp < " . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) ),
412 $cond,
413 ],
414 __METHOD__,
415 [],
416 $actorQuery['joins']
417 ) );
418 }
419 $rcids = array_values( array_unique( $rcids ) );
420
421 foreach ( array_chunk( $rcids, $this->getBatchSize() ) as $rcidBatch ) {
422 $dbw->update(
423 'recentchanges',
424 [ 'rc_bot' => 1 ],
425 [ 'rc_id' => $rcidBatch ],
426 __METHOD__
427 );
428 $lbFactory->waitForReplication();
429 }
430 }
431 }
432
433 # Flag our recent autopatrolled edits
434 if ( !$wgMiserMode && $autopatrolgroups ) {
435 $patrolusers = [];
436
437 $this->output( "Flagging auto-patrolled edits...\n" );
438
439 # Find all users in RC with autopatrol rights
440 $res = $dbw->select(
441 array_merge( [ 'user_groups' ], $userQuery['tables'] ),
442 $userQuery['fields'],
443 [ 'ug_group' => $autopatrolgroups ],
444 __METHOD__,
445 [ 'DISTINCT' ],
446 [ 'user_groups' => [ 'JOIN', 'user_id = ug_user' ] ] + $userQuery['joins']
447 );
448
449 foreach ( $res as $obj ) {
450 $patrolusers[] = User::newFromRow( $obj );
451 }
452
453 # Fill in the rc_patrolled field
454 if ( $patrolusers ) {
455 $actorQuery = ActorMigration::newMigration()->getWhere( $dbw, 'rc_user', $patrolusers, false );
456 foreach ( $actorQuery['orconds'] as $cond ) {
457 $dbw->update(
458 'recentchanges',
459 [ 'rc_patrolled' => 2 ],
460 [
461 $cond,
462 'rc_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
463 'rc_timestamp < ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) ),
464 'rc_patrolled' => 0
465 ],
466 __METHOD__
467 );
468 $lbFactory->waitForReplication();
469 }
470 }
471 }
472 }
473
478 private function rebuildRecentChangesTablePass5( ILBFactory $lbFactory ) {
479 $dbw = wfGetDB( DB_MASTER );
480
481 $this->output( "Removing duplicate revision and logging entries...\n" );
482
483 $res = $dbw->select(
484 [ 'logging', 'log_search' ],
485 [ 'ls_value', 'ls_log_id' ],
486 [
487 'ls_log_id = log_id',
488 'ls_field' => 'associated_rev_id',
489 'log_type' => 'upload',
490 'log_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffFrom ) ),
491 'log_timestamp < ' . $dbw->addQuotes( $dbw->timestamp( $this->cutoffTo ) ),
492 ],
493 __METHOD__
494 );
495
496 $updates = 0;
497 foreach ( $res as $obj ) {
498 $rev_id = $obj->ls_value;
499 $log_id = $obj->ls_log_id;
500
501 // Mark the logging row as having an associated rev id
502 $dbw->update(
503 'recentchanges',
504 /*SET*/ [ 'rc_this_oldid' => $rev_id ],
505 /*WHERE*/ [ 'rc_logid' => $log_id ],
506 __METHOD__
507 );
508
509 // Delete the revision row
510 $dbw->delete(
511 'recentchanges',
512 /*WHERE*/ [ 'rc_this_oldid' => $rev_id, 'rc_logid' => 0 ],
513 __METHOD__
514 );
515
516 if ( ( ++$updates % $this->getBatchSize() ) == 0 ) {
517 $lbFactory->waitForReplication();
518 }
519 }
520 }
521
525 private function purgeFeeds() {
526 global $wgFeedClasses;
527
528 $this->output( "Deleting feed timestamps.\n" );
529
530 $wanCache = MediaWikiServices::getInstance()->getMainWANObjectCache();
531 foreach ( $wgFeedClasses as $feed => $className ) {
532 $wanCache->delete( $wanCache->makeKey( 'rcfeed', $feed, 'timestamp' ) ); # Good enough for now.
533 }
534 }
535}
536
537$maintClass = RebuildRecentchanges::class;
538require_once RUN_MAINTENANCE_IF_MAIN;
$wgLogTypes
The logging system has two levels: an event type, which describes the general category and can be vie...
$wgLogRestrictions
This restricts log access to those who have a certain right Users without this will not see it in the...
$wgUseRCPatrol
Use RC Patrolling to check for vandalism (from recent changes and watchlists) New pages and new files...
$wgRCMaxAge
Recentchanges items are periodically purged; entries older than this many seconds will go.
$wgFeedClasses
Available feeds objects.
$wgMiserMode
Disable database-intensive features.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
getDB( $db, $groups=[], $wiki=false)
Returns a database to be used by current maintenance script.
hasOption( $name)
Checks to see if a particular param exists.
getBatchSize()
Returns batch size.
addDescription( $text)
Set the description text.
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
getOption( $name, $default=null)
Get an option, or return the default.
setBatchSize( $s=0)
Set the batch size.
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Maintenance script that rebuilds recent changes from scratch.
execute()
Do the actual work.
rebuildRecentChangesTablePass5(ILBFactory $lbFactory)
Rebuild pass 5: Delete duplicate entries where we generate both a page revision and a log entry for a...
int $cutoffFrom
UNIX timestamp.
rebuildRecentChangesTablePass3(ILBFactory $lbFactory)
Rebuild pass 3: Insert recentchanges entries for action logs.
rebuildRecentChangesTablePass4(ILBFactory $lbFactory)
Rebuild pass 4: Mark bot and autopatrolled entries.
int $cutoffTo
UNIX timestamp.
__construct()
Default constructor.
rebuildRecentChangesTablePass1(ILBFactory $lbFactory)
Rebuild pass 1: Insert recentchanges entries for page revisions.
rebuildRecentChangesTablePass2(ILBFactory $lbFactory)
Rebuild pass 2: Enhance entries for page revisions with references to the previous revision (rc_last_...
purgeFeeds()
Purge cached feeds in $wanCache.
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new user object.
Definition User.php:5672
static newFromAnyId( $userId, $userName, $actorId)
Static factory method for creation from an ID, name, and/or actor ID.
Definition User.php:657
static newFromRow( $row, $data=null)
Create a new user object from a user row.
Definition User.php:750
static getGroupsWithPermission( $role)
Get all the groups who have a given permission.
Definition User.php:4982
$res
Definition database.txt:21
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add in any and then calling output() to send it all. It could be easily changed to send incrementally if that becomes useful
const RC_NEW
Definition Defines.php:153
const RC_LOG
Definition Defines.php:154
const RC_EDIT
Definition Defines.php:152
An interface for generating database load balancers.
waitForReplication(array $opts=[])
Waits for the replica DBs to catch up to the current master position.
require_once RUN_MAINTENANCE_IF_MAIN
const DB_MASTER
Definition defines.php:29