Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 95
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ReassignEdits
0.00% covered (danger)
0.00%
0 / 92
0.00% covered (danger)
0.00%
0 / 4
342
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
42
 doReassignEdits
0.00% covered (danger)
0.00%
0 / 66
0.00% covered (danger)
0.00%
0 / 1
72
 initialiseUser
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Reassign edits from a user or IP address to another user
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Maintenance
22 * @author Rob Church <robchur@gmail.com>
23 * @license GPL-2.0-or-later
24 */
25
26use MediaWiki\User\User;
27use Wikimedia\IPUtils;
28
29require_once __DIR__ . '/Maintenance.php';
30
31/**
32 * Maintenance script that reassigns edits from a user or IP address
33 * to another user.
34 *
35 * @ingroup Maintenance
36 */
37class ReassignEdits extends Maintenance {
38    public function __construct() {
39        parent::__construct();
40        $this->addDescription( 'Reassign edits from one user to another' );
41        $this->addOption( "force", "Reassign even if the target user doesn't exist" );
42        $this->addOption( "norc", "Don't update the recent changes table" );
43        $this->addOption( "report", "Print out details of what would be changed, but don't update it" );
44        $this->addArg( 'from', 'Old user to take edits from' );
45        $this->addArg( 'to', 'New user to give edits to' );
46    }
47
48    public function execute() {
49        if ( $this->hasArg( 0 ) && $this->hasArg( 1 ) ) {
50            # Set up the users involved
51            $from = $this->initialiseUser( $this->getArg( 0 ) );
52            $to = $this->initialiseUser( $this->getArg( 1 ) );
53
54            # If the target doesn't exist, and --force is not set, stop here
55            if ( $to->getId() || $this->hasOption( 'force' ) ) {
56                # Reassign the edits
57                $report = $this->hasOption( 'report' );
58                $this->doReassignEdits( $from, $to, !$this->hasOption( 'norc' ), $report );
59                # If reporting, and there were items, advise the user to run without --report
60                if ( $report ) {
61                    $this->output( "Run the script again without --report to update.\n" );
62                }
63            } else {
64                $ton = $to->getName();
65                $this->error( "User '{$ton}' not found." );
66            }
67        }
68    }
69
70    /**
71     * Reassign edits from one user to another
72     *
73     * @param User &$from User to take edits from
74     * @param User &$to User to assign edits to
75     * @param bool $updateRC Update the recent changes table
76     * @param bool $report Don't change things; just echo numbers
77     * @return int The number of entries changed, or that would be changed
78     */
79    private function doReassignEdits( &$from, &$to, $updateRC = false, $report = false ) {
80        $dbw = $this->getPrimaryDB();
81        $this->beginTransaction( $dbw, __METHOD__ );
82        $actorNormalization = $this->getServiceContainer()->getActorNormalization();
83        $fromActorId = $actorNormalization->findActorId( $from, $dbw );
84
85        # Count things
86        $this->output( "Checking current edits..." );
87
88        $revisionRows = $dbw->newSelectQueryBuilder()
89            ->select( '*' )
90            ->from( 'revision' )
91            ->where( [ 'rev_actor' => $fromActorId ] )
92            ->caller( __METHOD__ )
93            ->fetchRowCount();
94
95        $this->output( "found {$revisionRows}.\n" );
96
97        $this->output( "Checking deleted edits..." );
98        $archiveRows = $dbw->newSelectQueryBuilder()
99            ->select( '*' )
100            ->from( 'archive' )
101            ->where( [ 'ar_actor' => $fromActorId ] )
102            ->caller( __METHOD__ )->fetchRowCount();
103        $this->output( "found {$archiveRows}.\n" );
104
105        # Don't count recent changes if we're not supposed to
106        if ( $updateRC ) {
107            $this->output( "Checking recent changes..." );
108            $recentChangesRows = $dbw->newSelectQueryBuilder()
109                ->select( '*' )
110                ->from( 'recentchanges' )
111                ->where( [ 'rc_actor' => $fromActorId ] )
112                ->caller( __METHOD__ )->fetchRowCount();
113            $this->output( "found {$recentChangesRows}.\n" );
114        } else {
115            $recentChangesRows = 0;
116        }
117
118        $total = $revisionRows + $archiveRows + $recentChangesRows;
119        $this->output( "\nTotal entries to change: {$total}\n" );
120
121        $toActorId = $actorNormalization->acquireActorId( $to, $dbw );
122        if ( !$report && $total ) {
123            $this->output( "\n" );
124            if ( $revisionRows ) {
125                # Reassign edits
126                $this->output( "Reassigning current edits..." );
127
128                $dbw->newUpdateQueryBuilder()
129                    ->update( 'revision' )
130                    ->set( [ 'rev_actor' => $toActorId ] )
131                    ->where( [ 'rev_actor' => $fromActorId ] )
132                    ->caller( __METHOD__ )->execute();
133
134                $this->output( "done.\n" );
135            }
136
137            if ( $archiveRows ) {
138                $this->output( "Reassigning deleted edits..." );
139
140                $dbw->newUpdateQueryBuilder()
141                    ->update( 'archive' )
142                    ->set( [ 'ar_actor' => $toActorId ] )
143                    ->where( [ 'ar_actor' => $fromActorId ] )
144                    ->caller( __METHOD__ )->execute();
145
146                $this->output( "done.\n" );
147            }
148            # Update recent changes if required
149            if ( $recentChangesRows ) {
150                $this->output( "Updating recent changes..." );
151
152                $dbw->newUpdateQueryBuilder()
153                    ->update( 'recentchanges' )
154                    ->set( [ 'rc_actor' => $toActorId ] )
155                    ->where( [ 'rc_actor' => $fromActorId ] )
156                    ->caller( __METHOD__ )->execute();
157
158                $this->output( "done.\n" );
159            }
160
161            # If $from is an IP, delete any relevant rows from the
162            # ip_changes. No update needed, as $to cannot be an IP.
163            if ( !$from->isRegistered() ) {
164                $this->output( "Deleting ip_changes..." );
165
166                $dbw->newDeleteQueryBuilder()
167                    ->deleteFrom( 'ip_changes' )
168                    ->where( [ 'ipc_hex' => IPUtils::toHex( $from->getName() ) ] )
169                    ->caller( __METHOD__ )->execute();
170
171                $this->output( "done.\n" );
172            }
173        }
174
175        $this->commitTransaction( $dbw, __METHOD__ );
176
177        return $total;
178    }
179
180    /**
181     * Initialise the user object
182     *
183     * @param string $username Username or IP address
184     * @return User
185     */
186    private function initialiseUser( $username ) {
187        $services = $this->getServiceContainer();
188        if ( $services->getUserNameUtils()->isIP( $username ) ) {
189            $user = User::newFromName( $username, false );
190            $user->getActorId();
191        } else {
192            $user = User::newFromName( $username );
193            if ( !$user ) {
194                $this->fatalError( "Invalid username" );
195            }
196        }
197        $user->load();
198
199        return $user;
200    }
201}
202
203$maintClass = ReassignEdits::class;
204require_once RUN_MAINTENANCE_IF_MAIN;