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->mDescription = "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 ( $this->hasOption( 'delete' ) + $this->hasOption( 'prefix' ) + $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 = MediaWikiServices::getInstance()->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 ) { $this->user = User::getCanonicalName( $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 master DB handle for the current user batch. This is provided for the benefit * of authentication extensions which subclass this and work with wiki farms. */ protected function getUserDB() { return $this->getDB( DB_MASTER ); } protected function processUsers( array $userBatch, IDatabase $dbw ) { if ( !$userBatch ) { return; } if ( $this->getOption( 'delete' ) ) { $dbw->update( 'user', [ 'user_password' => PasswordFactory::newInvalidPassword()->toString() ], [ 'user_name' => $userBatch ], __METHOD__ ); } elseif ( $this->getOption( 'prefix' ) ) { $dbw->update( 'user', [ 'user_password = ' . $dbw->buildConcat( [ $dbw->addQuotes( ':null:' ), 'user_password' ] ) ], [ 'NOT (user_password ' . $dbw->buildLike( ':null:', $dbw->anyString() ) . ')', "user_password != " . $dbw->addQuotes( PasswordFactory::newInvalidPassword()->toString() ), 'user_password IS NOT NULL', 'user_name' => $userBatch, ], __METHOD__ ); } elseif ( $this->getOption( 'unprefix' ) ) { $dbw->update( 'user', [ 'user_password = ' . $dbw->buildSubString( 'user_password', strlen( ':null:' ) + 1 ) ], [ 'user_password ' . $dbw->buildLike( ':null:', $dbw->anyString() ), 'user_name' => $userBatch, ], __METHOD__ ); } $this->total += $dbw->affectedRows(); } /** * 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 ( !is_null( $this->user ) ) { $this->output( "\t ... querying '$this->user'\n" ); yield [ $this->user ]; return; } $lastUsername = ''; $dbw = $this->getDB( DB_MASTER ); do { $this->output( "\t ... querying from '$lastUsername'\n" ); $users = $dbw->selectFieldValues( 'user', 'user_name', [ 'user_name > ' .$dbw->addQuotes( $lastUsername ), ], __METHOD__, [ 'LIMIT' => $this->getBatchSize(), 'ORDER BY' => 'user_name ASC', ] ); if ( $users ) { yield $users; $lastUsername = end( $users ); } } while ( count( $users ) === $this->getBatchSize() ); } }