Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 107
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
AttachAccount
0.00% covered (danger)
0.00%
0 / 101
0.00% covered (danger)
0.00%
0 / 5
420
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 attach
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
132
 reportPcnt
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 report
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @section LICENSE
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 2 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along
15 * with this program; if not, write to the Free Software Foundation, Inc.,
16 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 * http://www.gnu.org/copyleft/gpl.html
18 *
19 * @file
20 */
21
22$IP = getenv( 'MW_INSTALL_PATH' );
23if ( $IP === false ) {
24    $IP = __DIR__ . '/../../..';
25}
26require_once "$IP/maintenance/Maintenance.php";
27
28use MediaWiki\Extension\CentralAuth\CentralAuthServices;
29use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
30
31/**
32 * @copyright © 2016 Wikimedia Foundation and contributors.
33 */
34class AttachAccount extends Maintenance {
35
36    /** @var float */
37    protected $start;
38
39    /** @var int */
40    protected $missing;
41
42    /** @var int */
43    protected $partial;
44
45    /** @var int */
46    protected $failed;
47
48    /** @var int */
49    protected $attached;
50
51    /** @var int */
52    protected $ok;
53
54    /** @var int */
55    protected $total;
56
57    /** @var bool */
58    protected $dryRun;
59
60    /** @var bool */
61    protected $quiet;
62
63    public function __construct() {
64        parent::__construct();
65        $this->requireExtension( 'CentralAuth' );
66        $this->addDescription( 'Attaches the specified usernames to a global account' );
67        $this->start = microtime( true );
68        $this->missing = 0;
69        $this->partial = 0;
70        $this->failed = 0;
71        $this->attached = 0;
72        $this->ok = 0;
73        $this->total = 0;
74        $this->dryRun = false;
75        $this->quiet = false;
76
77        $this->addOption( 'userlist',
78            'File with the list of usernames to attach, one per line', true, true );
79        $this->addOption( 'dry-run', 'Do not update database' );
80        $this->addOption( 'quiet',
81            'Only report database changes and final statistics' );
82        $this->setBatchSize( 1000 );
83    }
84
85    public function execute() {
86        $databaseManager = CentralAuthServices::getDatabaseManager();
87
88        $this->dryRun = $this->hasOption( 'dry-run' );
89        $this->quiet = $this->hasOption( 'quiet' );
90
91        $list = $this->getOption( 'userlist' );
92        if ( !is_file( $list ) ) {
93            $this->fatalError( "ERROR - File not found: {$list}" );
94        }
95        $file = fopen( $list, 'r' );
96        if ( $file === false ) {
97            $this->fatalError( "ERROR - Could not open file: {$list}" );
98        }
99        // phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
100        while ( strlen( $username = trim( fgets( $file ) ) ) ) {
101            $this->attach( $username );
102            if ( $this->total % $this->mBatchSize == 0 ) {
103                $this->output( "Waiting for replicas to catch up ... " );
104                $databaseManager->waitForReplication();
105                $this->output( "done\n" );
106            }
107        }
108        fclose( $file );
109
110        $this->report();
111        $this->output( "done.\n" );
112    }
113
114    /**
115     * @param string $username
116     */
117    protected function attach( $username ) {
118        $this->total++;
119        if ( !$this->quiet ) {
120            $this->output( "CentralAuth account attach for: {$username}\n" );
121        }
122
123        $central = new CentralAuthUser(
124            $username, IDBAccessObject::READ_LATEST );
125
126        if ( !$central->exists() ) {
127            $this->missing++;
128            $this->output( "ERROR: No CA account found for: {$username}\n" );
129            return;
130        }
131
132        try {
133            $unattached = $central->listUnattached();
134        } catch ( Exception $e ) {
135            // This might happen due to localnames inconsistencies (T69350)
136            $this->missing++;
137            $this->output(
138                "ERROR: Fetching unattached accounts for {$username} failed.\n"
139            );
140            return;
141        }
142
143        if ( count( $unattached ) === 0 ) {
144            $this->ok++;
145            if ( !$this->quiet ) {
146                $this->output( "OK: {$username}\n" );
147            }
148            return;
149        }
150
151        foreach ( $unattached as $wikiID ) {
152            $this->output( "ATTACHING: {$username}@{$wikiID}\n" );
153            if ( !$this->dryRun ) {
154                $central->attach(
155                    $wikiID,
156                    'login',
157                    false
158                );
159            }
160        }
161
162        if ( $this->dryRun ) {
163            // Don't recheck if we aren't changing the db
164            return;
165        }
166
167        $unattachedAfter = $central->listUnattached();
168        $numUnattached = count( $unattachedAfter );
169        if ( $numUnattached === 0 ) {
170            $this->attached++;
171        } elseif ( $numUnattached == count( $unattached ) ) {
172            $this->failed++;
173            $this->output(
174                "WARN: No accounts attached for {$username}" .
175                "({$numUnattached} unattached)\n" );
176        } else {
177            $this->partial++;
178            $this->output(
179                "INFO: Incomplete attachment for {$username}" .
180                "({$numUnattached} unattached)\n" );
181        }
182    }
183
184    /**
185     * @param int|float $val
186     *
187     * @return float|int
188     */
189    protected function reportPcnt( $val ) {
190        if ( $this->total > 0 ) {
191            return $val / $this->total * 100.0;
192        }
193        return 0;
194    }
195
196    protected function report() {
197        $delta = microtime( true ) - $this->start;
198        $format = '[%s]' .
199            ' processed: %d (%.1f/sec);' .
200            ' ok: %d (%.1f%%);' .
201            ' attached: %d (%.1f%%);' .
202            ' partial: %d (%.1f%%);' .
203            ' failed: %d (%.1f%%);' .
204            ' missing: %d (%.1f%%);' .
205            "\n";
206        $this->output( sprintf( $format,
207            wfTimestamp( TS_DB ),
208            $this->total, $this->total / $delta,
209            $this->ok, $this->reportPcnt( $this->ok ),
210            $this->attached, $this->reportPcnt( $this->attached ),
211            $this->partial, $this->reportPcnt( $this->partial ),
212            $this->failed, $this->reportPcnt( $this->failed ),
213            $this->missing, $this->reportPcnt( $this->missing )
214        ) );
215    }
216}
217
218$maintClass = AttachAccount::class;
219require_once RUN_MAINTENANCE_IF_MAIN;