]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - maintenance/Maintenance.php
MediaWiki 1.17.1
[autoinstalls/mediawiki.git] / maintenance / Maintenance.php
1 <?php
2 /**
3  * @file
4  * @ingroup Maintenance
5  * @defgroup Maintenance Maintenance
6  */
7
8 // Define this so scripts can easily find doMaintenance.php
9 define( 'RUN_MAINTENANCE_IF_MAIN', dirname( __FILE__ ) . '/doMaintenance.php' );
10 define( 'DO_MAINTENANCE', RUN_MAINTENANCE_IF_MAIN ); // original name, harmless
11
12 $maintClass = false;
13
14 // Make sure we're on PHP5 or better
15 if ( version_compare( PHP_VERSION, '5.2.3' ) < 0 ) {
16         die ( "Sorry! This version of MediaWiki requires PHP 5.2.3; you are running " .
17                 PHP_VERSION . ".\n\n" .
18                 "If you are sure you already have PHP 5.2.3 or higher installed, it may be\n" .
19                 "installed in a different path from PHP " . PHP_VERSION . ". Check with your system\n" .
20                 "administrator.\n" );
21 }
22
23 // Wrapper for posix_isatty()
24 if ( !function_exists( 'posix_isatty' ) ) {
25         # We default as considering stdin a tty (for nice readline methods)
26         # but treating stout as not a tty to avoid color codes
27         function posix_isatty( $fd ) {
28                 return !$fd;
29         }
30 }
31
32 /**
33  * Abstract maintenance class for quickly writing and churning out
34  * maintenance scripts with minimal effort. All that _must_ be defined
35  * is the execute() method. See docs/maintenance.txt for more info
36  * and a quick demo of how to use it.
37  *
38  * This program is free software; you can redistribute it and/or modify
39  * it under the terms of the GNU General Public License as published by
40  * the Free Software Foundation; either version 2 of the License, or
41  * (at your option) any later version.
42  *
43  * This program is distributed in the hope that it will be useful,
44  * but WITHOUT ANY WARRANTY; without even the implied warranty of
45  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
46  * GNU General Public License for more details.
47  *
48  * You should have received a copy of the GNU General Public License along
49  * with this program; if not, write to the Free Software Foundation, Inc.,
50  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
51  * http://www.gnu.org/copyleft/gpl.html
52  *
53  * @author Chad Horohoe <chad@anyonecanedit.org>
54  * @since 1.16
55  * @ingroup Maintenance
56  */
57 abstract class Maintenance {
58
59         /**
60          * Constants for DB access type
61          * @see Maintenance::getDbType()
62          */
63         const DB_NONE  = 0;
64         const DB_STD   = 1;
65         const DB_ADMIN = 2;
66
67         // Const for getStdin()
68         const STDIN_ALL = 'all';
69
70         // This is the desired params
71         protected $mParams = array();
72
73         // Array of desired args
74         protected $mArgList = array();
75
76         // This is the list of options that were actually passed
77         protected $mOptions = array();
78
79         // This is the list of arguments that were actually passed
80         protected $mArgs = array();
81
82         // Name of the script currently running
83         protected $mSelf;
84
85         // Special vars for params that are always used
86         protected $mQuiet = false;
87         protected $mDbUser, $mDbPass;
88
89         // A description of the script, children should change this
90         protected $mDescription = '';
91
92         // Have we already loaded our user input?
93         protected $mInputLoaded = false;
94
95         // Batch size. If a script supports this, they should set
96         // a default with setBatchSize()
97         protected $mBatchSize = null;
98
99         /**
100          * List of all the core maintenance scripts. This is added
101          * to scripts added by extensions in $wgMaintenanceScripts
102          * and returned by getMaintenanceScripts()
103          */
104         protected static $mCoreScripts = null;
105
106         /**
107          * Default constructor. Children should call this *first* if implementing
108          * their own constructors
109          */
110         public function __construct() {
111                 // Setup $IP, using MW_INSTALL_PATH if it exists
112                 global $IP;
113                 $IP = strval( getenv( 'MW_INSTALL_PATH' ) ) !== ''
114                         ? getenv( 'MW_INSTALL_PATH' )
115                         : realpath( dirname( __FILE__ ) . '/..' );
116
117                 $this->addDefaultParams();
118                 register_shutdown_function( array( $this, 'outputChanneled' ), false );
119         }
120
121         /**
122          * Should we execute the maintenance script, or just allow it to be included
123          * as a standalone class? It checks that the call stack only includes this
124          * function and a require (meaning was called from the file scope)
125          *
126          * @return Boolean
127          */
128         public static function shouldExecute() {
129                 $bt = debug_backtrace();
130                 if( count( $bt ) !== 2 ) {
131                         return false;
132                 }
133                 return $bt[1]['function'] == 'require_once' &&
134                         $bt[0]['class'] == 'Maintenance' &&
135                         $bt[0]['function'] == 'shouldExecute';
136         }
137
138         /**
139          * Do the actual work. All child classes will need to implement this
140          */
141         abstract public function execute();
142
143         /**
144          * Add a parameter to the script. Will be displayed on --help
145          * with the associated description
146          *
147          * @param $name String: the name of the param (help, version, etc)
148          * @param $description String: the description of the param to show on --help
149          * @param $required Boolean: is the param required?
150          * @param $withArg Boolean: is an argument required with this option?
151          */
152         protected function addOption( $name, $description, $required = false, $withArg = false ) {
153                 $this->mParams[$name] = array( 'desc' => $description, 'require' => $required, 'withArg' => $withArg );
154         }
155
156         /**
157          * Checks to see if a particular param exists.
158          * @param $name String: the name of the param
159          * @return Boolean
160          */
161         protected function hasOption( $name ) {
162                 return isset( $this->mOptions[$name] );
163         }
164
165         /**
166          * Get an option, or return the default
167          * @param $name String: the name of the param
168          * @param $default Mixed: anything you want, default null
169          * @return Mixed
170          */
171         protected function getOption( $name, $default = null ) {
172                 if ( $this->hasOption( $name ) ) {
173                         return $this->mOptions[$name];
174                 } else {
175                         // Set it so we don't have to provide the default again
176                         $this->mOptions[$name] = $default;
177                         return $this->mOptions[$name];
178                 }
179         }
180
181         /**
182          * Add some args that are needed
183          * @param $arg String: name of the arg, like 'start'
184          * @param $description String: short description of the arg
185          * @param $required Boolean: is this required?
186          */
187         protected function addArg( $arg, $description, $required = true ) {
188                 $this->mArgList[] = array(
189                         'name' => $arg,
190                         'desc' => $description,
191                         'require' => $required
192                 );
193         }
194
195         /**
196          * Remove an option.  Useful for removing options that won't be used in your script.
197          * @param $name String: the option to remove.
198          */
199         protected function deleteOption( $name ) {
200                 unset( $this->mParams[$name] );
201         }
202
203         /**
204          * Set the description text.
205          * @param $text String: the text of the description
206          */
207         protected function addDescription( $text ) {
208                 $this->mDescription = $text;
209         }
210
211         /**
212          * Does a given argument exist?
213          * @param $argId Integer: the integer value (from zero) for the arg
214          * @return Boolean
215          */
216         protected function hasArg( $argId = 0 ) {
217                 return isset( $this->mArgs[$argId] );
218         }
219
220         /**
221          * Get an argument.
222          * @param $argId Integer: the integer value (from zero) for the arg
223          * @param $default Mixed: the default if it doesn't exist
224          * @return mixed
225          */
226         protected function getArg( $argId = 0, $default = null ) {
227                 return $this->hasArg( $argId ) ? $this->mArgs[$argId] : $default;
228         }
229
230         /**
231          * Set the batch size.
232          * @param $s Integer: the number of operations to do in a batch
233          */
234         protected function setBatchSize( $s = 0 ) {
235                 $this->mBatchSize = $s;
236         }
237
238         /**
239          * Get the script's name
240          * @return String
241          */
242         public function getName() {
243                 return $this->mSelf;
244         }
245
246         /**
247          * Return input from stdin.
248          * @param $len Integer: the number of bytes to read. If null,
249          *        just return the handle. Maintenance::STDIN_ALL returns
250          *        the full length
251          * @return Mixed
252          */
253         protected function getStdin( $len = null ) {
254                 if ( $len == Maintenance::STDIN_ALL ) {
255                         return file_get_contents( 'php://stdin' );
256                 }
257                 $f = fopen( 'php://stdin', 'rt' );
258                 if ( !$len ) {
259                         return $f;
260                 }
261                 $input = fgets( $f, $len );
262                 fclose( $f );
263                 return rtrim( $input );
264         }
265
266         public function isQuiet() {
267                 return $this->mQuiet;
268         }
269
270         /**
271          * Throw some output to the user. Scripts can call this with no fears,
272          * as we handle all --quiet stuff here
273          * @param $out String: the text to show to the user
274          * @param $channel Mixed: unique identifier for the channel. See
275          *     function outputChanneled.
276          */
277         protected function output( $out, $channel = null ) {
278                 if ( $this->mQuiet ) {
279                         return;
280                 }
281                 if ( $channel === null ) {
282                         $this->cleanupChanneled();
283
284                         $f = fopen( 'php://stdout', 'w' );
285                         fwrite( $f, $out );
286                         fclose( $f );
287                 }
288                 else {
289                         $out = preg_replace( '/\n\z/', '', $out );
290                         $this->outputChanneled( $out, $channel );
291                 }
292         }
293
294         /**
295          * Throw an error to the user. Doesn't respect --quiet, so don't use
296          * this for non-error output
297          * @param $err String: the error to display
298          * @param $die Boolean: If true, go ahead and die out.
299          */
300         protected function error( $err, $die = false ) {
301                 $this->outputChanneled( false );
302                 if ( php_sapi_name() == 'cli' ) {
303                         fwrite( STDERR, $err . "\n" );
304                 } else {
305                         $f = fopen( 'php://stderr', 'w' );
306                         fwrite( $f, $err . "\n" );
307                         fclose( $f );
308                 }
309                 if ( $die ) {
310                         die();
311                 }
312         }
313
314         private $atLineStart = true;
315         private $lastChannel = null;
316
317         /**
318          * Clean up channeled output.  Output a newline if necessary.
319          */
320         public function cleanupChanneled() {
321                 if ( !$this->atLineStart ) {
322                         $handle = fopen( 'php://stdout', 'w' );
323                         fwrite( $handle, "\n" );
324                         fclose( $handle );
325                         $this->atLineStart = true;
326                 }
327         }
328
329         /**
330          * Message outputter with channeled message support. Messages on the
331          * same channel are concatenated, but any intervening messages in another
332          * channel start a new line.
333          * @param $msg String: the message without trailing newline
334          * @param $channel Channel identifier or null for no
335          *     channel. Channel comparison uses ===.
336          */
337         public function outputChanneled( $msg, $channel = null ) {
338                 if ( $msg === false ) {
339                         $this->cleanupChanneled();
340                         return;
341                 }
342
343                 $handle = fopen( 'php://stdout', 'w' );
344
345                 // End the current line if necessary
346                 if ( !$this->atLineStart && $channel !== $this->lastChannel ) {
347                         fwrite( $handle, "\n" );
348                 }
349
350                 fwrite( $handle, $msg );
351
352                 $this->atLineStart = false;
353                 if ( $channel === null ) {
354                         // For unchanneled messages, output trailing newline immediately
355                         fwrite( $handle, "\n" );
356                         $this->atLineStart = true;
357                 }
358                 $this->lastChannel = $channel;
359
360                 // Cleanup handle
361                 fclose( $handle );
362         }
363
364         /**
365          * Does the script need different DB access? By default, we give Maintenance
366          * scripts normal rights to the DB. Sometimes, a script needs admin rights
367          * access for a reason and sometimes they want no access. Subclasses should
368          * override and return one of the following values, as needed:
369          *    Maintenance::DB_NONE  -  For no DB access at all
370          *    Maintenance::DB_STD   -  For normal DB access, default
371          *    Maintenance::DB_ADMIN -  For admin DB access
372          * @return Integer
373          */
374         public function getDbType() {
375                 return Maintenance::DB_STD;
376         }
377
378         /**
379          * Add the default parameters to the scripts
380          */
381         protected function addDefaultParams() {
382                 $this->addOption( 'help', 'Display this help message' );
383                 $this->addOption( 'quiet', 'Whether to supress non-error output' );
384                 $this->addOption( 'conf', 'Location of LocalSettings.php, if not default', false, true );
385                 $this->addOption( 'wiki', 'For specifying the wiki ID', false, true );
386                 $this->addOption( 'globals', 'Output globals at the end of processing for debugging' );
387                 $this->addOption( 'memory-limit', 'Set a specific memory limit for the script, "max" for no limit or "default" to avoid changing it' );
388                 $this->addOption( 'server', "The protocol and server name to use in URLs, e.g. " .
389                                 "http://en.wikipedia.org. This is sometimes necessary because " .
390                                 "server name detection may fail in command line scripts.", false, true );
391                 // If we support a DB, show the options
392                 if ( $this->getDbType() > 0 ) {
393                         $this->addOption( 'dbuser', 'The DB user to use for this script', false, true );
394                         $this->addOption( 'dbpass', 'The password to use for this script', false, true );
395                 }
396                 // If we support $mBatchSize, show the option
397                 if ( $this->mBatchSize ) {
398                         $this->addOption( 'batch-size', 'Run this many operations ' .
399                                 'per batch, default: ' . $this->mBatchSize, false, true );
400                 }
401         }
402
403         /**
404          * Run a child maintenance script. Pass all of the current arguments
405          * to it.
406          * @param $maintClass String: a name of a child maintenance class
407          * @param $classFile String: full path of where the child is
408          * @return Maintenance child
409          */
410         public function runChild( $maintClass, $classFile = null ) {
411                 // Make sure the class is loaded first
412                 if ( !class_exists( $maintClass ) ) {
413                         if ( $classFile ) {
414                                 require_once( $classFile );
415                         }
416                         if ( !class_exists( $maintClass ) ) {
417                                 $this->error( "Cannot spawn child: $maintClass" );
418                         }
419                 }
420
421                 $child = new $maintClass();
422                 $child->loadParamsAndArgs( $this->mSelf, $this->mOptions, $this->mArgs );
423                 return $child;
424         }
425
426         /**
427          * Do some sanity checking and basic setup
428          */
429         public function setup() {
430                 global $wgCommandLineMode, $wgRequestTime;
431
432                 # Abort if called from a web server
433                 if ( isset( $_SERVER ) && isset( $_SERVER['REQUEST_METHOD'] ) ) {
434                         $this->error( 'This script must be run from the command line', true );
435                 }
436
437                 # Make sure we can handle script parameters
438                 if ( !ini_get( 'register_argc_argv' ) ) {
439                         $this->error( 'Cannot get command line arguments, register_argc_argv is set to false', true );
440                 }
441
442                 if ( version_compare( phpversion(), '5.2.4' ) >= 0 ) {
443                         // Send PHP warnings and errors to stderr instead of stdout.
444                         // This aids in diagnosing problems, while keeping messages
445                         // out of redirected output.
446                         if ( ini_get( 'display_errors' ) ) {
447                                 ini_set( 'display_errors', 'stderr' );
448                         }
449
450                         // Don't touch the setting on earlier versions of PHP,
451                         // as setting it would disable output if you'd wanted it.
452
453                         // Note that exceptions are also sent to stderr when
454                         // command-line mode is on, regardless of PHP version.
455                 }
456
457                 $this->loadParamsAndArgs();
458                 $this->maybeHelp();
459
460                 # Set the memory limit
461                 # Note we need to set it again later in cache LocalSettings changed it
462                 $this->adjustMemoryLimit();
463
464                 # Set max execution time to 0 (no limit). PHP.net says that
465                 # "When running PHP from the command line the default setting is 0."
466                 # But sometimes this doesn't seem to be the case.
467                 ini_set( 'max_execution_time', 0 );
468
469                 $wgRequestTime = microtime( true );
470
471                 # Define us as being in MediaWiki
472                 define( 'MEDIAWIKI', true );
473
474                 $wgCommandLineMode = true;
475                 # Turn off output buffering if it's on
476                 @ob_end_flush();
477
478                 $this->validateParamsAndArgs();
479         }
480
481         /**
482          * Normally we disable the memory_limit when running admin scripts.
483          * Some scripts may wish to actually set a limit, however, to avoid
484          * blowing up unexpectedly. We also support a --memory-limit option,
485          * to allow sysadmins to explicitly set one if they'd prefer to override
486          * defaults (or for people using Suhosin which yells at you for trying
487          * to disable the limits)
488          */
489         public function memoryLimit() {
490                 $limit = $this->getOption( 'memory-limit', 'max' );
491                 $limit = trim( $limit, "\" '" ); // trim quotes in case someone misunderstood
492                 return $limit;
493         }
494
495         /**
496          * Adjusts PHP's memory limit to better suit our needs, if needed.
497          */
498         protected function adjustMemoryLimit() {
499                 $limit = $this->memoryLimit();
500                 if ( $limit == 'max' ) {
501                         $limit = -1; // no memory limit
502                 }
503                 if ( $limit != 'default' ) {
504                         ini_set( 'memory_limit', $limit );
505                 }
506         }
507
508         /**
509          * Clear all params and arguments.
510          */
511         public function clearParamsAndArgs() {
512                 $this->mOptions = array();
513                 $this->mArgs = array();
514                 $this->mInputLoaded = false;
515         }
516
517         /**
518          * Process command line arguments
519          * $mOptions becomes an array with keys set to the option names
520          * $mArgs becomes a zero-based array containing the non-option arguments
521          *
522          * @param $self String The name of the script, if any
523          * @param $opts Array An array of options, in form of key=>value
524          * @param $args Array An array of command line arguments
525          */
526         public function loadParamsAndArgs( $self = null, $opts = null, $args = null ) {
527                 # If we were given opts or args, set those and return early
528                 if ( $self ) {
529                         $this->mSelf = $self;
530                         $this->mInputLoaded = true;
531                 }
532                 if ( $opts ) {
533                         $this->mOptions = $opts;
534                         $this->mInputLoaded = true;
535                 }
536                 if ( $args ) {
537                         $this->mArgs = $args;
538                         $this->mInputLoaded = true;
539                 }
540
541                 # If we've already loaded input (either by user values or from $argv)
542                 # skip on loading it again. The array_shift() will corrupt values if
543                 # it's run again and again
544                 if ( $this->mInputLoaded ) {
545                         $this->loadSpecialVars();
546                         return;
547                 }
548
549                 global $argv;
550                 $this->mSelf = array_shift( $argv );
551
552                 $options = array();
553                 $args = array();
554
555                 # Parse arguments
556                 for ( $arg = reset( $argv ); $arg !== false; $arg = next( $argv ) ) {
557                         if ( $arg == '--' ) {
558                                 # End of options, remainder should be considered arguments
559                                 $arg = next( $argv );
560                                 while ( $arg !== false ) {
561                                         $args[] = $arg;
562                                         $arg = next( $argv );
563                                 }
564                                 break;
565                         } elseif ( substr( $arg, 0, 2 ) == '--' ) {
566                                 # Long options
567                                 $option = substr( $arg, 2 );
568                                 if ( array_key_exists( $option, $options ) ) {
569                                         $this->error( "\nERROR: $option parameter given twice\n" );
570                                         $this->maybeHelp( true );
571                                 }
572                                 if ( isset( $this->mParams[$option] ) && $this->mParams[$option]['withArg'] ) {
573                                         $param = next( $argv );
574                                         if ( $param === false ) {
575                                                 $this->error( "\nERROR: $option parameter needs a value after it\n" );
576                                                 $this->maybeHelp( true );
577                                         }
578                                         $options[$option] = $param;
579                                 } else {
580                                         $bits = explode( '=', $option, 2 );
581                                         if ( count( $bits ) > 1 ) {
582                                                 $option = $bits[0];
583                                                 $param = $bits[1];
584                                         } else {
585                                                 $param = 1;
586                                         }
587                                         $options[$option] = $param;
588                                 }
589                         } elseif ( substr( $arg, 0, 1 ) == '-' ) {
590                                 # Short options
591                                 for ( $p = 1; $p < strlen( $arg ); $p++ ) {
592                                         $option = $arg { $p } ;
593                                         if ( array_key_exists( $option, $options ) ) {
594                                                 $this->error( "\nERROR: $option parameter given twice\n" );
595                                                 $this->maybeHelp( true );
596                                         }
597                                         if ( isset( $this->mParams[$option]['withArg'] ) && $this->mParams[$option]['withArg'] ) {
598                                                 $param = next( $argv );
599                                                 if ( $param === false ) {
600                                                         $this->error( "\nERROR: $option parameter needs a value after it\n" );
601                                                         $this->maybeHelp( true );
602                                                 }
603                                                 $options[$option] = $param;
604                                         } else {
605                                                 $options[$option] = 1;
606                                         }
607                                 }
608                         } else {
609                                 $args[] = $arg;
610                         }
611                 }
612
613                 $this->mOptions = $options;
614                 $this->mArgs = $args;
615                 $this->loadSpecialVars();
616                 $this->mInputLoaded = true;
617         }
618
619         /**
620          * Run some validation checks on the params, etc
621          */
622         protected function validateParamsAndArgs() {
623                 $die = false;
624                 # Check to make sure we've got all the required options
625                 foreach ( $this->mParams as $opt => $info ) {
626                         if ( $info['require'] && !$this->hasOption( $opt ) ) {
627                                 $this->error( "Param $opt required!" );
628                                 $die = true;
629                         }
630                 }
631                 # Check arg list too
632                 foreach ( $this->mArgList as $k => $info ) {
633                         if ( $info['require'] && !$this->hasArg( $k ) ) {
634                                 $this->error( 'Argument <' . $info['name'] . '> required!' );
635                                 $die = true;
636                         }
637                 }
638
639                 if ( $die ) {
640                         $this->maybeHelp( true );
641                 }
642         }
643
644         /**
645          * Handle the special variables that are global to all scripts
646          */
647         protected function loadSpecialVars() {
648                 if ( $this->hasOption( 'dbuser' ) ) {
649                         $this->mDbUser = $this->getOption( 'dbuser' );
650                 }
651                 if ( $this->hasOption( 'dbpass' ) ) {
652                         $this->mDbPass = $this->getOption( 'dbpass' );
653                 }
654                 if ( $this->hasOption( 'quiet' ) ) {
655                         $this->mQuiet = true;
656                 }
657                 if ( $this->hasOption( 'batch-size' ) ) {
658                         $this->mBatchSize = $this->getOption( 'batch-size' );
659                 }
660         }
661
662         /**
663          * Maybe show the help.
664          * @param $force boolean Whether to force the help to show, default false
665          */
666         protected function maybeHelp( $force = false ) {
667                 if( !$force && !$this->hasOption( 'help' ) ) {
668                         return;
669                 }
670
671                 $screenWidth = 80; // TODO: Caculate this!
672                 $tab = "    ";
673                 $descWidth = $screenWidth - ( 2 * strlen( $tab ) );
674
675                 ksort( $this->mParams );
676                 $this->mQuiet = false;
677
678                 // Description ...
679                 if ( $this->mDescription ) {
680                         $this->output( "\n" . $this->mDescription . "\n" );
681                 }
682                 $output = "\nUsage: php " . basename( $this->mSelf );
683
684                 // ... append parameters ...
685                 if ( $this->mParams ) {
686                         $output .= " [--" . implode( array_keys( $this->mParams ), "|--" ) . "]";
687                 }
688
689                 // ... and append arguments.
690                 if ( $this->mArgList ) {
691                         $output .= ' ';
692                         foreach ( $this->mArgList as $k => $arg ) {
693                                 if ( $arg['require'] ) {
694                                         $output .= '<' . $arg['name'] . '>';
695                                 } else {
696                                         $output .= '[' . $arg['name'] . ']';
697                                 }
698                                 if ( $k < count( $this->mArgList ) - 1 )
699                                         $output .= ' ';
700                         }
701                 }
702                 $this->output( "$output\n\n" );
703
704                 // Parameters description
705                 foreach ( $this->mParams as $par => $info ) {
706                         $this->output(
707                                 wordwrap( "$tab--$par: " . $info['desc'], $descWidth,
708                                                 "\n$tab$tab" ) . "\n"
709                         );
710                 }
711
712                 // Arguments description
713                 foreach ( $this->mArgList as $info ) {
714                         $openChar = $info['require'] ? '<' : '[';
715                         $closeChar = $info['require'] ? '>' : ']';
716                         $this->output(
717                                 wordwrap( "$tab$openChar" . $info['name'] . "$closeChar: " .
718                                         $info['desc'], $descWidth, "\n$tab$tab" ) . "\n"
719                         );
720                 }
721
722                 die( 1 );
723         }
724
725         /**
726          * Handle some last-minute setup here.
727          */
728         public function finalSetup() {
729                 global $wgCommandLineMode, $wgShowSQLErrors, $wgServer;
730                 global $wgProfiling, $wgDBadminuser, $wgDBadminpassword;
731                 global $wgDBuser, $wgDBpassword, $wgDBservers, $wgLBFactoryConf;
732
733                 # Turn off output buffering again, it might have been turned on in the settings files
734                 if ( ob_get_level() ) {
735                         ob_end_flush();
736                 }
737                 # Same with these
738                 $wgCommandLineMode = true;
739
740                 # Override $wgServer
741                 if( $this->hasOption( 'server') ) {
742                         $wgServer = $this->getOption( 'server', $wgServer );
743                 }
744
745                 # If these were passed, use them
746                 if ( $this->mDbUser ) {
747                         $wgDBadminuser = $this->mDbUser;
748                 }
749                 if ( $this->mDbPass ) {
750                         $wgDBadminpassword = $this->mDbPass;
751                 }
752
753                 if ( $this->getDbType() == self::DB_ADMIN && isset( $wgDBadminuser ) ) {
754                         $wgDBuser = $wgDBadminuser;
755                         $wgDBpassword = $wgDBadminpassword;
756
757                         if ( $wgDBservers ) {
758                                 foreach ( $wgDBservers as $i => $server ) {
759                                         $wgDBservers[$i]['user'] = $wgDBuser;
760                                         $wgDBservers[$i]['password'] = $wgDBpassword;
761                                 }
762                         }
763                         if ( isset( $wgLBFactoryConf['serverTemplate'] ) ) {
764                                 $wgLBFactoryConf['serverTemplate']['user'] = $wgDBuser;
765                                 $wgLBFactoryConf['serverTemplate']['password'] = $wgDBpassword;
766                         }
767                         LBFactory::destroyInstance();
768                 }
769
770                 $this->afterFinalSetup();
771
772                 $wgShowSQLErrors = true;
773                 @set_time_limit( 0 );
774                 $this->adjustMemoryLimit();
775
776                 $wgProfiling = false; // only for Profiler.php mode; avoids OOM errors
777         }
778
779         /**
780          * Execute a callback function at the end of initialisation
781          */
782         protected function afterFinalSetup() {
783                 if ( defined( 'MW_CMDLINE_CALLBACK' ) ) {
784                         call_user_func( MW_CMDLINE_CALLBACK );
785                 }
786         }
787
788         /**
789          * Potentially debug globals. Originally a feature only
790          * for refreshLinks
791          */
792         public function globals() {
793                 if ( $this->hasOption( 'globals' ) ) {
794                         print_r( $GLOBALS );
795                 }
796         }
797
798         /**
799          * Do setup specific to WMF
800          */
801         public function loadWikimediaSettings() {
802                 global $IP, $wgNoDBParam, $wgUseNormalUser, $wgConf, $site, $lang;
803
804                 if ( empty( $wgNoDBParam ) ) {
805                         # Check if we were passed a db name
806                         if ( isset( $this->mOptions['wiki'] ) ) {
807                                 $db = $this->mOptions['wiki'];
808                         } else {
809                                 $db = array_shift( $this->mArgs );
810                         }
811                         list( $site, $lang ) = $wgConf->siteFromDB( $db );
812
813                         # If not, work out the language and site the old way
814                         if ( is_null( $site ) || is_null( $lang ) ) {
815                                 if ( !$db ) {
816                                         $lang = 'aa';
817                                 } else {
818                                         $lang = $db;
819                                 }
820                                 if ( isset( $this->mArgs[0] ) ) {
821                                         $site = array_shift( $this->mArgs );
822                                 } else {
823                                         $site = 'wikipedia';
824                                 }
825                         }
826                 } else {
827                         $lang = 'aa';
828                         $site = 'wikipedia';
829                 }
830
831                 # This is for the IRC scripts, which now run as the apache user
832                 # The apache user doesn't have access to the wikiadmin_pass command
833                 if ( $_ENV['USER'] == 'apache' ) {
834                 # if ( posix_geteuid() == 48 ) {
835                         $wgUseNormalUser = true;
836                 }
837
838                 putenv( 'wikilang=' . $lang );
839
840                 ini_set( 'include_path', ".:$IP:$IP/includes:$IP/languages:$IP/maintenance" );
841
842                 if ( $lang == 'test' && $site == 'wikipedia' ) {
843                         define( 'TESTWIKI', 1 );
844                 }
845         }
846
847         /**
848          * Generic setup for most installs. Returns the location of LocalSettings
849          * @return String
850          */
851         public function loadSettings() {
852                 global $wgWikiFarm, $wgCommandLineMode, $IP;
853
854                 $wgWikiFarm = false;
855                 if ( isset( $this->mOptions['conf'] ) ) {
856                         $settingsFile = $this->mOptions['conf'];
857                 } else if ( defined("MW_CONFIG_FILE") ) {
858                         $settingsFile = MW_CONFIG_FILE;
859                 } else {
860                         $settingsFile = "$IP/LocalSettings.php";
861                 }
862                 if ( isset( $this->mOptions['wiki'] ) ) {
863                         $bits = explode( '-', $this->mOptions['wiki'] );
864                         if ( count( $bits ) == 1 ) {
865                                 $bits[] = '';
866                         }
867                         define( 'MW_DB', $bits[0] );
868                         define( 'MW_PREFIX', $bits[1] );
869                 }
870
871                 if ( !is_readable( $settingsFile ) ) {
872                         $this->error( "A copy of your installation's LocalSettings.php\n" .
873                                                 "must exist and be readable in the source directory.\n" .
874                                                 "Use --conf to specify it." , true );
875                 }
876                 $wgCommandLineMode = true;
877                 return $settingsFile;
878         }
879
880         /**
881          * Support function for cleaning up redundant text records
882          * @param $delete Boolean: whether or not to actually delete the records
883          * @author Rob Church <robchur@gmail.com>
884          */
885         public function purgeRedundantText( $delete = true ) {
886                 # Data should come off the master, wrapped in a transaction
887                 $dbw = wfGetDB( DB_MASTER );
888                 $dbw->begin();
889
890                 $tbl_arc = $dbw->tableName( 'archive' );
891                 $tbl_rev = $dbw->tableName( 'revision' );
892                 $tbl_txt = $dbw->tableName( 'text' );
893
894                 # Get "active" text records from the revisions table
895                 $this->output( 'Searching for active text records in revisions table...' );
896                 $res = $dbw->query( "SELECT DISTINCT rev_text_id FROM $tbl_rev" );
897                 foreach ( $res as $row ) {
898                         $cur[] = $row->rev_text_id;
899                 }
900                 $this->output( "done.\n" );
901
902                 # Get "active" text records from the archive table
903                 $this->output( 'Searching for active text records in archive table...' );
904                 $res = $dbw->query( "SELECT DISTINCT ar_text_id FROM $tbl_arc" );
905                 foreach ( $res as $row ) {
906                         $cur[] = $row->ar_text_id;
907                 }
908                 $this->output( "done.\n" );
909
910                 # Get the IDs of all text records not in these sets
911                 $this->output( 'Searching for inactive text records...' );
912                 $set = implode( ', ', $cur );
913                 $res = $dbw->query( "SELECT old_id FROM $tbl_txt WHERE old_id NOT IN ( $set )" );
914                 $old = array();
915                 foreach ( $res as $row ) {
916                         $old[] = $row->old_id;
917                 }
918                 $this->output( "done.\n" );
919
920                 # Inform the user of what we're going to do
921                 $count = count( $old );
922                 $this->output( "$count inactive items found.\n" );
923
924                 # Delete as appropriate
925                 if ( $delete && $count ) {
926                         $this->output( 'Deleting...' );
927                         $set = implode( ', ', $old );
928                         $dbw->query( "DELETE FROM $tbl_txt WHERE old_id IN ( $set )" );
929                         $this->output( "done.\n" );
930                 }
931
932                 # Done
933                 $dbw->commit();
934         }
935
936         /**
937          * Get the maintenance directory.
938          */
939         protected function getDir() {
940                 return dirname( __FILE__ );
941         }
942
943         /**
944          * Get the list of available maintenance scripts. Note
945          * that if you call this _before_ calling doMaintenance
946          * you won't have any extensions in it yet
947          * @return Array
948          */
949         public static function getMaintenanceScripts() {
950                 global $wgMaintenanceScripts;
951                 return $wgMaintenanceScripts + self::getCoreScripts();
952         }
953
954         /**
955          * Return all of the core maintenance scripts
956          * @return array
957          */
958         protected static function getCoreScripts() {
959                 if ( !self::$mCoreScripts ) {
960                         $paths = array(
961                                 dirname( __FILE__ ),
962                                 dirname( __FILE__ ) . '/gearman',
963                                 dirname( __FILE__ ) . '/language',
964                                 dirname( __FILE__ ) . '/storage',
965                         );
966                         self::$mCoreScripts = array();
967                         foreach ( $paths as $p ) {
968                                 $handle = opendir( $p );
969                                 while ( ( $file = readdir( $handle ) ) !== false ) {
970                                         if ( $file == 'Maintenance.php' ) {
971                                                 continue;
972                                         }
973                                         $file = $p . '/' . $file;
974                                         if ( is_dir( $file ) || !strpos( $file, '.php' ) ||
975                                                 ( strpos( file_get_contents( $file ), '$maintClass' ) === false ) ) {
976                                                 continue;
977                                         }
978                                         require( $file );
979                                         $vars = get_defined_vars();
980                                         if ( array_key_exists( 'maintClass', $vars ) ) {
981                                                 self::$mCoreScripts[$vars['maintClass']] = $file;
982                                         }
983                                 }
984                                 closedir( $handle );
985                         }
986                 }
987                 return self::$mCoreScripts;
988         }
989
990         /**
991          * Lock the search index
992          * @param &$db Database object
993          */
994         private function lockSearchindex( &$db ) {
995                 $write = array( 'searchindex' );
996                 $read = array( 'page', 'revision', 'text', 'interwiki', 'l10n_cache' );
997                 $db->lockTables( $read, $write, __CLASS__ . '::' . __METHOD__ );
998         }
999
1000         /**
1001          * Unlock the tables
1002          * @param &$db Database object
1003          */
1004         private function unlockSearchindex( &$db ) {
1005                 $db->unlockTables(  __CLASS__ . '::' . __METHOD__ );
1006         }
1007
1008         /**
1009          * Unlock and lock again
1010          * Since the lock is low-priority, queued reads will be able to complete
1011          * @param &$db Database object
1012          */
1013         private function relockSearchindex( &$db ) {
1014                 $this->unlockSearchindex( $db );
1015                 $this->lockSearchindex( $db );
1016         }
1017
1018         /**
1019          * Perform a search index update with locking
1020          * @param $maxLockTime Integer: the maximum time to keep the search index locked.
1021          * @param $callback callback String: the function that will update the function.
1022          * @param $dbw Database object
1023          * @param $results
1024          */
1025         public function updateSearchIndex( $maxLockTime, $callback, $dbw, $results ) {
1026                 $lockTime = time();
1027
1028                 # Lock searchindex
1029                 if ( $maxLockTime ) {
1030                         $this->output( "   --- Waiting for lock ---" );
1031                         $this->lockSearchindex( $dbw );
1032                         $lockTime = time();
1033                         $this->output( "\n" );
1034                 }
1035
1036                 # Loop through the results and do a search update
1037                 foreach ( $results as $row ) {
1038                         # Allow reads to be processed
1039                         if ( $maxLockTime && time() > $lockTime + $maxLockTime ) {
1040                                 $this->output( "    --- Relocking ---" );
1041                                 $this->relockSearchindex( $dbw );
1042                                 $lockTime = time();
1043                                 $this->output( "\n" );
1044                         }
1045                         call_user_func( $callback, $dbw, $row );
1046                 }
1047
1048                 # Unlock searchindex
1049                 if ( $maxLockTime ) {
1050                         $this->output( "    --- Unlocking --" );
1051                         $this->unlockSearchindex( $dbw );
1052                         $this->output( "\n" );
1053                 }
1054
1055         }
1056
1057         /**
1058          * Update the searchindex table for a given pageid
1059          * @param $dbw Database: a database write handle
1060          * @param $pageId Integer: the page ID to update.
1061          */
1062         public function updateSearchIndexForPage( $dbw, $pageId ) {
1063                 // Get current revision
1064                 $rev = Revision::loadFromPageId( $dbw, $pageId );
1065                 $title = null;
1066                 if ( $rev ) {
1067                         $titleObj = $rev->getTitle();
1068                         $title = $titleObj->getPrefixedDBkey();
1069                         $this->output( "$title..." );
1070                         # Update searchindex
1071                         $u = new SearchUpdate( $pageId, $titleObj->getText(), $rev->getText() );
1072                         $u->doUpdate();
1073                         $this->output( "\n" );
1074                 }
1075                 return $title;
1076         }
1077
1078         /**
1079          * Prompt the console for input
1080          * @param $prompt String what to begin the line with, like '> '
1081          * @return String response
1082          */
1083         public static function readconsole( $prompt = '> ' ) {
1084                 static $isatty = null;
1085                 if ( is_null( $isatty ) ) {
1086                         if ( posix_isatty( 0 /*STDIN*/ ) ) {
1087                                 $isatty = true;
1088                         } else {
1089                                 $isatty = false;
1090                         }
1091                 }
1092
1093                 if ( $isatty && function_exists( 'readline' ) ) {
1094                         return readline( $prompt );
1095                 } else {
1096                         if ( $isatty ) {
1097                                 $st = self::readlineEmulation( $prompt );
1098                         } else {
1099                                 if ( feof( STDIN ) ) {
1100                                         $st = false;
1101                                 } else {
1102                                         $st = fgets( STDIN, 1024 );
1103                                 }
1104                         }
1105                         if ( $st === false ) return false;
1106                         $resp = trim( $st );
1107                         return $resp;
1108                 }
1109         }
1110
1111         /**
1112          * Emulate readline()
1113          * @param $prompt String what to begin the line with, like '> '
1114          * @return String
1115          */
1116         private static function readlineEmulation( $prompt ) {
1117                 $bash = Installer::locateExecutableInDefaultPaths( array( 'bash' ) );
1118                 if ( !wfIsWindows() && $bash ) {
1119                         $retval = false;
1120                         $encPrompt = wfEscapeShellArg( $prompt );
1121                         $command = "read -er -p $encPrompt && echo \"\$REPLY\"";
1122                         $encCommand = wfEscapeShellArg( $command );
1123                         $line = wfShellExec( "$bash -c $encCommand", $retval );
1124
1125                         if ( $retval == 0 ) {
1126                                 return $line;
1127                         } elseif ( $retval == 127 ) {
1128                                 // Couldn't execute bash even though we thought we saw it.
1129                                 // Shell probably spit out an error message, sorry :(
1130                                 // Fall through to fgets()...
1131                         } else {
1132                                 // EOF/ctrl+D
1133                                 return false;
1134                         }
1135                 }
1136
1137                 // Fallback... we'll have no editing controls, EWWW
1138                 if ( feof( STDIN ) ) {
1139                         return false;
1140                 }
1141                 print $prompt;
1142                 return fgets( STDIN, 1024 );
1143         }
1144 }
1145
1146 class FakeMaintenance extends Maintenance {
1147         protected $mSelf = "FakeMaintenanceScript";
1148         public function execute() {
1149                 return;
1150         }
1151 }
1152