2014-10-06 02:39:42 +00:00
|
|
|
#!/usr/bin/php
|
|
|
|
<?php
|
|
|
|
/**
|
2015-04-30 14:53:45 +00:00
|
|
|
* Insert jobs into the job queue to replace text bits.
|
2014-10-06 02:39:42 +00:00
|
|
|
* Or execute immediately... your choice.
|
|
|
|
*
|
2017-12-15 21:46:51 +00:00
|
|
|
* Copyright © 2014 NicheWork, LLC
|
2014-10-06 02:39:42 +00:00
|
|
|
*
|
|
|
|
* 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
|
|
|
|
*
|
|
|
|
* PHP version 5
|
|
|
|
*
|
|
|
|
* @file
|
|
|
|
* @category Maintenance
|
|
|
|
* @package ReplaceText
|
|
|
|
* @author Mark A. Hershberger <mah@nichework.com>
|
|
|
|
* @license GPLv3 http://www.gnu.org/copyleft/gpl.html
|
|
|
|
* @link https://www.mediawiki.org/wiki/Extension:Replace_Text
|
|
|
|
*
|
|
|
|
*/
|
2016-05-30 21:35:28 +00:00
|
|
|
// @codingStandardsIgnoreStart
|
2017-12-15 21:46:51 +00:00
|
|
|
$IP = getenv( "MW_INSTALL_PATH" ) ? getenv( "MW_INSTALL_PATH" ) : "../..";
|
|
|
|
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" );
|
2016-05-30 21:35:28 +00:00
|
|
|
// @codingStandardsIgnoreEnd
|
2014-10-06 02:39:42 +00:00
|
|
|
|
|
|
|
/**
|
2017-12-15 21:46:51 +00:00
|
|
|
* Maintenance script that replaces text in pages
|
2014-10-06 02:39:42 +00:00
|
|
|
*
|
|
|
|
* @ingroup Maintenance
|
|
|
|
* @SuppressWarnings(StaticAccess)
|
|
|
|
* @SuppressWarnings(LongVariable)
|
|
|
|
*/
|
|
|
|
class ReplaceText extends Maintenance {
|
|
|
|
protected $user;
|
|
|
|
protected $target;
|
|
|
|
protected $replacement;
|
|
|
|
protected $summaryMsg;
|
|
|
|
protected $namespaces;
|
|
|
|
protected $category;
|
|
|
|
protected $prefix;
|
|
|
|
protected $useRegex;
|
|
|
|
protected $titles;
|
|
|
|
protected $defaultContinue;
|
2017-12-15 21:46:51 +00:00
|
|
|
protected $doAnnounce;
|
2014-10-06 02:39:42 +00:00
|
|
|
|
|
|
|
public function __construct() {
|
|
|
|
parent::__construct();
|
|
|
|
$this->mDescription = "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' );
|
2016-12-08 22:20:17 +00:00
|
|
|
$this->addOption( "nsall", "Search all canonical namespaces (false). " .
|
|
|
|
"If true, this option overrides the ns option.", false, false, 'a' );
|
2017-12-15 21:46:51 +00:00
|
|
|
$this->addOption( "ns", "Comma separated namespaces to search in " .
|
|
|
|
"(Main) .", false, true );
|
|
|
|
$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" );
|
2014-10-06 02:39:42 +00:00
|
|
|
$this->addOption( "debug", "Display replacements being made.", false, false );
|
|
|
|
$this->addOption( "listns", "List out the namespaces on this wiki.",
|
|
|
|
false, false );
|
2018-01-28 19:02:18 +00:00
|
|
|
|
2018-02-08 17:28:00 +00:00
|
|
|
// MW 1.28
|
|
|
|
if ( method_exists( $this, 'requireExtension' ) ) {
|
|
|
|
$this->requireExtension( 'Replace Text' );
|
|
|
|
}
|
2014-10-06 02:39:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
protected function getUser() {
|
|
|
|
$userReplacing = $this->getOption( "user", 1 );
|
|
|
|
|
|
|
|
$user = is_numeric( $userReplacing ) ?
|
|
|
|
User::newFromId( $userReplacing ) :
|
|
|
|
User::newFromName( $userReplacing );
|
|
|
|
|
|
|
|
if ( get_class( $user ) !== 'User' ) {
|
|
|
|
$this->error(
|
|
|
|
"Couldn't translate '$userReplacing' to a user.", true
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $user;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function getTarget() {
|
|
|
|
$ret = $this->getArg( 0 );
|
|
|
|
if ( !$ret ) {
|
|
|
|
$this->error( "You have to specify a target.", true );
|
|
|
|
}
|
2017-06-20 07:20:41 +00:00
|
|
|
return [ $ret ];
|
2014-10-06 02:39:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
protected function getReplacement() {
|
|
|
|
$ret = $this->getArg( 1 );
|
|
|
|
if ( !$ret ) {
|
|
|
|
$this->error( "You have to specify replacement text.", true );
|
|
|
|
}
|
2017-06-20 07:20:41 +00:00
|
|
|
return [ $ret ];
|
2014-10-06 02:39:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
protected function getReplacements() {
|
|
|
|
$file = $this->getOption( "replacements" );
|
|
|
|
if ( !$file ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !is_readable( $file ) ) {
|
2017-12-15 21:46:51 +00:00
|
|
|
throw new MWException( "File does not exist or is not readable: "
|
|
|
|
. "$file\n" );
|
2014-10-06 02:39:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
$handle = fopen( $file, "r" );
|
|
|
|
if ( $handle === false ) {
|
|
|
|
throw new MWException( "Trouble opening file: $file\n" );
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->defaultContinue = true;
|
2016-05-30 21:35:28 +00:00
|
|
|
// @codingStandardsIgnoreStart
|
2014-10-06 02:39:42 +00:00
|
|
|
while ( ( $line = fgets( $handle ) ) !== false ) {
|
2016-05-30 21:35:28 +00:00
|
|
|
// @codingStandardsIgnoreEnd
|
2014-10-06 02:39:42 +00:00
|
|
|
$field = explode( "\t", $line );
|
|
|
|
if ( !isset( $field[1] ) ) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->target[] = $field[0];
|
|
|
|
$this->replacement[] = $field[1];
|
|
|
|
$this->useRegex[] = isset( $field[2] ) ? true : false;
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function shouldContinueByDefault() {
|
|
|
|
if ( !is_bool( $this->defaultContinue ) ) {
|
|
|
|
$this->defaultContinue =
|
|
|
|
$this->getOption( "yes" ) ?
|
|
|
|
true :
|
|
|
|
false;
|
|
|
|
}
|
|
|
|
return $this->defaultContinue;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function getSummary() {
|
|
|
|
$msg = wfMessage( 'replacetext_editsummary' )->
|
|
|
|
rawParams( $this->target )->rawParams( $this->replacement );
|
|
|
|
if ( $this->getOption( "summary" ) !== null ) {
|
2017-06-20 07:20:41 +00:00
|
|
|
$msg = str_replace( [ '%f', '%r' ],
|
|
|
|
[ $this->target, $this->replacement ],
|
2014-10-06 02:39:42 +00:00
|
|
|
$this->getOption( "summary" ) );
|
|
|
|
}
|
|
|
|
return $msg;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function listNamespaces() {
|
|
|
|
echo "Index\tNamespace\n";
|
|
|
|
$nsList = MWNamespace::getCanonicalNamespaces();
|
|
|
|
ksort( $nsList );
|
|
|
|
foreach ( $nsList as $int => $val ) {
|
2016-05-30 21:35:28 +00:00
|
|
|
if ( $val == "" ) {
|
2014-10-06 02:39:42 +00:00
|
|
|
$val = "(main)";
|
|
|
|
}
|
|
|
|
echo " $int\t$val\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function showFileFormat() {
|
|
|
|
echo <<<EOF
|
|
|
|
|
|
|
|
The format of the replacements file is tab separated with three fields.
|
|
|
|
Any line that does not have a tab is ignored and can be considered a comment.
|
|
|
|
|
|
|
|
Fields are:
|
|
|
|
|
|
|
|
1. String to search for.
|
|
|
|
2. String to replace found text with.
|
|
|
|
3. (optional) The presence of this field indicates that the previous two
|
|
|
|
are considered a regular expression.
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
This is a comment
|
|
|
|
TARGET REPLACE
|
|
|
|
regex(p*) Count the Ps; \\1 true
|
|
|
|
|
|
|
|
|
|
|
|
EOF;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function getNamespaces() {
|
2016-12-08 22:20:17 +00:00
|
|
|
$nsall = $this->getOption( "nsall" );
|
|
|
|
$ns = $this->getOption( "ns" );
|
|
|
|
if ( !$nsall && !$ns ) {
|
|
|
|
$namespaces = [ NS_MAIN ];
|
|
|
|
} else {
|
|
|
|
$canonical = MWNamespace::getCanonicalNamespaces();
|
|
|
|
$canonical[NS_MAIN] = "_";
|
|
|
|
$namespaces = array_flip( $canonical );
|
|
|
|
if ( !$nsall ) {
|
|
|
|
$namespaces = array_map(
|
2017-06-20 07:20:41 +00:00
|
|
|
function ( $n ) use ( $canonical, $namespaces ) {
|
2016-12-08 22:20:17 +00:00
|
|
|
if ( is_numeric( $n ) ) {
|
|
|
|
if ( isset( $canonical[ $n ] ) ) {
|
|
|
|
return intval( $n );
|
2014-10-06 02:39:42 +00:00
|
|
|
}
|
2016-12-08 22:20:17 +00:00
|
|
|
} else {
|
|
|
|
if ( isset( $namespaces[ $n ] ) ) {
|
|
|
|
return $namespaces[ $n ];
|
2014-10-06 02:39:42 +00:00
|
|
|
}
|
2016-12-08 22:20:17 +00:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}, explode( ",", $ns ) );
|
|
|
|
$namespaces = array_filter(
|
|
|
|
$namespaces,
|
2017-06-20 07:20:41 +00:00
|
|
|
function ( $val ) {
|
2014-10-06 02:39:42 +00:00
|
|
|
return $val !== null;
|
2016-12-08 22:20:17 +00:00
|
|
|
} );
|
|
|
|
}
|
2014-10-06 02:39:42 +00:00
|
|
|
}
|
|
|
|
return $namespaces;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function getCategory() {
|
|
|
|
$cat = null;
|
|
|
|
return $cat;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function getPrefix() {
|
|
|
|
$prefix = null;
|
|
|
|
return $prefix;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function useRegex() {
|
2017-06-20 07:20:41 +00:00
|
|
|
return [ $this->getOption( "regex" ) ];
|
2014-10-06 02:39:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
protected function getTitles( $res ) {
|
2017-12-15 21:46:51 +00:00
|
|
|
if ( !$this->titles || count( $this->titles ) == 0 ) {
|
2017-06-20 07:20:41 +00:00
|
|
|
$this->titles = [];
|
2017-12-15 21:46:51 +00:00
|
|
|
foreach ( $res as $row ) {
|
2014-10-06 02:39:42 +00:00
|
|
|
$this->titles[] = Title::makeTitleSafe(
|
|
|
|
$row->page_namespace,
|
|
|
|
$row->page_title
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $this->titles;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function listTitles( $res ) {
|
|
|
|
$ret = false;
|
|
|
|
foreach ( $this->getTitles( $res ) as $title ) {
|
|
|
|
$ret = true;
|
|
|
|
echo "$title\n";
|
|
|
|
}
|
|
|
|
return $ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected function replaceTitles( $res, $target, $replacement, $useRegex ) {
|
|
|
|
foreach ( $this->getTitles( $res ) as $title ) {
|
2017-06-20 07:20:41 +00:00
|
|
|
$param = [
|
2014-10-06 02:39:42 +00:00
|
|
|
'target_str' => $target,
|
|
|
|
'replacement_str' => $replacement,
|
|
|
|
'use_regex' => $useRegex,
|
|
|
|
'user_id' => $this->user->getId(),
|
|
|
|
'edit_summary' => $this->summaryMsg,
|
2017-12-15 21:46:51 +00:00
|
|
|
'doAnnounce' => $this->doAnnounce
|
2017-06-20 07:20:41 +00:00
|
|
|
];
|
2014-10-06 02:39:42 +00:00
|
|
|
echo "Replacing on $title... ";
|
|
|
|
$job = new ReplaceTextJob( $title, $param, 0 );
|
|
|
|
if ( $job->run() !== true ) {
|
|
|
|
$this->error( "Trouble on the page '$title'." );
|
|
|
|
}
|
|
|
|
echo "done.\n";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected 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";
|
|
|
|
}
|
|
|
|
|
|
|
|
protected 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->summaryMsg = $this->getSummary();
|
|
|
|
$this->namespaces = $this->getNamespaces();
|
|
|
|
$this->category = $this->getCategory();
|
|
|
|
$this->prefix = $this->getPrefix();
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
public function execute() {
|
|
|
|
global $wgShowExceptionDetails;
|
|
|
|
$wgShowExceptionDetails = true;
|
|
|
|
|
2017-12-15 21:46:51 +00:00
|
|
|
$this->doAnnounce = true;
|
2014-10-06 02:39:42 +00:00
|
|
|
if ( $this->localSetup() ) {
|
2016-12-08 22:20:17 +00:00
|
|
|
if ( $this->namespaces === [] ) {
|
|
|
|
$this->error( "No matching namespaces.", true );
|
|
|
|
}
|
|
|
|
|
2014-10-06 02:39:42 +00:00
|
|
|
foreach ( array_keys( $this->target ) as $index ) {
|
|
|
|
$target = $this->target[$index];
|
|
|
|
$replacement = $this->replacement[$index];
|
|
|
|
$useRegex = $this->useRegex[$index];
|
|
|
|
|
|
|
|
if ( $this->getOption( "debug" ) ) {
|
|
|
|
echo "Replacing '$target' with '$replacement'";
|
|
|
|
if ( $useRegex ) {
|
|
|
|
echo " as regular expression.";
|
|
|
|
}
|
|
|
|
echo "\n";
|
|
|
|
}
|
|
|
|
$res = ReplaceTextSearch::doSearchQuery( $target,
|
2017-12-15 21:46:51 +00:00
|
|
|
$this->namespaces, $this->category, $this->prefix,
|
|
|
|
$useRegex );
|
2014-10-06 02:39:42 +00:00
|
|
|
|
|
|
|
if ( $res->numRows() === 0 ) {
|
|
|
|
$this->error( "No targets found to replace.", true );
|
|
|
|
}
|
2016-12-08 22:20:17 +00:00
|
|
|
if ( $this->getOption( "dry-run" ) ) {
|
|
|
|
$this->listTitles( $res );
|
|
|
|
return;
|
|
|
|
}
|
2017-12-15 21:46:51 +00:00
|
|
|
if ( !$this->shouldContinueByDefault() &&
|
|
|
|
$this->listTitles( $res ) ) {
|
|
|
|
if ( !$this->getReply(
|
|
|
|
"Replace instances on these pages?"
|
|
|
|
) ) {
|
2014-10-06 02:39:42 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$comment = "";
|
|
|
|
if ( $this->getOption( "user", null ) === null ) {
|
|
|
|
$comment = " (Use --user to override)";
|
|
|
|
}
|
2017-12-15 21:46:51 +00:00
|
|
|
if ( $this->getOption( "no-announce", false ) ) {
|
|
|
|
$this->doAnnounce = false;
|
|
|
|
}
|
|
|
|
if ( !$this->getReply(
|
|
|
|
"Attribute changes to the user '{$this->user}'?$comment"
|
|
|
|
) ) {
|
2014-10-06 02:39:42 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
if ( $res->numRows() > 0 ) {
|
2017-12-15 21:46:51 +00:00
|
|
|
$this->replaceTitles(
|
|
|
|
$res, $target, $replacement, $useRegex
|
|
|
|
);
|
2014-10-06 02:39:42 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$maintClass = "ReplaceText";
|
|
|
|
require_once RUN_MAINTENANCE_IF_MAIN;
|