Also a few other fixes of PHP class aliases spotted by phan. Bug: T353458 Change-Id: Ie79d65722c47c24f8f20f1293355cfd3c2e8c2ad
202 lines
6.9 KiB
PHP
202 lines
6.9 KiB
PHP
<?php
|
|
/**
|
|
* Helper for deleting unused local passwords.
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
* http://www.gnu.org/copyleft/gpl.html
|
|
*
|
|
* @file
|
|
* @ingroup Maintenance
|
|
*/
|
|
|
|
namespace MediaWiki\Maintenance;
|
|
|
|
use MediaWiki\Password\InvalidPassword;
|
|
use MediaWiki\Password\PasswordFactory;
|
|
use Wikimedia\Rdbms\IDatabase;
|
|
use Wikimedia\Rdbms\IExpression;
|
|
use Wikimedia\Rdbms\LikeValue;
|
|
use Wikimedia\Rdbms\RawSQLValue;
|
|
|
|
// @codeCoverageIgnoreStart
|
|
require_once __DIR__ . '/../Maintenance.php';
|
|
// @codeCoverageIgnoreEnd
|
|
|
|
/**
|
|
* Delete unused local passwords.
|
|
*
|
|
* Mainly intended to be used as a base class by authentication extensions to provide maintenance
|
|
* scripts which allow deleting local passwords for users who have another way of logging in.
|
|
* Such scripts would customize how to locate users who have other login methods and don't need
|
|
* local login anymore.
|
|
* Make sure to set LocalPasswordPrimaryAuthenticationProvider to loginOnly => true or disable it
|
|
* completely before running this, otherwise it might recreate passwords.
|
|
*
|
|
* This class can also be used directly to just delete all local passwords, or those for a specific
|
|
* user. Deleting all passwords is useful when the wiki has used local password login in the past
|
|
* but it has been disabled.
|
|
*/
|
|
class DeleteLocalPasswords extends Maintenance {
|
|
/** @var string|null User to run on, or null for all. */
|
|
protected $user;
|
|
|
|
/** @var int Number of deleted passwords. */
|
|
protected $total;
|
|
|
|
public function __construct() {
|
|
parent::__construct();
|
|
$this->addDescription( "Deletes local password for users." );
|
|
$this->setBatchSize( 1000 );
|
|
|
|
$this->addOption( 'user', 'If specified, only checks the given user', false, true );
|
|
$this->addOption( 'delete', 'Really delete. To prevent accidents, you must provide this flag.' );
|
|
$this->addOption( 'prefix', "Instead of deleting, make passwords invalid by prefixing with "
|
|
. "':null:'. Make sure PasswordConfig has a 'null' entry. This is meant for testing before "
|
|
. "hard delete." );
|
|
$this->addOption( 'unprefix', 'Instead of deleting, undo the effect of --prefix.' );
|
|
}
|
|
|
|
protected function initialize() {
|
|
if (
|
|
(int)$this->hasOption( 'delete' ) + (int)$this->hasOption( 'prefix' )
|
|
+ (int)$this->hasOption( 'unprefix' ) !== 1
|
|
) {
|
|
$this->fatalError( "Exactly one of the 'delete', 'prefix', 'unprefix' options must be used\n" );
|
|
}
|
|
if ( $this->hasOption( 'prefix' ) || $this->hasOption( 'unprefix' ) ) {
|
|
$passwordHashTypes = $this->getServiceContainer()->getPasswordFactory()->getTypes();
|
|
if (
|
|
!isset( $passwordHashTypes['null'] )
|
|
|| $passwordHashTypes['null']['class'] !== InvalidPassword::class
|
|
) {
|
|
$this->fatalError(
|
|
<<<'ERROR'
|
|
'null' password entry missing. To use password prefixing, add
|
|
$wgPasswordConfig['null'] = [ 'class' => InvalidPassword::class ];
|
|
to your configuration (and remove once the passwords were deleted).
|
|
ERROR
|
|
);
|
|
}
|
|
}
|
|
|
|
$user = $this->getOption( 'user', false );
|
|
if ( $user !== false ) {
|
|
$userNameUtils = $this->getServiceContainer()->getUserNameUtils();
|
|
$this->user = $userNameUtils->getCanonical( $user );
|
|
if ( $this->user === false ) {
|
|
$this->fatalError( "Invalid user name\n" );
|
|
}
|
|
}
|
|
}
|
|
|
|
public function execute() {
|
|
$this->initialize();
|
|
|
|
foreach ( $this->getUserBatches() as $userBatch ) {
|
|
$this->processUsers( $userBatch, $this->getUserDB() );
|
|
}
|
|
|
|
$this->output( "done. (wrote $this->total rows)\n" );
|
|
}
|
|
|
|
/**
|
|
* Get the primary DB handle for the current user batch. This is provided for the benefit
|
|
* of authentication extensions which subclass this and work with wiki farms.
|
|
* @return IDatabase
|
|
*/
|
|
protected function getUserDB() {
|
|
return $this->getPrimaryDB();
|
|
}
|
|
|
|
protected function processUsers( array $userBatch, IDatabase $dbw ) {
|
|
if ( !$userBatch ) {
|
|
return;
|
|
}
|
|
if ( $this->getOption( 'delete' ) ) {
|
|
$dbw->newUpdateQueryBuilder()
|
|
->update( 'user' )
|
|
->set( [ 'user_password' => PasswordFactory::newInvalidPassword()->toString() ] )
|
|
->where( [ 'user_name' => $userBatch ] )
|
|
->caller( __METHOD__ )->execute();
|
|
} elseif ( $this->getOption( 'prefix' ) ) {
|
|
$dbw->newUpdateQueryBuilder()
|
|
->update( 'user' )
|
|
->set( [
|
|
'user_password' => new RawSQLValue(
|
|
$dbw->buildConcat( [ $dbw->addQuotes( ':null:' ), 'user_password' ] )
|
|
)
|
|
] )
|
|
->where( [
|
|
$dbw->expr( 'user_password', IExpression::NOT_LIKE, new LikeValue( ':null:', $dbw->anyString() ) ),
|
|
$dbw->expr( 'user_password', '!=', PasswordFactory::newInvalidPassword()->toString() ),
|
|
$dbw->expr( 'user_password', '!=', null ),
|
|
'user_name' => $userBatch,
|
|
] )
|
|
->caller( __METHOD__ )->execute();
|
|
} elseif ( $this->getOption( 'unprefix' ) ) {
|
|
$dbw->newUpdateQueryBuilder()
|
|
->update( 'user' )
|
|
->set( [
|
|
'user_password' => new RawSQLValue(
|
|
$dbw->buildSubString( 'user_password', strlen( ':null:' ) + 1 )
|
|
)
|
|
] )
|
|
->where( [
|
|
$dbw->expr( 'user_password', IExpression::LIKE, new LikeValue( ':null:', $dbw->anyString() ) ),
|
|
'user_name' => $userBatch,
|
|
] )
|
|
->caller( __METHOD__ )->execute();
|
|
}
|
|
$this->total += $dbw->affectedRows();
|
|
$this->waitForReplication();
|
|
}
|
|
|
|
/**
|
|
* This method iterates through the requested users and returns their names in batches of
|
|
* self::$mBatchSize.
|
|
*
|
|
* Subclasses should reimplement this and locate users who use the specific authentication
|
|
* method. The default implementation just iterates through all users. Extensions that work
|
|
* with wikifarm should also update self::getUserDB() as necessary.
|
|
* @return \Generator
|
|
*/
|
|
protected function getUserBatches() {
|
|
if ( $this->user !== null ) {
|
|
$this->output( "\t ... querying '$this->user'\n" );
|
|
yield [ [ $this->user ] ];
|
|
return;
|
|
}
|
|
|
|
$lastUsername = '';
|
|
$dbw = $this->getPrimaryDB();
|
|
do {
|
|
$this->output( "\t ... querying from '$lastUsername'\n" );
|
|
$users = $dbw->newSelectQueryBuilder()
|
|
->select( 'user_name' )
|
|
->from( 'user' )
|
|
->where( $dbw->expr( 'user_name', '>', $lastUsername ) )
|
|
->orderBy( 'user_name ASC' )
|
|
->limit( $this->getBatchSize() )
|
|
->caller( __METHOD__ )->fetchFieldValues();
|
|
if ( $users ) {
|
|
yield $users;
|
|
$lastUsername = end( $users );
|
|
}
|
|
} while ( count( $users ) === $this->getBatchSize() );
|
|
}
|
|
}
|
|
|
|
/** @deprecated class alias since 1.43 */
|
|
class_alias( DeleteLocalPasswords::class, 'DeleteLocalPasswords' );
|