MediaWiki REL1_34
replaceAll.php
Go to the documentation of this file.
1#!/usr/bin/php
2<?php
31// @codingStandardsIgnoreStart
32$IP = getenv( "MW_INSTALL_PATH" ) ?: __DIR__ . "/../../..";
33if ( !is_readable( "$IP/maintenance/Maintenance.php" ) ) {
34 die( "MW_INSTALL_PATH needs to be set to your MediaWiki installation.\n" );
35}
36require_once ( "$IP/maintenance/Maintenance.php" );
37// @codingStandardsIgnoreEnd
38
46class ReplaceAll extends Maintenance {
47 private $user;
48 private $target;
49 private $replacement;
50 private $namespaces;
51 private $category;
52 private $prefix;
53 private $useRegex;
54 private $titles;
56 private $doAnnounce;
57 private $rename;
58
59 public function __construct() {
60 parent::__construct();
61 $this->addDescription( "CLI utility to replace text wherever it is " .
62 "found in the wiki." );
63
64 $this->addArg( "target", "Target text to find.", false );
65 $this->addArg( "replace", "Text to replace.", false );
66
67 $this->addOption( "dry-run", "Only find the texts, don't replace.",
68 false, false, 'n' );
69 $this->addOption( "regex", "This is a regex (false).",
70 false, false, 'r' );
71 $this->addOption( "user", "The user to attribute this to (uid 1).",
72 false, true, 'u' );
73 $this->addOption( "yes", "Skip all prompts with an assumed 'yes'.",
74 false, false, 'y' );
75 $this->addOption( "summary", "Alternate edit summary. (%r is where to " .
76 " place the replacement text, %f the text to look for.)",
77 false, true, 's' );
78 $this->addOption( "nsall", "Search all canonical namespaces (false). " .
79 "If true, this option overrides the ns option.", false, false, 'a' );
80 $this->addOption( "ns", "Comma separated namespaces to search in " .
81 "(Main) .", false, true );
82 $this->addOption( "replacements", "File containing the list of " .
83 "replacements to be made. Fields in the file are tab-separated. " .
84 "See --show-file-format for more information.", false, true, "f" );
85 $this->addOption( "show-file-format", "Show a description of the " .
86 "file format to use with --replacements.", false, false );
87 $this->addOption( "no-announce", "Do not announce edits on Special:RecentChanges or " .
88 "watchlists.", false, false, "m" );
89 $this->addOption( "debug", "Display replacements being made.", false, false );
90 $this->addOption( "listns", "List out the namespaces on this wiki.",
91 false, false );
92 $this->addOption( 'rename', "Rename page titles instead of replacing contents.",
93 false, false );
94
95 // MW 1.28
96 if ( method_exists( $this, 'requireExtension' ) ) {
97 $this->requireExtension( 'Replace Text' );
98 }
99 }
100
101 private function getUser() {
102 $userReplacing = $this->getOption( "user", 1 );
103
104 $user = is_numeric( $userReplacing ) ?
105 User::newFromId( $userReplacing ) :
106 User::newFromName( $userReplacing );
107
108 if ( get_class( $user ) !== 'User' ) {
109 $this->error(
110 "Couldn't translate '$userReplacing' to a user.", true
111 );
112 }
113
114 return $user;
115 }
116
117 private function getTarget() {
118 $ret = $this->getArg( 0 );
119 if ( !$ret ) {
120 $this->error( "You have to specify a target.", true );
121 }
122 return [ $ret ];
123 }
124
125 private function getReplacement() {
126 $ret = $this->getArg( 1 );
127 if ( !$ret ) {
128 $this->error( "You have to specify replacement text.", true );
129 }
130 return [ $ret ];
131 }
132
133 private function getReplacements() {
134 $file = $this->getOption( "replacements" );
135 if ( !$file ) {
136 return false;
137 }
138
139 if ( !is_readable( $file ) ) {
140 throw new MWException( "File does not exist or is not readable: "
141 . "$file\n" );
142 }
143
144 $handle = fopen( $file, "r" );
145 if ( $handle === false ) {
146 throw new MWException( "Trouble opening file: $file\n" );
147 return false;
148 }
149
150 $this->defaultContinue = true;
151 // @codingStandardsIgnoreStart
152 while ( ( $line = fgets( $handle ) ) !== false ) {
153 // @codingStandardsIgnoreEnd
154 $field = explode( "\t", substr( $line, 0, -1 ) );
155 if ( !isset( $field[1] ) ) {
156 continue;
157 }
158
159 $this->target[] = $field[0];
160 $this->replacement[] = $field[1];
161 $this->useRegex[] = isset( $field[2] ) ? true : false;
162 }
163 return true;
164 }
165
166 private function shouldContinueByDefault() {
167 if ( !is_bool( $this->defaultContinue ) ) {
168 $this->defaultContinue =
169 $this->getOption( "yes" ) ?
170 true :
171 false;
172 }
174 }
175
176 private function getSummary( $target, $replacement ) {
177 $msg = wfMessage( 'replacetext_editsummary', $target, $replacement )->
178 plain();
179 if ( $this->getOption( "summary" ) !== null ) {
180 $msg = str_replace( [ '%f', '%r' ],
181 [ $this->target, $this->replacement ],
182 $this->getOption( "summary" ) );
183 }
184 return $msg;
185 }
186
187 private function listNamespaces() {
188 echo "Index\tNamespace\n";
190 ksort( $nsList );
191 foreach ( $nsList as $int => $val ) {
192 if ( $val == "" ) {
193 $val = "(main)";
194 }
195 echo " $int\t$val\n";
196 }
197 }
198
199 private function showFileFormat() {
200echo <<<EOF
201
202The format of the replacements file is tab separated with three fields.
203Any line that does not have a tab is ignored and can be considered a comment.
204
205Fields are:
206
207 1. String to search for.
208 2. String to replace found text with.
209 3. (optional) The presence of this field indicates that the previous two
210 are considered a regular expression.
211
212Example:
213
214This is a comment
215TARGET REPLACE
216regex(p*) Count the Ps; \\1 true
217
218
219EOF;
220 }
221
222 private function getNamespaces() {
223 $nsall = $this->getOption( "nsall" );
224 $ns = $this->getOption( "ns" );
225 if ( !$nsall && !$ns ) {
226 $namespaces = [ NS_MAIN ];
227 } else {
229 $canonical[NS_MAIN] = "_";
230 $namespaces = array_flip( $canonical );
231 if ( !$nsall ) {
232 $namespaces = array_map(
233 function ( $n ) use ( $canonical, $namespaces ) {
234 if ( is_numeric( $n ) ) {
235 if ( isset( $canonical[ $n ] ) ) {
236 return intval( $n );
237 }
238 } else {
239 if ( isset( $namespaces[ $n ] ) ) {
240 return $namespaces[ $n ];
241 }
242 }
243 return null;
244 }, explode( ",", $ns ) );
245 $namespaces = array_filter(
247 function ( $val ) {
248 return $val !== null;
249 } );
250 }
251 }
252 return $namespaces;
253 }
254
255 private function getCategory() {
256 return null;
257 }
258
259 private function getPrefix() {
260 return null;
261 }
262
263 private function useRegex() {
264 return [ $this->getOption( "regex" ) ];
265 }
266
267 private function getRename() {
268 return $this->hasOption( 'rename' );
269 }
270
271 private function listTitles( $titles, $target, $replacement, $regex, $rename ) {
272 foreach ( $titles as $title ) {
273 if ( $rename ) {
275 // Implicit conversion of objects to strings
276 $this->output( "$title -> $newTitle\n" );
277 } else {
278 echo "$title\n";
279 }
280 }
281 }
282
284 foreach ( $titles as $title ) {
285 $params = [
286 'target_str' => $target,
287 'replacement_str' => $replacement,
288 'use_regex' => $useRegex,
289 'user_id' => $this->user->getId(),
290 'edit_summary' => $this->getSummary( $target, $replacement ),
291 'doAnnounce' => $this->doAnnounce
292 ];
293
294 if ( $rename ) {
295 $params[ 'move_page' ] = true;
296 $params[ 'create_redirect' ] = false;
297 $params[ 'watch_page' ] = false;
298 }
299
300 echo "Replacing on $title... ";
301 $job = new ReplaceTextJob( $title, $params );
302 if ( $job->run() !== true ) {
303 $this->error( "Trouble on the page '$title'." );
304 }
305 echo "done.\n";
306 }
307 }
308
309 private function getReply( $question ) {
310 $reply = "";
311 if ( $this->shouldContinueByDefault() ) {
312 return true;
313 }
314 while ( $reply !== "y" && $reply !== "n" ) {
315 $reply = $this->readconsole( "$question (Y/N) " );
316 $reply = substr( strtolower( $reply ), 0, 1 );
317 }
318 return $reply === "y";
319 }
320
321 private function localSetup() {
322 if ( $this->getOption( "listns" ) ) {
323 $this->listNamespaces();
324 return false;
325 }
326 if ( $this->getOption( "show-file-format" ) ) {
327 $this->showFileFormat();
328 return false;
329 }
330 $this->user = $this->getUser();
331 if ( !$this->getReplacements() ) {
332 $this->target = $this->getTarget();
333 $this->replacement = $this->getReplacement();
334 $this->useRegex = $this->useRegex();
335 }
336 $this->namespaces = $this->getNamespaces();
337 $this->category = $this->getCategory();
338 $this->prefix = $this->getPrefix();
339 $this->rename = $this->getRename();
340
341 return true;
342 }
343
347 public function execute() {
350
351 $this->doAnnounce = true;
352 if ( !$this->localSetup() ) {
353 return;
354 }
355
356 if ( $this->namespaces === [] ) {
357 $this->error( "No matching namespaces.", true );
358 }
359
360 foreach ( array_keys( $this->target ) as $index ) {
361 $target = $this->target[$index];
362 $replacement = $this->replacement[$index];
363 $useRegex = $this->useRegex[$index];
364
365 if ( $this->getOption( "debug" ) ) {
366 echo "Replacing '$target' with '$replacement'";
367 if ( $useRegex ) {
368 echo " as regular expression.";
369 }
370 echo "\n";
371 }
372
373 if ( $this->rename ) {
375 $target,
376 $this->namespaces,
377 $this->category,
378 $this->prefix,
380 );
381 } else {
383 $target,
384 $this->namespaces,
385 $this->category,
386 $this->prefix,
388 );
389 }
390
392
393 if ( count( $titles ) === 0 ) {
394 $this->error( 'No targets found to replace.', true );
395 }
396
397 if ( $this->getOption( "dry-run" ) ) {
398 $this->listTitles( $titles, $target, $replacement, $useRegex, $this->rename );
399 continue;
400 }
401
402 if (
403 !$this->shouldContinueByDefault() &&
404 $this->listTitles( $titles, $target, $replacement, $useRegex, $this->rename )
405 ) {
406 if ( !$this->getReply( 'Replace instances on these pages?' ) ) {
407 return;
408 }
409 }
410
411 $comment = "";
412 if ( $this->getOption( "user", null ) === null ) {
413 $comment = " (Use --user to override)";
414 }
415 if ( $this->getOption( "no-announce", false ) ) {
416 $this->doAnnounce = false;
417 }
418 if ( !$this->getReply(
419 "Attribute changes to the user '{$this->user}'?$comment"
420 ) ) {
421 return;
422 }
423
424 $this->replaceTitles(
425 $titles, $target, $replacement, $useRegex, $this->rename
426 );
427 }
428 }
429}
430
431$maintClass = "ReplaceAll";
432require_once RUN_MAINTENANCE_IF_MAIN;
$wgShowExceptionDetails
If set to true, uncaught exceptions will print the exception message and a complete stack trace to ou...
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
const RUN_MAINTENANCE_IF_MAIN
$line
Definition cdb.php:59
MediaWiki exception.
static getCanonicalNamespaces()
Returns array of all defined namespaces with their canonical (English) names.
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
error( $err, $die=0)
Throw an error to the user.
addArg( $arg, $description, $required=true)
Add some args that are needed.
requireExtension( $name)
Indicate that the specified extension must be loaded before the script can run.
output( $out, $channel=null)
Throw some output to the user.
hasOption( $name)
Checks to see if a particular option exists.
static readconsole( $prompt='> ')
Prompt the console for input.
getArg( $argId=0, $default=null)
Get an argument.
addDescription( $text)
Set the description text.
addOption( $name, $description, $required=false, $withArg=false, $shortName=false, $multiOccurrence=false)
Add a parameter to the script.
getOption( $name, $default=null)
Get an option, or return the default.
Maintenance script that replaces text in pages.
listTitles( $titles, $target, $replacement, $regex, $rename)
__construct()
Default constructor.
getSummary( $target, $replacement)
execute()
Do the actual work.All child classes will need to implement thisbool|null|void True for success,...
shouldContinueByDefault()
replaceTitles( $titles, $target, $replacement, $useRegex, $rename)
getReply( $question)
Background job to replace text in a given page.
static doSearchQuery( $search, $namespaces, $category, $prefix, $use_regex=false)
static getReplacedTitle(Title $title, $search, $replacement, $regex)
Do a replacement on a title.
static getMatchingTitles( $str, $namespaces, $category, $prefix, $use_regex=false)
const NS_MAIN
Definition Defines.php:69
$IP
$maintClass
if(count( $args)< 1) $job
return true
Definition router.php:94
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42