#!/usr/bin/php * @license GPL-2.0-or-later * @link https://www.mediawiki.org/wiki/Extension:Replace_Text * */ namespace MediaWiki\Extension\ReplaceText; use Maintenance; use MediaWiki\MediaWikiServices; use MWException; use TitleArrayFromResult; use User; $IP = getenv( "MW_INSTALL_PATH" ) ?: __DIR__ . "/../../.."; if ( !is_readable( "$IP/maintenance/Maintenance.php" ) ) { die( "MW_INSTALL_PATH needs to be set to your MediaWiki installation.\n" ); } require_once "$IP/maintenance/Maintenance.php"; /** * Maintenance script that replaces text in pages * * @ingroup Maintenance * @SuppressWarnings(StaticAccess) * @SuppressWarnings(LongVariable) */ class ReplaceAll extends Maintenance { private $user; private $target; private $replacement; private $namespaces; private $category; private $prefix; private $useRegex; private $titles; private $defaultContinue; private $doAnnounce; private $rename; public function __construct() { parent::__construct(); $this->addDescription( "CLI utility to replace text wherever it is " . "found in the wiki." ); $this->addArg( "target", "Target text to find.", false ); $this->addArg( "replace", "Text to replace.", false ); $this->addOption( "dry-run", "Only find the texts, don't replace.", false, false, 'n' ); $this->addOption( "regex", "This is a regex (false).", false, false, 'r' ); $this->addOption( "user", "The user to attribute this to (uid 1).", false, true, 'u' ); $this->addOption( "yes", "Skip all prompts with an assumed 'yes'.", false, false, 'y' ); $this->addOption( "summary", "Alternate edit summary. (%r is where to " . " place the replacement text, %f the text to look for.)", false, true, 's' ); $this->addOption( "nsall", "Search all canonical namespaces (false). " . "If true, this option overrides the ns option.", false, false, 'a' ); $this->addOption( "ns", "Comma separated namespaces to search in " . "(Main) .", false, true ); $this->addOption( 'category', "Search only pages within this category.", false, true, 'c' ); $this->addOption( 'prefix', "Search only pages whose names start with this string.", false, true, 'p' ); $this->addOption( "replacements", "File containing the list of " . "replacements to be made. Fields in the file are tab-separated. " . "See --show-file-format for more information.", false, true, "f" ); $this->addOption( "show-file-format", "Show a description of the " . "file format to use with --replacements.", false, false ); $this->addOption( "no-announce", "Do not announce edits on Special:RecentChanges or " . "watchlists.", false, false, "m" ); $this->addOption( "debug", "Display replacements being made.", false, false ); $this->addOption( "listns", "List out the namespaces on this wiki.", false, false ); $this->addOption( 'rename', "Rename page titles instead of replacing contents.", false, false ); $this->requireExtension( 'Replace Text' ); } private function getUser() { $userReplacing = $this->getOption( "user", 1 ); $user = is_numeric( $userReplacing ) ? User::newFromId( $userReplacing ) : User::newFromName( $userReplacing ); if ( get_class( $user ) !== 'User' ) { $this->fatalError( "Couldn't translate '$userReplacing' to a user." ); } return $user; } private function getTarget() { $ret = $this->getArg( 0 ); if ( !$ret ) { $this->fatalError( "You have to specify a target." ); } return [ $ret ]; } private function getReplacement() { $ret = $this->getArg( 1 ); if ( !$ret ) { $this->fatalError( "You have to specify replacement text." ); } return [ $ret ]; } private function getReplacements() { $file = $this->getOption( "replacements" ); if ( !$file ) { return false; } if ( !is_readable( $file ) ) { throw new MWException( "File does not exist or is not readable: " . "$file\n" ); } $handle = fopen( $file, "r" ); if ( $handle === false ) { throw new MWException( "Trouble opening file: $file\n" ); } $this->defaultContinue = true; // phpcs:ignore MediaWiki.ControlStructures.AssignmentInControlStructures.AssignmentInControlStructures while ( ( $line = fgets( $handle ) ) !== false ) { $field = explode( "\t", substr( $line, 0, -1 ) ); if ( !isset( $field[1] ) ) { continue; } $this->target[] = $field[0]; $this->replacement[] = $field[1]; $this->useRegex[] = isset( $field[2] ); } return true; } private function shouldContinueByDefault() { if ( !is_bool( $this->defaultContinue ) ) { $this->defaultContinue = $this->getOption( "yes" ) ? true : false; } return $this->defaultContinue; } private function getSummary( $target, $replacement ) { $msg = wfMessage( 'replacetext_editsummary', $target, $replacement )-> plain(); if ( $this->getOption( "summary" ) !== null ) { $msg = str_replace( [ '%f', '%r' ], [ $this->target, $this->replacement ], $this->getOption( "summary" ) ); } return $msg; } private function listNamespaces() { $this->output( "Index\tNamespace\n" ); $nsList = MediaWikiServices::getInstance()->getNamespaceInfo()->getCanonicalNamespaces(); ksort( $nsList ); foreach ( $nsList as $int => $val ) { if ( $val == "" ) { $val = "(main)"; } $this->output( " $int\t$val\n" ); } } private function showFileFormat() { $text = <<output( $text ); } private function getNamespaces() { $nsall = $this->getOption( "nsall" ); $ns = $this->getOption( "ns" ); if ( !$nsall && !$ns ) { $namespaces = [ NS_MAIN ]; } else { $canonical = MediaWikiServices::getInstance()->getNamespaceInfo()->getCanonicalNamespaces(); $canonical[NS_MAIN] = "_"; $namespaces = array_flip( $canonical ); if ( !$nsall ) { $namespaces = array_map( static function ( $n ) use ( $canonical, $namespaces ) { if ( is_numeric( $n ) ) { if ( isset( $canonical[ $n ] ) ) { return intval( $n ); } } else { if ( isset( $namespaces[ $n ] ) ) { return $namespaces[ $n ]; } } return null; }, explode( ",", $ns ) ); $namespaces = array_filter( $namespaces, static function ( $val ) { return $val !== null; } ); } } return $namespaces; } private function getCategory() { return $this->getOption( 'category' ); } private function getPrefix() { return $this->getOption( 'prefix' ); } private function useRegex() { return [ $this->getOption( "regex" ) ]; } private function getRename() { return $this->hasOption( 'rename' ); } private function listTitles( $titles, $target, $replacement, $regex, $rename ) { foreach ( $titles as $title ) { if ( $rename ) { $newTitle = Search::getReplacedTitle( $title, $target, $replacement, $regex ); // Implicit conversion of objects to strings $this->output( "$title -> $newTitle\n" ); } else { $this->output( "$title\n" ); } } } private function replaceTitles( $titles, $target, $replacement, $useRegex, $rename ) { foreach ( $titles as $title ) { $params = [ 'target_str' => $target, 'replacement_str' => $replacement, 'use_regex' => $useRegex, 'user_id' => $this->user->getId(), 'edit_summary' => $this->getSummary( $target, $replacement ), 'doAnnounce' => $this->doAnnounce ]; if ( $rename ) { $params[ 'move_page' ] = true; $params[ 'create_redirect' ] = false; $params[ 'watch_page' ] = false; } $this->output( "Replacing on $title... " ); $job = new Job( $title, $params ); if ( $job->run() !== true ) { $this->error( "Trouble on the page '$title'." ); } $this->output( "done.\n" ); } } private function getReply( $question ) { $reply = ""; if ( $this->shouldContinueByDefault() ) { return true; } while ( $reply !== "y" && $reply !== "n" ) { $reply = $this->readconsole( "$question (Y/N) " ); $reply = substr( strtolower( $reply ), 0, 1 ); } return $reply === "y"; } private function localSetup() { if ( $this->getOption( "listns" ) ) { $this->listNamespaces(); return false; } if ( $this->getOption( "show-file-format" ) ) { $this->showFileFormat(); return false; } $this->user = $this->getUser(); if ( !$this->getReplacements() ) { $this->target = $this->getTarget(); $this->replacement = $this->getReplacement(); $this->useRegex = $this->useRegex(); } $this->namespaces = $this->getNamespaces(); $this->category = $this->getCategory(); $this->prefix = $this->getPrefix(); $this->rename = $this->getRename(); return true; } /** * @inheritDoc */ public function execute() { global $wgShowExceptionDetails; $wgShowExceptionDetails = true; $this->doAnnounce = true; if ( !$this->localSetup() ) { return; } if ( $this->namespaces === [] ) { $this->fatalError( "No matching namespaces." ); } foreach ( array_keys( $this->target ) as $index ) { $target = $this->target[$index]; $replacement = $this->replacement[$index]; $useRegex = $this->useRegex[$index]; if ( $this->getOption( "debug" ) ) { $this->output( "Replacing '$target' with '$replacement'" ); if ( $useRegex ) { $this->output( " as regular expression." ); } $this->output( "\n" ); } if ( $this->rename ) { $res = Search::getMatchingTitles( $target, $this->namespaces, $this->category, $this->prefix, $useRegex ); } else { $res = Search::doSearchQuery( $target, $this->namespaces, $this->category, $this->prefix, $useRegex ); } $titles = new TitleArrayFromResult( $res ); if ( count( $titles ) === 0 ) { $this->fatalError( 'No targets found to replace.' ); } if ( $this->getOption( "dry-run" ) ) { $this->listTitles( $titles, $target, $replacement, $useRegex, $this->rename ); continue; } if ( !$this->shouldContinueByDefault() ) { $this->listTitles( $titles, $target, $replacement, $useRegex, $this->rename ); if ( !$this->getReply( 'Replace instances on these pages?' ) ) { return; } } $comment = ""; if ( $this->getOption( "user", null ) === null ) { $comment = " (Use --user to override)"; } if ( $this->getOption( "no-announce", false ) ) { $this->doAnnounce = false; } if ( !$this->getReply( "Attribute changes to the user '{$this->user}'?$comment" ) ) { return; } $this->replaceTitles( $titles, $target, $replacement, $useRegex, $this->rename ); } } } $maintClass = ReplaceAll::class; require_once RUN_MAINTENANCE_IF_MAIN;