Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 99
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
SendConfirmAndMigrateEmail
0.00% covered (danger)
0.00%
0 / 93
0.00% covered (danger)
0.00%
0 / 4
506
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
110
 resendConfirmationEmail
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
90
 report
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
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\Extension\CentralAuth\User\EmailableUser;
12use MediaWiki\WikiMap\WikiMap;
13
14/**
15 * This maintenance script is used to resend confirmation emails to users with
16 * unattached accounts in the hopes that we will then be able to automatically
17 * attach some unattached accounts.  In addition, by using an EmailableUser object
18 * we are able to override the contents of the email and send them to Special:MergeAccount
19 * instead of Special:ConfirmEmail and thus put them right into the workflow to attach
20 * accounts by password.
21 *
22 * THIS MUST BE RUN ON THE LOCAL WIKI SO THAT THE EMAIL IS SENT IN THE CORRECT LANGUAGE
23 */
24class SendConfirmAndMigrateEmail extends Maintenance {
25
26    /**
27     * Whether to send to accounts with confirmed emails
28     * @var bool
29     */
30    private $sendToConfirmed;
31
32    /**
33     * How long to wait in between emails
34     *
35     * @var int
36     */
37    private $sleep;
38
39    /**
40     * @var bool
41     */
42    private $dryrun;
43
44    /**
45     * @var string|bool
46     */
47    private $resume;
48
49    /** @var float */
50    protected $start;
51
52    /** @var int */
53    protected $sent;
54
55    /** @var int */
56    protected $total;
57
58    public function __construct() {
59        parent::__construct();
60        $this->requireExtension( 'CentralAuth' );
61        $this->addDescription( "Resends the 'confirm your email address email' with a link to " .
62            "Special:MergeAccount" );
63        $this->start = microtime( true );
64        $this->sent = 0;
65        $this->total = 0;
66
67        $this->addOption( 'userlist', 'List of usernames', false, true );
68        $this->addOption( 'username', 'The user name to migrate', false, true, 'u' );
69        $this->addOption( 'confirmed', 'Send email to confirmed accounts', false, false );
70        $this->addOption( 'sleep',
71            'How long to wait in between emails (default 1 second)', false, true );
72        $this->addOption( 'dryrun', 'Don\'t actually send any emails', false, false );
73        $this->addOption( 'resume', 'Which username to resume after', false, true );
74        $this->setBatchSize( 1000 );
75    }
76
77    public function execute() {
78        $this->sendToConfirmed = $this->getOption( 'confirmed', false );
79        $this->sleep = (int)$this->getOption( 'sleep', 1 );
80        $this->dryrun = $this->hasOption( 'dryrun' );
81        $this->resume = $this->getOption( 'resume', true );
82
83        // check to see if we are processing a single username
84        if ( $this->getOption( 'username', false ) !== false ) {
85            $username = $this->getOption( 'username' );
86
87            $this->resendConfirmationEmail( $username );
88
89        } elseif ( $this->getOption( 'userlist', false ) !== false ) {
90            $list = $this->getOption( 'userlist' );
91            if ( !is_file( $list ) ) {
92                $this->output( "ERROR - File not found: $list\n" );
93                exit( 1 );
94            }
95            $file = fopen( $list, 'r' );
96            if ( $file === false ) {
97                $this->output( "ERROR - Could not open file: $list\n" );
98                exit( 1 );
99            }
100            $databaseManager = CentralAuthServices::getDatabaseManager();
101            // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
102            while ( $username = fgets( $file ) ) {
103                // trim the \n
104                $username = trim( $username );
105                if ( $this->resume !== true ) {
106                    if ( $username === $this->resume ) {
107                        $this->resume = true;
108                    }
109                    continue;
110                }
111                $this->resendConfirmationEmail( $username );
112
113                if ( $this->total % $this->mBatchSize == 0 ) {
114                    $this->output( "Waiting for replicas to catch up ... " );
115                    if ( !$this->dryrun ) {
116                        $databaseManager->waitForReplication();
117                    }
118                    $this->output( "done\n" );
119                }
120            }
121            fclose( $file );
122
123        } else {
124            $this->output( "ERROR - No username or list of usernames given\n" );
125            exit( 1 );
126        }
127
128        $this->report();
129        $this->output( "done.\n" );
130    }
131
132    /**
133     * @param string $username
134     */
135    private function resendConfirmationEmail( $username ) {
136        $wikiID = WikiMap::getCurrentWikiId();
137
138        $this->total++;
139        $this->output( "Sending confirmation email for: '$username@$wikiID'\n" );
140
141        // we want to start with the local user
142        $user = EmailableUser::newFromName( $username );
143        if ( $user === false ) {
144            $this->output( "ERROR: $username is an invalid username\n" );
145            return;
146        }
147        $user->load();
148
149        if ( !$this->sendToConfirmed && $user->isEmailConfirmed() ) {
150            $this->output(
151                "ERROR: The user '$username@$wikiID' already has a confirmed email address\n"
152            );
153            return;
154        }
155
156        $central = CentralAuthUser::getInstance( $user );
157
158        if ( !$central->exists() ) {
159            $this->output( "ERROR: No global account for '$username'\n" );
160            return;
161        }
162        if ( !$central->isAttached() ) {
163            $this->output( "ERROR: '$username@$wikiID' is not attached to the global user\n" );
164            return;
165        }
166
167        $unattached = $central->queryUnattached();
168        if ( count( $unattached ) == 0 ) {
169            $this->output( "ERROR: No unattached accounts for '$username'\n" );
170            return;
171        }
172
173        if ( $this->dryrun ) {
174            $this->output( "Would have sent email\n" );
175            return;
176        }
177
178        if ( $user->sendConfirmAndMigrateMail() ) {
179            $this->output( "Sent email to $username\n" );
180            $this->sent++;
181            sleep( $this->sleep );
182        } else {
183            $this->output(
184                "ERROR: Sending confirm and migrate email failed for '$username@$wikiID'\n"
185            );
186        }
187    }
188
189    private function report() {
190        $delta = microtime( true ) - $this->start;
191        $this->output( sprintf(
192            "%s: %s processed %d usernames (%.1f/sec), %d (%.1f%%) emails sent\n",
193            WikiMap::getCurrentWikiId(),
194            wfTimestamp( TS_DB ),
195            $this->total,
196            $this->total / $delta,
197            $this->sent,
198            $this->total > 0 ? ( $this->sent / $this->total * 100.0 ) : 0
199        ) );
200    }
201}
202
203$maintClass = SendConfirmAndMigrateEmail::class;
204require_once RUN_MAINTENANCE_IF_MAIN;