MediaWiki REL1_31
replaceAll.php
Go to the documentation of this file.
1#!/usr/bin/php
2<?php
32// @codingStandardsIgnoreStart
33$IP = getenv( "MW_INSTALL_PATH" ) ? getenv( "MW_INSTALL_PATH" ) : __DIR__ . "/../../..";
34if ( !is_readable( "$IP/maintenance/Maintenance.php" ) ) {
35 die( "MW_INSTALL_PATH needs to be set to your MediaWiki installation.\n" );
36}
37require_once ( "$IP/maintenance/Maintenance.php" );
38// @codingStandardsIgnoreEnd
39
47class ReplaceAll extends Maintenance {
48 private $user;
49 private $target;
50 private $replacement;
51 private $namespaces;
52 private $category;
53 private $prefix;
54 private $useRegex;
55 private $titles;
57 private $doAnnounce;
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
93 // MW 1.28
94 if ( method_exists( $this, 'requireExtension' ) ) {
95 $this->requireExtension( 'Replace Text' );
96 }
97 }
98
99 private function getUser() {
100 $userReplacing = $this->getOption( "user", 1 );
101
102 $user = is_numeric( $userReplacing ) ?
103 User::newFromId( $userReplacing ) :
104 User::newFromName( $userReplacing );
105
106 if ( get_class( $user ) !== 'User' ) {
107 $this->error(
108 "Couldn't translate '$userReplacing' to a user.", true
109 );
110 }
111
112 return $user;
113 }
114
115 private function getTarget() {
116 $ret = $this->getArg( 0 );
117 if ( !$ret ) {
118 $this->error( "You have to specify a target.", true );
119 }
120 return [ $ret ];
121 }
122
123 private function getReplacement() {
124 $ret = $this->getArg( 1 );
125 if ( !$ret ) {
126 $this->error( "You have to specify replacement text.", true );
127 }
128 return [ $ret ];
129 }
130
131 private function getReplacements() {
132 $file = $this->getOption( "replacements" );
133 if ( !$file ) {
134 return false;
135 }
136
137 if ( !is_readable( $file ) ) {
138 throw new MWException( "File does not exist or is not readable: "
139 . "$file\n" );
140 }
141
142 $handle = fopen( $file, "r" );
143 if ( $handle === false ) {
144 throw new MWException( "Trouble opening file: $file\n" );
145 return false;
146 }
147
148 $this->defaultContinue = true;
149 // @codingStandardsIgnoreStart
150 while ( ( $line = fgets( $handle ) ) !== false ) {
151 // @codingStandardsIgnoreEnd
152 $field = explode( "\t", substr( $line, 0, -1 ) );
153 if ( !isset( $field[1] ) ) {
154 continue;
155 }
156
157 $this->target[] = $field[0];
158 $this->replacement[] = $field[1];
159 $this->useRegex[] = isset( $field[2] ) ? true : false;
160 }
161 return true;
162 }
163
164 private function shouldContinueByDefault() {
165 if ( !is_bool( $this->defaultContinue ) ) {
166 $this->defaultContinue =
167 $this->getOption( "yes" ) ?
168 true :
169 false;
170 }
172 }
173
174 private function getSummary( $target, $replacement ) {
175 $msg = wfMessage( 'replacetext_editsummary', $target, $replacement )->
176 plain();
177 if ( $this->getOption( "summary" ) !== null ) {
178 $msg = str_replace( [ '%f', '%r' ],
179 [ $this->target, $this->replacement ],
180 $this->getOption( "summary" ) );
181 }
182 return $msg;
183 }
184
185 private function listNamespaces() {
186 echo "Index\tNamespace\n";
187 $nsList = MWNamespace::getCanonicalNamespaces();
188 ksort( $nsList );
189 foreach ( $nsList as $int => $val ) {
190 if ( $val == "" ) {
191 $val = "(main)";
192 }
193 echo " $int\t$val\n";
194 }
195 }
196
197 private function showFileFormat() {
198echo <<<EOF
199
200The format of the replacements file is tab separated with three fields.
201Any line that does not have a tab is ignored and can be considered a comment.
202
203Fields are:
204
205 1. String to search for.
206 2. String to replace found text with.
207 3. (optional) The presence of this field indicates that the previous two
208 are considered a regular expression.
209
210Example:
211
212This is a comment
213TARGET REPLACE
214regex(p*) Count the Ps; \\1 true
215
216
217EOF;
218 }
219
220 private function getNamespaces() {
221 $nsall = $this->getOption( "nsall" );
222 $ns = $this->getOption( "ns" );
223 if ( !$nsall && !$ns ) {
224 $namespaces = [ NS_MAIN ];
225 } else {
226 $canonical = MWNamespace::getCanonicalNamespaces();
227 $canonical[NS_MAIN] = "_";
228 $namespaces = array_flip( $canonical );
229 if ( !$nsall ) {
230 $namespaces = array_map(
231 function ( $n ) use ( $canonical, $namespaces ) {
232 if ( is_numeric( $n ) ) {
233 if ( isset( $canonical[ $n ] ) ) {
234 return intval( $n );
235 }
236 } else {
237 if ( isset( $namespaces[ $n ] ) ) {
238 return $namespaces[ $n ];
239 }
240 }
241 return null;
242 }, explode( ",", $ns ) );
243 $namespaces = array_filter(
245 function ( $val ) {
246 return $val !== null;
247 } );
248 }
249 }
250 return $namespaces;
251 }
252
253 private function getCategory() {
254 $cat = null;
255 return $cat;
256 }
257
258 private function getPrefix() {
259 $prefix = null;
260 return $prefix;
261 }
262
263 private function useRegex() {
264 return [ $this->getOption( "regex" ) ];
265 }
266
267 private function getTitles( $res ) {
268 if ( !$this->titles || count( $this->titles ) == 0 ) {
269 $this->titles = [];
270 foreach ( $res as $row ) {
271 $this->titles[] = Title::makeTitleSafe(
272 $row->page_namespace,
273 $row->page_title
274 );
275 }
276 }
277 return $this->titles;
278 }
279
280 private function listTitles( $res ) {
281 $ret = false;
282 foreach ( $this->getTitles( $res ) as $title ) {
283 $ret = true;
284 echo "$title\n";
285 }
286 return $ret;
287 }
288
290 foreach ( $this->getTitles( $res ) as $title ) {
291 $param = [
292 'target_str' => $target,
293 'replacement_str' => $replacement,
294 'use_regex' => $useRegex,
295 'user_id' => $this->user->getId(),
296 'edit_summary' => $this->getSummary( $target, $replacement ),
297 'doAnnounce' => $this->doAnnounce
298 ];
299 echo "Replacing on $title... ";
300 $job = new ReplaceTextJob( $title, $param );
301 if ( $job->run() !== true ) {
302 $this->error( "Trouble on the page '$title'." );
303 }
304 echo "done.\n";
305 }
306 }
307
308 private function getReply( $question ) {
309 $reply = "";
310 if ( $this->shouldContinueByDefault() ) {
311 return true;
312 }
313 while ( $reply !== "y" && $reply !== "n" ) {
314 $reply = $this->readconsole( "$question (Y/N) " );
315 $reply = substr( strtolower( $reply ), 0, 1 );
316 }
317 return $reply === "y";
318 }
319
320 private function localSetup() {
321 if ( $this->getOption( "listns" ) ) {
322 $this->listNamespaces();
323 return false;
324 }
325 if ( $this->getOption( "show-file-format" ) ) {
326 $this->showFileFormat();
327 return false;
328 }
329 $this->user = $this->getUser();
330 if ( ! $this->getReplacements() ) {
331 $this->target = $this->getTarget();
332 $this->replacement = $this->getReplacement();
333 $this->useRegex = $this->useRegex();
334 }
335 $this->namespaces = $this->getNamespaces();
336 $this->category = $this->getCategory();
337 $this->prefix = $this->getPrefix();
338 return true;
339 }
340
344 public function execute() {
347
348 $this->doAnnounce = true;
349 if ( $this->localSetup() ) {
350 if ( $this->namespaces === [] ) {
351 $this->error( "No matching namespaces.", true );
352 }
353
354 foreach ( array_keys( $this->target ) as $index ) {
355 $target = $this->target[$index];
356 $replacement = $this->replacement[$index];
357 $useRegex = $this->useRegex[$index];
358
359 if ( $this->getOption( "debug" ) ) {
360 echo "Replacing '$target' with '$replacement'";
361 if ( $useRegex ) {
362 echo " as regular expression.";
363 }
364 echo "\n";
365 }
367 $this->namespaces, $this->category, $this->prefix,
368 $useRegex );
369
370 if ( $res->numRows() === 0 ) {
371 $this->error( "No targets found to replace.", true );
372 }
373 if ( $this->getOption( "dry-run" ) ) {
374 $this->listTitles( $res );
375 return;
376 }
377 if ( !$this->shouldContinueByDefault() &&
378 $this->listTitles( $res ) ) {
379 if ( !$this->getReply(
380 "Replace instances on these pages?"
381 ) ) {
382 return;
383 }
384 }
385 $comment = "";
386 if ( $this->getOption( "user", null ) === null ) {
387 $comment = " (Use --user to override)";
388 }
389 if ( $this->getOption( "no-announce", false ) ) {
390 $this->doAnnounce = false;
391 }
392 if ( !$this->getReply(
393 "Attribute changes to the user '{$this->user}'?$comment"
394 ) ) {
395 return;
396 }
397 if ( $res->numRows() > 0 ) {
398 $this->replaceTitles(
400 );
401 }
402 }
403 }
404 }
405}
406
407$maintClass = "ReplaceAll";
408require_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 a complete stack trace to output.
$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.
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.
__construct()
Default constructor.
getSummary( $target, $replacement)
listTitles( $res)
execute()
@inheritDoc
shouldContinueByDefault()
getTitles( $res)
getReply( $question)
replaceTitles( $res, $target, $replacement, $useRegex)
Background job to replace text in a given page.
static doSearchQuery( $search, $namespaces, $category, $prefix, $use_regex=false)
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:591
static newFromId( $id)
Static factory method for creation from a given user ID.
Definition User.php:614
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
I won t presume to tell you how to I m just describing the methods I chose to use for myself If you do choose to follow these it will probably be easier for you to collaborate with others on the but if you want to contribute without by all means do which work well I also use K &R brace matching style I know that s a religious issue for so if you want to use a style that puts opening braces on the next line
Definition design.txt:80
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:2612
either a plain
Definition hooks.txt:2056
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 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
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:2006
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:2005
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title after the basic globals have been set but before ordinary actions take place replace
Definition hooks.txt:2255
const NS_MAIN
Definition Defines.php:74
$IP
$maintClass
require_once RUN_MAINTENANCE_IF_MAIN
if(count( $args)< 1) $job