]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - maintenance/Maintenance.php
MediaWiki 1.30.2-scripts
[autoinstalls/mediawiki.git] / maintenance / Maintenance.php
1 <?php
2 /**
3  * This program is free software; you can redistribute it and/or modify
4  * it under the terms of the GNU General Public License as published by
5  * the Free Software Foundation; either version 2 of the License, or
6  * (at your option) any later version.
7  *
8  * This program is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  * GNU General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License along
14  * with this program; if not, write to the Free Software Foundation, Inc.,
15  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16  * http://www.gnu.org/copyleft/gpl.html
17  *
18  * @file
19  * @ingroup Maintenance
20  * @defgroup Maintenance Maintenance
21  */
22
23 // Bail on old versions of PHP, or if composer has not been run yet to install
24 // dependencies.
25 require_once __DIR__ . '/../includes/PHPVersionCheck.php';
26 wfEntryPointCheck( 'cli' );
27
28 use Wikimedia\Rdbms\DBReplicationWaitError;
29
30 /**
31  * @defgroup MaintenanceArchive Maintenance archives
32  * @ingroup Maintenance
33  */
34
35 // Define this so scripts can easily find doMaintenance.php
36 define( 'RUN_MAINTENANCE_IF_MAIN', __DIR__ . '/doMaintenance.php' );
37 define( 'DO_MAINTENANCE', RUN_MAINTENANCE_IF_MAIN ); // original name, harmless
38
39 $maintClass = false;
40
41 use Wikimedia\Rdbms\IDatabase;
42 use MediaWiki\Logger\LoggerFactory;
43 use MediaWiki\MediaWikiServices;
44 use Wikimedia\Rdbms\LBFactory;
45 use Wikimedia\Rdbms\IMaintainableDatabase;
46
47 /**
48  * Abstract maintenance class for quickly writing and churning out
49  * maintenance scripts with minimal effort. All that _must_ be defined
50  * is the execute() method. See docs/maintenance.txt for more info
51  * and a quick demo of how to use it.
52  *
53  * @author Chad Horohoe <chad@anyonecanedit.org>
54  * @since 1.16
55  * @ingroup Maintenance
56  */
57 abstract class Maintenance {
58         /**
59          * Constants for DB access type
60          * @see Maintenance::getDbType()
61          */
62         const DB_NONE = 0;
63         const DB_STD = 1;
64         const DB_ADMIN = 2;
65
66         // Const for getStdin()
67         const STDIN_ALL = 'all';
68
69         // This is the desired params
70         protected $mParams = [];
71
72         // Array of mapping short parameters to long ones
73         protected $mShortParamsMap = [];
74
75         // Array of desired args
76         protected $mArgList = [];
77
78         // This is the list of options that were actually passed
79         protected $mOptions = [];
80
81         // This is the list of arguments that were actually passed
82         protected $mArgs = [];
83
84         // Name of the script currently running
85         protected $mSelf;
86
87         // Special vars for params that are always used
88         protected $mQuiet = false;
89         protected $mDbUser, $mDbPass;
90
91         // A description of the script, children should change this via addDescription()
92         protected $mDescription = '';
93
94         // Have we already loaded our user input?
95         protected $mInputLoaded = false;
96
97         /**
98          * Batch size. If a script supports this, they should set
99          * a default with setBatchSize()
100          *
101          * @var int
102          */
103         protected $mBatchSize = null;
104
105         // Generic options added by addDefaultParams()
106         private $mGenericParameters = [];
107         // Generic options which might or not be supported by the script
108         private $mDependantParameters = [];
109
110         /**
111          * Used by getDB() / setDB()
112          * @var IMaintainableDatabase
113          */
114         private $mDb = null;
115
116         /** @var float UNIX timestamp */
117         private $lastReplicationWait = 0.0;
118
119         /**
120          * Used when creating separate schema files.
121          * @var resource
122          */
123         public $fileHandle;
124
125         /**
126          * Accessible via getConfig()
127          *
128          * @var Config
129          */
130         private $config;
131
132         /**
133          * @see Maintenance::requireExtension
134          * @var array
135          */
136         private $requiredExtensions = [];
137
138         /**
139          * Used to read the options in the order they were passed.
140          * Useful for option chaining (Ex. dumpBackup.php). It will
141          * be an empty array if the options are passed in through
142          * loadParamsAndArgs( $self, $opts, $args ).
143          *
144          * This is an array of arrays where
145          * 0 => the option and 1 => parameter value.
146          *
147          * @var array
148          */
149         public $orderedOptions = [];
150
151         /**
152          * Default constructor. Children should call this *first* if implementing
153          * their own constructors
154          */
155         public function __construct() {
156                 // Setup $IP, using MW_INSTALL_PATH if it exists
157                 global $IP;
158                 $IP = strval( getenv( 'MW_INSTALL_PATH' ) ) !== ''
159                         ? getenv( 'MW_INSTALL_PATH' )
160                         : realpath( __DIR__ . '/..' );
161
162                 $this->addDefaultParams();
163                 register_shutdown_function( [ $this, 'outputChanneled' ], false );
164         }
165
166         /**
167          * Should we execute the maintenance script, or just allow it to be included
168          * as a standalone class? It checks that the call stack only includes this
169          * function and "requires" (meaning was called from the file scope)
170          *
171          * @return bool
172          */
173         public static function shouldExecute() {
174                 global $wgCommandLineMode;
175
176                 if ( !function_exists( 'debug_backtrace' ) ) {
177                         // If someone has a better idea...
178                         return $wgCommandLineMode;
179                 }
180
181                 $bt = debug_backtrace();
182                 $count = count( $bt );
183                 if ( $count < 2 ) {
184                         return false; // sanity
185                 }
186                 if ( $bt[0]['class'] !== 'Maintenance' || $bt[0]['function'] !== 'shouldExecute' ) {
187                         return false; // last call should be to this function
188                 }
189                 $includeFuncs = [ 'require_once', 'require', 'include', 'include_once' ];
190                 for ( $i = 1; $i < $count; $i++ ) {
191                         if ( !in_array( $bt[$i]['function'], $includeFuncs ) ) {
192                                 return false; // previous calls should all be "requires"
193                         }
194                 }
195
196                 return true;
197         }
198
199         /**
200          * Do the actual work. All child classes will need to implement this
201          */
202         abstract public function execute();
203
204         /**
205          * Add a parameter to the script. Will be displayed on --help
206          * with the associated description
207          *
208          * @param string $name The name of the param (help, version, etc)
209          * @param string $description The description of the param to show on --help
210          * @param bool $required Is the param required?
211          * @param bool $withArg Is an argument required with this option?
212          * @param string|bool $shortName Character to use as short name
213          * @param bool $multiOccurrence Can this option be passed multiple times?
214          */
215         protected function addOption( $name, $description, $required = false,
216                 $withArg = false, $shortName = false, $multiOccurrence = false
217         ) {
218                 $this->mParams[$name] = [
219                         'desc' => $description,
220                         'require' => $required,
221                         'withArg' => $withArg,
222                         'shortName' => $shortName,
223                         'multiOccurrence' => $multiOccurrence
224                 ];
225
226                 if ( $shortName !== false ) {
227                         $this->mShortParamsMap[$shortName] = $name;
228                 }
229         }
230
231         /**
232          * Checks to see if a particular param exists.
233          * @param string $name The name of the param
234          * @return bool
235          */
236         protected function hasOption( $name ) {
237                 return isset( $this->mOptions[$name] );
238         }
239
240         /**
241          * Get an option, or return the default.
242          *
243          * If the option was added to support multiple occurrences,
244          * this will return an array.
245          *
246          * @param string $name The name of the param
247          * @param mixed $default Anything you want, default null
248          * @return mixed
249          */
250         protected function getOption( $name, $default = null ) {
251                 if ( $this->hasOption( $name ) ) {
252                         return $this->mOptions[$name];
253                 } else {
254                         // Set it so we don't have to provide the default again
255                         $this->mOptions[$name] = $default;
256
257                         return $this->mOptions[$name];
258                 }
259         }
260
261         /**
262          * Add some args that are needed
263          * @param string $arg Name of the arg, like 'start'
264          * @param string $description Short description of the arg
265          * @param bool $required Is this required?
266          */
267         protected function addArg( $arg, $description, $required = true ) {
268                 $this->mArgList[] = [
269                         'name' => $arg,
270                         'desc' => $description,
271                         'require' => $required
272                 ];
273         }
274
275         /**
276          * Remove an option.  Useful for removing options that won't be used in your script.
277          * @param string $name The option to remove.
278          */
279         protected function deleteOption( $name ) {
280                 unset( $this->mParams[$name] );
281         }
282
283         /**
284          * Set the description text.
285          * @param string $text The text of the description
286          */
287         protected function addDescription( $text ) {
288                 $this->mDescription = $text;
289         }
290
291         /**
292          * Does a given argument exist?
293          * @param int $argId The integer value (from zero) for the arg
294          * @return bool
295          */
296         protected function hasArg( $argId = 0 ) {
297                 return isset( $this->mArgs[$argId] );
298         }
299
300         /**
301          * Get an argument.
302          * @param int $argId The integer value (from zero) for the arg
303          * @param mixed $default The default if it doesn't exist
304          * @return mixed
305          */
306         protected function getArg( $argId = 0, $default = null ) {
307                 return $this->hasArg( $argId ) ? $this->mArgs[$argId] : $default;
308         }
309
310         /**
311          * Set the batch size.
312          * @param int $s The number of operations to do in a batch
313          */
314         protected function setBatchSize( $s = 0 ) {
315                 $this->mBatchSize = $s;
316
317                 // If we support $mBatchSize, show the option.
318                 // Used to be in addDefaultParams, but in order for that to
319                 // work, subclasses would have to call this function in the constructor
320                 // before they called parent::__construct which is just weird
321                 // (and really wasn't done).
322                 if ( $this->mBatchSize ) {
323                         $this->addOption( 'batch-size', 'Run this many operations ' .
324                                 'per batch, default: ' . $this->mBatchSize, false, true );
325                         if ( isset( $this->mParams['batch-size'] ) ) {
326                                 // This seems a little ugly...
327                                 $this->mDependantParameters['batch-size'] = $this->mParams['batch-size'];
328                         }
329                 }
330         }
331
332         /**
333          * Get the script's name
334          * @return string
335          */
336         public function getName() {
337                 return $this->mSelf;
338         }
339
340         /**
341          * Return input from stdin.
342          * @param int $len The number of bytes to read. If null, just return the handle.
343          *   Maintenance::STDIN_ALL returns the full length
344          * @return mixed
345          */
346         protected function getStdin( $len = null ) {
347                 if ( $len == self::STDIN_ALL ) {
348                         return file_get_contents( 'php://stdin' );
349                 }
350                 $f = fopen( 'php://stdin', 'rt' );
351                 if ( !$len ) {
352                         return $f;
353                 }
354                 $input = fgets( $f, $len );
355                 fclose( $f );
356
357                 return rtrim( $input );
358         }
359
360         /**
361          * @return bool
362          */
363         public function isQuiet() {
364                 return $this->mQuiet;
365         }
366
367         /**
368          * Throw some output to the user. Scripts can call this with no fears,
369          * as we handle all --quiet stuff here
370          * @param string $out The text to show to the user
371          * @param mixed $channel Unique identifier for the channel. See function outputChanneled.
372          */
373         protected function output( $out, $channel = null ) {
374                 if ( $this->mQuiet ) {
375                         return;
376                 }
377                 if ( $channel === null ) {
378                         $this->cleanupChanneled();
379                         print $out;
380                 } else {
381                         $out = preg_replace( '/\n\z/', '', $out );
382                         $this->outputChanneled( $out, $channel );
383                 }
384         }
385
386         /**
387          * Throw an error to the user. Doesn't respect --quiet, so don't use
388          * this for non-error output
389          * @param string $err The error to display
390          * @param int $die If > 0, go ahead and die out using this int as the code
391          */
392         protected function error( $err, $die = 0 ) {
393                 $this->outputChanneled( false );
394                 if ( PHP_SAPI == 'cli' ) {
395                         fwrite( STDERR, $err . "\n" );
396                 } else {
397                         print $err;
398                 }
399                 $die = intval( $die );
400                 if ( $die > 0 ) {
401                         die( $die );
402                 }
403         }
404
405         private $atLineStart = true;
406         private $lastChannel = null;
407
408         /**
409          * Clean up channeled output.  Output a newline if necessary.
410          */
411         public function cleanupChanneled() {
412                 if ( !$this->atLineStart ) {
413                         print "\n";
414                         $this->atLineStart = true;
415                 }
416         }
417
418         /**
419          * Message outputter with channeled message support. Messages on the
420          * same channel are concatenated, but any intervening messages in another
421          * channel start a new line.
422          * @param string $msg The message without trailing newline
423          * @param string $channel Channel identifier or null for no
424          *     channel. Channel comparison uses ===.
425          */
426         public function outputChanneled( $msg, $channel = null ) {
427                 if ( $msg === false ) {
428                         $this->cleanupChanneled();
429
430                         return;
431                 }
432
433                 // End the current line if necessary
434                 if ( !$this->atLineStart && $channel !== $this->lastChannel ) {
435                         print "\n";
436                 }
437
438                 print $msg;
439
440                 $this->atLineStart = false;
441                 if ( $channel === null ) {
442                         // For unchanneled messages, output trailing newline immediately
443                         print "\n";
444                         $this->atLineStart = true;
445                 }
446                 $this->lastChannel = $channel;
447         }
448
449         /**
450          * Does the script need different DB access? By default, we give Maintenance
451          * scripts normal rights to the DB. Sometimes, a script needs admin rights
452          * access for a reason and sometimes they want no access. Subclasses should
453          * override and return one of the following values, as needed:
454          *    Maintenance::DB_NONE  -  For no DB access at all
455          *    Maintenance::DB_STD   -  For normal DB access, default
456          *    Maintenance::DB_ADMIN -  For admin DB access
457          * @return int
458          */
459         public function getDbType() {
460                 return self::DB_STD;
461         }
462
463         /**
464          * Add the default parameters to the scripts
465          */
466         protected function addDefaultParams() {
467                 # Generic (non script dependant) options:
468
469                 $this->addOption( 'help', 'Display this help message', false, false, 'h' );
470                 $this->addOption( 'quiet', 'Whether to supress non-error output', false, false, 'q' );
471                 $this->addOption( 'conf', 'Location of LocalSettings.php, if not default', false, true );
472                 $this->addOption( 'wiki', 'For specifying the wiki ID', false, true );
473                 $this->addOption( 'globals', 'Output globals at the end of processing for debugging' );
474                 $this->addOption(
475                         'memory-limit',
476                         'Set a specific memory limit for the script, '
477                                 . '"max" for no limit or "default" to avoid changing it'
478                 );
479                 $this->addOption( 'server', "The protocol and server name to use in URLs, e.g. " .
480                         "http://en.wikipedia.org. This is sometimes necessary because " .
481                         "server name detection may fail in command line scripts.", false, true );
482                 $this->addOption( 'profiler', 'Profiler output format (usually "text")', false, true );
483
484                 # Save generic options to display them separately in help
485                 $this->mGenericParameters = $this->mParams;
486
487                 # Script dependant options:
488
489                 // If we support a DB, show the options
490                 if ( $this->getDbType() > 0 ) {
491                         $this->addOption( 'dbuser', 'The DB user to use for this script', false, true );
492                         $this->addOption( 'dbpass', 'The password to use for this script', false, true );
493                 }
494
495                 # Save additional script dependant options to display
496                 # Â them separately in help
497                 $this->mDependantParameters = array_diff_key( $this->mParams, $this->mGenericParameters );
498         }
499
500         /**
501          * @since 1.24
502          * @return Config
503          */
504         public function getConfig() {
505                 if ( $this->config === null ) {
506                         $this->config = MediaWikiServices::getInstance()->getMainConfig();
507                 }
508
509                 return $this->config;
510         }
511
512         /**
513          * @since 1.24
514          * @param Config $config
515          */
516         public function setConfig( Config $config ) {
517                 $this->config = $config;
518         }
519
520         /**
521          * Indicate that the specified extension must be
522          * loaded before the script can run.
523          *
524          * This *must* be called in the constructor.
525          *
526          * @since 1.28
527          * @param string $name
528          */
529         protected function requireExtension( $name ) {
530                 $this->requiredExtensions[] = $name;
531         }
532
533         /**
534          * Verify that the required extensions are installed
535          *
536          * @since 1.28
537          */
538         public function checkRequiredExtensions() {
539                 $registry = ExtensionRegistry::getInstance();
540                 $missing = [];
541                 foreach ( $this->requiredExtensions as $name ) {
542                         if ( !$registry->isLoaded( $name ) ) {
543                                 $missing[] = $name;
544                         }
545                 }
546
547                 if ( $missing ) {
548                         $joined = implode( ', ', $missing );
549                         $msg = "The following extensions are required to be installed "
550                                 . "for this script to run: $joined. Please enable them and then try again.";
551                         $this->error( $msg, 1 );
552                 }
553         }
554
555         /**
556          * Set triggers like when to try to run deferred updates
557          * @since 1.28
558          */
559         public function setAgentAndTriggers() {
560                 if ( function_exists( 'posix_getpwuid' ) ) {
561                         $agent = posix_getpwuid( posix_geteuid() )['name'];
562                 } else {
563                         $agent = 'sysadmin';
564                 }
565                 $agent .= '@' . wfHostname();
566
567                 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
568                 // Add a comment for easy SHOW PROCESSLIST interpretation
569                 $lbFactory->setAgentName(
570                         mb_strlen( $agent ) > 15 ? mb_substr( $agent, 0, 15 ) . '...' : $agent
571                 );
572                 self::setLBFactoryTriggers( $lbFactory );
573         }
574
575         /**
576          * @param LBFactory $LBFactory
577          * @since 1.28
578          */
579         public static function setLBFactoryTriggers( LBFactory $LBFactory ) {
580                 // Hook into period lag checks which often happen in long-running scripts
581                 $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
582                 $lbFactory->setWaitForReplicationListener(
583                         __METHOD__,
584                         function () {
585                                 global $wgCommandLineMode;
586                                 // Check config in case of JobRunner and unit tests
587                                 if ( $wgCommandLineMode ) {
588                                         DeferredUpdates::tryOpportunisticExecute( 'run' );
589                                 }
590                         }
591                 );
592                 // Check for other windows to run them. A script may read or do a few writes
593                 // to the master but mostly be writing to something else, like a file store.
594                 $lbFactory->getMainLB()->setTransactionListener(
595                         __METHOD__,
596                         function ( $trigger ) {
597                                 global $wgCommandLineMode;
598                                 // Check config in case of JobRunner and unit tests
599                                 if ( $wgCommandLineMode && $trigger === IDatabase::TRIGGER_COMMIT ) {
600                                         DeferredUpdates::tryOpportunisticExecute( 'run' );
601                                 }
602                         }
603                 );
604         }
605
606         /**
607          * Run a child maintenance script. Pass all of the current arguments
608          * to it.
609          * @param string $maintClass A name of a child maintenance class
610          * @param string $classFile Full path of where the child is
611          * @return Maintenance
612          */
613         public function runChild( $maintClass, $classFile = null ) {
614                 // Make sure the class is loaded first
615                 if ( !class_exists( $maintClass ) ) {
616                         if ( $classFile ) {
617                                 require_once $classFile;
618                         }
619                         if ( !class_exists( $maintClass ) ) {
620                                 $this->error( "Cannot spawn child: $maintClass" );
621                         }
622                 }
623
624                 /**
625                  * @var $child Maintenance
626                  */
627                 $child = new $maintClass();
628                 $child->loadParamsAndArgs( $this->mSelf, $this->mOptions, $this->mArgs );
629                 if ( !is_null( $this->mDb ) ) {
630                         $child->setDB( $this->mDb );
631                 }
632
633                 return $child;
634         }
635
636         /**
637          * Do some sanity checking and basic setup
638          */
639         public function setup() {
640                 global $IP, $wgCommandLineMode, $wgRequestTime;
641
642                 # Abort if called from a web server
643                 if ( isset( $_SERVER ) && isset( $_SERVER['REQUEST_METHOD'] ) ) {
644                         $this->error( 'This script must be run from the command line', true );
645                 }
646
647                 if ( $IP === null ) {
648                         $this->error( "\$IP not set, aborting!\n" .
649                                 '(Did you forget to call parent::__construct() in your maintenance script?)', 1 );
650                 }
651
652                 # Make sure we can handle script parameters
653                 if ( !defined( 'HPHP_VERSION' ) && !ini_get( 'register_argc_argv' ) ) {
654                         $this->error( 'Cannot get command line arguments, register_argc_argv is set to false', true );
655                 }
656
657                 // Send PHP warnings and errors to stderr instead of stdout.
658                 // This aids in diagnosing problems, while keeping messages
659                 // out of redirected output.
660                 if ( ini_get( 'display_errors' ) ) {
661                         ini_set( 'display_errors', 'stderr' );
662                 }
663
664                 $this->loadParamsAndArgs();
665                 $this->maybeHelp();
666
667                 # Set the memory limit
668                 # Note we need to set it again later in cache LocalSettings changed it
669                 $this->adjustMemoryLimit();
670
671                 # Set max execution time to 0 (no limit). PHP.net says that
672                 # "When running PHP from the command line the default setting is 0."
673                 # But sometimes this doesn't seem to be the case.
674                 ini_set( 'max_execution_time', 0 );
675
676                 $wgRequestTime = microtime( true );
677
678                 # Define us as being in MediaWiki
679                 define( 'MEDIAWIKI', true );
680
681                 $wgCommandLineMode = true;
682
683                 # Turn off output buffering if it's on
684                 while ( ob_get_level() > 0 ) {
685                         ob_end_flush();
686                 }
687
688                 $this->validateParamsAndArgs();
689         }
690
691         /**
692          * Normally we disable the memory_limit when running admin scripts.
693          * Some scripts may wish to actually set a limit, however, to avoid
694          * blowing up unexpectedly. We also support a --memory-limit option,
695          * to allow sysadmins to explicitly set one if they'd prefer to override
696          * defaults (or for people using Suhosin which yells at you for trying
697          * to disable the limits)
698          * @return string
699          */
700         public function memoryLimit() {
701                 $limit = $this->getOption( 'memory-limit', 'max' );
702                 $limit = trim( $limit, "\" '" ); // trim quotes in case someone misunderstood
703                 return $limit;
704         }
705
706         /**
707          * Adjusts PHP's memory limit to better suit our needs, if needed.
708          */
709         protected function adjustMemoryLimit() {
710                 $limit = $this->memoryLimit();
711                 if ( $limit == 'max' ) {
712                         $limit = -1; // no memory limit
713                 }
714                 if ( $limit != 'default' ) {
715                         ini_set( 'memory_limit', $limit );
716                 }
717         }
718
719         /**
720          * Activate the profiler (assuming $wgProfiler is set)
721          */
722         protected function activateProfiler() {
723                 global $wgProfiler, $wgProfileLimit, $wgTrxProfilerLimits;
724
725                 $output = $this->getOption( 'profiler' );
726                 if ( !$output ) {
727                         return;
728                 }
729
730                 if ( is_array( $wgProfiler ) && isset( $wgProfiler['class'] ) ) {
731                         $class = $wgProfiler['class'];
732                         /** @var Profiler $profiler */
733                         $profiler = new $class(
734                                 [ 'sampling' => 1, 'output' => [ $output ] ]
735                                         + $wgProfiler
736                                         + [ 'threshold' => $wgProfileLimit ]
737                         );
738                         $profiler->setTemplated( true );
739                         Profiler::replaceStubInstance( $profiler );
740                 }
741
742                 $trxProfiler = Profiler::instance()->getTransactionProfiler();
743                 $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
744                 $trxProfiler->setExpectations( $wgTrxProfilerLimits['Maintenance'], __METHOD__ );
745         }
746
747         /**
748          * Clear all params and arguments.
749          */
750         public function clearParamsAndArgs() {
751                 $this->mOptions = [];
752                 $this->mArgs = [];
753                 $this->mInputLoaded = false;
754         }
755
756         /**
757          * Load params and arguments from a given array
758          * of command-line arguments
759          *
760          * @since 1.27
761          * @param array $argv
762          */
763         public function loadWithArgv( $argv ) {
764                 $options = [];
765                 $args = [];
766                 $this->orderedOptions = [];
767
768                 # Parse arguments
769                 for ( $arg = reset( $argv ); $arg !== false; $arg = next( $argv ) ) {
770                         if ( $arg == '--' ) {
771                                 # End of options, remainder should be considered arguments
772                                 $arg = next( $argv );
773                                 while ( $arg !== false ) {
774                                         $args[] = $arg;
775                                         $arg = next( $argv );
776                                 }
777                                 break;
778                         } elseif ( substr( $arg, 0, 2 ) == '--' ) {
779                                 # Long options
780                                 $option = substr( $arg, 2 );
781                                 if ( isset( $this->mParams[$option] ) && $this->mParams[$option]['withArg'] ) {
782                                         $param = next( $argv );
783                                         if ( $param === false ) {
784                                                 $this->error( "\nERROR: $option parameter needs a value after it\n" );
785                                                 $this->maybeHelp( true );
786                                         }
787
788                                         $this->setParam( $options, $option, $param );
789                                 } else {
790                                         $bits = explode( '=', $option, 2 );
791                                         if ( count( $bits ) > 1 ) {
792                                                 $option = $bits[0];
793                                                 $param = $bits[1];
794                                         } else {
795                                                 $param = 1;
796                                         }
797
798                                         $this->setParam( $options, $option, $param );
799                                 }
800                         } elseif ( $arg == '-' ) {
801                                 # Lonely "-", often used to indicate stdin or stdout.
802                                 $args[] = $arg;
803                         } elseif ( substr( $arg, 0, 1 ) == '-' ) {
804                                 # Short options
805                                 $argLength = strlen( $arg );
806                                 for ( $p = 1; $p < $argLength; $p++ ) {
807                                         $option = $arg[$p];
808                                         if ( !isset( $this->mParams[$option] ) && isset( $this->mShortParamsMap[$option] ) ) {
809                                                 $option = $this->mShortParamsMap[$option];
810                                         }
811
812                                         if ( isset( $this->mParams[$option]['withArg'] ) && $this->mParams[$option]['withArg'] ) {
813                                                 $param = next( $argv );
814                                                 if ( $param === false ) {
815                                                         $this->error( "\nERROR: $option parameter needs a value after it\n" );
816                                                         $this->maybeHelp( true );
817                                                 }
818                                                 $this->setParam( $options, $option, $param );
819                                         } else {
820                                                 $this->setParam( $options, $option, 1 );
821                                         }
822                                 }
823                         } else {
824                                 $args[] = $arg;
825                         }
826                 }
827
828                 $this->mOptions = $options;
829                 $this->mArgs = $args;
830                 $this->loadSpecialVars();
831                 $this->mInputLoaded = true;
832         }
833
834         /**
835          * Helper function used solely by loadParamsAndArgs
836          * to prevent code duplication
837          *
838          * This sets the param in the options array based on
839          * whether or not it can be specified multiple times.
840          *
841          * @since 1.27
842          * @param array $options
843          * @param string $option
844          * @param mixed $value
845          */
846         private function setParam( &$options, $option, $value ) {
847                 $this->orderedOptions[] = [ $option, $value ];
848
849                 if ( isset( $this->mParams[$option] ) ) {
850                         $multi = $this->mParams[$option]['multiOccurrence'];
851                 } else {
852                         $multi = false;
853                 }
854                 $exists = array_key_exists( $option, $options );
855                 if ( $multi && $exists ) {
856                         $options[$option][] = $value;
857                 } elseif ( $multi ) {
858                         $options[$option] = [ $value ];
859                 } elseif ( !$exists ) {
860                         $options[$option] = $value;
861                 } else {
862                         $this->error( "\nERROR: $option parameter given twice\n" );
863                         $this->maybeHelp( true );
864                 }
865         }
866
867         /**
868          * Process command line arguments
869          * $mOptions becomes an array with keys set to the option names
870          * $mArgs becomes a zero-based array containing the non-option arguments
871          *
872          * @param string $self The name of the script, if any
873          * @param array $opts An array of options, in form of key=>value
874          * @param array $args An array of command line arguments
875          */
876         public function loadParamsAndArgs( $self = null, $opts = null, $args = null ) {
877                 # If we were given opts or args, set those and return early
878                 if ( $self ) {
879                         $this->mSelf = $self;
880                         $this->mInputLoaded = true;
881                 }
882                 if ( $opts ) {
883                         $this->mOptions = $opts;
884                         $this->mInputLoaded = true;
885                 }
886                 if ( $args ) {
887                         $this->mArgs = $args;
888                         $this->mInputLoaded = true;
889                 }
890
891                 # If we've already loaded input (either by user values or from $argv)
892                 # skip on loading it again. The array_shift() will corrupt values if
893                 # it's run again and again
894                 if ( $this->mInputLoaded ) {
895                         $this->loadSpecialVars();
896
897                         return;
898                 }
899
900                 global $argv;
901                 $this->mSelf = $argv[0];
902                 $this->loadWithArgv( array_slice( $argv, 1 ) );
903         }
904
905         /**
906          * Run some validation checks on the params, etc
907          */
908         protected function validateParamsAndArgs() {
909                 $die = false;
910                 # Check to make sure we've got all the required options
911                 foreach ( $this->mParams as $opt => $info ) {
912                         if ( $info['require'] && !$this->hasOption( $opt ) ) {
913                                 $this->error( "Param $opt required!" );
914                                 $die = true;
915                         }
916                 }
917                 # Check arg list too
918                 foreach ( $this->mArgList as $k => $info ) {
919                         if ( $info['require'] && !$this->hasArg( $k ) ) {
920                                 $this->error( 'Argument <' . $info['name'] . '> required!' );
921                                 $die = true;
922                         }
923                 }
924
925                 if ( $die ) {
926                         $this->maybeHelp( true );
927                 }
928         }
929
930         /**
931          * Handle the special variables that are global to all scripts
932          */
933         protected function loadSpecialVars() {
934                 if ( $this->hasOption( 'dbuser' ) ) {
935                         $this->mDbUser = $this->getOption( 'dbuser' );
936                 }
937                 if ( $this->hasOption( 'dbpass' ) ) {
938                         $this->mDbPass = $this->getOption( 'dbpass' );
939                 }
940                 if ( $this->hasOption( 'quiet' ) ) {
941                         $this->mQuiet = true;
942                 }
943                 if ( $this->hasOption( 'batch-size' ) ) {
944                         $this->mBatchSize = intval( $this->getOption( 'batch-size' ) );
945                 }
946         }
947
948         /**
949          * Maybe show the help.
950          * @param bool $force Whether to force the help to show, default false
951          */
952         protected function maybeHelp( $force = false ) {
953                 if ( !$force && !$this->hasOption( 'help' ) ) {
954                         return;
955                 }
956
957                 $screenWidth = 80; // TODO: Calculate this!
958                 $tab = "    ";
959                 $descWidth = $screenWidth - ( 2 * strlen( $tab ) );
960
961                 ksort( $this->mParams );
962                 $this->mQuiet = false;
963
964                 // Description ...
965                 if ( $this->mDescription ) {
966                         $this->output( "\n" . wordwrap( $this->mDescription, $screenWidth ) . "\n" );
967                 }
968                 $output = "\nUsage: php " . basename( $this->mSelf );
969
970                 // ... append parameters ...
971                 if ( $this->mParams ) {
972                         $output .= " [--" . implode( array_keys( $this->mParams ), "|--" ) . "]";
973                 }
974
975                 // ... and append arguments.
976                 if ( $this->mArgList ) {
977                         $output .= ' ';
978                         foreach ( $this->mArgList as $k => $arg ) {
979                                 if ( $arg['require'] ) {
980                                         $output .= '<' . $arg['name'] . '>';
981                                 } else {
982                                         $output .= '[' . $arg['name'] . ']';
983                                 }
984                                 if ( $k < count( $this->mArgList ) - 1 ) {
985                                         $output .= ' ';
986                                 }
987                         }
988                 }
989                 $this->output( "$output\n\n" );
990
991                 # TODO abstract some repetitive code below
992
993                 // Generic parameters
994                 $this->output( "Generic maintenance parameters:\n" );
995                 foreach ( $this->mGenericParameters as $par => $info ) {
996                         if ( $info['shortName'] !== false ) {
997                                 $par .= " (-{$info['shortName']})";
998                         }
999                         $this->output(
1000                                 wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
1001                                         "\n$tab$tab" ) . "\n"
1002                         );
1003                 }
1004                 $this->output( "\n" );
1005
1006                 $scriptDependantParams = $this->mDependantParameters;
1007                 if ( count( $scriptDependantParams ) > 0 ) {
1008                         $this->output( "Script dependant parameters:\n" );
1009                         // Parameters description
1010                         foreach ( $scriptDependantParams as $par => $info ) {
1011                                 if ( $info['shortName'] !== false ) {
1012                                         $par .= " (-{$info['shortName']})";
1013                                 }
1014                                 $this->output(
1015                                         wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
1016                                                 "\n$tab$tab" ) . "\n"
1017                                 );
1018                         }
1019                         $this->output( "\n" );
1020                 }
1021
1022                 // Script specific parameters not defined on construction by
1023                 // Maintenance::addDefaultParams()
1024                 $scriptSpecificParams = array_diff_key(
1025                         # all script parameters:
1026                         $this->mParams,
1027                         # remove the Maintenance default parameters:
1028                         $this->mGenericParameters,
1029                         $this->mDependantParameters
1030                 );
1031                 if ( count( $scriptSpecificParams ) > 0 ) {
1032                         $this->output( "Script specific parameters:\n" );
1033                         // Parameters description
1034                         foreach ( $scriptSpecificParams as $par => $info ) {
1035                                 if ( $info['shortName'] !== false ) {
1036                                         $par .= " (-{$info['shortName']})";
1037                                 }
1038                                 $this->output(
1039                                         wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
1040                                                 "\n$tab$tab" ) . "\n"
1041                                 );
1042                         }
1043                         $this->output( "\n" );
1044                 }
1045
1046                 // Print arguments
1047                 if ( count( $this->mArgList ) > 0 ) {
1048                         $this->output( "Arguments:\n" );
1049                         // Arguments description
1050                         foreach ( $this->mArgList as $info ) {
1051                                 $openChar = $info['require'] ? '<' : '[';
1052                                 $closeChar = $info['require'] ? '>' : ']';
1053                                 $this->output(
1054                                         wordwrap( "$tab$openChar" . $info['name'] . "$closeChar: " .
1055                                                 $info['desc'], $descWidth, "\n$tab$tab" ) . "\n"
1056                                 );
1057                         }
1058                         $this->output( "\n" );
1059                 }
1060
1061                 die( 1 );
1062         }
1063
1064         /**
1065          * Handle some last-minute setup here.
1066          */
1067         public function finalSetup() {
1068                 global $wgCommandLineMode, $wgShowSQLErrors, $wgServer;
1069                 global $wgDBadminuser, $wgDBadminpassword;
1070                 global $wgDBuser, $wgDBpassword, $wgDBservers, $wgLBFactoryConf;
1071
1072                 # Turn off output buffering again, it might have been turned on in the settings files
1073                 if ( ob_get_level() ) {
1074                         ob_end_flush();
1075                 }
1076                 # Same with these
1077                 $wgCommandLineMode = true;
1078
1079                 # Override $wgServer
1080                 if ( $this->hasOption( 'server' ) ) {
1081                         $wgServer = $this->getOption( 'server', $wgServer );
1082                 }
1083
1084                 # If these were passed, use them
1085                 if ( $this->mDbUser ) {
1086                         $wgDBadminuser = $this->mDbUser;
1087                 }
1088                 if ( $this->mDbPass ) {
1089                         $wgDBadminpassword = $this->mDbPass;
1090                 }
1091
1092                 if ( $this->getDbType() == self::DB_ADMIN && isset( $wgDBadminuser ) ) {
1093                         $wgDBuser = $wgDBadminuser;
1094                         $wgDBpassword = $wgDBadminpassword;
1095
1096                         if ( $wgDBservers ) {
1097                                 /**
1098                                  * @var $wgDBservers array
1099                                  */
1100                                 foreach ( $wgDBservers as $i => $server ) {
1101                                         $wgDBservers[$i]['user'] = $wgDBuser;
1102                                         $wgDBservers[$i]['password'] = $wgDBpassword;
1103                                 }
1104                         }
1105                         if ( isset( $wgLBFactoryConf['serverTemplate'] ) ) {
1106                                 $wgLBFactoryConf['serverTemplate']['user'] = $wgDBuser;
1107                                 $wgLBFactoryConf['serverTemplate']['password'] = $wgDBpassword;
1108                         }
1109                         MediaWikiServices::getInstance()->getDBLoadBalancerFactory()->destroy();
1110                 }
1111
1112                 // Per-script profiling; useful for debugging
1113                 $this->activateProfiler();
1114
1115                 $this->afterFinalSetup();
1116
1117                 $wgShowSQLErrors = true;
1118
1119                 MediaWiki\suppressWarnings();
1120                 set_time_limit( 0 );
1121                 MediaWiki\restoreWarnings();
1122
1123                 $this->adjustMemoryLimit();
1124         }
1125
1126         /**
1127          * Execute a callback function at the end of initialisation
1128          */
1129         protected function afterFinalSetup() {
1130                 if ( defined( 'MW_CMDLINE_CALLBACK' ) ) {
1131                         call_user_func( MW_CMDLINE_CALLBACK );
1132                 }
1133         }
1134
1135         /**
1136          * Potentially debug globals. Originally a feature only
1137          * for refreshLinks
1138          */
1139         public function globals() {
1140                 if ( $this->hasOption( 'globals' ) ) {
1141                         print_r( $GLOBALS );
1142                 }
1143         }
1144
1145         /**
1146          * Generic setup for most installs. Returns the location of LocalSettings
1147          * @return string
1148          */
1149         public function loadSettings() {
1150                 global $wgCommandLineMode, $IP;
1151
1152                 if ( isset( $this->mOptions['conf'] ) ) {
1153                         $settingsFile = $this->mOptions['conf'];
1154                 } elseif ( defined( "MW_CONFIG_FILE" ) ) {
1155                         $settingsFile = MW_CONFIG_FILE;
1156                 } else {
1157                         $settingsFile = "$IP/LocalSettings.php";
1158                 }
1159                 if ( isset( $this->mOptions['wiki'] ) ) {
1160                         $bits = explode( '-', $this->mOptions['wiki'] );
1161                         if ( count( $bits ) == 1 ) {
1162                                 $bits[] = '';
1163                         }
1164                         define( 'MW_DB', $bits[0] );
1165                         define( 'MW_PREFIX', $bits[1] );
1166                 }
1167
1168                 if ( !is_readable( $settingsFile ) ) {
1169                         $this->error( "A copy of your installation's LocalSettings.php\n" .
1170                                 "must exist and be readable in the source directory.\n" .
1171                                 "Use --conf to specify it.", true );
1172                 }
1173                 $wgCommandLineMode = true;
1174
1175                 return $settingsFile;
1176         }
1177
1178         /**
1179          * Support function for cleaning up redundant text records
1180          * @param bool $delete Whether or not to actually delete the records
1181          * @author Rob Church <robchur@gmail.com>
1182          */
1183         public function purgeRedundantText( $delete = true ) {
1184                 # Data should come off the master, wrapped in a transaction
1185                 $dbw = $this->getDB( DB_MASTER );
1186                 $this->beginTransaction( $dbw, __METHOD__ );
1187
1188                 # Get "active" text records from the revisions table
1189                 $cur = [];
1190                 $this->output( 'Searching for active text records in revisions table...' );
1191                 $res = $dbw->select( 'revision', 'rev_text_id', [], __METHOD__, [ 'DISTINCT' ] );
1192                 foreach ( $res as $row ) {
1193                         $cur[] = $row->rev_text_id;
1194                 }
1195                 $this->output( "done.\n" );
1196
1197                 # Get "active" text records from the archive table
1198                 $this->output( 'Searching for active text records in archive table...' );
1199                 $res = $dbw->select( 'archive', 'ar_text_id', [], __METHOD__, [ 'DISTINCT' ] );
1200                 foreach ( $res as $row ) {
1201                         # old pre-MW 1.5 records can have null ar_text_id's.
1202                         if ( $row->ar_text_id !== null ) {
1203                                 $cur[] = $row->ar_text_id;
1204                         }
1205                 }
1206                 $this->output( "done.\n" );
1207
1208                 # Get the IDs of all text records not in these sets
1209                 $this->output( 'Searching for inactive text records...' );
1210                 $cond = 'old_id NOT IN ( ' . $dbw->makeList( $cur ) . ' )';
1211                 $res = $dbw->select( 'text', 'old_id', [ $cond ], __METHOD__, [ 'DISTINCT' ] );
1212                 $old = [];
1213                 foreach ( $res as $row ) {
1214                         $old[] = $row->old_id;
1215                 }
1216                 $this->output( "done.\n" );
1217
1218                 # Inform the user of what we're going to do
1219                 $count = count( $old );
1220                 $this->output( "$count inactive items found.\n" );
1221
1222                 # Delete as appropriate
1223                 if ( $delete && $count ) {
1224                         $this->output( 'Deleting...' );
1225                         $dbw->delete( 'text', [ 'old_id' => $old ], __METHOD__ );
1226                         $this->output( "done.\n" );
1227                 }
1228
1229                 # Done
1230                 $this->commitTransaction( $dbw, __METHOD__ );
1231         }
1232
1233         /**
1234          * Get the maintenance directory.
1235          * @return string
1236          */
1237         protected function getDir() {
1238                 return __DIR__;
1239         }
1240
1241         /**
1242          * Returns a database to be used by current maintenance script. It can be set by setDB().
1243          * If not set, wfGetDB() will be used.
1244          * This function has the same parameters as wfGetDB()
1245          *
1246          * @param int $db DB index (DB_REPLICA/DB_MASTER)
1247          * @param array $groups default: empty array
1248          * @param string|bool $wiki default: current wiki
1249          * @return IMaintainableDatabase
1250          */
1251         protected function getDB( $db, $groups = [], $wiki = false ) {
1252                 if ( is_null( $this->mDb ) ) {
1253                         return wfGetDB( $db, $groups, $wiki );
1254                 } else {
1255                         return $this->mDb;
1256                 }
1257         }
1258
1259         /**
1260          * Sets database object to be returned by getDB().
1261          *
1262          * @param IDatabase $db
1263          */
1264         public function setDB( IDatabase $db ) {
1265                 $this->mDb = $db;
1266         }
1267
1268         /**
1269          * Begin a transcation on a DB
1270          *
1271          * This method makes it clear that begin() is called from a maintenance script,
1272          * which has outermost scope. This is safe, unlike $dbw->begin() called in other places.
1273          *
1274          * @param IDatabase $dbw
1275          * @param string $fname Caller name
1276          * @since 1.27
1277          */
1278         protected function beginTransaction( IDatabase $dbw, $fname ) {
1279                 $dbw->begin( $fname );
1280         }
1281
1282         /**
1283          * Commit the transcation on a DB handle and wait for replica DBs to catch up
1284          *
1285          * This method makes it clear that commit() is called from a maintenance script,
1286          * which has outermost scope. This is safe, unlike $dbw->commit() called in other places.
1287          *
1288          * @param IDatabase $dbw
1289          * @param string $fname Caller name
1290          * @return bool Whether the replica DB wait succeeded
1291          * @since 1.27
1292          */
1293         protected function commitTransaction( IDatabase $dbw, $fname ) {
1294                 $dbw->commit( $fname );
1295                 try {
1296                         $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
1297                         $lbFactory->waitForReplication(
1298                                 [ 'timeout' => 30, 'ifWritesSince' => $this->lastReplicationWait ]
1299                         );
1300                         $this->lastReplicationWait = microtime( true );
1301
1302                         return true;
1303                 } catch ( DBReplicationWaitError $e ) {
1304                         return false;
1305                 }
1306         }
1307
1308         /**
1309          * Rollback the transcation on a DB handle
1310          *
1311          * This method makes it clear that rollback() is called from a maintenance script,
1312          * which has outermost scope. This is safe, unlike $dbw->rollback() called in other places.
1313          *
1314          * @param IDatabase $dbw
1315          * @param string $fname Caller name
1316          * @since 1.27
1317          */
1318         protected function rollbackTransaction( IDatabase $dbw, $fname ) {
1319                 $dbw->rollback( $fname );
1320         }
1321
1322         /**
1323          * Lock the search index
1324          * @param IMaintainableDatabase &$db
1325          */
1326         private function lockSearchindex( $db ) {
1327                 $write = [ 'searchindex' ];
1328                 $read = [
1329                         'page',
1330                         'revision',
1331                         'text',
1332                         'interwiki',
1333                         'l10n_cache',
1334                         'user',
1335                         'page_restrictions'
1336                 ];
1337                 $db->lockTables( $read, $write, __CLASS__ . '::' . __METHOD__ );
1338         }
1339
1340         /**
1341          * Unlock the tables
1342          * @param IMaintainableDatabase &$db
1343          */
1344         private function unlockSearchindex( $db ) {
1345                 $db->unlockTables( __CLASS__ . '::' . __METHOD__ );
1346         }
1347
1348         /**
1349          * Unlock and lock again
1350          * Since the lock is low-priority, queued reads will be able to complete
1351          * @param IMaintainableDatabase &$db
1352          */
1353         private function relockSearchindex( $db ) {
1354                 $this->unlockSearchindex( $db );
1355                 $this->lockSearchindex( $db );
1356         }
1357
1358         /**
1359          * Perform a search index update with locking
1360          * @param int $maxLockTime The maximum time to keep the search index locked.
1361          * @param string $callback The function that will update the function.
1362          * @param IMaintainableDatabase $dbw
1363          * @param array $results
1364          */
1365         public function updateSearchIndex( $maxLockTime, $callback, $dbw, $results ) {
1366                 $lockTime = time();
1367
1368                 # Lock searchindex
1369                 if ( $maxLockTime ) {
1370                         $this->output( "   --- Waiting for lock ---" );
1371                         $this->lockSearchindex( $dbw );
1372                         $lockTime = time();
1373                         $this->output( "\n" );
1374                 }
1375
1376                 # Loop through the results and do a search update
1377                 foreach ( $results as $row ) {
1378                         # Allow reads to be processed
1379                         if ( $maxLockTime && time() > $lockTime + $maxLockTime ) {
1380                                 $this->output( "    --- Relocking ---" );
1381                                 $this->relockSearchindex( $dbw );
1382                                 $lockTime = time();
1383                                 $this->output( "\n" );
1384                         }
1385                         call_user_func( $callback, $dbw, $row );
1386                 }
1387
1388                 # Unlock searchindex
1389                 if ( $maxLockTime ) {
1390                         $this->output( "    --- Unlocking --" );
1391                         $this->unlockSearchindex( $dbw );
1392                         $this->output( "\n" );
1393                 }
1394         }
1395
1396         /**
1397          * Update the searchindex table for a given pageid
1398          * @param IDatabase $dbw A database write handle
1399          * @param int $pageId The page ID to update.
1400          * @return null|string
1401          */
1402         public function updateSearchIndexForPage( $dbw, $pageId ) {
1403                 // Get current revision
1404                 $rev = Revision::loadFromPageId( $dbw, $pageId );
1405                 $title = null;
1406                 if ( $rev ) {
1407                         $titleObj = $rev->getTitle();
1408                         $title = $titleObj->getPrefixedDBkey();
1409                         $this->output( "$title..." );
1410                         # Update searchindex
1411                         $u = new SearchUpdate( $pageId, $titleObj->getText(), $rev->getContent() );
1412                         $u->doUpdate();
1413                         $this->output( "\n" );
1414                 }
1415
1416                 return $title;
1417         }
1418
1419         /**
1420          * Wrapper for posix_isatty()
1421          * We default as considering stdin a tty (for nice readline methods)
1422          * but treating stout as not a tty to avoid color codes
1423          *
1424          * @param mixed $fd File descriptor
1425          * @return bool
1426          */
1427         public static function posix_isatty( $fd ) {
1428                 if ( !function_exists( 'posix_isatty' ) ) {
1429                         return !$fd;
1430                 } else {
1431                         return posix_isatty( $fd );
1432                 }
1433         }
1434
1435         /**
1436          * Prompt the console for input
1437          * @param string $prompt What to begin the line with, like '> '
1438          * @return string Response
1439          */
1440         public static function readconsole( $prompt = '> ' ) {
1441                 static $isatty = null;
1442                 if ( is_null( $isatty ) ) {
1443                         $isatty = self::posix_isatty( 0 /*STDIN*/ );
1444                 }
1445
1446                 if ( $isatty && function_exists( 'readline' ) ) {
1447                         $resp = readline( $prompt );
1448                         if ( $resp === null ) {
1449                                 // Workaround for https://github.com/facebook/hhvm/issues/4776
1450                                 return false;
1451                         } else {
1452                                 return $resp;
1453                         }
1454                 } else {
1455                         if ( $isatty ) {
1456                                 $st = self::readlineEmulation( $prompt );
1457                         } else {
1458                                 if ( feof( STDIN ) ) {
1459                                         $st = false;
1460                                 } else {
1461                                         $st = fgets( STDIN, 1024 );
1462                                 }
1463                         }
1464                         if ( $st === false ) {
1465                                 return false;
1466                         }
1467                         $resp = trim( $st );
1468
1469                         return $resp;
1470                 }
1471         }
1472
1473         /**
1474          * Emulate readline()
1475          * @param string $prompt What to begin the line with, like '> '
1476          * @return string
1477          */
1478         private static function readlineEmulation( $prompt ) {
1479                 $bash = Installer::locateExecutableInDefaultPaths( [ 'bash' ] );
1480                 if ( !wfIsWindows() && $bash ) {
1481                         $retval = false;
1482                         $encPrompt = wfEscapeShellArg( $prompt );
1483                         $command = "read -er -p $encPrompt && echo \"\$REPLY\"";
1484                         $encCommand = wfEscapeShellArg( $command );
1485                         $line = wfShellExec( "$bash -c $encCommand", $retval, [], [ 'walltime' => 0 ] );
1486
1487                         if ( $retval == 0 ) {
1488                                 return $line;
1489                         } elseif ( $retval == 127 ) {
1490                                 // Couldn't execute bash even though we thought we saw it.
1491                                 // Shell probably spit out an error message, sorry :(
1492                                 // Fall through to fgets()...
1493                         } else {
1494                                 // EOF/ctrl+D
1495                                 return false;
1496                         }
1497                 }
1498
1499                 // Fallback... we'll have no editing controls, EWWW
1500                 if ( feof( STDIN ) ) {
1501                         return false;
1502                 }
1503                 print $prompt;
1504
1505                 return fgets( STDIN, 1024 );
1506         }
1507
1508         /**
1509          * Get the terminal size as a two-element array where the first element
1510          * is the width (number of columns) and the second element is the height
1511          * (number of rows).
1512          *
1513          * @return array
1514          */
1515         public static function getTermSize() {
1516                 $default = [ 80, 50 ];
1517                 if ( wfIsWindows() ) {
1518                         return $default;
1519                 }
1520                 // It's possible to get the screen size with VT-100 terminal escapes,
1521                 // but reading the responses is not possible without setting raw mode
1522                 // (unless you want to require the user to press enter), and that
1523                 // requires an ioctl(), which we can't do. So we have to shell out to
1524                 // something that can do the relevant syscalls. There are a few
1525                 // options. Linux and Mac OS X both have "stty size" which does the
1526                 // job directly.
1527                 $retval = false;
1528                 $size = wfShellExec( 'stty size', $retval );
1529                 if ( $retval !== 0 ) {
1530                         return $default;
1531                 }
1532                 if ( !preg_match( '/^(\d+) (\d+)$/', $size, $m ) ) {
1533                         return $default;
1534                 }
1535                 return [ intval( $m[2] ), intval( $m[1] ) ];
1536         }
1537
1538         /**
1539          * Call this to set up the autoloader to allow classes to be used from the
1540          * tests directory.
1541          */
1542         public static function requireTestsAutoloader() {
1543                 require_once __DIR__ . '/../tests/common/TestsAutoLoader.php';
1544         }
1545 }
1546
1547 /**
1548  * Fake maintenance wrapper, mostly used for the web installer/updater
1549  */
1550 class FakeMaintenance extends Maintenance {
1551         protected $mSelf = "FakeMaintenanceScript";
1552
1553         public function execute() {
1554                 return;
1555         }
1556 }
1557
1558 /**
1559  * Class for scripts that perform database maintenance and want to log the
1560  * update in `updatelog` so we can later skip it
1561  */
1562 abstract class LoggedUpdateMaintenance extends Maintenance {
1563         public function __construct() {
1564                 parent::__construct();
1565                 $this->addOption( 'force', 'Run the update even if it was completed already' );
1566                 $this->setBatchSize( 200 );
1567         }
1568
1569         public function execute() {
1570                 $db = $this->getDB( DB_MASTER );
1571                 $key = $this->getUpdateKey();
1572
1573                 if ( !$this->hasOption( 'force' )
1574                         && $db->selectRow( 'updatelog', '1', [ 'ul_key' => $key ], __METHOD__ )
1575                 ) {
1576                         $this->output( "..." . $this->updateSkippedMessage() . "\n" );
1577
1578                         return true;
1579                 }
1580
1581                 if ( !$this->doDBUpdates() ) {
1582                         return false;
1583                 }
1584
1585                 if ( $db->insert( 'updatelog', [ 'ul_key' => $key ], __METHOD__, 'IGNORE' ) ) {
1586                         return true;
1587                 } else {
1588                         $this->output( $this->updatelogFailedMessage() . "\n" );
1589
1590                         return false;
1591                 }
1592         }
1593
1594         /**
1595          * Message to show that the update was done already and was just skipped
1596          * @return string
1597          */
1598         protected function updateSkippedMessage() {
1599                 $key = $this->getUpdateKey();
1600
1601                 return "Update '{$key}' already logged as completed.";
1602         }
1603
1604         /**
1605          * Message to show that the update log was unable to log the completion of this update
1606          * @return string
1607          */
1608         protected function updatelogFailedMessage() {
1609                 $key = $this->getUpdateKey();
1610
1611                 return "Unable to log update '{$key}' as completed.";
1612         }
1613
1614         /**
1615          * Do the actual work. All child classes will need to implement this.
1616          * Return true to log the update as done or false (usually on failure).
1617          * @return bool
1618          */
1619         abstract protected function doDBUpdates();
1620
1621         /**
1622          * Get the update key name to go in the update log table
1623          * @return string
1624          */
1625         abstract protected function getUpdateKey();
1626 }