MediaWiki REL1_32
replaceAll.php
Go to the documentation of this file.
1#!/usr/bin/php
2<?php
31// @codingStandardsIgnoreStart
32$IP = getenv( "MW_INSTALL_PATH" ) ? 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->mDescription = "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";
189 $nsList = MWNamespace::getCanonicalNamespaces();
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 {
228 $canonical = MWNamespace::getCanonicalNamespaces();
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 ) {
274 $newTitle = ReplaceTextSearch::getReplacedTitle( $title, $target, $replacement, $regex );
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;
to move a page</td >< td > &*You are moving the page across namespaces
$wgShowExceptionDetails
If set to true, uncaught exceptions will print the exception message and a complete stack trace to ou...
$line
Definition cdb.php:59
MediaWiki exception.
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
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.
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()
@inheritDoc
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)
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:592
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition User.php:615
The ContentHandler facility adds support for arbitrary content types on wiki instead of relying on wikitext for everything It was introduced in MediaWiki Each kind of and so on Built in content types are
$res
Definition database.txt:21
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title e g db for database replication lag or jobqueue for job queue size converted to pseudo seconds It is possible to add more fields and they will be returned to the user in the API response after the basic globals have been set but before ordinary actions take place replace
Definition hooks.txt:2317
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults error
Definition hooks.txt:2683
either a plain
Definition hooks.txt:2105
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition hooks.txt:2055
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition hooks.txt:2054
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt;div ...>$1&lt;/div>"). - flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException':Called before an exception(or PHP error) is logged. This is meant for integration with external error aggregation services
const NS_MAIN
Definition Defines.php:64
$IP
$maintClass
require_once RUN_MAINTENANCE_IF_MAIN
if(count( $args)< 1) $job
$params