28require_once __DIR__ .
'/Maintenance.php';
48 private $reason =
'Uppercasing title for Unicode upgrade';
69 parent::__construct();
71 "Rename titles when changing behavior of Language::ucfirst().\n"
73 .
"This script skips User and User_talk pages for registered users, as renaming of users "
74 .
"is too complex to try to implement here. Use something like Extension:Renameuser to "
75 .
"clean those up; this script can provide a list of user names affected."
78 'charmap',
'Character map generated by maintenance/language/generateUcfirstOverrides.php',
82 'user',
'System user to use to do the renames. Default is "Maintenance script".',
false,
true
86 'If the username specified by --user exists, specify this to force conversion to a system user.'
89 'run',
'If not specified, the script will not actually perform any moves (i.e. it will dry-run).'
92 'prefix',
'When the new title already exists, add this prefix.',
false,
true
95 'suffix',
'When the new title already exists, add this suffix.',
false,
true
97 $this->
addOption(
'reason',
'Reason to use when moving pages.',
false,
true );
98 $this->
addOption(
'tag',
'Change tag to apply when moving pages.',
false,
true );
99 $this->
addOption(
'tables',
'Comma-separated list of database tables to process.',
false,
true );
101 'userlist',
'Filename to which to output usernames needing rename.',
false,
true
107 $this->run = $this->
getOption(
'run',
false );
110 $username = $this->
getOption(
'user',
'Maintenance script' );
111 $steal = $this->
getOption(
'steal',
false );
112 $this->user = User::newSystemUser( $username, [
'steal' => $steal ] );
113 if ( !$this->user ) {
114 $user = User::newFromName( $username );
116 $this->
fatalError(
"User $username already exists.\n"
117 .
"Use --steal if you really want to steal it from the human who currently owns it."
120 $this->
fatalError(
"Could not obtain system user $username." );
126 $this->tables = explode(
',',
$tables );
132 if ( !
$title || substr(
$title->getDBkey(), -1 ) !==
'X' ) {
136 $this->
fatalError(
'Invalid --prefix. It must not be in namespace 0 and must not be external' );
138 $this->prefixNs =
$title->getNamespace();
139 $this->prefix = substr(
$title->getText(), 0, -1 );
141 $this->suffix = $this->
getOption(
'suffix' );
144 $this->tags = (array)$this->
getOption(
'tag',
null );
146 $charmapFile = $this->
getOption(
'charmap' );
147 if ( !file_exists( $charmapFile ) ) {
148 $this->
fatalError(
"Charmap file $charmapFile does not exist." );
150 if ( !is_file( $charmapFile ) || !is_readable( $charmapFile ) ) {
151 $this->
fatalError(
"Charmap file $charmapFile is not readable." );
153 $this->charmap = require $charmapFile;
154 if ( !is_array( $this->charmap ) ) {
155 $this->
fatalError(
"Charmap file $charmapFile did not return a PHP array." );
157 $this->charmap = array_filter(
159 function ( $v, $k ) {
160 if ( mb_strlen( $k ) !== 1 ) {
161 $this->
error(
"Ignoring mapping from multi-character key '$k' to '$v'" );
166 ARRAY_FILTER_USE_BOTH
168 if ( !$this->charmap ) {
169 $this->
fatalError(
"Charmap file $charmapFile did not contain any usable character mappings." );
173 $this->
processTable( $db,
true,
'page',
'page_namespace',
'page_title', [
'page_id' ] );
176 $db,
false,
'archive',
'ar_namespace',
'ar_title', [
'ar_timestamp',
'ar_id' ]
178 $this->
processTable( $db,
false,
'filearchive',
NS_FILE,
'fa_name', [
'fa_timestamp',
'fa_id' ] );
179 $this->
processTable( $db,
false,
'logging',
'log_namespace',
'log_title', [
'log_id' ] );
180 $this->
processTable( $db,
false,
'redirect',
'rd_namespace',
'rd_title', [
'rd_from' ] );
181 $this->
processTable( $db,
false,
'protected_titles',
'pt_namespace',
'pt_title', [] );
195 foreach ( $this->charmap as $from => $to ) {
197 if ( count( $likes ) >= $batchSize ) {
198 $ret[] = $db->
makeList( $likes, $db::LIST_OR );
203 $ret[] = $db->
makeList( $likes, $db::LIST_OR );
217 if ( $this->namespaces ===
null ) {
218 $nsinfo = MediaWikiServices::getInstance()->getNamespaceInfo();
219 $this->namespaces = array_filter(
220 array_keys( $nsinfo->getCanonicalNamespaces() ),
221 function ( $ns ) use ( $nsinfo ) {
222 return $nsinfo->isMovable( $ns ) && $nsinfo->isCapitalized( $ns );
225 usort( $this->namespaces,
function ( $ns1, $ns2 ) use ( $nsinfo ) {
226 if ( $ns1 === $ns2 ) {
230 $s1 = $nsinfo->getSubject( $ns1 );
231 $s2 = $nsinfo->getSubject( $ns2 );
235 return $s1 < $s2 ? -1 : 1;
239 if ( $s1 === $ns1 ) {
242 if ( $s2 === $ns2 ) {
268 if ( !isset( $this->seenUsers[
$base] ) ) {
273 [
'user_name' => strtr(
$base,
'_',
' ' ) ],
277 return $this->seenUsers[
$base];
292 $munge =
'Target title\'s user exists';
294 $mp =
new MovePage( $oldTitle, $newTitle );
295 $status = $mp->isValidMove();
296 if ( !$status->isOK() && $status->hasMessage(
'articleexists' ) ) {
297 $munge =
'Target title exists';
304 if ( $this->prefix !==
null ) {
305 $newTitle = Title::makeTitle(
309 } elseif ( $this->suffix !==
null ) {
310 $newTitle = Title::makeTitle( $newTitle->
getNamespace(), $newTitle->
getText() . $this->suffix );
313 "Cannot move {$oldTitle->getPrefixedText()} → $nt: "
314 .
"$munge and no --prefix or --suffix was given"
321 "Cannot move {$oldTitle->getPrefixedText()} → $nt: "
322 .
"$munge and munged title '{$newTitle->getPrefixedText()}' is not valid"
326 if ( $newTitle->
exists() ) {
328 "Cannot move {$oldTitle->getPrefixedText()} → $nt: "
329 .
"$munge and munged title '{$newTitle->getPrefixedText()}' also exists"
345 $char = mb_substr(
$title, 0, 1 );
346 if ( !array_key_exists( $char, $this->charmap ) ) {
348 "Query returned NS$ns $title, which does not begin with a character in the charmap."
354 $this->
output(
"... Skipping user page NS$ns $title\n" );
358 $oldTitle = Title::makeTitle( $ns,
$title );
359 $newTitle = Title::makeTitle( $ns, $this->charmap[$char] . mb_substr(
$title, 1 ) );
360 if ( !$this->
mungeTitle( $db, $oldTitle, $newTitle ) ) {
364 $mp =
new MovePage( $oldTitle, $newTitle );
365 $status = $mp->isValidMove();
366 if ( !$status->isOK() ) {
368 "Invalid move {$oldTitle->getPrefixedText()} → {$newTitle->getPrefixedText()}: "
369 . $status->getMessage(
false,
false,
'en' )->useDatabase(
false )->plain()
376 "Would rename {$oldTitle->getPrefixedText()} → {$newTitle->getPrefixedText()}\n"
381 $status = $mp->move( $this->user, $this->reason,
false, $this->tags );
382 if ( !$status->isOK() ) {
384 "Move {$oldTitle->getPrefixedText()} → {$newTitle->getPrefixedText()} failed: "
385 . $status->getMessage(
false,
false,
'en' )->useDatabase(
false )->plain()
388 return $status->isOK();
400 $ns = is_int( $nsField ) ? $nsField : (int)$row->$nsField;
401 $title = $row->$titleField;
403 $char = mb_substr(
$title, 0, 1 );
404 if ( !array_key_exists( $char, $this->charmap ) ) {
405 $r = json_encode( $row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
407 "Query returned $r, but title does not begin with a character in the charmap."
413 $this->
output(
"... Skipping user page NS$ns $title\n" );
417 $oldTitle = Title::makeTitle( $ns,
$title );
418 $newTitle = Title::makeTitle( $ns, $this->charmap[$char] . mb_substr(
$title, 1 ) );
419 if ( !$this->
mungeTitle( $db, $oldTitle, $newTitle ) ) {
427 is_int( $nsField ) ? [] : [ $nsField => $newTitle->getNamespace() ],
428 [ $titleField => $newTitle->getDBkey() ]
434 $r = json_encode( $row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
435 $this->
output(
"Would set $r to {$newTitle->getPrefixedText()}\n" );
452 if ( $this->tables !==
null && !in_array( $table, $this->tables,
true ) ) {
453 $this->
output(
"Skipping table `$table`, not in --tables.\n" );
460 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
462 if ( is_int( $nsField ) ) {
467 $this->
output(
"Skipping table `$table`, no valid namespaces.\n" );
471 $this->
output(
"Processing table `$table`...\n" );
473 $selectFields = array_merge(
474 is_int( $nsField ) ? [] : [ $nsField ],
478 $contFields = array_reverse( array_merge( [ $titleField ], $pkFields ) );
484 foreach ( $likes as $like ) {
490 array_merge( [
"$nsField = $ns", $like ], $cont ),
492 [
'ORDER BY' => array_merge( [ $titleField ], $pkFields ),
'LIMIT' => $batchSize ]
495 foreach (
$res as $row ) {
497 foreach ( $contFields as $field ) {
499 if ( $cont ===
'' ) {
500 $cont =
"$field > $v";
502 $cont =
"$field > $v OR $field = $v AND ($cont)";
508 $ns = is_int( $nsField ) ? $nsField : (int)$row->$nsField;
509 $ret = $this->
doMove( $db, $ns, $row->$titleField );
511 $ret = $this->
doUpdate( $db, $table, $nsField, $titleField, $row );
513 if ( $ret ===
true ) {
515 } elseif ( $ret ===
false ) {
521 $r = $cont ? json_encode( $row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) :
'<end>';
522 $this->
output(
"... $table: $count renames, $errors errors at $r\n" );
523 $lbFactory->waitForReplication(
532 $this->
output(
"Done processing table `$table`.\n" );
540 $userlistFile = $this->
getOption(
'userlist' );
541 if ( $userlistFile ===
null ) {
542 $this->
output(
"Not generating user list, --userlist was not specified.\n" );
546 $fh = fopen( $userlistFile,
'wb' );
548 $this->
error(
"Could not open user list file $userlistFile" );
552 $this->
output(
"Generating user list...\n" );
561 array_merge( [ $like ], $cont ),
563 [
'ORDER BY' =>
'user_name',
'LIMIT' => $batchSize ]
569 $last = end( $names );
571 foreach ( $names as $name ) {
572 $char = mb_substr( $name, 0, 1 );
573 if ( !array_key_exists( $char, $this->charmap ) ) {
575 "Query returned $name, but user name does not begin with a character in the charmap."
579 $newName = $this->charmap[$char] . mb_substr( $name, 1 );
580 fprintf(
$fh,
"%s\t%s\n", $name, $newName );
583 $this->
output(
"... at $last, $count names so far\n" );
587 if ( !fclose(
$fh ) ) {
588 $this->
error(
"fclose on $userlistFile failed" );
590 $this->
output(
"User list output to $userlistFile, $count users need renaming.\n" );
const RUN_MAINTENANCE_IF_MAIN
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
error( $err, $die=0)
Throw an error to the user.
output( $out, $channel=null)
Throw some output to the user.
float $lastReplicationWait
UNIX timestamp.
getBatchSize()
Returns batch size.
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.
setBatchSize( $s=0)
Set the batch size.
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.
Handles the backend logic of moving a page from one title to another.
Represents a title within MediaWiki.
getNamespace()
Get the namespace index, i.e.
exists( $flags=0)
Check if page exists.
getText()
Get the text form (spaces not underscores) of the main part.
isValid()
Returns true if the title is valid, false if it is invalid.
getPrefixedText()
Get the prefixed title with spaces.
Maintenance script to rename titles affected by changes to Unicode (or otherwise to Language::ucfirst...
doMove(IDatabase $db, $ns, $title)
Use MovePage to move a title.
processTable(IDatabase $db, $doMove, $table, $nsField, $titleField, $pkFields)
Rename entries in other tables.
isUserPage(IDatabase $db, $ns, $title)
Check if a ns+title is a registered user's page.
mungeTitle(IDatabase $db, Title $oldTitle, Title &$newTitle)
Munge a target title, if necessary.
getLikeBatches(IDatabase $db, $field, $batchSize=100)
Get batched LIKE conditions from the charmap.
execute()
Do the actual work.
getNamespaces()
Get the list of namespaces to operate on.
processUsers(IDatabase $db)
List users needing renaming.
__construct()
Default constructor.
doUpdate(IDatabase $db, $table, $nsField, $titleField, $row)
Directly update a database row.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
isLoggedIn()
Get whether the user is logged in.