Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 97
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
DeleteEmptyAccounts
0.00% covered (danger)
0.00%
0 / 91
0.00% covered (danger)
0.00%
0 / 3
506
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
72
 process
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
182
1<?php
2
3$IP = getenv( 'MW_INSTALL_PATH' );
4if ( $IP === false ) {
5    $IP = __DIR__ . '/../../..';
6}
7require_once "$IP/maintenance/Maintenance.php";
8
9use MediaWiki\Extension\CentralAuth\CentralAuthServices;
10use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
11use MediaWiki\User\User;
12
13class DeleteEmptyAccounts extends Maintenance {
14
15    /** @var bool */
16    protected $fix;
17
18    /** @var bool */
19    protected $safe;
20
21    /** @var bool */
22    protected $migrate;
23
24    /** @var bool */
25    protected $suppressRC;
26
27    public function __construct() {
28        parent::__construct();
29        $this->requireExtension( 'CentralAuth' );
30        $this->addDescription( 'Delete all global accounts with no attached local accounts, ' .
31            'then attempt to migrate a local account' );
32        $this->fix = false;
33        $this->safe = false;
34        $this->migrate = false;
35        $this->suppressRC = false;
36
37        $this->addOption( 'fix', 'Actually update the database. Otherwise, ' .
38            'only prints what would be done.', false, false );
39        $this->addOption( 'migrate', 'Migrate a local account; the winner is picked using ' .
40            'CentralAuthUser::attemptAutoMigration defaults', false, false );
41        $this->addOption( 'safe-migrate', 'Migrate a local account, only if all accounts ' .
42            'can be attached', false, false );
43        $this->addOption( 'suppressrc', 'Do not send entries to RC feed', false, false );
44        $this->setBatchSize( 500 );
45    }
46
47    public function execute() {
48        // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgUser
49        global $wgUser;
50
51        $original = $wgUser;
52
53        $user = User::newFromName( User::MAINTENANCE_SCRIPT_USER );
54        $wgUser = $user;
55        RequestContext::getMain()->setUser( $user );
56
57        $dbr = CentralAuthServices::getDatabaseManager()->getCentralReplicaDB();
58
59        if ( $this->getOption( 'fix', false ) !== false ) {
60            $this->fix = true;
61        }
62        if ( $this->getOption( 'safe-migrate', false ) !== false ) {
63            $this->safe = true;
64            $this->migrate = true;
65        }
66        if ( $this->getOption( 'migrate', false ) !== false ) {
67            $this->migrate = true;
68        }
69        if ( $this->getOption( 'suppressrc', false ) !== false ) {
70            $this->suppressRC = true;
71        }
72
73        $end = $dbr->newSelectQueryBuilder()
74            ->select( 'MAX(gu_id)' )
75            ->from( 'globaluser' )
76            ->caller( __METHOD__ )
77            ->fetchField();
78
79        for ( $cur = 0; $cur <= $end; $cur += $this->mBatchSize ) {
80            $this->output( "PROGRESS: $cur / $end\n" );
81            $result = $dbr->newSelectQueryBuilder()
82                ->select( 'gu_name' )
83                ->from( 'globaluser' )
84                ->leftJoin( 'localuser', null, 'gu_name=lu_name' )
85                ->where( [
86                    'lu_name' => null,
87                    $dbr->expr( 'gu_id', '>=', $cur ),
88                    $dbr->expr( 'gu_id', '<', $cur + $this->mBatchSize ),
89                ] )
90                ->orderBy( 'gu_id' )
91                ->caller( __METHOD__ )
92                ->fetchResultSet();
93
94            foreach ( $result as $row ) {
95                $this->process( $row->gu_name, $user );
96            }
97            if ( $this->fix ) {
98                CentralAuthServices::getDatabaseManager()->waitForReplication();
99            }
100        }
101
102        $this->output( "done.\n" );
103
104        // Restore old $wgUser value
105        $wgUser = $original;
106    }
107
108    /**
109     * @param string $username
110     * @param User $deleter
111     */
112    private function process( $username, User $deleter ) {
113        $central = new CentralAuthUser( $username, IDBAccessObject::READ_LATEST );
114        if ( !$central->exists() ) {
115            $this->output(
116                "ERROR: [$username] Central account does not exist. So how'd we find it?\n"
117            );
118            return;
119        }
120
121        try {
122            $unattached = $central->queryUnattached();
123        } catch ( Exception $e ) {
124            // This might happen due to localnames inconsistencies (T69350)
125            $this->output( "ERROR: [$username] Fetching unattached accounts failed.\n" );
126            return;
127        }
128
129        foreach ( $unattached as $wiki => $local ) {
130            if ( $local['email'] === '' && $local['password'] === '' ) {
131                $this->output( "SKIP: [$username] Account on $wiki has no password or email\n" );
132                return;
133            }
134        }
135
136        if ( $this->fix ) {
137            $reason = wfMessage( 'centralauth-delete-empty-account' )->inContentLanguage()->text();
138            $status = $central->adminDelete( $reason, $deleter );
139            if ( !$status->isGood() ) {
140                $msg = $status->getErrors()[0]['message'];
141                if ( $msg instanceof Message ) {
142                    $msg = $msg->getKey();
143                }
144                $this->output( "ERROR: [$username] Delete failed ($msg)\n" );
145                return;
146            }
147            $this->output( "DELETE: [$username] Deleted\n" );
148        } else {
149            $this->output( "DELETE: [$username] Would delete\n" );
150        }
151
152        if ( count( $unattached ) !== 0 && $this->migrate ) {
153            if ( $this->fix ) {
154                $central = CentralAuthUser::newUnattached( $username, true );
155                if ( $central->storeAndMigrate( [], !$this->suppressRC, $this->safe ) ) {
156                    $unattachedAfter = count( $central->queryUnattached() );
157                    $this->output(
158                        "MIGRATE: [$username] Success; $unattachedAfter left unattached\n"
159                    );
160                } else {
161                    $this->output( "MIGRATE: [$username] Fail\n" );
162                }
163            } else {
164                $this->output( "MIGRATE: [$username] Would attempt\n" );
165            }
166        }
167    }
168}
169
170$maintClass = DeleteEmptyAccounts::class;
171require_once RUN_MAINTENANCE_IF_MAIN;