Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 88
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
DeleteLocalPasswords
0.00% covered (danger)
0.00%
0 / 87
0.00% covered (danger)
0.00%
0 / 6
420
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
 initialize
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
72
 execute
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getUserDB
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 processUsers
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
30
 getUserBatches
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Helper for deleting unused local passwords.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup Maintenance
22 */
23
24namespace MediaWiki\Maintenance;
25
26use MediaWiki\Password\InvalidPassword;
27use MediaWiki\Password\PasswordFactory;
28use Wikimedia\Rdbms\IDatabase;
29use Wikimedia\Rdbms\IExpression;
30use Wikimedia\Rdbms\LikeValue;
31use Wikimedia\Rdbms\RawSQLValue;
32
33// @codeCoverageIgnoreStart
34require_once __DIR__ . '/../Maintenance.php';
35// @codeCoverageIgnoreEnd
36
37/**
38 * Delete unused local passwords.
39 *
40 * Mainly intended to be used as a base class by authentication extensions to provide maintenance
41 * scripts which allow deleting local passwords for users who have another way of logging in.
42 * Such scripts would customize how to locate users who have other login methods and don't need
43 * local login anymore.
44 * Make sure to set LocalPasswordPrimaryAuthenticationProvider to loginOnly => true or disable it
45 * completely before running this, otherwise it might recreate passwords.
46 *
47 * This class can also be used directly to just delete all local passwords, or those for a specific
48 * user. Deleting all passwords is useful when the wiki has used local password login in the past
49 * but it has been disabled.
50 */
51class DeleteLocalPasswords extends Maintenance {
52    /** @var string|null User to run on, or null for all. */
53    protected $user;
54
55    /** @var int Number of deleted passwords. */
56    protected $total;
57
58    public function __construct() {
59        parent::__construct();
60        $this->addDescription( "Deletes local password for users." );
61        $this->setBatchSize( 1000 );
62
63        $this->addOption( 'user', 'If specified, only checks the given user', false, true );
64        $this->addOption( 'delete', 'Really delete. To prevent accidents, you must provide this flag.' );
65        $this->addOption( 'prefix', "Instead of deleting, make passwords invalid by prefixing with "
66            . "':null:'. Make sure PasswordConfig has a 'null' entry. This is meant for testing before "
67            . "hard delete." );
68        $this->addOption( 'unprefix', 'Instead of deleting, undo the effect of --prefix.' );
69    }
70
71    protected function initialize() {
72        if (
73            (int)$this->hasOption( 'delete' ) + (int)$this->hasOption( 'prefix' )
74            + (int)$this->hasOption( 'unprefix' ) !== 1
75        ) {
76            $this->fatalError( "Exactly one of the 'delete', 'prefix', 'unprefix' options must be used\n" );
77        }
78        if ( $this->hasOption( 'prefix' ) || $this->hasOption( 'unprefix' ) ) {
79            $passwordHashTypes = $this->getServiceContainer()->getPasswordFactory()->getTypes();
80            if (
81                !isset( $passwordHashTypes['null'] )
82                || $passwordHashTypes['null']['class'] !== InvalidPassword::class
83            ) {
84                $this->fatalError(
85<<<'ERROR'
86'null' password entry missing. To use password prefixing, add
87    $wgPasswordConfig['null'] = [ 'class' => InvalidPassword::class ];
88to your configuration (and remove once the passwords were deleted).
89ERROR
90                );
91            }
92        }
93
94        $user = $this->getOption( 'user', false );
95        if ( $user !== false ) {
96            $userNameUtils = $this->getServiceContainer()->getUserNameUtils();
97            $this->user = $userNameUtils->getCanonical( $user );
98            if ( $this->user === false ) {
99                $this->fatalError( "Invalid user name\n" );
100            }
101        }
102    }
103
104    public function execute() {
105        $this->initialize();
106
107        foreach ( $this->getUserBatches() as $userBatch ) {
108            $this->processUsers( $userBatch, $this->getUserDB() );
109        }
110
111        $this->output( "done. (wrote $this->total rows)\n" );
112    }
113
114    /**
115     * Get the primary DB handle for the current user batch. This is provided for the benefit
116     * of authentication extensions which subclass this and work with wiki farms.
117     * @return IDatabase
118     */
119    protected function getUserDB() {
120        return $this->getPrimaryDB();
121    }
122
123    protected function processUsers( array $userBatch, IDatabase $dbw ) {
124        if ( !$userBatch ) {
125            return;
126        }
127        if ( $this->getOption( 'delete' ) ) {
128            $dbw->newUpdateQueryBuilder()
129                ->update( 'user' )
130                ->set( [ 'user_password' => PasswordFactory::newInvalidPassword()->toString() ] )
131                ->where( [ 'user_name' => $userBatch ] )
132                ->caller( __METHOD__ )->execute();
133        } elseif ( $this->getOption( 'prefix' ) ) {
134            $dbw->newUpdateQueryBuilder()
135                ->update( 'user' )
136                ->set( [
137                    'user_password' => new RawSQLValue(
138                        $dbw->buildConcat( [ $dbw->addQuotes( ':null:' ), 'user_password' ] )
139                    )
140                ] )
141                ->where( [
142                    $dbw->expr( 'user_password', IExpression::NOT_LIKE, new LikeValue( ':null:', $dbw->anyString() ) ),
143                    $dbw->expr( 'user_password', '!=', PasswordFactory::newInvalidPassword()->toString() ),
144                    $dbw->expr( 'user_password', '!=', null ),
145                    'user_name' => $userBatch,
146                ] )
147                ->caller( __METHOD__ )->execute();
148        } elseif ( $this->getOption( 'unprefix' ) ) {
149            $dbw->newUpdateQueryBuilder()
150                ->update( 'user' )
151                ->set( [
152                    'user_password' => new RawSQLValue(
153                        $dbw->buildSubString( 'user_password', strlen( ':null:' ) + 1 )
154                    )
155                ] )
156                ->where( [
157                    $dbw->expr( 'user_password', IExpression::LIKE, new LikeValue( ':null:', $dbw->anyString() ) ),
158                    'user_name' => $userBatch,
159                ] )
160                ->caller( __METHOD__ )->execute();
161        }
162        $this->total += $dbw->affectedRows();
163        $this->waitForReplication();
164    }
165
166    /**
167     * This method iterates through the requested users and returns their names in batches of
168     * self::$mBatchSize.
169     *
170     * Subclasses should reimplement this and locate users who use the specific authentication
171     * method. The default implementation just iterates through all users. Extensions that work
172     * with wikifarm should also update self::getUserDB() as necessary.
173     * @return \Generator
174     */
175    protected function getUserBatches() {
176        if ( $this->user !== null ) {
177            $this->output( "\t ... querying '$this->user'\n" );
178            yield [ [ $this->user ] ];
179            return;
180        }
181
182        $lastUsername = '';
183        $dbw = $this->getPrimaryDB();
184        do {
185            $this->output( "\t ... querying from '$lastUsername'\n" );
186            $users = $dbw->newSelectQueryBuilder()
187                ->select( 'user_name' )
188                ->from( 'user' )
189                ->where( $dbw->expr( 'user_name', '>', $lastUsername ) )
190                ->orderBy( 'user_name ASC' )
191                ->limit( $this->getBatchSize() )
192                ->caller( __METHOD__ )->fetchFieldValues();
193            if ( $users ) {
194                yield $users;
195                $lastUsername = end( $users );
196            }
197        } while ( count( $users ) === $this->getBatchSize() );
198    }
199}
200
201/** @deprecated class alias since 1.43 */
202class_alias( DeleteLocalPasswords::class, 'DeleteLocalPasswords' );