[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
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 // Make sure we're on PHP5.3.2 or better 24 if ( !function_exists( 'version_compare' ) || version_compare( PHP_VERSION, '5.3.2' ) < 0 ) { 25 // We need to use dirname( __FILE__ ) here cause __DIR__ is PHP5.3+ 26 require_once dirname( __FILE__ ) . '/../includes/PHPVersionError.php'; 27 wfPHPVersionError( 'cli' ); 28 } 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 /** 42 * Abstract maintenance class for quickly writing and churning out 43 * maintenance scripts with minimal effort. All that _must_ be defined 44 * is the execute() method. See docs/maintenance.txt for more info 45 * and a quick demo of how to use it. 46 * 47 * @author Chad Horohoe <[email protected]> 48 * @since 1.16 49 * @ingroup Maintenance 50 */ 51 abstract class Maintenance { 52 /** 53 * Constants for DB access type 54 * @see Maintenance::getDbType() 55 */ 56 const DB_NONE = 0; 57 const DB_STD = 1; 58 const DB_ADMIN = 2; 59 60 // Const for getStdin() 61 const STDIN_ALL = 'all'; 62 63 // This is the desired params 64 protected $mParams = array(); 65 66 // Array of mapping short parameters to long ones 67 protected $mShortParamsMap = array(); 68 69 // Array of desired args 70 protected $mArgList = array(); 71 72 // This is the list of options that were actually passed 73 protected $mOptions = array(); 74 75 // This is the list of arguments that were actually passed 76 protected $mArgs = array(); 77 78 // Name of the script currently running 79 protected $mSelf; 80 81 // Special vars for params that are always used 82 protected $mQuiet = false; 83 protected $mDbUser, $mDbPass; 84 85 // A description of the script, children should change this 86 protected $mDescription = ''; 87 88 // Have we already loaded our user input? 89 protected $mInputLoaded = false; 90 91 /** 92 * Batch size. If a script supports this, they should set 93 * a default with setBatchSize() 94 * 95 * @var int 96 */ 97 protected $mBatchSize = null; 98 99 // Generic options added by addDefaultParams() 100 private $mGenericParameters = array(); 101 // Generic options which might or not be supported by the script 102 private $mDependantParameters = array(); 103 104 /** 105 * Used by getDD() / setDB() 106 * @var DatabaseBase 107 */ 108 private $mDb = null; 109 110 /** 111 * Used when creating separate schema files. 112 * @var resource 113 */ 114 public $fileHandle; 115 116 /** 117 * Accessible via getConfig() 118 * 119 * @var Config 120 */ 121 private $config; 122 123 /** 124 * Default constructor. Children should call this *first* if implementing 125 * their own constructors 126 */ 127 public function __construct() { 128 // Setup $IP, using MW_INSTALL_PATH if it exists 129 global $IP; 130 $IP = strval( getenv( 'MW_INSTALL_PATH' ) ) !== '' 131 ? getenv( 'MW_INSTALL_PATH' ) 132 : realpath( __DIR__ . '/..' ); 133 134 $this->addDefaultParams(); 135 register_shutdown_function( array( $this, 'outputChanneled' ), false ); 136 } 137 138 /** 139 * Should we execute the maintenance script, or just allow it to be included 140 * as a standalone class? It checks that the call stack only includes this 141 * function and "requires" (meaning was called from the file scope) 142 * 143 * @return bool 144 */ 145 public static function shouldExecute() { 146 global $wgCommandLineMode; 147 148 if ( !function_exists( 'debug_backtrace' ) ) { 149 // If someone has a better idea... 150 return $wgCommandLineMode; 151 } 152 153 $bt = debug_backtrace(); 154 $count = count( $bt ); 155 if ( $count < 2 ) { 156 return false; // sanity 157 } 158 if ( $bt[0]['class'] !== 'Maintenance' || $bt[0]['function'] !== 'shouldExecute' ) { 159 return false; // last call should be to this function 160 } 161 $includeFuncs = array( 'require_once', 'require', 'include', 'include_once' ); 162 for ( $i = 1; $i < $count; $i++ ) { 163 if ( !in_array( $bt[$i]['function'], $includeFuncs ) ) { 164 return false; // previous calls should all be "requires" 165 } 166 } 167 168 return true; 169 } 170 171 /** 172 * Do the actual work. All child classes will need to implement this 173 */ 174 abstract public function execute(); 175 176 /** 177 * Add a parameter to the script. Will be displayed on --help 178 * with the associated description 179 * 180 * @param string $name The name of the param (help, version, etc) 181 * @param string $description The description of the param to show on --help 182 * @param bool $required Is the param required? 183 * @param bool $withArg Is an argument required with this option? 184 * @param string $shortName Character to use as short name 185 */ 186 protected function addOption( $name, $description, $required = false, 187 $withArg = false, $shortName = false 188 ) { 189 $this->mParams[$name] = array( 190 'desc' => $description, 191 'require' => $required, 192 'withArg' => $withArg, 193 'shortName' => $shortName 194 ); 195 196 if ( $shortName !== false ) { 197 $this->mShortParamsMap[$shortName] = $name; 198 } 199 } 200 201 /** 202 * Checks to see if a particular param exists. 203 * @param string $name The name of the param 204 * @return bool 205 */ 206 protected function hasOption( $name ) { 207 return isset( $this->mOptions[$name] ); 208 } 209 210 /** 211 * Get an option, or return the default 212 * @param string $name The name of the param 213 * @param mixed $default Anything you want, default null 214 * @return mixed 215 */ 216 protected function getOption( $name, $default = null ) { 217 if ( $this->hasOption( $name ) ) { 218 return $this->mOptions[$name]; 219 } else { 220 // Set it so we don't have to provide the default again 221 $this->mOptions[$name] = $default; 222 223 return $this->mOptions[$name]; 224 } 225 } 226 227 /** 228 * Add some args that are needed 229 * @param string $arg Name of the arg, like 'start' 230 * @param string $description Short description of the arg 231 * @param bool $required Is this required? 232 */ 233 protected function addArg( $arg, $description, $required = true ) { 234 $this->mArgList[] = array( 235 'name' => $arg, 236 'desc' => $description, 237 'require' => $required 238 ); 239 } 240 241 /** 242 * Remove an option. Useful for removing options that won't be used in your script. 243 * @param string $name The option to remove. 244 */ 245 protected function deleteOption( $name ) { 246 unset( $this->mParams[$name] ); 247 } 248 249 /** 250 * Set the description text. 251 * @param string $text The text of the description 252 */ 253 protected function addDescription( $text ) { 254 $this->mDescription = $text; 255 } 256 257 /** 258 * Does a given argument exist? 259 * @param int $argId The integer value (from zero) for the arg 260 * @return bool 261 */ 262 protected function hasArg( $argId = 0 ) { 263 return isset( $this->mArgs[$argId] ); 264 } 265 266 /** 267 * Get an argument. 268 * @param int $argId The integer value (from zero) for the arg 269 * @param mixed $default The default if it doesn't exist 270 * @return mixed 271 */ 272 protected function getArg( $argId = 0, $default = null ) { 273 return $this->hasArg( $argId ) ? $this->mArgs[$argId] : $default; 274 } 275 276 /** 277 * Set the batch size. 278 * @param int $s The number of operations to do in a batch 279 */ 280 protected function setBatchSize( $s = 0 ) { 281 $this->mBatchSize = $s; 282 283 // If we support $mBatchSize, show the option. 284 // Used to be in addDefaultParams, but in order for that to 285 // work, subclasses would have to call this function in the constructor 286 // before they called parent::__construct which is just weird 287 // (and really wasn't done). 288 if ( $this->mBatchSize ) { 289 $this->addOption( 'batch-size', 'Run this many operations ' . 290 'per batch, default: ' . $this->mBatchSize, false, true ); 291 if ( isset( $this->mParams['batch-size'] ) ) { 292 // This seems a little ugly... 293 $this->mDependantParameters['batch-size'] = $this->mParams['batch-size']; 294 } 295 } 296 } 297 298 /** 299 * Get the script's name 300 * @return string 301 */ 302 public function getName() { 303 return $this->mSelf; 304 } 305 306 /** 307 * Return input from stdin. 308 * @param int $len The number of bytes to read. If null, just return the handle. 309 * Maintenance::STDIN_ALL returns the full length 310 * @return mixed 311 */ 312 protected function getStdin( $len = null ) { 313 if ( $len == Maintenance::STDIN_ALL ) { 314 return file_get_contents( 'php://stdin' ); 315 } 316 $f = fopen( 'php://stdin', 'rt' ); 317 if ( !$len ) { 318 return $f; 319 } 320 $input = fgets( $f, $len ); 321 fclose( $f ); 322 323 return rtrim( $input ); 324 } 325 326 /** 327 * @return bool 328 */ 329 public function isQuiet() { 330 return $this->mQuiet; 331 } 332 333 /** 334 * Throw some output to the user. Scripts can call this with no fears, 335 * as we handle all --quiet stuff here 336 * @param string $out The text to show to the user 337 * @param mixed $channel Unique identifier for the channel. See function outputChanneled. 338 */ 339 protected function output( $out, $channel = null ) { 340 if ( $this->mQuiet ) { 341 return; 342 } 343 if ( $channel === null ) { 344 $this->cleanupChanneled(); 345 print $out; 346 } else { 347 $out = preg_replace( '/\n\z/', '', $out ); 348 $this->outputChanneled( $out, $channel ); 349 } 350 } 351 352 /** 353 * Throw an error to the user. Doesn't respect --quiet, so don't use 354 * this for non-error output 355 * @param string $err The error to display 356 * @param int $die If > 0, go ahead and die out using this int as the code 357 */ 358 protected function error( $err, $die = 0 ) { 359 $this->outputChanneled( false ); 360 if ( PHP_SAPI == 'cli' ) { 361 fwrite( STDERR, $err . "\n" ); 362 } else { 363 print $err; 364 } 365 $die = intval( $die ); 366 if ( $die > 0 ) { 367 die( $die ); 368 } 369 } 370 371 private $atLineStart = true; 372 private $lastChannel = null; 373 374 /** 375 * Clean up channeled output. Output a newline if necessary. 376 */ 377 public function cleanupChanneled() { 378 if ( !$this->atLineStart ) { 379 print "\n"; 380 $this->atLineStart = true; 381 } 382 } 383 384 /** 385 * Message outputter with channeled message support. Messages on the 386 * same channel are concatenated, but any intervening messages in another 387 * channel start a new line. 388 * @param string $msg The message without trailing newline 389 * @param string $channel Channel identifier or null for no 390 * channel. Channel comparison uses ===. 391 */ 392 public function outputChanneled( $msg, $channel = null ) { 393 if ( $msg === false ) { 394 $this->cleanupChanneled(); 395 396 return; 397 } 398 399 // End the current line if necessary 400 if ( !$this->atLineStart && $channel !== $this->lastChannel ) { 401 print "\n"; 402 } 403 404 print $msg; 405 406 $this->atLineStart = false; 407 if ( $channel === null ) { 408 // For unchanneled messages, output trailing newline immediately 409 print "\n"; 410 $this->atLineStart = true; 411 } 412 $this->lastChannel = $channel; 413 } 414 415 /** 416 * Does the script need different DB access? By default, we give Maintenance 417 * scripts normal rights to the DB. Sometimes, a script needs admin rights 418 * access for a reason and sometimes they want no access. Subclasses should 419 * override and return one of the following values, as needed: 420 * Maintenance::DB_NONE - For no DB access at all 421 * Maintenance::DB_STD - For normal DB access, default 422 * Maintenance::DB_ADMIN - For admin DB access 423 * @return int 424 */ 425 public function getDbType() { 426 return Maintenance::DB_STD; 427 } 428 429 /** 430 * Add the default parameters to the scripts 431 */ 432 protected function addDefaultParams() { 433 434 # Generic (non script dependant) options: 435 436 $this->addOption( 'help', 'Display this help message', false, false, 'h' ); 437 $this->addOption( 'quiet', 'Whether to supress non-error output', false, false, 'q' ); 438 $this->addOption( 'conf', 'Location of LocalSettings.php, if not default', false, true ); 439 $this->addOption( 'wiki', 'For specifying the wiki ID', false, true ); 440 $this->addOption( 'globals', 'Output globals at the end of processing for debugging' ); 441 $this->addOption( 442 'memory-limit', 443 'Set a specific memory limit for the script, ' 444 . '"max" for no limit or "default" to avoid changing it' 445 ); 446 $this->addOption( 'server', "The protocol and server name to use in URLs, e.g. " . 447 "http://en.wikipedia.org. This is sometimes necessary because " . 448 "server name detection may fail in command line scripts.", false, true ); 449 $this->addOption( 'profiler', 'Set to "text" or "trace" to show profiling output', false, true ); 450 451 # Save generic options to display them separately in help 452 $this->mGenericParameters = $this->mParams; 453 454 # Script dependant options: 455 456 // If we support a DB, show the options 457 if ( $this->getDbType() > 0 ) { 458 $this->addOption( 'dbuser', 'The DB user to use for this script', false, true ); 459 $this->addOption( 'dbpass', 'The password to use for this script', false, true ); 460 } 461 462 # Save additional script dependant options to display 463 #Â them separately in help 464 $this->mDependantParameters = array_diff_key( $this->mParams, $this->mGenericParameters ); 465 } 466 467 /** 468 * @since 1.24 469 * @return Config 470 */ 471 public function getConfig() { 472 if ( $this->config === null ) { 473 $this->config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); 474 } 475 476 return $this->config; 477 } 478 479 /** 480 * @since 1.24 481 * @param Config $config 482 */ 483 public function setConfig( Config $config ) { 484 $this->config = $config; 485 } 486 487 /** 488 * Run a child maintenance script. Pass all of the current arguments 489 * to it. 490 * @param string $maintClass A name of a child maintenance class 491 * @param string $classFile Full path of where the child is 492 * @return Maintenance 493 */ 494 public function runChild( $maintClass, $classFile = null ) { 495 // Make sure the class is loaded first 496 if ( !class_exists( $maintClass ) ) { 497 if ( $classFile ) { 498 require_once $classFile; 499 } 500 if ( !class_exists( $maintClass ) ) { 501 $this->error( "Cannot spawn child: $maintClass" ); 502 } 503 } 504 505 /** 506 * @var $child Maintenance 507 */ 508 $child = new $maintClass(); 509 $child->loadParamsAndArgs( $this->mSelf, $this->mOptions, $this->mArgs ); 510 if ( !is_null( $this->mDb ) ) { 511 $child->setDB( $this->mDb ); 512 } 513 514 return $child; 515 } 516 517 /** 518 * Do some sanity checking and basic setup 519 */ 520 public function setup() { 521 global $IP, $wgCommandLineMode, $wgRequestTime; 522 523 # Abort if called from a web server 524 if ( isset( $_SERVER ) && isset( $_SERVER['REQUEST_METHOD'] ) ) { 525 $this->error( 'This script must be run from the command line', true ); 526 } 527 528 if ( $IP === null ) { 529 $this->error( "\$IP not set, aborting!\n" . 530 '(Did you forget to call parent::__construct() in your maintenance script?)', 1 ); 531 } 532 533 # Make sure we can handle script parameters 534 if ( !defined( 'HPHP_VERSION' ) && !ini_get( 'register_argc_argv' ) ) { 535 $this->error( 'Cannot get command line arguments, register_argc_argv is set to false', true ); 536 } 537 538 // Send PHP warnings and errors to stderr instead of stdout. 539 // This aids in diagnosing problems, while keeping messages 540 // out of redirected output. 541 if ( ini_get( 'display_errors' ) ) { 542 ini_set( 'display_errors', 'stderr' ); 543 } 544 545 $this->loadParamsAndArgs(); 546 $this->maybeHelp(); 547 548 # Set the memory limit 549 # Note we need to set it again later in cache LocalSettings changed it 550 $this->adjustMemoryLimit(); 551 552 # Set max execution time to 0 (no limit). PHP.net says that 553 # "When running PHP from the command line the default setting is 0." 554 # But sometimes this doesn't seem to be the case. 555 ini_set( 'max_execution_time', 0 ); 556 557 $wgRequestTime = microtime( true ); 558 559 # Define us as being in MediaWiki 560 define( 'MEDIAWIKI', true ); 561 562 $wgCommandLineMode = true; 563 564 # Turn off output buffering if it's on 565 while ( ob_get_level() > 0 ) { 566 ob_end_flush(); 567 } 568 569 $this->validateParamsAndArgs(); 570 } 571 572 /** 573 * Normally we disable the memory_limit when running admin scripts. 574 * Some scripts may wish to actually set a limit, however, to avoid 575 * blowing up unexpectedly. We also support a --memory-limit option, 576 * to allow sysadmins to explicitly set one if they'd prefer to override 577 * defaults (or for people using Suhosin which yells at you for trying 578 * to disable the limits) 579 * @return string 580 */ 581 public function memoryLimit() { 582 $limit = $this->getOption( 'memory-limit', 'max' ); 583 $limit = trim( $limit, "\" '" ); // trim quotes in case someone misunderstood 584 return $limit; 585 } 586 587 /** 588 * Adjusts PHP's memory limit to better suit our needs, if needed. 589 */ 590 protected function adjustMemoryLimit() { 591 $limit = $this->memoryLimit(); 592 if ( $limit == 'max' ) { 593 $limit = -1; // no memory limit 594 } 595 if ( $limit != 'default' ) { 596 ini_set( 'memory_limit', $limit ); 597 } 598 } 599 600 /** 601 * Clear all params and arguments. 602 */ 603 public function clearParamsAndArgs() { 604 $this->mOptions = array(); 605 $this->mArgs = array(); 606 $this->mInputLoaded = false; 607 } 608 609 /** 610 * Process command line arguments 611 * $mOptions becomes an array with keys set to the option names 612 * $mArgs becomes a zero-based array containing the non-option arguments 613 * 614 * @param string $self The name of the script, if any 615 * @param array $opts An array of options, in form of key=>value 616 * @param array $args An array of command line arguments 617 */ 618 public function loadParamsAndArgs( $self = null, $opts = null, $args = null ) { 619 # If we were given opts or args, set those and return early 620 if ( $self ) { 621 $this->mSelf = $self; 622 $this->mInputLoaded = true; 623 } 624 if ( $opts ) { 625 $this->mOptions = $opts; 626 $this->mInputLoaded = true; 627 } 628 if ( $args ) { 629 $this->mArgs = $args; 630 $this->mInputLoaded = true; 631 } 632 633 # If we've already loaded input (either by user values or from $argv) 634 # skip on loading it again. The array_shift() will corrupt values if 635 # it's run again and again 636 if ( $this->mInputLoaded ) { 637 $this->loadSpecialVars(); 638 639 return; 640 } 641 642 global $argv; 643 $this->mSelf = array_shift( $argv ); 644 645 $options = array(); 646 $args = array(); 647 648 # Parse arguments 649 for ( $arg = reset( $argv ); $arg !== false; $arg = next( $argv ) ) { 650 if ( $arg == '--' ) { 651 # End of options, remainder should be considered arguments 652 $arg = next( $argv ); 653 while ( $arg !== false ) { 654 $args[] = $arg; 655 $arg = next( $argv ); 656 } 657 break; 658 } elseif ( substr( $arg, 0, 2 ) == '--' ) { 659 # Long options 660 $option = substr( $arg, 2 ); 661 if ( array_key_exists( $option, $options ) ) { 662 $this->error( "\nERROR: $option parameter given twice\n" ); 663 $this->maybeHelp( true ); 664 } 665 if ( isset( $this->mParams[$option] ) && $this->mParams[$option]['withArg'] ) { 666 $param = next( $argv ); 667 if ( $param === false ) { 668 $this->error( "\nERROR: $option parameter needs a value after it\n" ); 669 $this->maybeHelp( true ); 670 } 671 $options[$option] = $param; 672 } else { 673 $bits = explode( '=', $option, 2 ); 674 if ( count( $bits ) > 1 ) { 675 $option = $bits[0]; 676 $param = $bits[1]; 677 } else { 678 $param = 1; 679 } 680 $options[$option] = $param; 681 } 682 } elseif ( substr( $arg, 0, 1 ) == '-' ) { 683 # Short options 684 $argLength = strlen( $arg ); 685 for ( $p = 1; $p < $argLength; $p++ ) { 686 $option = $arg[$p]; 687 if ( !isset( $this->mParams[$option] ) && isset( $this->mShortParamsMap[$option] ) ) { 688 $option = $this->mShortParamsMap[$option]; 689 } 690 if ( array_key_exists( $option, $options ) ) { 691 $this->error( "\nERROR: $option parameter given twice\n" ); 692 $this->maybeHelp( true ); 693 } 694 if ( isset( $this->mParams[$option]['withArg'] ) && $this->mParams[$option]['withArg'] ) { 695 $param = next( $argv ); 696 if ( $param === false ) { 697 $this->error( "\nERROR: $option parameter needs a value after it\n" ); 698 $this->maybeHelp( true ); 699 } 700 $options[$option] = $param; 701 } else { 702 $options[$option] = 1; 703 } 704 } 705 } else { 706 $args[] = $arg; 707 } 708 } 709 710 $this->mOptions = $options; 711 $this->mArgs = $args; 712 $this->loadSpecialVars(); 713 $this->mInputLoaded = true; 714 } 715 716 /** 717 * Run some validation checks on the params, etc 718 */ 719 protected function validateParamsAndArgs() { 720 $die = false; 721 # Check to make sure we've got all the required options 722 foreach ( $this->mParams as $opt => $info ) { 723 if ( $info['require'] && !$this->hasOption( $opt ) ) { 724 $this->error( "Param $opt required!" ); 725 $die = true; 726 } 727 } 728 # Check arg list too 729 foreach ( $this->mArgList as $k => $info ) { 730 if ( $info['require'] && !$this->hasArg( $k ) ) { 731 $this->error( 'Argument <' . $info['name'] . '> required!' ); 732 $die = true; 733 } 734 } 735 736 if ( $die ) { 737 $this->maybeHelp( true ); 738 } 739 } 740 741 /** 742 * Handle the special variables that are global to all scripts 743 */ 744 protected function loadSpecialVars() { 745 if ( $this->hasOption( 'dbuser' ) ) { 746 $this->mDbUser = $this->getOption( 'dbuser' ); 747 } 748 if ( $this->hasOption( 'dbpass' ) ) { 749 $this->mDbPass = $this->getOption( 'dbpass' ); 750 } 751 if ( $this->hasOption( 'quiet' ) ) { 752 $this->mQuiet = true; 753 } 754 if ( $this->hasOption( 'batch-size' ) ) { 755 $this->mBatchSize = intval( $this->getOption( 'batch-size' ) ); 756 } 757 } 758 759 /** 760 * Maybe show the help. 761 * @param bool $force Whether to force the help to show, default false 762 */ 763 protected function maybeHelp( $force = false ) { 764 if ( !$force && !$this->hasOption( 'help' ) ) { 765 return; 766 } 767 768 $screenWidth = 80; // TODO: Calculate this! 769 $tab = " "; 770 $descWidth = $screenWidth - ( 2 * strlen( $tab ) ); 771 772 ksort( $this->mParams ); 773 $this->mQuiet = false; 774 775 // Description ... 776 if ( $this->mDescription ) { 777 $this->output( "\n" . $this->mDescription . "\n" ); 778 } 779 $output = "\nUsage: php " . basename( $this->mSelf ); 780 781 // ... append parameters ... 782 if ( $this->mParams ) { 783 $output .= " [--" . implode( array_keys( $this->mParams ), "|--" ) . "]"; 784 } 785 786 // ... and append arguments. 787 if ( $this->mArgList ) { 788 $output .= ' '; 789 foreach ( $this->mArgList as $k => $arg ) { 790 if ( $arg['require'] ) { 791 $output .= '<' . $arg['name'] . '>'; 792 } else { 793 $output .= '[' . $arg['name'] . ']'; 794 } 795 if ( $k < count( $this->mArgList ) - 1 ) { 796 $output .= ' '; 797 } 798 } 799 } 800 $this->output( "$output\n\n" ); 801 802 # TODO abstract some repetitive code below 803 804 // Generic parameters 805 $this->output( "Generic maintenance parameters:\n" ); 806 foreach ( $this->mGenericParameters as $par => $info ) { 807 if ( $info['shortName'] !== false ) { 808 $par .= " (-{$info['shortName']})"; 809 } 810 $this->output( 811 wordwrap( "$tab--$par: " . $info['desc'], $descWidth, 812 "\n$tab$tab" ) . "\n" 813 ); 814 } 815 $this->output( "\n" ); 816 817 $scriptDependantParams = $this->mDependantParameters; 818 if ( count( $scriptDependantParams ) > 0 ) { 819 $this->output( "Script dependant parameters:\n" ); 820 // Parameters description 821 foreach ( $scriptDependantParams as $par => $info ) { 822 if ( $info['shortName'] !== false ) { 823 $par .= " (-{$info['shortName']})"; 824 } 825 $this->output( 826 wordwrap( "$tab--$par: " . $info['desc'], $descWidth, 827 "\n$tab$tab" ) . "\n" 828 ); 829 } 830 $this->output( "\n" ); 831 } 832 833 // Script specific parameters not defined on construction by 834 // Maintenance::addDefaultParams() 835 $scriptSpecificParams = array_diff_key( 836 # all script parameters: 837 $this->mParams, 838 # remove the Maintenance default parameters: 839 $this->mGenericParameters, 840 $this->mDependantParameters 841 ); 842 if ( count( $scriptSpecificParams ) > 0 ) { 843 $this->output( "Script specific parameters:\n" ); 844 // Parameters description 845 foreach ( $scriptSpecificParams as $par => $info ) { 846 if ( $info['shortName'] !== false ) { 847 $par .= " (-{$info['shortName']})"; 848 } 849 $this->output( 850 wordwrap( "$tab--$par: " . $info['desc'], $descWidth, 851 "\n$tab$tab" ) . "\n" 852 ); 853 } 854 $this->output( "\n" ); 855 } 856 857 // Print arguments 858 if ( count( $this->mArgList ) > 0 ) { 859 $this->output( "Arguments:\n" ); 860 // Arguments description 861 foreach ( $this->mArgList as $info ) { 862 $openChar = $info['require'] ? '<' : '['; 863 $closeChar = $info['require'] ? '>' : ']'; 864 $this->output( 865 wordwrap( "$tab$openChar" . $info['name'] . "$closeChar: " . 866 $info['desc'], $descWidth, "\n$tab$tab" ) . "\n" 867 ); 868 } 869 $this->output( "\n" ); 870 } 871 872 die( 1 ); 873 } 874 875 /** 876 * Handle some last-minute setup here. 877 */ 878 public function finalSetup() { 879 global $wgCommandLineMode, $wgShowSQLErrors, $wgServer; 880 global $wgDBadminuser, $wgDBadminpassword; 881 global $wgDBuser, $wgDBpassword, $wgDBservers, $wgLBFactoryConf; 882 883 # Turn off output buffering again, it might have been turned on in the settings files 884 if ( ob_get_level() ) { 885 ob_end_flush(); 886 } 887 # Same with these 888 $wgCommandLineMode = true; 889 890 # Override $wgServer 891 if ( $this->hasOption( 'server' ) ) { 892 $wgServer = $this->getOption( 'server', $wgServer ); 893 } 894 895 # If these were passed, use them 896 if ( $this->mDbUser ) { 897 $wgDBadminuser = $this->mDbUser; 898 } 899 if ( $this->mDbPass ) { 900 $wgDBadminpassword = $this->mDbPass; 901 } 902 903 if ( $this->getDbType() == self::DB_ADMIN && isset( $wgDBadminuser ) ) { 904 $wgDBuser = $wgDBadminuser; 905 $wgDBpassword = $wgDBadminpassword; 906 907 if ( $wgDBservers ) { 908 /** 909 * @var $wgDBservers array 910 */ 911 foreach ( $wgDBservers as $i => $server ) { 912 $wgDBservers[$i]['user'] = $wgDBuser; 913 $wgDBservers[$i]['password'] = $wgDBpassword; 914 } 915 } 916 if ( isset( $wgLBFactoryConf['serverTemplate'] ) ) { 917 $wgLBFactoryConf['serverTemplate']['user'] = $wgDBuser; 918 $wgLBFactoryConf['serverTemplate']['password'] = $wgDBpassword; 919 } 920 LBFactory::destroyInstance(); 921 } 922 923 $this->afterFinalSetup(); 924 925 $wgShowSQLErrors = true; 926 927 // @codingStandardsIgnoreStart Allow error supppression. wfSuppressWarnings() 928 // is not avaiable. 929 @set_time_limit( 0 ); 930 // @codingStandardsIgnoreStart 931 932 $this->adjustMemoryLimit(); 933 934 // Per-script profiling; useful for debugging 935 $forcedProfiler = $this->getOption( 'profiler' ); 936 if ( $forcedProfiler === 'text' ) { 937 Profiler::setInstance( new ProfilerSimpleText( array() ) ); 938 Profiler::instance()->setTemplated( true ); 939 } elseif ( $forcedProfiler === 'trace' ) { 940 Profiler::setInstance( new ProfilerSimpleTrace( array() ) ); 941 Profiler::instance()->setTemplated( true ); 942 } 943 } 944 945 /** 946 * Execute a callback function at the end of initialisation 947 */ 948 protected function afterFinalSetup() { 949 if ( defined( 'MW_CMDLINE_CALLBACK' ) ) { 950 call_user_func( MW_CMDLINE_CALLBACK ); 951 } 952 } 953 954 /** 955 * Potentially debug globals. Originally a feature only 956 * for refreshLinks 957 */ 958 public function globals() { 959 if ( $this->hasOption( 'globals' ) ) { 960 print_r( $GLOBALS ); 961 } 962 } 963 964 /** 965 * Generic setup for most installs. Returns the location of LocalSettings 966 * @return string 967 */ 968 public function loadSettings() { 969 global $wgCommandLineMode, $IP; 970 971 if ( isset( $this->mOptions['conf'] ) ) { 972 $settingsFile = $this->mOptions['conf']; 973 } elseif ( defined( "MW_CONFIG_FILE" ) ) { 974 $settingsFile = MW_CONFIG_FILE; 975 } else { 976 $settingsFile = "$IP/LocalSettings.php"; 977 } 978 if ( isset( $this->mOptions['wiki'] ) ) { 979 $bits = explode( '-', $this->mOptions['wiki'] ); 980 if ( count( $bits ) == 1 ) { 981 $bits[] = ''; 982 } 983 define( 'MW_DB', $bits[0] ); 984 define( 'MW_PREFIX', $bits[1] ); 985 } 986 987 if ( !is_readable( $settingsFile ) ) { 988 $this->error( "A copy of your installation's LocalSettings.php\n" . 989 "must exist and be readable in the source directory.\n" . 990 "Use --conf to specify it.", true ); 991 } 992 $wgCommandLineMode = true; 993 994 return $settingsFile; 995 } 996 997 /** 998 * Support function for cleaning up redundant text records 999 * @param bool $delete Whether or not to actually delete the records 1000 * @author Rob Church <[email protected]> 1001 */ 1002 public function purgeRedundantText( $delete = true ) { 1003 # Data should come off the master, wrapped in a transaction 1004 $dbw = $this->getDB( DB_MASTER ); 1005 $dbw->begin( __METHOD__ ); 1006 1007 # Get "active" text records from the revisions table 1008 $this->output( 'Searching for active text records in revisions table...' ); 1009 $res = $dbw->select( 'revision', 'rev_text_id', array(), __METHOD__, array( 'DISTINCT' ) ); 1010 foreach ( $res as $row ) { 1011 $cur[] = $row->rev_text_id; 1012 } 1013 $this->output( "done.\n" ); 1014 1015 # Get "active" text records from the archive table 1016 $this->output( 'Searching for active text records in archive table...' ); 1017 $res = $dbw->select( 'archive', 'ar_text_id', array(), __METHOD__, array( 'DISTINCT' ) ); 1018 foreach ( $res as $row ) { 1019 # old pre-MW 1.5 records can have null ar_text_id's. 1020 if ( $row->ar_text_id !== null ) { 1021 $cur[] = $row->ar_text_id; 1022 } 1023 } 1024 $this->output( "done.\n" ); 1025 1026 # Get the IDs of all text records not in these sets 1027 $this->output( 'Searching for inactive text records...' ); 1028 $cond = 'old_id NOT IN ( ' . $dbw->makeList( $cur ) . ' )'; 1029 $res = $dbw->select( 'text', 'old_id', array( $cond ), __METHOD__, array( 'DISTINCT' ) ); 1030 $old = array(); 1031 foreach ( $res as $row ) { 1032 $old[] = $row->old_id; 1033 } 1034 $this->output( "done.\n" ); 1035 1036 # Inform the user of what we're going to do 1037 $count = count( $old ); 1038 $this->output( "$count inactive items found.\n" ); 1039 1040 # Delete as appropriate 1041 if ( $delete && $count ) { 1042 $this->output( 'Deleting...' ); 1043 $dbw->delete( 'text', array( 'old_id' => $old ), __METHOD__ ); 1044 $this->output( "done.\n" ); 1045 } 1046 1047 # Done 1048 $dbw->commit( __METHOD__ ); 1049 } 1050 1051 /** 1052 * Get the maintenance directory. 1053 * @return string 1054 */ 1055 protected function getDir() { 1056 return __DIR__; 1057 } 1058 1059 /** 1060 * Returns a database to be used by current maintenance script. It can be set by setDB(). 1061 * If not set, wfGetDB() will be used. 1062 * This function has the same parameters as wfGetDB() 1063 * 1064 * @return DatabaseBase 1065 */ 1066 protected function &getDB( $db, $groups = array(), $wiki = false ) { 1067 if ( is_null( $this->mDb ) ) { 1068 return wfGetDB( $db, $groups, $wiki ); 1069 } else { 1070 return $this->mDb; 1071 } 1072 } 1073 1074 /** 1075 * Sets database object to be returned by getDB(). 1076 * 1077 * @param DatabaseBase $db Database object to be used 1078 */ 1079 public function setDB( &$db ) { 1080 $this->mDb = $db; 1081 } 1082 1083 /** 1084 * Lock the search index 1085 * @param DatabaseBase &$db 1086 */ 1087 private function lockSearchindex( &$db ) { 1088 $write = array( 'searchindex' ); 1089 $read = array( 'page', 'revision', 'text', 'interwiki', 'l10n_cache', 'user' ); 1090 $db->lockTables( $read, $write, __CLASS__ . '::' . __METHOD__ ); 1091 } 1092 1093 /** 1094 * Unlock the tables 1095 * @param DatabaseBase &$db 1096 */ 1097 private function unlockSearchindex( &$db ) { 1098 $db->unlockTables( __CLASS__ . '::' . __METHOD__ ); 1099 } 1100 1101 /** 1102 * Unlock and lock again 1103 * Since the lock is low-priority, queued reads will be able to complete 1104 * @param DatabaseBase &$db 1105 */ 1106 private function relockSearchindex( &$db ) { 1107 $this->unlockSearchindex( $db ); 1108 $this->lockSearchindex( $db ); 1109 } 1110 1111 /** 1112 * Perform a search index update with locking 1113 * @param int $maxLockTime The maximum time to keep the search index locked. 1114 * @param string $callback The function that will update the function. 1115 * @param DatabaseBase $dbw 1116 * @param array $results 1117 */ 1118 public function updateSearchIndex( $maxLockTime, $callback, $dbw, $results ) { 1119 $lockTime = time(); 1120 1121 # Lock searchindex 1122 if ( $maxLockTime ) { 1123 $this->output( " --- Waiting for lock ---" ); 1124 $this->lockSearchindex( $dbw ); 1125 $lockTime = time(); 1126 $this->output( "\n" ); 1127 } 1128 1129 # Loop through the results and do a search update 1130 foreach ( $results as $row ) { 1131 # Allow reads to be processed 1132 if ( $maxLockTime && time() > $lockTime + $maxLockTime ) { 1133 $this->output( " --- Relocking ---" ); 1134 $this->relockSearchindex( $dbw ); 1135 $lockTime = time(); 1136 $this->output( "\n" ); 1137 } 1138 call_user_func( $callback, $dbw, $row ); 1139 } 1140 1141 # Unlock searchindex 1142 if ( $maxLockTime ) { 1143 $this->output( " --- Unlocking --" ); 1144 $this->unlockSearchindex( $dbw ); 1145 $this->output( "\n" ); 1146 } 1147 } 1148 1149 /** 1150 * Update the searchindex table for a given pageid 1151 * @param DatabaseBase $dbw A database write handle 1152 * @param int $pageId The page ID to update. 1153 * @return null|string 1154 */ 1155 public function updateSearchIndexForPage( $dbw, $pageId ) { 1156 // Get current revision 1157 $rev = Revision::loadFromPageId( $dbw, $pageId ); 1158 $title = null; 1159 if ( $rev ) { 1160 $titleObj = $rev->getTitle(); 1161 $title = $titleObj->getPrefixedDBkey(); 1162 $this->output( "$title..." ); 1163 # Update searchindex 1164 $u = new SearchUpdate( $pageId, $titleObj->getText(), $rev->getContent() ); 1165 $u->doUpdate(); 1166 $this->output( "\n" ); 1167 } 1168 1169 return $title; 1170 } 1171 1172 /** 1173 * Wrapper for posix_isatty() 1174 * We default as considering stdin a tty (for nice readline methods) 1175 * but treating stout as not a tty to avoid color codes 1176 * 1177 * @param int $fd File descriptor 1178 * @return bool 1179 */ 1180 public static function posix_isatty( $fd ) { 1181 if ( !function_exists( 'posix_isatty' ) ) { 1182 return !$fd; 1183 } else { 1184 return posix_isatty( $fd ); 1185 } 1186 } 1187 1188 /** 1189 * Prompt the console for input 1190 * @param string $prompt What to begin the line with, like '> ' 1191 * @return string Response 1192 */ 1193 public static function readconsole( $prompt = '> ' ) { 1194 static $isatty = null; 1195 if ( is_null( $isatty ) ) { 1196 $isatty = self::posix_isatty( 0 /*STDIN*/ ); 1197 } 1198 1199 if ( $isatty && function_exists( 'readline' ) ) { 1200 return readline( $prompt ); 1201 } else { 1202 if ( $isatty ) { 1203 $st = self::readlineEmulation( $prompt ); 1204 } else { 1205 if ( feof( STDIN ) ) { 1206 $st = false; 1207 } else { 1208 $st = fgets( STDIN, 1024 ); 1209 } 1210 } 1211 if ( $st === false ) { 1212 return false; 1213 } 1214 $resp = trim( $st ); 1215 1216 return $resp; 1217 } 1218 } 1219 1220 /** 1221 * Emulate readline() 1222 * @param string $prompt What to begin the line with, like '> ' 1223 * @return string 1224 */ 1225 private static function readlineEmulation( $prompt ) { 1226 $bash = Installer::locateExecutableInDefaultPaths( array( 'bash' ) ); 1227 if ( !wfIsWindows() && $bash ) { 1228 $retval = false; 1229 $encPrompt = wfEscapeShellArg( $prompt ); 1230 $command = "read -er -p $encPrompt && echo \"\$REPLY\""; 1231 $encCommand = wfEscapeShellArg( $command ); 1232 $line = wfShellExec( "$bash -c $encCommand", $retval, array(), array( 'walltime' => 0 ) ); 1233 1234 if ( $retval == 0 ) { 1235 return $line; 1236 } elseif ( $retval == 127 ) { 1237 // Couldn't execute bash even though we thought we saw it. 1238 // Shell probably spit out an error message, sorry :( 1239 // Fall through to fgets()... 1240 } else { 1241 // EOF/ctrl+D 1242 return false; 1243 } 1244 } 1245 1246 // Fallback... we'll have no editing controls, EWWW 1247 if ( feof( STDIN ) ) { 1248 return false; 1249 } 1250 print $prompt; 1251 1252 return fgets( STDIN, 1024 ); 1253 } 1254 } 1255 1256 /** 1257 * Fake maintenance wrapper, mostly used for the web installer/updater 1258 */ 1259 class FakeMaintenance extends Maintenance { 1260 protected $mSelf = "FakeMaintenanceScript"; 1261 1262 public function execute() { 1263 return; 1264 } 1265 } 1266 1267 /** 1268 * Class for scripts that perform database maintenance and want to log the 1269 * update in `updatelog` so we can later skip it 1270 */ 1271 abstract class LoggedUpdateMaintenance extends Maintenance { 1272 public function __construct() { 1273 parent::__construct(); 1274 $this->addOption( 'force', 'Run the update even if it was completed already' ); 1275 $this->setBatchSize( 200 ); 1276 } 1277 1278 public function execute() { 1279 $db = $this->getDB( DB_MASTER ); 1280 $key = $this->getUpdateKey(); 1281 1282 if ( !$this->hasOption( 'force' ) 1283 && $db->selectRow( 'updatelog', '1', array( 'ul_key' => $key ), __METHOD__ ) 1284 ) { 1285 $this->output( "..." . $this->updateSkippedMessage() . "\n" ); 1286 1287 return true; 1288 } 1289 1290 if ( !$this->doDBUpdates() ) { 1291 return false; 1292 } 1293 1294 if ( $db->insert( 'updatelog', array( 'ul_key' => $key ), __METHOD__, 'IGNORE' ) ) { 1295 return true; 1296 } else { 1297 $this->output( $this->updatelogFailedMessage() . "\n" ); 1298 1299 return false; 1300 } 1301 } 1302 1303 /** 1304 * Message to show that the update was done already and was just skipped 1305 * @return string 1306 */ 1307 protected function updateSkippedMessage() { 1308 $key = $this->getUpdateKey(); 1309 1310 return "Update '{$key}' already logged as completed."; 1311 } 1312 1313 /** 1314 * Message to show the the update log was unable to log the completion of this update 1315 * @return string 1316 */ 1317 protected function updatelogFailedMessage() { 1318 $key = $this->getUpdateKey(); 1319 1320 return "Unable to log update '{$key}' as completed."; 1321 } 1322 1323 /** 1324 * Do the actual work. All child classes will need to implement this. 1325 * Return true to log the update as done or false (usually on failure). 1326 * @return bool 1327 */ 1328 abstract protected function doDBUpdates(); 1329 1330 /** 1331 * Get the update key name to go in the update log table 1332 * @return string 1333 */ 1334 abstract protected function getUpdateKey(); 1335 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |