Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 92
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\Maintenance\Maintenance;
27use MediaWiki\User\User;
28use Wikimedia\IPUtils;
29
30// @codeCoverageIgnoreStart
31require_once __DIR__ . '/Maintenance.php';
32// @codeCoverageIgnoreEnd
33
34/**
35 * Maintenance script that reassigns edits from a user or IP address
36 * to another user.
37 *
38 * @ingroup Maintenance
39 */
40class ReassignEdits extends Maintenance {
41    public function __construct() {
42        parent::__construct();
43        $this->addDescription( 'Reassign edits from one user to another' );
44        $this->addOption( "force", "Reassign even if the target user doesn't exist" );
45        $this->addOption( "norc", "Don't update the recent changes table" );
46        $this->addOption( "report", "Print out details of what would be changed, but don't update it" );
47        $this->addArg( 'from', 'Old user to take edits from' );
48        $this->addArg( 'to', 'New user to give edits to' );
49    }
50
51    public function execute() {
52        if ( $this->hasArg( 0 ) && $this->hasArg( 1 ) ) {
53            # Set up the users involved
54            $from = $this->initialiseUser( $this->getArg( 0 ) );
55            $to = $this->initialiseUser( $this->getArg( 1 ) );
56
57            # If the target doesn't exist, and --force is not set, stop here
58            if ( $to->getId() || $this->hasOption( 'force' ) ) {
59                # Reassign the edits
60                $report = $this->hasOption( 'report' );
61                $this->doReassignEdits( $from, $to, !$this->hasOption( 'norc' ), $report );
62                # If reporting, and there were items, advise the user to run without --report
63                if ( $report ) {
64                    $this->output( "Run the script again without --report to update.\n" );
65                }
66            } else {
67                $ton = $to->getName();
68                $this->error( "User '{$ton}' not found." );
69            }
70        }
71    }
72
73    /**
74     * Reassign edits from one user to another
75     *
76     * @param User &$from User to take edits from
77     * @param User &$to User to assign edits to
78     * @param bool $updateRC Update the recent changes table
79     * @param bool $report Don't change things; just echo numbers
80     * @return int The number of entries changed, or that would be changed
81     */
82    private function doReassignEdits( &$from, &$to, $updateRC = false, $report = false ) {
83        $dbw = $this->getPrimaryDB();
84        $this->beginTransaction( $dbw, __METHOD__ );
85        $actorNormalization = $this->getServiceContainer()->getActorNormalization();
86        $fromActorId = $actorNormalization->findActorId( $from, $dbw );
87
88        # Count things
89        $this->output( "Checking current edits..." );
90
91        $revisionRows = $dbw->newSelectQueryBuilder()
92            ->select( '*' )
93            ->from( 'revision' )
94            ->where( [ 'rev_actor' => $fromActorId ] )
95            ->caller( __METHOD__ )
96            ->fetchRowCount();
97
98        $this->output( "found {$revisionRows}.\n" );
99
100        $this->output( "Checking deleted edits..." );
101        $archiveRows = $dbw->newSelectQueryBuilder()
102            ->select( '*' )
103            ->from( 'archive' )
104            ->where( [ 'ar_actor' => $fromActorId ] )
105            ->caller( __METHOD__ )->fetchRowCount();
106        $this->output( "found {$archiveRows}.\n" );
107
108        # Don't count recent changes if we're not supposed to
109        if ( $updateRC ) {
110            $this->output( "Checking recent changes..." );
111            $recentChangesRows = $dbw->newSelectQueryBuilder()
112                ->select( '*' )
113                ->from( 'recentchanges' )
114                ->where( [ 'rc_actor' => $fromActorId ] )
115                ->caller( __METHOD__ )->fetchRowCount();
116            $this->output( "found {$recentChangesRows}.\n" );
117        } else {
118            $recentChangesRows = 0;
119        }
120
121        $total = $revisionRows + $archiveRows + $recentChangesRows;
122        $this->output( "\nTotal entries to change: {$total}\n" );
123
124        $toActorId = $actorNormalization->acquireActorId( $to, $dbw );
125        if ( !$report && $total ) {
126            $this->output( "\n" );
127            if ( $revisionRows ) {
128                # Reassign edits
129                $this->output( "Reassigning current edits..." );
130
131                $dbw->newUpdateQueryBuilder()
132                    ->update( 'revision' )
133                    ->set( [ 'rev_actor' => $toActorId ] )
134                    ->where( [ 'rev_actor' => $fromActorId ] )
135                    ->caller( __METHOD__ )->execute();
136
137                $this->output( "done.\n" );
138            }
139
140            if ( $archiveRows ) {
141                $this->output( "Reassigning deleted edits..." );
142
143                $dbw->newUpdateQueryBuilder()
144                    ->update( 'archive' )
145                    ->set( [ 'ar_actor' => $toActorId ] )
146                    ->where( [ 'ar_actor' => $fromActorId ] )
147                    ->caller( __METHOD__ )->execute();
148
149                $this->output( "done.\n" );
150            }
151            # Update recent changes if required
152            if ( $recentChangesRows ) {
153                $this->output( "Updating recent changes..." );
154
155                $dbw->newUpdateQueryBuilder()
156                    ->update( 'recentchanges' )
157                    ->set( [ 'rc_actor' => $toActorId ] )
158                    ->where( [ 'rc_actor' => $fromActorId ] )
159                    ->caller( __METHOD__ )->execute();
160
161                $this->output( "done.\n" );
162            }
163
164            # If $from is an IP, delete any relevant rows from the
165            # ip_changes. No update needed, as $to cannot be an IP.
166            if ( !$from->isRegistered() ) {
167                $this->output( "Deleting ip_changes..." );
168
169                $dbw->newDeleteQueryBuilder()
170                    ->deleteFrom( 'ip_changes' )
171                    ->where( [ 'ipc_hex' => IPUtils::toHex( $from->getName() ) ] )
172                    ->caller( __METHOD__ )->execute();
173
174                $this->output( "done.\n" );
175            }
176        }
177
178        $this->commitTransaction( $dbw, __METHOD__ );
179
180        return $total;
181    }
182
183    /**
184     * Initialise the user object
185     *
186     * @param string $username Username or IP address
187     * @return User
188     */
189    private function initialiseUser( $username ) {
190        $services = $this->getServiceContainer();
191        if ( $services->getUserNameUtils()->isIP( $username ) ) {
192            $user = User::newFromName( $username, false );
193            $user->getActorId();
194        } else {
195            $user = User::newFromName( $username );
196            if ( !$user ) {
197                $this->fatalError( "Invalid username" );
198            }
199        }
200        $user->load();
201
202        return $user;
203    }
204}
205
206// @codeCoverageIgnoreStart
207$maintClass = ReassignEdits::class;
208require_once RUN_MAINTENANCE_IF_MAIN;
209// @codeCoverageIgnoreEnd