28 require_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 );
113 if ( !$this->user ) {
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 ) {
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 );
296 if ( !
$status->isOK() &&
$status->hasMessage(
'articleexists' ) ) {
297 $munge =
'Target title exists';
304 if ( $this->prefix !==
null ) {
309 } elseif ( $this->suffix !==
null ) {
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" );
360 if ( !$this->
mungeTitle( $db, $oldTitle, $newTitle ) ) {
364 $mp =
new MovePage( $oldTitle, $newTitle );
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 );
384 "Move {$oldTitle->getPrefixedText()} → {$newTitle->getPrefixedText()} failed: "
385 .
$status->getMessage(
false,
false,
'en' )->useDatabase(
false )->plain()
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" );
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" );