MediaWiki master
uppercaseTitlesForUnicodeTransition.php
Go to the documentation of this file.
1<?php
19
20// @codeCoverageIgnoreStart
21require_once __DIR__ . '/Maintenance.php';
22// @codeCoverageIgnoreEnd
23
31
32 private const MOVE = 0;
33 private const INPLACE_MOVE = 1;
34 private const UPPERCASE = 2;
35
37 private $run = false;
38
40 private $charmap = [];
41
43 private $user;
44
46 private $reason = 'Uppercasing title for Unicode upgrade';
47
49 private $tags = [];
50
52 private $seenUsers = [];
53
55 private $namespaces = null;
56
57 private ?string $prefix = null;
58 private ?string $suffix = null;
59
61 private $prefixNs = null;
62
64 private $tables = null;
65
66 public function __construct() {
67 parent::__construct();
68 $this->addDescription(
69 "Rename titles when changing behavior of Language::ucfirst().\n"
70 . "\n"
71 . "This script skips User and User_talk pages for registered users, as renaming of users "
72 . "is too complex to try to implement here. Use something like Extension:Renameuser to "
73 . "clean those up; this script can provide a list of user names affected."
74 );
75 $this->addOption(
76 'charmap', 'Character map generated by maintenance/language/generateUcfirstOverrides.php',
77 true, true
78 );
79 $this->addOption(
80 'user', 'System user to use to do the renames. Default is "Maintenance script".', false, true
81 );
82 $this->addOption(
83 'steal',
84 'If the username specified by --user exists, specify this to force conversion to a system user.'
85 );
86 $this->addOption(
87 'run', 'If not specified, the script will not actually perform any moves (i.e. it will dry-run).'
88 );
89 $this->addOption(
90 'prefix', 'When the new title already exists, add this prefix.', false, true
91 );
92 $this->addOption(
93 'suffix', 'When the new title already exists, add this suffix.', false, true
94 );
95 $this->addOption( 'reason', 'Reason to use when moving pages.', false, true );
96 $this->addOption( 'tag', 'Change tag to apply when moving pages.', false, true );
97 $this->addOption( 'tables', 'Comma-separated list of database tables to process.', false, true );
98 $this->addOption(
99 'userlist', 'Filename to which to output usernames needing rename. ' .
100 'This file can then be used directly by renameInvalidUsernames.php maintenance script',
101 false,
102 true
103 );
104 $this->setBatchSize( 1000 );
105 }
106
107 public function execute() {
108 $this->run = $this->getOption( 'run', false );
109
110 if ( $this->run ) {
111 $username = $this->getOption( 'user', User::MAINTENANCE_SCRIPT_USER );
112 $steal = $this->getOption( 'steal', false );
113 $this->user = User::newSystemUser( $username, [ 'steal' => $steal ] );
114 if ( !$this->user ) {
115 $user = User::newFromName( $username );
116 if ( !$steal && $user && $user->isRegistered() ) {
117 $this->fatalError( "User $username already exists.\n"
118 . "Use --steal if you really want to steal it from the human who currently owns it."
119 );
120 }
121 $this->fatalError( "Could not obtain system user $username." );
122 }
123 }
124
125 $tables = $this->getOption( 'tables' );
126 if ( $tables !== null ) {
127 $this->tables = explode( ',', $tables );
128 }
129
130 $prefix = $this->getOption( 'prefix' );
131 if ( $prefix !== null ) {
132 $title = Title::newFromText( $prefix . 'X' );
133 if ( !$title || substr( $title->getDBkey(), -1 ) !== 'X' ) {
134 $this->fatalError( 'Invalid --prefix.' );
135 }
136 if ( $title->getNamespace() <= NS_MAIN || $title->isExternal() ) {
137 $this->fatalError( 'Invalid --prefix. It must not be in namespace 0 and must not be external' );
138 }
139 $this->prefixNs = $title->getNamespace();
140 $this->prefix = substr( $title->getText(), 0, -1 );
141 }
142 $this->suffix = $this->getOption( 'suffix' );
143
144 $this->reason = $this->getOption( 'reason' ) ?: $this->reason;
145 $this->tags = (array)$this->getOption( 'tag', null );
146
147 $charmapFile = $this->getOption( 'charmap' );
148 if ( !file_exists( $charmapFile ) ) {
149 $this->fatalError( "Charmap file $charmapFile does not exist." );
150 }
151 if ( !is_file( $charmapFile ) || !is_readable( $charmapFile ) ) {
152 $this->fatalError( "Charmap file $charmapFile is not readable." );
153 }
154 $this->charmap = require $charmapFile;
155 if ( !is_array( $this->charmap ) ) {
156 $this->fatalError( "Charmap file $charmapFile did not return a PHP array." );
157 }
158 $this->charmap = array_filter(
159 $this->charmap,
160 function ( $v, $k ) {
161 if ( mb_strlen( $k ) !== 1 ) {
162 $this->error( "Ignoring mapping from multi-character key '$k' to '$v'" );
163 return false;
164 }
165 return $k !== $v;
166 },
167 ARRAY_FILTER_USE_BOTH
168 );
169 if ( !$this->charmap ) {
170 $this->fatalError( "Charmap file $charmapFile did not contain any usable character mappings." );
171 }
172
173 $db = $this->run ? $this->getPrimaryDB() : $this->getReplicaDB();
174
175 // Process inplace moves first, before actual moves, so mungeTitle() doesn't get confused
176 $this->processTable(
177 $db, self::INPLACE_MOVE, 'archive', 'ar_namespace', 'ar_title', [ 'ar_timestamp', 'ar_id' ]
178 );
179 $this->processTable(
180 $db, self::INPLACE_MOVE, 'filearchive', NS_FILE, 'fa_name', [ 'fa_timestamp', 'fa_id' ]
181 );
182 $this->processTable(
183 $db, self::INPLACE_MOVE, 'logging', 'log_namespace', 'log_title', [ 'log_id' ]
184 );
185 $this->processTable(
186 $db, self::INPLACE_MOVE, 'protected_titles', 'pt_namespace', 'pt_title', []
187 );
188 $this->processTable( $db, self::MOVE, 'page', 'page_namespace', 'page_title', [ 'page_id' ] );
189 $this->processTable( $db, self::MOVE, 'image', NS_FILE, 'img_name', [] );
190 $this->processTable( $db, self::MOVE, 'file', NS_FILE, 'file_name', [ 'file_id' ] );
191 $this->processTable(
192 $db, self::UPPERCASE, 'redirect', 'rd_namespace', 'rd_title', [ 'rd_from' ]
193 );
194 $this->processUsers( $db );
195 }
196
204 private function getLikeBatches( IReadableDatabase $db, $field, $batchSize = 100 ) {
205 $ret = [];
206 $likes = [];
207 foreach ( $this->charmap as $from => $to ) {
208 $likes[] = $db->expr(
209 $field,
210 IExpression::LIKE,
211 new LikeValue( $from, $db->anyString() )
212 );
213 if ( count( $likes ) >= $batchSize ) {
214 $ret[] = $db->orExpr( $likes );
215 $likes = [];
216 }
217 }
218 if ( $likes ) {
219 $ret[] = $db->orExpr( $likes );
220 }
221 return $ret;
222 }
223
232 private function getNamespaces() {
233 if ( $this->namespaces === null ) {
234 $nsinfo = $this->getServiceContainer()->getNamespaceInfo();
235 $this->namespaces = array_filter(
236 array_keys( $nsinfo->getCanonicalNamespaces() ),
237 static function ( $ns ) use ( $nsinfo ) {
238 return $nsinfo->isMovable( $ns ) && $nsinfo->isCapitalized( $ns );
239 }
240 );
241 usort( $this->namespaces, static function ( $ns1, $ns2 ) use ( $nsinfo ) {
242 if ( $ns1 === $ns2 ) {
243 return 0;
244 }
245
246 $s1 = $nsinfo->getSubject( $ns1 );
247 $s2 = $nsinfo->getSubject( $ns2 );
248
249 // Order by subject namespace number first
250 if ( $s1 !== $s2 ) {
251 return $s1 < $s2 ? -1 : 1;
252 }
253
254 // Second, put subject namespaces before non-subject namespaces
255 if ( $s1 === $ns1 ) {
256 return -1;
257 }
258 if ( $s2 === $ns2 ) {
259 return 1;
260 }
261
262 // Don't care about the relative order if there are somehow
263 // multiple non-subject namespaces for a namespace.
264 return 0;
265 } );
266 }
267
268 return $this->namespaces;
269 }
270
278 private function isUserPage( IReadableDatabase $db, $ns, $title ) {
279 if ( $ns !== NS_USER && $ns !== NS_USER_TALK ) {
280 return false;
281 }
282
283 [ $base ] = explode( '/', $title, 2 );
284 if ( !isset( $this->seenUsers[$base] ) ) {
285 // Can't use User directly because it might uppercase the name
286 $this->seenUsers[$base] = (bool)$db->newSelectQueryBuilder()
287 ->select( 'user_id' )
288 ->from( 'user' )
289 ->where( [ 'user_name' => strtr( $base, '_', ' ' ) ] )
290 ->caller( __METHOD__ )->fetchField();
291 }
292 return $this->seenUsers[$base];
293 }
294
302 private function mungeTitle( IReadableDatabase $db, Title $oldTitle, Title &$newTitle ) {
303 $nt = $newTitle->getPrefixedText();
304
305 $munge = false;
306 if ( $this->isUserPage( $db, $newTitle->getNamespace(), $newTitle->getText() ) ) {
307 $munge = 'Target title\'s user exists';
308 } else {
309 $mpFactory = $this->getServiceContainer()->getMovePageFactory();
310 $status = $mpFactory->newMovePage( $oldTitle, $newTitle )->isValidMove();
311 if ( !$status->isOK() && (
312 $status->hasMessage( 'articleexists' ) || $status->hasMessage( 'redirectexists' ) ) ) {
313 $munge = 'Target title exists';
314 }
315 }
316 if ( !$munge ) {
317 return true;
318 }
319
320 if ( $this->prefix !== null ) {
321 $newTitle = Title::makeTitle(
322 $this->prefixNs,
323 $this->prefix . $oldTitle->getPrefixedText() . ( $this->suffix ?? '' )
324 );
325 } elseif ( $this->suffix !== null ) {
326 $dbkey = $newTitle->getText();
327 $i = $newTitle->getNamespace() === NS_FILE ? strrpos( $dbkey, '.' ) : false;
328 if ( $i !== false ) {
329 $newTitle = Title::makeTitle(
330 $newTitle->getNamespace(),
331 substr( $dbkey, 0, $i ) . $this->suffix . substr( $dbkey, $i )
332 );
333 } else {
334 $newTitle = Title::makeTitle( $newTitle->getNamespace(), $dbkey . $this->suffix );
335 }
336 } else {
337 $this->error(
338 "Cannot move {$oldTitle->getPrefixedText()} → $nt: "
339 . "$munge and no --prefix or --suffix was given"
340 );
341 return false;
342 }
343
344 if ( !$newTitle->canExist() ) {
345 $this->error(
346 "Cannot move {$oldTitle->getPrefixedText()} → $nt: "
347 . "$munge and munged title '{$newTitle->getPrefixedText()}' is not valid"
348 );
349 return false;
350 }
351 if ( $newTitle->exists() ) {
352 $this->error(
353 "Cannot move {$oldTitle->getPrefixedText()} → $nt: "
354 . "$munge and munged title '{$newTitle->getPrefixedText()}' also exists"
355 );
356 return false;
357 }
358
359 return true;
360 }
361
369 private function doMove( IDatabase $db, $ns, $title ) {
370 $char = mb_substr( $title, 0, 1 );
371 if ( !array_key_exists( $char, $this->charmap ) ) {
372 $this->error(
373 "Query returned NS$ns $title, which does not begin with a character in the charmap."
374 );
375 return false;
376 }
377
378 if ( $this->isUserPage( $db, $ns, $title ) ) {
379 $this->output( "... Skipping user page NS$ns $title\n" );
380 return null;
381 }
382
383 $oldTitle = Title::makeTitle( $ns, $title );
384 $newTitle = Title::makeTitle( $ns, $this->charmap[$char] . mb_substr( $title, 1 ) );
385 $deletionReason = $this->shouldDelete( $db, $oldTitle, $newTitle );
386 if ( !$this->mungeTitle( $db, $oldTitle, $newTitle ) ) {
387 return false;
388 }
389
390 $services = $this->getServiceContainer();
391 $mpFactory = $services->getMovePageFactory();
392 $movePage = $mpFactory->newMovePage( $oldTitle, $newTitle );
393 $status = $movePage->isValidMove();
394 if ( !$status->isOK() ) {
395 $this->error( "Invalid move {$oldTitle->getPrefixedText()} → {$newTitle->getPrefixedText()}:" );
396 $this->error( $status );
397 return false;
398 }
399
400 if ( !$this->run ) {
401 $this->output(
402 "Would rename {$oldTitle->getPrefixedText()} → {$newTitle->getPrefixedText()}\n"
403 );
404 if ( $deletionReason ) {
405 $this->output(
406 "Would then delete {$newTitle->getPrefixedText()}: $deletionReason\n"
407 );
408 }
409 return true;
410 }
411
412 $status = $movePage->move( $this->user, $this->reason, false, $this->tags );
413 if ( !$status->isOK() ) {
414 $this->error( "Move {$oldTitle->getPrefixedText()} → {$newTitle->getPrefixedText()} failed:" );
415 $this->error( $status );
416 return false;
417 }
418 $this->output( "Renamed {$oldTitle->getPrefixedText()} → {$newTitle->getPrefixedText()}\n" );
419
420 // The move created a log entry under the old invalid title. Fix it.
422 ->update( 'logging' )
423 ->set( [
424 'log_title' => $this->charmap[$char] . mb_substr( $title, 1 ),
425 ] )
426 ->where( [
427 'log_namespace' => $oldTitle->getNamespace(),
428 'log_title' => $oldTitle->getDBkey(),
429 'log_page' => $newTitle->getArticleID(),
430 ] )
431 ->caller( __METHOD__ )
432 ->execute();
433
434 if ( $deletionReason !== null ) {
435 $page = $services->getWikiPageFactory()->newFromTitle( $newTitle );
436 $delPage = $services->getDeletePageFactory()->newDeletePage( $page, $this->user );
437 $status = $delPage
438 ->forceImmediate( true )
439 ->deleteUnsafe( $deletionReason );
440 if ( !$status->isOK() ) {
441 $this->error( "Deletion of {$newTitle->getPrefixedText()} failed:" );
442 $this->error( $status );
443 return false;
444 }
445 $this->output( "Deleted {$newTitle->getPrefixedText()}\n" );
446 }
447
448 return true;
449 }
450
465 private function shouldDelete( IReadableDatabase $db, Title $oldTitle, Title $newTitle ) {
466 $oldRow = $db->newSelectQueryBuilder()
467 ->select( [ 'ns' => 'rd_namespace', 'title' => 'rd_title' ] )
468 ->from( 'page' )
469 ->join( 'redirect', null, 'rd_from = page_id' )
470 ->where( [ 'page_namespace' => $oldTitle->getNamespace(), 'page_title' => $oldTitle->getDBkey() ] )
471 ->caller( __METHOD__ )->fetchRow();
472 if ( !$oldRow ) {
473 // Not a redirect
474 return null;
475 }
476
477 if ( (int)$oldRow->ns === $newTitle->getNamespace() &&
478 $oldRow->title === $newTitle->getDBkey()
479 ) {
480 return $this->reason . ", and found that [[{$oldTitle->getPrefixedText()}]] is "
481 . "already a redirect to [[{$newTitle->getPrefixedText()}]]";
482 } else {
483 $newRow = $db->newSelectQueryBuilder()
484 ->select( [ 'ns' => 'rd_namespace', 'title' => 'rd_title' ] )
485 ->from( 'page' )
486 ->join( 'redirect', null, 'rd_from = page_id' )
487 ->where( [ 'page_namespace' => $newTitle->getNamespace(), 'page_title' => $newTitle->getDBkey() ] )
488 ->caller( __METHOD__ )->fetchRow();
489 if ( $newRow && $oldRow->ns === $newRow->ns && $oldRow->title === $newRow->title ) {
490 $nt = Title::makeTitle( $newRow->ns, $newRow->title );
491 return $this->reason . ", and found that [[{$oldTitle->getPrefixedText()}]] and "
492 . "[[{$newTitle->getPrefixedText()}]] both redirect to [[{$nt->getPrefixedText()}]].";
493 }
494 }
495
496 return null;
497 }
498
511 private function doUpdate( IDatabase $db, $op, $table, $nsField, $titleField, $row ) {
512 $ns = is_int( $nsField ) ? $nsField : (int)$row->$nsField;
513 $title = $row->$titleField;
514
515 $char = mb_substr( $title, 0, 1 );
516 if ( !array_key_exists( $char, $this->charmap ) ) {
517 $r = json_encode( $row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
518 $this->error(
519 "Query returned $r, but title does not begin with a character in the charmap."
520 );
521 return false;
522 }
523
524 $oldTitle = Title::makeTitle( $ns, $title );
525 $newTitle = Title::makeTitle( $ns, $this->charmap[$char] . mb_substr( $title, 1 ) );
526 if ( $op !== self::UPPERCASE && !$this->mungeTitle( $db, $oldTitle, $newTitle ) ) {
527 return false;
528 }
529
530 if ( $this->run ) {
531 $db->newUpdateQueryBuilder()
532 ->update( $table )
533 ->set( array_merge(
534 is_int( $nsField ) ? [] : [ $nsField => $newTitle->getNamespace() ],
535 [ $titleField => $newTitle->getDBkey() ]
536 ) )
537 ->where( (array)$row )
538 ->caller( __METHOD__ )
539 ->execute();
540 $r = json_encode( $row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
541 $this->output( "Set $r to {$newTitle->getPrefixedText()}\n" );
542 } else {
543 $r = json_encode( $row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
544 $this->output( "Would set $r to {$newTitle->getPrefixedText()}\n" );
545 }
546
547 return true;
548 }
549
563 private function processTable( IDatabase $db, $op, $table, $nsField, $titleField, $pkFields ) {
564 if ( $this->tables !== null && !in_array( $table, $this->tables, true ) ) {
565 $this->output( "Skipping table `$table`, not in --tables.\n" );
566 return;
567 }
568
569 $batchSize = $this->getBatchSize();
570 $namespaces = $this->getNamespaces();
571 $likes = $this->getLikeBatches( $db, $titleField );
572
573 if ( is_int( $nsField ) ) {
574 $namespaces = array_intersect( $namespaces, [ $nsField ] );
575 }
576
577 if ( !$namespaces ) {
578 $this->output( "Skipping table `$table`, no valid namespaces.\n" );
579 return;
580 }
581
582 $this->output( "Processing table `$table`...\n" );
583
584 $selectFields = array_merge(
585 is_int( $nsField ) ? [] : [ $nsField ],
586 [ $titleField ],
587 $pkFields
588 );
589 $contFields = array_merge( [ $titleField ], $pkFields );
590
591 $lastReplicationWait = 0.0;
592 $count = 0;
593 $errors = 0;
594 foreach ( $namespaces as $ns ) {
595 foreach ( $likes as $like ) {
596 $cont = [];
597 do {
598 $res = $db->newSelectQueryBuilder()
599 ->select( $selectFields )
600 ->from( $table )
601 ->where( [ "$nsField = $ns", $like, $cont ? $db->buildComparison( '>', $cont ) : '1=1' ] )
602 ->orderBy( array_merge( [ $titleField ], $pkFields ) )
603 ->limit( $batchSize )
604 ->caller( __METHOD__ )->fetchResultSet();
605 $cont = [];
606
607 $this->beginTransactionRound( __METHOD__ );
608 foreach ( $res as $row ) {
609 $cont = [];
610 foreach ( $contFields as $field ) {
611 $cont[ $field ] = $row->$field;
612 }
613
614 if ( $op === self::MOVE ) {
615 $ns = is_int( $nsField ) ? $nsField : (int)$row->$nsField;
616 $ret = $this->doMove( $db, $ns, $row->$titleField );
617 } else {
618 $ret = $this->doUpdate( $db, $op, $table, $nsField, $titleField, $row );
619 }
620 if ( $ret === true ) {
621 $count++;
622 } elseif ( $ret === false ) {
623 $errors++;
624 }
625 }
626 $this->commitTransactionRound( __METHOD__ );
627
628 if ( $this->run ) {
629 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable rows contains at least one item
630 $r = $cont ? json_encode( $row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) : '<end>';
631 $this->output( "... $table: $count renames, $errors errors at $r\n" );
632 }
633 } while ( $cont );
634 }
635 }
636
637 $this->output( "Done processing table `$table`.\n" );
638 }
639
644 private function processUsers( IReadableDatabase $db ) {
645 $userlistFile = $this->getOption( 'userlist' );
646 if ( $userlistFile === null ) {
647 $this->output( "Not generating user list, --userlist was not specified.\n" );
648 return;
649 }
650
651 $fh = fopen( $userlistFile, 'ab' );
652 if ( !$fh ) {
653 $this->error( "Could not open user list file $userlistFile" );
654 return;
655 }
656
657 $this->output( "Generating user list...\n" );
658 $count = 0;
659 $batchSize = $this->getBatchSize();
660 foreach ( $this->getLikeBatches( $db, 'user_name' ) as $like ) {
661 $cont = [];
662 while ( true ) {
663 $rows = $db->newSelectQueryBuilder()
664 ->select( [ 'user_id', 'user_name' ] )
665 ->from( 'user' )
666 ->where( $like )
667 ->andWhere( $cont )
668 ->orderBy( 'user_name' )
669 ->limit( $batchSize )
670 ->caller( __METHOD__ )->fetchResultSet();
671
672 if ( !$rows->numRows() ) {
673 break;
674 }
675
676 foreach ( $rows as $row ) {
677 $char = mb_substr( $row->user_name, 0, 1 );
678 if ( !array_key_exists( $char, $this->charmap ) ) {
679 $this->error(
680 "Query returned $row->user_name, but user name does not " .
681 "begin with a character in the charmap."
682 );
683 continue;
684 }
685 $newName = $this->charmap[$char] . mb_substr( $row->user_name, 1 );
686 fprintf( $fh, "%s\t%s\t%s\n", WikiMap::getCurrentWikiId(), $row->user_id, $newName );
687 $count++;
688 $cont = [ $db->expr( 'user_name', '>', $row->user_name ) ];
689 }
690 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable rows contains at least one item
691 $this->output( "... at $row->user_name, $count names so far\n" );
692 }
693 }
694
695 if ( !fclose( $fh ) ) {
696 $this->error( "fclose on $userlistFile failed" );
697 }
698 $this->output( "User list output to $userlistFile, $count users need renaming.\n" );
699 }
700}
701
702// @codeCoverageIgnoreStart
703$maintClass = UppercaseTitlesForUnicodeTransition::class;
704require_once RUN_MAINTENANCE_IF_MAIN;
705// @codeCoverageIgnoreEnd
const NS_USER
Definition Defines.php:53
const NS_FILE
Definition Defines.php:57
const NS_MAIN
Definition Defines.php:51
const NS_USER_TALK
Definition Defines.php:54
Abstract maintenance class for quickly writing and churning out maintenance scripts with minimal effo...
getBatchSize()
Returns batch size.
output( $out, $channel=null)
Throw some output to the user.
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.
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.
commitTransactionRound( $fname)
Commit a transactional batch of DB operations and wait for replica DB servers to catch up.
getReplicaDB(string|false $virtualDomain=false)
beginTransactionRound( $fname)
Start a transactional batch of DB operations.
error( $err, $die=0)
Throw an error to the user.
getServiceContainer()
Returns the main service container.
getPrimaryDB(string|false $virtualDomain=false)
addDescription( $text)
Set the description text.
Represents a title within MediaWiki.
Definition Title.php:69
canExist()
Can this title represent a page in the wiki's database?
Definition Title.php:1205
exists( $flags=0)
Check if page exists.
Definition Title.php:3129
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition Title.php:2558
getNamespace()
Get the namespace index, i.e.
Definition Title.php:1037
getDBkey()
Get the main part with underscores.
Definition Title.php:1028
getText()
Get the text form (spaces not underscores) of the main part.
Definition Title.php:1010
getPrefixedText()
Get the prefixed title with spaces.
Definition Title.php:1857
User class for the MediaWiki software.
Definition User.php:110
isRegistered()
Get whether the user is registered.
Definition User.php:2071
Tools for dealing with other locally-hosted wikis.
Definition WikiMap.php:19
Maintenance script to rename titles affected by changes to Unicode (or otherwise to Language::ucfirst...
Content of like value.
Definition LikeValue.php:14
Interface to a relational database.
Definition IDatabase.php:31
newUpdateQueryBuilder()
Get an UpdateQueryBuilder bound to this connection.
A database connection without write operations.
newSelectQueryBuilder()
Create an empty SelectQueryBuilder which can be used to run queries against this connection.
expr(string $field, string $op, $value)
See Expression::__construct()
orExpr(array $conds)
See Expression::__construct()
anyString()
Returns a token for buildLike() that denotes a '' to be used in a LIKE query.
buildComparison(string $op, array $conds)
Build a condition comparing multiple values, for use with indexes that cover multiple fields,...