From f97d90a5969dce9afe796c553d611f931df14abc Mon Sep 17 00:00:00 2001 From: Tim Starling Date: Fri, 3 Mar 2023 14:45:33 +1100 Subject: [PATCH] Renameuser: mostly rewrite the maintenance script * Use arguments instead of options for the old and new username. * Improve validation/canonicalization. * Refuse to rename CentralAuth attached users. In production we use user rights to prevent access to Special:Renameuser on CentralAuth wikis, but the maintenance script does not have that protection. * Move user pages. Don't respect $wgMaximumMovedPages. Bug: T27482 Change-Id: I78dd4012e71d7a2d185bd96da7055fa14dc7fcb8 --- includes/page/MovePage.php | 22 +++++++-- maintenance/renameUser.php | 98 +++++++++++++++++++++++++++++++------- 2 files changed, 100 insertions(+), 20 deletions(-) diff --git a/includes/page/MovePage.php b/includes/page/MovePage.php index f38903e74fc..3853af5576a 100644 --- a/includes/page/MovePage.php +++ b/includes/page/MovePage.php @@ -145,6 +145,9 @@ class MovePage { /** @var RestrictionStore */ private $restrictionStore; + /** @var int */ + private $maximumMovedPages; + /** * @internal For use by PageCommandFactory */ @@ -214,6 +217,17 @@ class MovePage { $this->collationFactory = $collationFactory; $this->pageUpdaterFactory = $pageUpdaterFactory; $this->restrictionStore = $restrictionStore; + + $this->maximumMovedPages = $this->options->get( MainConfigNames::MaximumMovedPages ); + } + + /** + * Override $wgMaximumMovedPages. + * + * @param int $max The maximum number of subpages to move, or -1 for no limit + */ + public function setMaximumMovedPages( $max ) { + $this->maximumMovedPages = $max; } /** @@ -609,15 +623,15 @@ class MovePage { // Return a status for the overall result. Its value will be an array with per-title // status for each subpage. Merge any errors from the per-title statuses into the // top-level status without resetting the overall result. - $maximumMovedPages = $this->options->get( MainConfigNames::MaximumMovedPages ); + $max = $this->maximumMovedPages; $topStatus = Status::newGood(); $perTitleStatus = []; - $subpages = $this->oldTitle->getSubpages( $maximumMovedPages + 1 ); + $subpages = $this->oldTitle->getSubpages( $max >= 0 ? $max + 1 : -1 ); $count = 0; foreach ( $subpages as $oldSubpage ) { $count++; - if ( $count > $maximumMovedPages ) { - $status = Status::newFatal( 'movepage-max-pages', $maximumMovedPages ); + if ( $max >= 0 && $count > $max ) { + $status = Status::newFatal( 'movepage-max-pages', $max ); $perTitleStatus[$oldSubpage->getPrefixedText()] = $status; $topStatus->merge( $status ); $topStatus->setOK( true ); diff --git a/maintenance/renameUser.php b/maintenance/renameUser.php index bda77922aa7..41a4d3de6dd 100644 --- a/maintenance/renameUser.php +++ b/maintenance/renameUser.php @@ -22,6 +22,7 @@ require_once __DIR__ . '/Maintenance.php'; use MediaWiki\MediaWikiServices; +use MediaWiki\Page\MovePageFactory; use MediaWiki\RenameUser\RenameuserSQL; use MediaWiki\User\UserFactory; @@ -29,29 +30,59 @@ class RenameUser extends Maintenance { /** @var UserFactory */ private $userFactory; + /** @var CentralIdLookup|null */ + private $centralLookup; + + /** @var MovePageFactory */ + private $movePageFactory; + public function __construct() { parent::__construct(); - $this->addDescription( 'Rename an user' ); - $this->addOption( 'oldname', 'Current username of the to-be-renamed user', true, true ); - $this->addOption( 'newname', 'New username of the to-be-renamed user', true, true ); + $this->addDescription( 'Rename a user' ); + $this->addArg( 'old-name', 'Current username of the to-be-renamed user' ); + $this->addArg( 'new-name', 'New username of the to-be-renamed user' ); $this->addOption( 'performer', 'Performer of the rename action', false, true ); $this->addOption( 'reason', 'Reason of the rename', false, true ); + $this->addOption( 'force-global-detach', + 'Rename the local user even if it is attached to a global account' ); + $this->addOption( 'suppress-redirect', 'Don\'t create redirects when moving pages' ); + $this->addOption( 'skip-page-moves', 'Don\'t move associated user pages' ); } private function initServices() { $services = MediaWikiServices::getInstance(); $this->userFactory = $services->getUserFactory(); + $this->centralLookup = $services->getCentralIdLookupFactory()->getNonLocalLookup(); + $this->movePageFactory = $services->getMovePageFactory(); } public function execute() { $this->initServices(); - $user = $this->userFactory->newFromName( $this->getOption( 'oldname' ) ); - if ( $user->getId() === 0 ) { + + $oldName = $this->getArg( 'old-name' ); + $newName = $this->getArg( 'new-name' ); + + $oldUser = $this->userFactory->newFromName( $oldName ); + if ( !$oldUser ) { + $this->fatalError( 'The specified old username is invalid' ); + } + + if ( !$oldUser->isRegistered() ) { $this->fatalError( 'The user does not exist' ); } - if ( $this->userFactory->newFromName( $this->getOption( 'newname' ) )->getId() > 0 ) { + if ( !$this->getOption( 'force-global-detach' ) + && $this->centralLookup + && $this->centralLookup->isAttached( $oldUser ) + ) { + $this->fatalError( 'The user is globally attached. Use CentralAuth to rename this account.' ); + } + + $newUser = $this->userFactory->newFromName( $newName, UserFactory::RIGOR_CREATABLE ); + if ( !$newUser ) { + $this->fatalError( 'The specified new username is invalid' ); + } elseif ( $newUser->isRegistered() ) { $this->fatalError( 'New username must be free' ); } @@ -61,27 +92,62 @@ class RenameUser extends Maintenance { $performer = $this->userFactory->newFromName( $this->getOption( 'performer' ) ); } - if ( !( $performer instanceof User ) || $performer->getId() === 0 ) { + if ( !( $performer instanceof User ) || !$performer->isRegistered() ) { $this->fatalError( 'Performer does not exist.' ); } - '@phan-var User $performer'; - $renameJob = new RenameuserSQL( - $user->getName(), - $this->getOption( 'newname' ), - $user->getId(), + $renamer = new RenameuserSQL( + $oldUser->getName(), + $newUser->getName(), + $oldUser->getId(), $performer, [ 'reason' => $this->getOption( 'reason' ) ] ); - if ( !$renameJob->rename() ) { + if ( !$renamer->rename() ) { $this->fatalError( 'Renaming failed.' ); } else { - $oldname = $this->getOption( 'oldname' ); - $newname = $this->getOption( 'newname' ); - $this->output( "$oldname was successfully renamed to $newname.\n" ); + $this->output( "{$oldUser->getName()} was successfully renamed to {$newUser->getName()}.\n" ); + } + + if ( !$this->getOption( 'skip-page-moves' ) ) { + $movePage = $this->movePageFactory->newMovePage( + $oldUser->getUserPage(), + $newUser->getUserPage(), + ); + $movePage->setMaximumMovedPages( -1 ); + $logMessage = wfMessage( + 'renameuser-move-log', $oldUser->getName(), $newUser->getName() + )->inContentLanguage()->text(); + $createRedirect = !$this->getOption( 'suppress-redirect' ); + + $numRenames = 0; + if ( $oldUser->getUserPage()->exists() ) { + $status = $movePage->move( $performer, $logMessage, $createRedirect ); + if ( $status->isGood() ) { + $numRenames++; + } else { + $this->output( "Failed to rename user page: " . + $status->getWikiText( false, false, 'en' ) . + "\n" ); + } + } + + $batchStatus = $movePage->moveSubpages( $performer, $logMessage, $createRedirect ); + foreach ( $batchStatus->getValue() as $titleText => $status ) { + if ( $status->isGood() ) { + $numRenames++; + } else { + $this->output( "Failed to rename user subpage \"$titleText\": " . + $status->getWikiText( false, false, 'en' ) . "\n" ); + } + } + + if ( $numRenames > 0 ) { + $this->output( "$numRenames user page(s) renamed\n" ); + } } } }