Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 75
0.00% covered (danger)
0.00%
0 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
WrapOldPasswordHashes
0.00% covered (danger)
0.00%
0 / 72
0.00% covered (danger)
0.00%
0 / 2
132
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 1
110
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 * @ingroup Maintenance
20 */
21
22require_once __DIR__ . '/Maintenance.php';
23
24use MediaWiki\Extension\CentralAuth\CentralAuthServices;
25use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
26use MediaWiki\MediaWikiServices;
27use Wikimedia\Rdbms\IExpression;
28use Wikimedia\Rdbms\LikeValue;
29
30/**
31 * CentralAuth version of WrapOldPasswords
32 */
33class WrapOldPasswordHashes extends Maintenance {
34    public function __construct() {
35        parent::__construct();
36        $this->addDescription( 'Wrap all passwords of a certain type in a new layered type. '
37            . 'The script runs in dry-run mode by default (use --update to update rows)' );
38        $this->addOption( 'type',
39            'Password type to wrap passwords in (must inherit LayeredParameterizedPassword)', true, true );
40        $this->addOption( 'verbose', 'Enables verbose output', false, false, 'v' );
41        $this->addOption( 'update', 'Actually wrap passwords', false, false, 'u' );
42        $this->setBatchSize( 100 );
43        $this->requireExtension( 'CentralAuth' );
44    }
45
46    public function execute() {
47        $passwordFactory = MediaWikiServices::getInstance()->getPasswordFactory();
48
49        $typeInfo = $passwordFactory->getTypes();
50        $layeredType = $this->getOption( 'type' );
51
52        // Check that type exists and is a layered type
53        if ( !isset( $typeInfo[$layeredType] ) ) {
54            $this->fatalError( 'Undefined password type: ' . $layeredType );
55        }
56
57        $passObj = $passwordFactory->newFromType( $layeredType );
58        if ( !$passObj instanceof LayeredParameterizedPassword ) {
59            $this->fatalError( 'Layered parameterized password type must be used.' );
60        }
61
62        // Extract the first layer type
63        $typeConfig = $typeInfo[$layeredType];
64        $firstType = $typeConfig['types'][0];
65
66        $update = $this->hasOption( 'update' );
67
68        $databaseManager = CentralAuthServices::getDatabaseManager();
69
70        // Get a list of password types that are applicable
71        $dbw = $databaseManager->getCentralPrimaryDB();
72        $typeCond = $dbw->expr( 'gu_password', IExpression::LIKE, new LikeValue( ":$firstType:", $dbw->anyString() ) );
73
74        $count = 0;
75        $minUserId = 0;
76        do {
77            if ( $update ) {
78                $this->beginTransaction( $dbw, __METHOD__ );
79            }
80
81            $res = $dbw->newSelectQueryBuilder()
82                ->select( [ 'gu_id', 'gu_name', 'gu_password' ] )
83                ->from( 'globaluser' )
84                ->where( [
85                    $dbw->expr( 'gu_id', '>', $minUserId ),
86                    $typeCond
87                ] )
88                ->orderBy( 'gu_id' )
89                ->limit( $this->getBatchSize() )
90                ->lockInShareMode()
91                ->caller( __METHOD__ )
92                ->fetchResultSet();
93
94            /** @var CentralAuthUser[] $updateUsers */
95            $updateUsers = [];
96            foreach ( $res as $row ) {
97                $user = CentralAuthUser::getPrimaryInstanceByName( $row->gu_name );
98
99                /** @var ParameterizedPassword $password */
100                $password = $passwordFactory->newFromCiphertext( $row->gu_password );
101                '@phan-var ParameterizedPassword $password';
102                /** @var LayeredParameterizedPassword $layeredPassword */
103                $layeredPassword = $passwordFactory->newFromType( $layeredType );
104                '@phan-var LayeredParameterizedPassword $layeredPassword';
105                $layeredPassword->partialCrypt( $password );
106                if ( $this->hasOption( 'verbose' ) ) {
107                    $this->output(
108                        "Updating password for user {$row->gu_name} ({$row->gu_id}) from " .
109                        "type {$password->getType()} to {$layeredPassword->getType()}.\n"
110                    );
111                }
112
113                if ( $update ) {
114                    $count++;
115                    $updateUsers[] = $user;
116                    $dbw->newUpdateQueryBuilder()
117                        ->update( 'globaluser' )
118                        ->set( [ 'gu_password' => $layeredPassword->toString() ] )
119                        ->where( [ 'gu_id' => $row->gu_id ] )
120                        ->caller( __METHOD__ )
121                        ->execute();
122                }
123
124                $minUserId = $row->gu_id;
125            }
126
127            if ( $update ) {
128                $this->commitTransaction( $dbw, __METHOD__ );
129                $databaseManager->waitForReplication();
130
131                // Clear memcached so old passwords are wiped out
132                foreach ( $updateUsers as $user ) {
133                    $user->invalidateCache();
134                }
135            }
136        } while ( $res->numRows() );
137
138        if ( $update ) {
139            $this->output( "$count users rows updated.\n" );
140        } else {
141            $this->output( "$count user rows found using old password formats. "
142                . "Run script again with --update to update these rows.\n" );
143        }
144    }
145}
146
147$maintClass = WrapOldPasswordHashes::class;
148require_once RUN_MAINTENANCE_IF_MAIN;