Fix condition if...else in getDB() & PHPDoc comment for getUserDB()
[lhc/web/wiklou.git] / maintenance / includes / DeleteLocalPasswords.php
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
24 use MediaWiki\MediaWikiServices;
25 use Wikimedia\Rdbms\IDatabase;
26
27 require_once __DIR__ . '/../Maintenance.php';
28
29 /**
30 * Delete unused local passwords.
31 *
32 * Mainly intended to be used as a base class by authentication extensions to provide maintenance
33 * scripts which allow deleting local passwords for users who have another way of logging in.
34 * Such scripts would customize how to locate users who have other login methods and don't need
35 * local login anymore.
36 * Make sure to set LocalPasswordPrimaryAuthenticationProvider to loginOnly => true or disable it
37 * completely before running this, otherwise it might recreate passwords.
38 *
39 * This class can also be used directly to just delete all local passwords, or those for a specific
40 * user. Deleting all passwords is useful when the wiki has used local password login in the past
41 * but it has been disabled.
42 */
43 class DeleteLocalPasswords extends Maintenance {
44 /** @var string|null User to run on, or null for all. */
45 protected $user;
46
47 /** @var int Number of deleted passwords. */
48 protected $total;
49
50 public function __construct() {
51 parent::__construct();
52 $this->mDescription = "Deletes local password for users.";
53 $this->setBatchSize( 1000 );
54
55 $this->addOption( 'user', 'If specified, only checks the given user', false, true );
56 $this->addOption( 'delete', 'Really delete. To prevent accidents, you must provide this flag.' );
57 $this->addOption( 'prefix', "Instead of deleting, make passwords invalid by prefixing with "
58 . "':null:'. Make sure PasswordConfig has a 'null' entry. This is meant for testing before "
59 . "hard delete." );
60 $this->addOption( 'unprefix', 'Instead of deleting, undo the effect of --prefix.' );
61 }
62
63 protected function initialize() {
64 if (
65 $this->hasOption( 'delete' ) + $this->hasOption( 'prefix' )
66 + $this->hasOption( 'unprefix' ) !== 1
67 ) {
68 $this->fatalError( "Exactly one of the 'delete', 'prefix', 'unprefix' options must be used\n" );
69 }
70 if ( $this->hasOption( 'prefix' ) || $this->hasOption( 'unprefix' ) ) {
71 $passwordHashTypes = MediaWikiServices::getInstance()->getPasswordFactory()->getTypes();
72 if (
73 !isset( $passwordHashTypes['null'] )
74 || $passwordHashTypes['null']['class'] !== InvalidPassword::class
75 ) {
76 $this->fatalError(
77 <<<'ERROR'
78 'null' password entry missing. To use password prefixing, add
79 $wgPasswordConfig['null'] = [ 'class' => InvalidPassword::class ];
80 to your configuration (and remove once the passwords were deleted).
81 ERROR
82 );
83 }
84 }
85
86 $user = $this->getOption( 'user', false );
87 if ( $user !== false ) {
88 $this->user = User::getCanonicalName( $user );
89 if ( $this->user === false ) {
90 $this->fatalError( "Invalid user name\n" );
91 }
92 }
93 }
94
95 public function execute() {
96 $this->initialize();
97
98 foreach ( $this->getUserBatches() as $userBatch ) {
99 $this->processUsers( $userBatch, $this->getUserDB() );
100 }
101
102 $this->output( "done. (wrote $this->total rows)\n" );
103 }
104
105 /**
106 * Get the master DB handle for the current user batch. This is provided for the benefit
107 * of authentication extensions which subclass this and work with wiki farms.
108 * @return IMaintainableDatabase
109 */
110 protected function getUserDB() {
111 return $this->getDB( DB_MASTER );
112 }
113
114 protected function processUsers( array $userBatch, IDatabase $dbw ) {
115 if ( !$userBatch ) {
116 return;
117 }
118 if ( $this->getOption( 'delete' ) ) {
119 $dbw->update( 'user',
120 [ 'user_password' => PasswordFactory::newInvalidPassword()->toString() ],
121 [ 'user_name' => $userBatch ],
122 __METHOD__
123 );
124 } elseif ( $this->getOption( 'prefix' ) ) {
125 $dbw->update( 'user',
126 [ 'user_password = ' . $dbw->buildConcat( [ $dbw->addQuotes( ':null:' ),
127 'user_password' ] ) ],
128 [
129 'NOT (user_password ' . $dbw->buildLike( ':null:', $dbw->anyString() ) . ')',
130 "user_password != " . $dbw->addQuotes( PasswordFactory::newInvalidPassword()->toString() ),
131 'user_password IS NOT NULL',
132 'user_name' => $userBatch,
133 ],
134 __METHOD__
135 );
136 } elseif ( $this->getOption( 'unprefix' ) ) {
137 $dbw->update( 'user',
138 [ 'user_password = ' . $dbw->buildSubString( 'user_password', strlen( ':null:' ) + 1 ) ],
139 [
140 'user_password ' . $dbw->buildLike( ':null:', $dbw->anyString() ),
141 'user_name' => $userBatch,
142 ],
143 __METHOD__
144 );
145 }
146 $this->total += $dbw->affectedRows();
147 MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->waitForReplication();
148 }
149
150 /**
151 * This method iterates through the requested users and returns their names in batches of
152 * self::$mBatchSize.
153 *
154 * Subclasses should reimplement this and locate users who use the specific authentication
155 * method. The default implementation just iterates through all users. Extensions that work
156 * with wikifarm should also update self::getUserDB() as necessary.
157 * @return Generator
158 */
159 protected function getUserBatches() {
160 if ( !is_null( $this->user ) ) {
161 $this->output( "\t ... querying '$this->user'\n" );
162 yield [ [ $this->user ] ];
163 return;
164 }
165
166 $lastUsername = '';
167 $dbw = $this->getDB( DB_MASTER );
168 do {
169 $this->output( "\t ... querying from '$lastUsername'\n" );
170 $users = $dbw->selectFieldValues(
171 'user',
172 'user_name',
173 [
174 'user_name > ' . $dbw->addQuotes( $lastUsername ),
175 ],
176 __METHOD__,
177 [
178 'LIMIT' => $this->getBatchSize(),
179 'ORDER BY' => 'user_name ASC',
180 ]
181 );
182 if ( $users ) {
183 yield $users;
184 $lastUsername = end( $users );
185 }
186 } while ( count( $users ) === $this->getBatchSize() );
187 }
188 }