MediaWiki REL1_39
uppercaseTitlesForUnicodeTransition.php
Go to the documentation of this file.
1<?php
27
28require_once __DIR__ . '/Maintenance.php';
29
37
38 private const MOVE = 0;
39 private const INPLACE_MOVE = 1;
40 private const UPPERCASE = 2;
41
43 private $run = false;
44
46 private $charmap = [];
47
49 private $user;
50
52 private $reason = 'Uppercasing title for Unicode upgrade';
53
55 private $tags = [];
56
58 private $seenUsers = [];
59
61 private $namespaces = null;
62
64 private $prefix = null, $suffix = null;
65
67 private $prefixNs = null;
68
70 private $tables = null;
71
72 public function __construct() {
73 parent::__construct();
74 $this->addDescription(
75 "Rename titles when changing behavior of Language::ucfirst().\n"
76 . "\n"
77 . "This script skips User and User_talk pages for registered users, as renaming of users "
78 . "is too complex to try to implement here. Use something like Extension:Renameuser to "
79 . "clean those up; this script can provide a list of user names affected."
80 );
81 $this->addOption(
82 'charmap', 'Character map generated by maintenance/language/generateUcfirstOverrides.php',
83 true, true
84 );
85 $this->addOption(
86 'user', 'System user to use to do the renames. Default is "Maintenance script".', false, true
87 );
88 $this->addOption(
89 'steal',
90 'If the username specified by --user exists, specify this to force conversion to a system user.'
91 );
92 $this->addOption(
93 'run', 'If not specified, the script will not actually perform any moves (i.e. it will dry-run).'
94 );
95 $this->addOption(
96 'prefix', 'When the new title already exists, add this prefix.', false, true
97 );
98 $this->addOption(
99 'suffix', 'When the new title already exists, add this suffix.', false, true
100 );
101 $this->addOption( 'reason', 'Reason to use when moving pages.', false, true );
102 $this->addOption( 'tag', 'Change tag to apply when moving pages.', false, true );
103 $this->addOption( 'tables', 'Comma-separated list of database tables to process.', false, true );
104 $this->addOption(
105 'userlist', 'Filename to which to output usernames needing rename. ' .
106 'This file can then be used directly by renameInvalidUsernames.php maintenance script',
107 false,
108 true
109 );
110 $this->setBatchSize( 1000 );
111 }
112
113 public function execute() {
114 $this->run = $this->getOption( 'run', false );
115
116 if ( $this->run ) {
117 $username = $this->getOption( 'user', User::MAINTENANCE_SCRIPT_USER );
118 $steal = $this->getOption( 'steal', false );
119 $this->user = User::newSystemUser( $username, [ 'steal' => $steal ] );
120 if ( !$this->user ) {
121 $user = User::newFromName( $username );
122 if ( !$steal && $user && $user->isRegistered() ) {
123 $this->fatalError( "User $username already exists.\n"
124 . "Use --steal if you really want to steal it from the human who currently owns it."
125 );
126 }
127 $this->fatalError( "Could not obtain system user $username." );
128 }
129 }
130
131 $tables = $this->getOption( 'tables' );
132 if ( $tables !== null ) {
133 $this->tables = explode( ',', $tables );
134 }
135
136 $prefix = $this->getOption( 'prefix' );
137 if ( $prefix !== null ) {
138 $title = Title::newFromText( $prefix . 'X' );
139 if ( !$title || substr( $title->getDBkey(), -1 ) !== 'X' ) {
140 $this->fatalError( 'Invalid --prefix.' );
141 }
142 if ( $title->getNamespace() <= NS_MAIN || $title->isExternal() ) {
143 $this->fatalError( 'Invalid --prefix. It must not be in namespace 0 and must not be external' );
144 }
145 $this->prefixNs = $title->getNamespace();
146 $this->prefix = substr( $title->getText(), 0, -1 );
147 }
148 $this->suffix = $this->getOption( 'suffix' );
149
150 $this->reason = $this->getOption( 'reason' ) ?: $this->reason;
151 $this->tags = (array)$this->getOption( 'tag', null );
152
153 $charmapFile = $this->getOption( 'charmap' );
154 if ( !file_exists( $charmapFile ) ) {
155 $this->fatalError( "Charmap file $charmapFile does not exist." );
156 }
157 if ( !is_file( $charmapFile ) || !is_readable( $charmapFile ) ) {
158 $this->fatalError( "Charmap file $charmapFile is not readable." );
159 }
160 $this->charmap = require $charmapFile;
161 if ( !is_array( $this->charmap ) ) {
162 $this->fatalError( "Charmap file $charmapFile did not return a PHP array." );
163 }
164 $this->charmap = array_filter(
165 $this->charmap,
166 function ( $v, $k ) {
167 if ( mb_strlen( $k ) !== 1 ) {
168 $this->error( "Ignoring mapping from multi-character key '$k' to '$v'" );
169 return false;
170 }
171 return $k !== $v;
172 },
173 ARRAY_FILTER_USE_BOTH
174 );
175 if ( !$this->charmap ) {
176 $this->fatalError( "Charmap file $charmapFile did not contain any usable character mappings." );
177 }
178
179 $db = $this->getDB( $this->run ? DB_PRIMARY : DB_REPLICA );
180
181 // Process inplace moves first, before actual moves, so mungeTitle() doesn't get confused
182 $this->processTable(
183 $db, self::INPLACE_MOVE, 'archive', 'ar_namespace', 'ar_title', [ 'ar_timestamp', 'ar_id' ]
184 );
185 $this->processTable(
186 $db, self::INPLACE_MOVE, 'filearchive', NS_FILE, 'fa_name', [ 'fa_timestamp', 'fa_id' ]
187 );
188 $this->processTable(
189 $db, self::INPLACE_MOVE, 'logging', 'log_namespace', 'log_title', [ 'log_id' ]
190 );
191 $this->processTable(
192 $db, self::INPLACE_MOVE, 'protected_titles', 'pt_namespace', 'pt_title', []
193 );
194 $this->processTable( $db, self::MOVE, 'page', 'page_namespace', 'page_title', [ 'page_id' ] );
195 $this->processTable( $db, self::MOVE, 'image', NS_FILE, 'img_name', [] );
196 $this->processTable(
197 $db, self::UPPERCASE, 'redirect', 'rd_namespace', 'rd_title', [ 'rd_from' ]
198 );
199 $this->processUsers( $db );
200 }
201
209 private function getLikeBatches( IDatabase $db, $field, $batchSize = 100 ) {
210 $ret = [];
211 $likes = [];
212 foreach ( $this->charmap as $from => $to ) {
213 $likes[] = $field . $db->buildLike( $from, $db->anyString() );
214 if ( count( $likes ) >= $batchSize ) {
215 $ret[] = $db->makeList( $likes, $db::LIST_OR );
216 $likes = [];
217 }
218 }
219 if ( $likes ) {
220 $ret[] = $db->makeList( $likes, $db::LIST_OR );
221 }
222 return $ret;
223 }
224
233 private function getNamespaces() {
234 if ( $this->namespaces === null ) {
235 $nsinfo = MediaWikiServices::getInstance()->getNamespaceInfo();
236 $this->namespaces = array_filter(
237 array_keys( $nsinfo->getCanonicalNamespaces() ),
238 static function ( $ns ) use ( $nsinfo ) {
239 return $nsinfo->isMovable( $ns ) && $nsinfo->isCapitalized( $ns );
240 }
241 );
242 usort( $this->namespaces, static function ( $ns1, $ns2 ) use ( $nsinfo ) {
243 if ( $ns1 === $ns2 ) {
244 return 0;
245 }
246
247 $s1 = $nsinfo->getSubject( $ns1 );
248 $s2 = $nsinfo->getSubject( $ns2 );
249
250 // Order by subject namespace number first
251 if ( $s1 !== $s2 ) {
252 return $s1 < $s2 ? -1 : 1;
253 }
254
255 // Second, put subject namespaces before non-subject namespaces
256 if ( $s1 === $ns1 ) {
257 return -1;
258 }
259 if ( $s2 === $ns2 ) {
260 return 1;
261 }
262
263 // Don't care about the relative order if there are somehow
264 // multiple non-subject namespaces for a namespace.
265 return 0;
266 } );
267 }
268
269 return $this->namespaces;
270 }
271
279 private function isUserPage( IDatabase $db, $ns, $title ) {
280 if ( $ns !== NS_USER && $ns !== NS_USER_TALK ) {
281 return false;
282 }
283
284 list( $base ) = explode( '/', $title, 2 );
285 if ( !isset( $this->seenUsers[$base] ) ) {
286 // Can't use User directly because it might uppercase the name
287 $this->seenUsers[$base] = (bool)$db->selectField(
288 'user',
289 'user_id',
290 [ 'user_name' => strtr( $base, '_', ' ' ) ],
291 __METHOD__
292 );
293 }
294 return $this->seenUsers[$base];
295 }
296
304 private function mungeTitle( IDatabase $db, Title $oldTitle, Title &$newTitle ) {
305 $nt = $newTitle->getPrefixedText();
306
307 $munge = false;
308 if ( $this->isUserPage( $db, $newTitle->getNamespace(), $newTitle->getText() ) ) {
309 $munge = 'Target title\'s user exists';
310 } else {
311 $mpFactory = MediaWikiServices::getInstance()->getMovePageFactory();
312 $status = $mpFactory->newMovePage( $oldTitle, $newTitle )->isValidMove();
313 if ( !$status->isOK() && (
314 $status->hasMessage( 'articleexists' ) || $status->hasMessage( 'redirectexists' ) ) ) {
315 $munge = 'Target title exists';
316 }
317 }
318 if ( !$munge ) {
319 return true;
320 }
321
322 if ( $this->prefix !== null ) {
323 $newTitle = Title::makeTitle(
324 $this->prefixNs,
325 $this->prefix . $oldTitle->getPrefixedText() . ( $this->suffix ?? '' )
326 );
327 } elseif ( $this->suffix !== null ) {
328 $dbkey = $newTitle->getText();
329 $i = $newTitle->getNamespace() === NS_FILE ? strrpos( $dbkey, '.' ) : false;
330 if ( $i !== false ) {
331 $newTitle = Title::makeTitle(
332 $newTitle->getNamespace(),
333 substr( $dbkey, 0, $i ) . $this->suffix . substr( $dbkey, $i )
334 );
335 } else {
336 $newTitle = Title::makeTitle( $newTitle->getNamespace(), $dbkey . $this->suffix );
337 }
338 } else {
339 $this->error(
340 "Cannot move {$oldTitle->getPrefixedText()} → $nt: "
341 . "$munge and no --prefix or --suffix was given"
342 );
343 return false;
344 }
345
346 if ( !$newTitle->canExist() ) {
347 $this->error(
348 "Cannot move {$oldTitle->getPrefixedText()} → $nt: "
349 . "$munge and munged title '{$newTitle->getPrefixedText()}' is not valid"
350 );
351 return false;
352 }
353 if ( $newTitle->exists() ) {
354 $this->error(
355 "Cannot move {$oldTitle->getPrefixedText()} → $nt: "
356 . "$munge and munged title '{$newTitle->getPrefixedText()}' also exists"
357 );
358 return false;
359 }
360
361 return true;
362 }
363
371 private function doMove( IDatabase $db, $ns, $title ) {
372 $char = mb_substr( $title, 0, 1 );
373 if ( !array_key_exists( $char, $this->charmap ) ) {
374 $this->error(
375 "Query returned NS$ns $title, which does not begin with a character in the charmap."
376 );
377 return false;
378 }
379
380 if ( $this->isUserPage( $db, $ns, $title ) ) {
381 $this->output( "... Skipping user page NS$ns $title\n" );
382 return null;
383 }
384
385 $oldTitle = Title::makeTitle( $ns, $title );
386 $newTitle = Title::makeTitle( $ns, $this->charmap[$char] . mb_substr( $title, 1 ) );
387 $deletionReason = $this->shouldDelete( $db, $oldTitle, $newTitle );
388 if ( !$this->mungeTitle( $db, $oldTitle, $newTitle ) ) {
389 return false;
390 }
391
392 $services = MediaWikiServices::getInstance();
393 $mpFactory = $services->getMovePageFactory();
394 $movePage = $mpFactory->newMovePage( $oldTitle, $newTitle );
395 $status = $movePage->isValidMove();
396 if ( !$status->isOK() ) {
397 $this->error(
398 "Invalid move {$oldTitle->getPrefixedText()} → {$newTitle->getPrefixedText()}: "
399 . $status->getMessage( false, false, 'en' )->useDatabase( false )->plain()
400 );
401 return false;
402 }
403
404 if ( !$this->run ) {
405 $this->output(
406 "Would rename {$oldTitle->getPrefixedText()} → {$newTitle->getPrefixedText()}\n"
407 );
408 if ( $deletionReason ) {
409 $this->output(
410 "Would then delete {$newTitle->getPrefixedText()}: $deletionReason\n"
411 );
412 }
413 return true;
414 }
415
416 $status = $movePage->move( $this->user, $this->reason, false, $this->tags );
417 if ( !$status->isOK() ) {
418 $this->error(
419 "Move {$oldTitle->getPrefixedText()} → {$newTitle->getPrefixedText()} failed: "
420 . $status->getMessage( false, false, 'en' )->useDatabase( false )->plain()
421 );
422 }
423 $this->output( "Renamed {$oldTitle->getPrefixedText()} → {$newTitle->getPrefixedText()}\n" );
424
425 // The move created a log entry under the old invalid title. Fix it.
426 $db->update(
427 'logging',
428 [
429 'log_title' => $this->charmap[$char] . mb_substr( $title, 1 ),
430 ],
431 [
432 'log_namespace' => $oldTitle->getNamespace(),
433 'log_title' => $oldTitle->getDBkey(),
434 'log_page' => $newTitle->getArticleID(),
435 ],
436 __METHOD__
437 );
438
439 if ( $deletionReason !== null ) {
440 $page = $services->getWikiPageFactory()->newFromTitle( $newTitle );
441 $error = '';
442 $status = $page->doDeleteArticleReal(
443 $deletionReason,
444 $this->user,
445 false, // don't suppress
446 null, // unused
447 $error,
448 null, // unused
449 [], // tags
450 'delete',
451 true // immediate
452 );
453 if ( !$status->isOK() ) {
454 $this->error(
455 "Deletion of {$newTitle->getPrefixedText()} failed: "
456 . $status->getMessage( false, false, 'en' )->useDatabase( false )->plain()
457 );
458 return false;
459 }
460 $this->output( "Deleted {$newTitle->getPrefixedText()}\n" );
461 }
462
463 return true;
464 }
465
480 private function shouldDelete( IDatabase $db, Title $oldTitle, Title $newTitle ) {
481 $oldRow = $db->selectRow(
482 [ 'page', 'redirect' ],
483 [ 'ns' => 'rd_namespace', 'title' => 'rd_title' ],
484 [ 'page_namespace' => $oldTitle->getNamespace(), 'page_title' => $oldTitle->getDBkey() ],
485 __METHOD__,
486 [],
487 [ 'redirect' => [ 'JOIN', 'rd_from = page_id' ] ]
488 );
489 if ( !$oldRow ) {
490 // Not a redirect
491 return null;
492 }
493
494 if ( (int)$oldRow->ns === $newTitle->getNamespace() &&
495 $oldRow->title === $newTitle->getDBkey()
496 ) {
497 return $this->reason . ", and found that [[{$oldTitle->getPrefixedText()}]] is "
498 . "already a redirect to [[{$newTitle->getPrefixedText()}]]";
499 } else {
500 $newRow = $db->selectRow(
501 [ 'page', 'redirect' ],
502 [ 'ns' => 'rd_namespace', 'title' => 'rd_title' ],
503 [ 'page_namespace' => $newTitle->getNamespace(), 'page_title' => $newTitle->getDBkey() ],
504 __METHOD__,
505 [],
506 [ 'redirect' => [ 'JOIN', 'rd_from = page_id' ] ]
507 );
508 if ( $newRow && $oldRow->ns === $newRow->ns && $oldRow->title === $newRow->title ) {
509 $nt = Title::makeTitle( $newRow->ns, $newRow->title );
510 return $this->reason . ", and found that [[{$oldTitle->getPrefixedText()}]] and "
511 . "[[{$newTitle->getPrefixedText()}]] both redirect to [[{$nt->getPrefixedText()}]].";
512 }
513 }
514
515 return null;
516 }
517
530 private function doUpdate( IDatabase $db, $op, $table, $nsField, $titleField, $row ) {
531 $ns = is_int( $nsField ) ? $nsField : (int)$row->$nsField;
532 $title = $row->$titleField;
533
534 $char = mb_substr( $title, 0, 1 );
535 if ( !array_key_exists( $char, $this->charmap ) ) {
536 $r = json_encode( $row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
537 $this->error(
538 "Query returned $r, but title does not begin with a character in the charmap."
539 );
540 return false;
541 }
542
543 $oldTitle = Title::makeTitle( $ns, $title );
544 $newTitle = Title::makeTitle( $ns, $this->charmap[$char] . mb_substr( $title, 1 ) );
545 if ( $op !== self::UPPERCASE && !$this->mungeTitle( $db, $oldTitle, $newTitle ) ) {
546 return false;
547 }
548
549 if ( $this->run ) {
550 $db->update(
551 $table,
552 array_merge(
553 is_int( $nsField ) ? [] : [ $nsField => $newTitle->getNamespace() ],
554 [ $titleField => $newTitle->getDBkey() ]
555 ),
556 (array)$row,
557 __METHOD__
558 );
559 $r = json_encode( $row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
560 $this->output( "Set $r to {$newTitle->getPrefixedText()}\n" );
561 } else {
562 $r = json_encode( $row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
563 $this->output( "Would set $r to {$newTitle->getPrefixedText()}\n" );
564 }
565
566 return true;
567 }
568
582 private function processTable( IDatabase $db, $op, $table, $nsField, $titleField, $pkFields ) {
583 if ( $this->tables !== null && !in_array( $table, $this->tables, true ) ) {
584 $this->output( "Skipping table `$table`, not in --tables.\n" );
585 return;
586 }
587
588 $batchSize = $this->getBatchSize();
589 $namespaces = $this->getNamespaces();
590 $likes = $this->getLikeBatches( $db, $titleField );
591 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
592
593 if ( is_int( $nsField ) ) {
594 $namespaces = array_intersect( $namespaces, [ $nsField ] );
595 }
596
597 if ( !$namespaces ) {
598 $this->output( "Skipping table `$table`, no valid namespaces.\n" );
599 return;
600 }
601
602 $this->output( "Processing table `$table`...\n" );
603
604 $selectFields = array_merge(
605 is_int( $nsField ) ? [] : [ $nsField ],
606 [ $titleField ],
607 $pkFields
608 );
609 $contFields = array_reverse( array_merge( [ $titleField ], $pkFields ) );
610
611 $lastReplicationWait = 0.0;
612 $count = 0;
613 $errors = 0;
614 foreach ( $namespaces as $ns ) {
615 foreach ( $likes as $like ) {
616 $cont = [];
617 do {
618 $res = $db->select(
619 $table,
620 $selectFields,
621 array_merge( [ "$nsField = $ns", $like ], $cont ),
622 __METHOD__,
623 [ 'ORDER BY' => array_merge( [ $titleField ], $pkFields ), 'LIMIT' => $batchSize ]
624 );
625 $cont = [];
626 foreach ( $res as $row ) {
627 $cont = '';
628 foreach ( $contFields as $field ) {
629 $v = $db->addQuotes( $row->$field );
630 if ( $cont === '' ) {
631 $cont = "$field > $v";
632 } else {
633 $cont = "$field > $v OR $field = $v AND ($cont)";
634 }
635 }
636 $cont = [ $cont ];
637
638 if ( $op === self::MOVE ) {
639 $ns = is_int( $nsField ) ? $nsField : (int)$row->$nsField;
640 $ret = $this->doMove( $db, $ns, $row->$titleField );
641 } else {
642 $ret = $this->doUpdate( $db, $op, $table, $nsField, $titleField, $row );
643 }
644 if ( $ret === true ) {
645 $count++;
646 } elseif ( $ret === false ) {
647 $errors++;
648 }
649 }
650
651 if ( $this->run ) {
652 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable rows contains at least one item
653 $r = $cont ? json_encode( $row, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) : '<end>';
654 $this->output( "... $table: $count renames, $errors errors at $r\n" );
655 $lbFactory->waitForReplication(
656 [ 'timeout' => 30, 'ifWritesSince' => $lastReplicationWait ]
657 );
658 $lastReplicationWait = microtime( true );
659 }
660 } while ( $cont );
661 }
662 }
663
664 $this->output( "Done processing table `$table`.\n" );
665 }
666
671 private function processUsers( IDatabase $db ) {
672 $userlistFile = $this->getOption( 'userlist' );
673 if ( $userlistFile === null ) {
674 $this->output( "Not generating user list, --userlist was not specified.\n" );
675 return;
676 }
677
678 $fh = fopen( $userlistFile, 'ab' );
679 if ( !$fh ) {
680 $this->error( "Could not open user list file $userlistFile" );
681 return;
682 }
683
684 $this->output( "Generating user list...\n" );
685 $count = 0;
686 $batchSize = $this->getBatchSize();
687 foreach ( $this->getLikeBatches( $db, 'user_name' ) as $like ) {
688 $cont = [];
689 while ( true ) {
690 $rows = $db->select(
691 'user',
692 [ 'user_id', 'user_name' ],
693 array_merge( [ $like ], $cont ),
694 __METHOD__,
695 [ 'ORDER BY' => 'user_name', 'LIMIT' => $batchSize ]
696 );
697
698 if ( !$rows->numRows() ) {
699 break;
700 }
701
702 foreach ( $rows as $row ) {
703 $char = mb_substr( $row->user_name, 0, 1 );
704 if ( !array_key_exists( $char, $this->charmap ) ) {
705 $this->error(
706 "Query returned $row->user_name, but user name does not " .
707 "begin with a character in the charmap."
708 );
709 continue;
710 }
711 $newName = $this->charmap[$char] . mb_substr( $row->user_name, 1 );
712 fprintf( $fh, "%s\t%s\t%s\n", WikiMap::getCurrentWikiId(), $row->user_id, $newName );
713 $count++;
714 $cont = [ 'user_name > ' . $db->addQuotes( $row->user_name ) ];
715 }
716 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable rows contains at least one item
717 $this->output( "... at $row->user_name, $count names so far\n" );
718 }
719 }
720
721 if ( !fclose( $fh ) ) {
722 $this->error( "fclose on $userlistFile failed" );
723 }
724 $this->output( "User list output to $userlistFile, $count users need renaming.\n" );
725 }
726}
727
728$maintClass = UppercaseTitlesForUnicodeTransition::class;
729require_once RUN_MAINTENANCE_IF_MAIN;
getDB()
const NS_USER
Definition Defines.php:66
const NS_FILE
Definition Defines.php:70
const NS_MAIN
Definition Defines.php:64
const NS_USER_TALK
Definition Defines.php:67
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.
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)
fatalError( $msg, $exitCode=1)
Output a message and terminate the current script.
Service locator for MediaWiki core services.
Represents a title within MediaWiki.
Definition Title.php:49
getNamespace()
Get the namespace index, i.e.
Definition Title.php:1066
exists( $flags=0)
Check if page exists.
Definition Title.php:3477
canExist()
Can this title represent a page in the wiki's database?
Definition Title.php:1232
getDBkey()
Get the main part with underscores.
Definition Title.php:1057
getText()
Get the text form (spaces not underscores) of the main part.
Definition Title.php:1039
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition Title.php:2824
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition Title.php:638
getPrefixedText()
Get the prefixed title with spaces.
Definition Title.php:1890
Maintenance script to rename titles affected by changes to Unicode (or otherwise to Language::ucfirst...
internal since 1.36
Definition User.php:70
static newFromName( $name, $validate='valid')
Definition User.php:598
isRegistered()
Get whether the user is registered.
Definition User.php:2311
static newSystemUser( $name, $options=[])
Static factory method for creation of a "system" user from username.
Definition User.php:806
const MAINTENANCE_SCRIPT_USER
Username used for various maintenance scripts.
Definition User.php:116
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:39
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
select( $table, $vars, $conds='', $fname=__METHOD__, $options=[], $join_conds=[])
Execute a SELECT query constructed using the various parameters provided.
update( $table, $set, $conds, $fname=__METHOD__, $options=[])
Update all rows in a table that match a given condition.
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a single field from a single result row.
makeList(array $a, $mode=self::LIST_COMMA)
Makes an encoded list of strings from an array.
buildLike( $param,... $params)
LIKE statement wrapper.
anyString()
Returns a token for buildLike() that denotes a '' to be used in a LIKE query.
const DB_REPLICA
Definition defines.php:26
const DB_PRIMARY
Definition defines.php:28