Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
65 / 65
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
RollbackEdits
100.00% covered (success)
100.00%
65 / 65
100.00% covered (success)
100.00%
3 / 3
14
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
 execute
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
1 / 1
11
 getRollbackTitles
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Rollback all edits by a given user or IP provided they're the most
4 * recent edit (just like real rollback)
5 *
6 * @license GPL-2.0-or-later
7 * @file
8 * @ingroup Maintenance
9 */
10
11use MediaWiki\Maintenance\Maintenance;
12use MediaWiki\Title\Title;
13use MediaWiki\User\User;
14
15// @codeCoverageIgnoreStart
16require_once __DIR__ . '/Maintenance.php';
17// @codeCoverageIgnoreEnd
18
19/**
20 * Maintenance script to rollback all edits by a given user or IP provided
21 * they're the most recent edit.
22 *
23 * @ingroup Maintenance
24 */
25class RollbackEdits extends Maintenance {
26    public function __construct() {
27        parent::__construct();
28        $this->addDescription(
29            "Rollback all edits by a given user or IP provided they're the most recent edit" );
30        $this->addOption(
31            'titles',
32            'A list of titles, none means all titles where the given user is the most recent',
33            false,
34            true
35        );
36        $this->addOption( 'user', 'A user or IP to rollback all edits for', true, true );
37        $this->addOption( 'summary', 'Edit summary to use', false, true );
38        $this->addOption( 'bot', 'Mark the edits as bot' );
39        $this->setBatchSize( 10 );
40    }
41
42    public function execute() {
43        $user = $this->getOption( 'user' );
44        $services = $this->getServiceContainer();
45        $userNameUtils = $services->getUserNameUtils();
46        $user = $userNameUtils->isIP( $user ) ? $user : $userNameUtils->getCanonical( $user );
47        if ( !$user ) {
48            $this->fatalError( 'Invalid username' );
49        }
50
51        $bot = $this->hasOption( 'bot' );
52        $summary = $this->getOption( 'summary', $this->mSelf . ' mass rollback' );
53        $titles = [];
54        if ( $this->hasOption( 'titles' ) ) {
55            foreach ( explode( '|', $this->getOption( 'titles' ) ) as $text ) {
56                $title = Title::newFromText( $text );
57                if ( !$title ) {
58                    $this->error( 'Invalid title, ' . $text );
59                } else {
60                    $titles[] = $title;
61                }
62            }
63        } else {
64            $titles = $this->getRollbackTitles( $user );
65        }
66
67        if ( !$titles ) {
68            $this->output( 'No suitable titles to be rolled back.' );
69
70            return;
71        }
72
73        $doer = User::newSystemUser( User::MAINTENANCE_SCRIPT_USER, [ 'steal' => true ] );
74        $byUser = $services->getUserIdentityLookup()->getUserIdentityByName( $user );
75
76        if ( !$byUser ) {
77            $this->fatalError( 'Unknown user.' );
78        }
79
80        $wikiPageFactory = $services->getWikiPageFactory();
81        $rollbackPageFactory = $services->getRollbackPageFactory();
82
83        /** @var iterable<Title[]> $titleBatches */
84        $titleBatches = $this->newBatchIterator( $titles );
85
86        foreach ( $titleBatches as $titleBatch ) {
87            foreach ( $titleBatch as $title ) {
88                $page = $wikiPageFactory->newFromTitle( $title );
89                $this->output( 'Processing ' . $title->getPrefixedText() . '...' );
90
91                $this->beginTransactionRound( __METHOD__ );
92                $rollbackResult = $rollbackPageFactory
93                    ->newRollbackPage( $page, $doer, $byUser )
94                    ->markAsBot( $bot )
95                    ->setSummary( $summary )
96                    ->rollback();
97                $this->commitTransactionRound( __METHOD__ );
98
99                if ( $rollbackResult->isGood() ) {
100                    $this->output( "Done!\n" );
101                } else {
102                    $this->output( "Failed!\n" );
103                }
104            }
105        }
106    }
107
108    /**
109     * Get all pages that should be rolled back for a given user
110     * @param string $user A name to check against
111     * @return Title[]
112     */
113    private function getRollbackTitles( $user ) {
114        $dbr = $this->getReplicaDB();
115        $titles = [];
116
117        $results = $dbr->newSelectQueryBuilder()
118            ->select( [ 'page_namespace', 'page_title' ] )
119            ->from( 'page' )
120            ->join( 'revision', null, 'page_latest = rev_id' )
121            ->join( 'actor', null, 'rev_actor = actor_id' )
122            ->where( [ 'actor_name' => $user ] )
123            ->caller( __METHOD__ )->fetchResultSet();
124        foreach ( $results as $row ) {
125            $titles[] = Title::makeTitle( $row->page_namespace, $row->page_title );
126        }
127
128        return $titles;
129    }
130}
131
132// @codeCoverageIgnoreStart
133$maintClass = RollbackEdits::class;
134require_once RUN_MAINTENANCE_IF_MAIN;
135// @codeCoverageIgnoreEnd